-
Notifications
You must be signed in to change notification settings - Fork 3
/
youtube.js
315 lines (269 loc) · 10.3 KB
/
youtube.js
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import { Innertube, UniversalCache } from 'youtubei.js';
import checkbox, { Separator } from '@inquirer/checkbox';
import { select } from '@inquirer/prompts';
import confirm from '@inquirer/confirm';
import open from 'open';
import clipboard from 'clipboardy';
const PLAYLIST_LIMIT = 100;
export class YouTube {
async getProfile(fields) {
const profile = {}
if (fields.channels) {
console.log('Reading Subscriptions...');
profile.channels = await this.getChannels();
}
if (fields.history) {
console.log('Reading Watch history...');
profile.history = await this.getWatchHistory();
}
if (fields.likedVideos) {
console.log('Reading Liked videos...');
profile.likedVideos = await this.getLikedVideos();
}
if (fields.watchLater) {
console.log('Reading Watch later...');
profile.watchLater = await this.getWatchLater();
}
if (fields.homeFeed) {
console.log('Reading Recommended videos...');
profile.homeFeed = await this.getHomeFeed();
}
if (fields.playlists) {
console.log('Reading Playlists...');
if (fields.playlists === true) {
profile.playlists = await this.getPlaylistsWithVideos();
} else {
profile.playlists = [];
const libraryPlaylists = await this.getLibraryPlaylists();
for (const playlist of libraryPlaylists) {
if (fields.playlists[playlist.id]) {
console.log(`Reading Playlist: ${playlist.title}`);
profile.playlists.push(await this.getPlaylistWithVideos(playlist.id));
}
}
}
}
return profile;
}
async createSession(useCache = false) {
if (!this.innertube) {
if (useCache) {
this.innertube = await Innertube.create({
cache: new UniversalCache(true, "./.cache")
});
} else {
this.innertube = await Innertube.create();
}
}
return this.innertube;
}
async logout() {
console.log('Signing out from YouTube...');
console.log()
await this.innertube.session.signOut();
}
async getChannels() {
await this.createSession();
let feed = await this.innertube.getChannelsFeed();
const channels = feed.channels.map(channel => {
return this.toChannel(channel);
});
while (feed.has_continuation) {
try {
feed = await feed.getContinuation();
channels.push(...feed.channels.map(channel => {
return this.toChannel(channel);
}));
} catch (error) {
console.error(error);
break;
}
}
return channels;
}
async getWatchHistory(limit = PLAYLIST_LIMIT) {
await this.createSession();
const history = await this.innertube.getHistory();
return this.getFeedVideosWithLimit(history, limit);
}
async getLikedVideos(limit = PLAYLIST_LIMIT) {
await this.createSession();
const feed = await this.innertube.getPlaylist('VLLL');
return this.getFeedVideosWithLimit(feed, limit);
}
async getWatchLater(limit = PLAYLIST_LIMIT) {
await this.createSession();
const feed = await this.innertube.getPlaylist('VLWL');
return this.getFeedVideosWithLimit(feed, limit);
}
async getHomeFeed(limit = PLAYLIST_LIMIT) {
await this.createSession();
const feed = await this.innertube.getHomeFeed();
return this.getFeedVideosWithLimit(feed, limit);
}
async getPlaylistsWithVideos(limit = PLAYLIST_LIMIT) {
const playlists = [];
const libraryPlaylists = await this.getLibraryPlaylists();
for (const playlist of libraryPlaylists) {
playlists.push(await this.getPlaylistWithVideos(playlist.id, limit));
}
return playlists;
}
async getLibraryPlaylists() {
await this.createSession();
const playlists = await this.innertube.getPlaylists();
return playlists.map(playlist => this.toPlaylist(playlist))
// filter out mix playlists, they are not viewable and will throw an error
.filter(playlist => !playlist.id.startsWith('RD'));
}
async getPlaylistWithVideos(playlistId, limit = PLAYLIST_LIMIT) {
await this.createSession();
let playlist = await this.innertube.getPlaylist(playlistId);
const videos = await this.getFeedVideosWithLimit(playlist, limit);
return this.toPlaylistWithVideos(playlist, videos);
}
async getFeedVideosWithLimit(feed, limit = PLAYLIST_LIMIT) {
const videos = [];
while (limit === -1 || videos.length < limit) {
try {
videos.push(...feed.videos);
if (!feed.has_continuation) {
break;
}
feed = await feed.getContinuation();
} catch (error) {
console.error(error);
break;
}
}
return videos.slice(0, limit)
.map(video => this.toVideo(video));
}
toVideo(video) {
return {
id: video.id,
title: video.title.text,
author: video.author?.name || "",
authorId: video.author?.id || "",
published: video.published?.text || "", //? undefined?
description: video.description,
viewCount: video.view_count?.text || "", // '1,926,729 views'
lengthSeconds: video.duration?.seconds || 0,
isLive: !!video.is_live,
}
}
toChannel(channel) {
let thumbnail = channel.author.best_thumbnail.url;
if (thumbnail && thumbnail.startsWith('//')) {
thumbnail = `https:${thumbnail}`;
}
return {
id: channel.id,
name: channel.author.name,
thumbnail: thumbnail,
}
}
toPlaylist(playlist) {
// from FEplaylist_aggregation
if (playlist.type === 'LockupView' && playlist.content_type === 'PLAYLIST') {
return {
id: playlist.content_id,
title: playlist.metadata.title.text,
};
}
return {
id: playlist.id,
title: playlist.title.text
}
}
toPlaylistWithVideos(playlist, videos) {
return {
id: playlist.id,
title: playlist.info.title,
description: playlist.info.description,
privacy: playlist.info.privacy,
videos: videos,
};
}
}
export class YouTubeInteractive {
static async loginDisclaimer() {
const initialAnswer = await confirm({
message: `This tool will log into your YouTube account, read your data, and allow
you to import it to other platforms.
You will get to choose which data to import and where to export it.
Continue?` });
console.log()
return initialAnswer;
}
static async login(youtube, cacheEnabled) {
const innertube = await youtube.createSession(cacheEnabled);
innertube.session.on('auth-pending', async (data) => {
console.log(`Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`);
const openBrowserAnswer = await confirm({ message: 'Copy code to clipboard and open url in the browser now?' });
if (openBrowserAnswer) {
clipboard.writeSync(data.user_code);
open(data.verification_url);
}
});
innertube.session.on('update-credentials', async ({ credentials }) => {
console.log('YouTube credentials updated.');
if (cacheEnabled) {
await innertube.session.oauth.cacheCredentials();
}
});
await innertube.session.signIn();
if (cacheEnabled) {
await innertube.session.oauth.cacheCredentials();
}
console.log()
}
static async chooseProfileFields(libraryPlaylists) {
const choices = [
{ name: 'Subscriptions', value: 'channels', checked: true },
{ name: 'Watch history', value: 'history', checked: true },
{ name: 'Liked videos', value: 'likedVideos', checked: true },
{ name: 'Watch later', value: 'watchLater', checked: true },
{ name: 'Recommended videos', value: 'homeFeed', checked: true },
]
if (libraryPlaylists.length > 0) {
choices.push(new Separator('-- Playlists --'));
for (const playlist of libraryPlaylists) {
choices.push({ name: playlist.title, value: { playlistId: playlist.id }, checked: true });
}
}
const importChoices = await checkbox({
message: 'Select the items to import from YouTube',
choices: choices,
pageSize: 15,
loop: false,
required: true,
});
console.log()
const fields = {};
for (const choice of importChoices) {
if (typeof choice === 'string') {
fields[choice] = true;
} else {
fields.playlists = fields.playlists || {};
fields.playlists[choice.playlistId] = true;
}
}
return fields;
}
static async chooseExportPlatform() {
const exportChoice = await select({
message: 'Select platform to export to',
choices: [
{ name: 'Invidious (API import)', value: 'invidious_api' },
{ name: 'Invidious (save to file)', value: 'invidious_file' },
{ name: 'Piped (save to file)', value: 'piped_file' },
{ name: 'NewPipe (Subscriptions only) (save to file)', value: 'newpipe_subs_file' },
{ name: 'FreeTube (Subscriptions and History only) (save to file)', value: 'freetube_file' },
{ name: 'ViewTube (Subscriptions only) (save to file)', value: 'viewtube_file' },
],
});
console.log();
return exportChoice;
}
}