forked from jerbear2008/tttakedown-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvideo.ts
168 lines (151 loc) · 4.29 KB
/
video.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { archiveYouTube, archiveYouTubeFile, archiveYouTubePage } from './archive.ts'
import { invidiousVideoData, playlistData, videoData } from './downloadData.ts'
type playlistEntriesData = playlistData['entries']
type playlistEntryData = playlistData['entries'][number]
export type change =
& { video: Video }
& ({
type: 'newVideo'
} )
export class VideoMap extends Map<string, Video> {
constructor(public listID: string) {
super()
}
async update() {
const publicVideos = (await playlistData(this.listID)).entries
const changes: change[] = []
for (const video of publicVideos) {
const existingVideo = this.get(video.id)
if (existingVideo) {
changes.push(...existingVideo.update(Video.playlistEntryToData(video)))
continue
}
const newVideo = new Video(Video.playlistEntryToData(video))
this.set(video.id, newVideo)
changes.push(newVideo.setNew())
}
return changes
}
toData() {
return {
listID: this.listID,
videos: [...this.values()].map((video) => video.toData()),
}
}
static fromData(data: ReturnType<typeof VideoMap.prototype.toData>) {
const videoMap = new VideoMap(data.listID)
for (const videoData of data.videos) {
videoMap.set(videoData.id, Video.fromData(videoData))
}
return videoMap
}
}
const bestThumbnail = (thumbnails: {
url: string
width: number
}[]) =>
thumbnails.reduce((prev, curr) => {
if (prev.width > curr.width) return prev
return curr
}).url
export type videoData = {
id: string
title: string
thumbnailURL: string
duration: number
listed: boolean
views?: number
channelName?: string
channelURL?: string
}
export class Video {
constructor(
public data: videoData,
) {}
update(data: videoData) {
const changes: change[] = []
let archivePage = false
if (this.data.listed !== data.listed) {
changes.push({
type: data.listed ? 'relistedVideo' as const : 'removedVideo' as const,
video: this,
})
archivePage = true
}
if (this.data.duration !== data.duration) {
changes.push({
type: 'lengthChange' as const,
video: this,
oldDuration: this.data.duration,
oldHumanDuration: this.humanDuration,
})
archivePage = true
}
if (this.data.title !== data.title) {
changes.push({
type: 'titleChange' as const,
video: this,
oldTitle: this.data.title,
})
archivePage = true
}
this.data = data
if (archivePage) archiveYouTubePage(this.data.id)
return changes
}
static playlistEntryToData(playlistEntry: playlistEntryData): videoData {
return {
id: playlistEntry.id,
title: playlistEntry.title,
thumbnailURL: bestThumbnail(playlistEntry.thumbnails),
duration: playlistEntry.duration,
views: playlistEntry.view_count,
channelName: playlistEntry.channel,
channelURL: playlistEntry.channel_url,
listed: true,
}
}
static invidiousVideoToData(invidiousVideo: invidiousVideoData): videoData {
return {
id: invidiousVideo.videoId,
title: invidiousVideo.title,
thumbnailURL: bestThumbnail(invidiousVideo.videoThumbnails),
duration: invidiousVideo.lengthSeconds,
views: invidiousVideo.viewCount,
channelName: invidiousVideo.author,
channelURL: invidiousVideo.authorUrl,
listed: invidiousVideo.isListed,
}
}
setNew() {
this.data.listed = true
archiveYouTube(this.data.id)
return {
type: 'newVideo' as const,
video: this,
}
}
get url() {
return `https://youtu.be/${this.data.id}`
}
get humanDuration() {
const hours = Math.floor(this.data.duration / 3600)
const minutes = Math.floor(this.data.duration / 60) % 60
const seconds = this.data.duration % 60
const hoursString = hours ? `${hours}:` : ''
const minutesString = hours ? `${minutes}`.padStart(2, '0') : `${minutes}`
const secondsString = `${seconds}`.padStart(2, '0')
return `${hoursString}${minutesString}:${secondsString}`
}
toString() {
return `Video ["${
this.data.title.replaceAll('"', '\\"')
}" ${this.humanDuration} ${this.data.id}]`
}
toData() {
return this.data
}
static fromData(data: videoData) {
return new Video(data)
}
}