Skip to content

Commit 1d5b299

Browse files
committed
fix(downloader): private playlist download
1 parent 572a023 commit 1d5b299

File tree

1 file changed

+72
-41
lines changed

1 file changed

+72
-41
lines changed

plugins/downloader/back.ts

+72-41
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,14 @@ import { join } from 'node:path';
33
import { randomBytes } from 'node:crypto';
44

55
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
6-
import { ClientType, Innertube, UniversalCache, Utils } from 'youtubei.js';
6+
import { ClientType, Innertube, UniversalCache, Utils, YTNodes } from 'youtubei.js';
77
import is from 'electron-is';
8-
import ytpl from 'ytpl';
9-
// REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
108
import filenamify from 'filenamify';
119
import { Mutex } from 'async-mutex';
1210
import { createFFmpeg } from '@ffmpeg.wasm/main';
1311

1412
import NodeID3, { TagConstants } from 'node-id3';
1513

16-
import PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
17-
import { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
18-
19-
import TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
20-
21-
import { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
22-
2314
import { cropMaxWidth, getFolder, presets, sendFeedback as sendFeedback_, setBadge } from './utils';
2415

2516
import config from './config';
@@ -32,8 +23,13 @@ import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
3223
import { injectCSS } from '../utils';
3324
import { cache } from '../../providers/decorators';
3425

35-
import type { GetPlayerResponse } from '../../types/get-player-response';
26+
import type { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
27+
import type PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
28+
import type { Playlist } from 'youtubei.js/dist/src/parser/ytmusic';
29+
import type { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
30+
import type TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
3631

32+
import type { GetPlayerResponse } from '../../types/get-player-response';
3733

3834
type CustomSongInfo = SongInfo & { trackId?: string };
3935

@@ -69,16 +65,19 @@ const sendError = (error: Error, source?: string) => {
6965
});
7066
};
7167

68+
export const getCookieFromWindow = async (win: BrowserWindow) => {
69+
return (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
70+
it.name + '=' + it.value + ';'
71+
).join('');
72+
};
73+
7274
export default async (win_: BrowserWindow) => {
7375
win = win_;
7476
injectCSS(win.webContents, style);
7577

76-
const cookie = (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
77-
it.name + '=' + it.value + ';'
78-
).join('');
7978
yt = await Innertube.create({
8079
cache: new UniversalCache(false),
81-
cookie,
80+
cookie: await getCookieFromWindow(win),
8281
generate_session_locally: true,
8382
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
8483
const url =
@@ -118,6 +117,7 @@ export async function downloadSong(
118117
let resolvedName;
119118
try {
120119
await downloadSongUnsafe(
120+
false,
121121
url,
122122
(name: string) => resolvedName = name,
123123
playlistFolder,
@@ -129,8 +129,31 @@ export async function downloadSong(
129129
}
130130
}
131131

132+
export async function downloadSongFromId(
133+
id: string,
134+
playlistFolder: string | undefined = undefined,
135+
trackId: string | undefined = undefined,
136+
increasePlaylistProgress: (value: number) => void = () => {
137+
},
138+
) {
139+
let resolvedName;
140+
try {
141+
await downloadSongUnsafe(
142+
true,
143+
id,
144+
(name: string) => resolvedName = name,
145+
playlistFolder,
146+
trackId,
147+
increasePlaylistProgress,
148+
);
149+
} catch (error: unknown) {
150+
sendError(error as Error, resolvedName || id);
151+
}
152+
}
153+
132154
async function downloadSongUnsafe(
133-
url: string,
155+
isId: boolean,
156+
idOrUrl: string,
134157
setName: (name: string) => void,
135158
playlistFolder: string | undefined = undefined,
136159
trackId: string | undefined = undefined,
@@ -147,8 +170,13 @@ async function downloadSongUnsafe(
147170

148171
sendFeedback('Downloading...', 2);
149172

150-
const id = getVideoId(url);
151-
if (typeof id !== 'string') throw new Error('Video not found');
173+
let id: string | null;
174+
if (isId) {
175+
id = idOrUrl;
176+
} else {
177+
id = getVideoId(idOrUrl);
178+
if (typeof id !== 'string') throw new Error('Video not found');
179+
}
152180

153181
let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
154182

@@ -417,34 +445,37 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
417445

418446
console.log(`trying to get playlist ID: '${playlistId}'`);
419447
sendFeedback('Getting playlist info…');
420-
let playlist: ytpl.Result;
448+
let playlist: Playlist;
421449
try {
422-
playlist = await ytpl(playlistId, {
423-
limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY,
424-
});
450+
playlist = await yt.music.getPlaylist(playlistId);
425451
} catch (error: unknown) {
426452
sendError(
427453
Error(`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(error)}`),
428454
);
429455
return;
430456
}
431457

432-
if (playlist.items.length === 0) {
458+
if (!playlist || !playlist.items || playlist.items.length === 0) {
433459
sendError(new Error('Playlist is empty'));
434460
}
435461

436-
if (playlist.items.length === 1) {
462+
const items = playlist.items!.as(YTNodes.MusicResponsiveListItem);
463+
if (items.length === 1) {
437464
sendFeedback('Playlist has only one item, downloading it directly');
438-
await downloadSong(playlist.items[0].url);
465+
await downloadSongFromId(items.at(0)!.id!);
439466
return;
440467
}
441468

442-
const isAlbum = playlist.title.startsWith('Album - ');
469+
let playlistTitle = playlist.header?.title?.text ?? '';
470+
const isAlbum = playlistTitle?.startsWith('Album - ');
443471
if (isAlbum) {
444-
playlist.title = playlist.title.slice(8);
472+
playlistTitle = playlistTitle.slice(8);
445473
}
446474

447-
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
475+
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
476+
if (!is.macOS()) {
477+
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
478+
}
448479

449480
const folder = getFolder(config.get('downloadFolder') ?? '');
450481
const playlistFolder = join(folder, safePlaylistTitle);
@@ -461,47 +492,47 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
461492
type: 'info',
462493
buttons: ['OK'],
463494
title: 'Started Download',
464-
message: `Downloading Playlist "${playlist.title}"`,
465-
detail: `(${playlist.items.length} songs)`,
495+
message: `Downloading Playlist "${playlistTitle}"`,
496+
detail: `(${items.length} songs)`,
466497
});
467498

468499
if (is.dev()) {
469500
console.log(
470-
`Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`,
501+
`Downloading playlist "${playlistTitle}" - ${items.length} songs (${playlistId})`,
471502
);
472503
}
473504

474505
win.setProgressBar(2); // Starts with indefinite bar
475506

476-
setBadge(playlist.items.length);
507+
setBadge(items.length);
477508

478509
let counter = 1;
479510

480-
const progressStep = 1 / playlist.items.length;
511+
const progressStep = 1 / items.length;
481512

482513
const increaseProgress = (itemPercentage: number) => {
483-
const currentProgress = (counter - 1) / (playlist.items.length ?? 1);
514+
const currentProgress = (counter - 1) / (items.length ?? 1);
484515
const newProgress = currentProgress + (progressStep * itemPercentage);
485516
win.setProgressBar(newProgress);
486517
};
487518

488519
try {
489-
for (const song of playlist.items) {
490-
sendFeedback(`Downloading ${counter}/${playlist.items.length}...`);
520+
for (const song of items) {
521+
sendFeedback(`Downloading ${counter}/${items.length}...`);
491522
const trackId = isAlbum ? counter : undefined;
492-
await downloadSong(
493-
song.url,
523+
await downloadSongFromId(
524+
song.id!,
494525
playlistFolder,
495526
trackId?.toString(),
496527
increaseProgress,
497528
).catch((error) =>
498529
sendError(
499-
new Error(`Error downloading "${song.author.name} - ${song.title}":\n ${error}`)
530+
new Error(`Error downloading "${song.author!.name} - ${song.title!}":\n ${error}`)
500531
),
501532
);
502533

503-
win.setProgressBar(counter / playlist.items.length);
504-
setBadge(playlist.items.length - counter);
534+
win.setProgressBar(counter / items.length);
535+
setBadge(items.length - counter);
505536
counter++;
506537
}
507538
} catch (error: unknown) {

0 commit comments

Comments
 (0)