fix(downloader): private playlist download

This commit is contained in:
JellyBrick
2023-10-12 00:41:58 +09:00
parent 572a023aaa
commit 1d5b2997bd

View File

@ -3,23 +3,14 @@ import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
import { ClientType, Innertube, UniversalCache, Utils } from 'youtubei.js';
import { ClientType, Innertube, UniversalCache, Utils, YTNodes } from 'youtubei.js';
import is from 'electron-is';
import ytpl from 'ytpl';
// REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
import filenamify from 'filenamify';
import { Mutex } from 'async-mutex';
import { createFFmpeg } from '@ffmpeg.wasm/main';
import NodeID3, { TagConstants } from 'node-id3';
import PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
import { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
import TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
import { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
import { cropMaxWidth, getFolder, presets, sendFeedback as sendFeedback_, setBadge } from './utils';
import config from './config';
@ -32,8 +23,13 @@ import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
import { injectCSS } from '../utils';
import { cache } from '../../providers/decorators';
import type { GetPlayerResponse } from '../../types/get-player-response';
import type { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
import type PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
import type { Playlist } from 'youtubei.js/dist/src/parser/ytmusic';
import type { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
import type TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
import type { GetPlayerResponse } from '../../types/get-player-response';
type CustomSongInfo = SongInfo & { trackId?: string };
@ -69,16 +65,19 @@ const sendError = (error: Error, source?: string) => {
});
};
export const getCookieFromWindow = async (win: BrowserWindow) => {
return (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
it.name + '=' + it.value + ';'
).join('');
};
export default async (win_: BrowserWindow) => {
win = win_;
injectCSS(win.webContents, style);
const cookie = (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
it.name + '=' + it.value + ';'
).join('');
yt = await Innertube.create({
cache: new UniversalCache(false),
cookie,
cookie: await getCookieFromWindow(win),
generate_session_locally: true,
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
const url =
@ -118,6 +117,7 @@ export async function downloadSong(
let resolvedName;
try {
await downloadSongUnsafe(
false,
url,
(name: string) => resolvedName = name,
playlistFolder,
@ -129,8 +129,31 @@ export async function downloadSong(
}
}
export async function downloadSongFromId(
id: string,
playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined,
increasePlaylistProgress: (value: number) => void = () => {
},
) {
let resolvedName;
try {
await downloadSongUnsafe(
true,
id,
(name: string) => resolvedName = name,
playlistFolder,
trackId,
increasePlaylistProgress,
);
} catch (error: unknown) {
sendError(error as Error, resolvedName || id);
}
}
async function downloadSongUnsafe(
url: string,
isId: boolean,
idOrUrl: string,
setName: (name: string) => void,
playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined,
@ -147,8 +170,13 @@ async function downloadSongUnsafe(
sendFeedback('Downloading...', 2);
const id = getVideoId(url);
if (typeof id !== 'string') throw new Error('Video not found');
let id: string | null;
if (isId) {
id = idOrUrl;
} else {
id = getVideoId(idOrUrl);
if (typeof id !== 'string') throw new Error('Video not found');
}
let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
@ -417,11 +445,9 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
console.log(`trying to get playlist ID: '${playlistId}'`);
sendFeedback('Getting playlist info…');
let playlist: ytpl.Result;
let playlist: Playlist;
try {
playlist = await ytpl(playlistId, {
limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY,
});
playlist = await yt.music.getPlaylist(playlistId);
} catch (error: unknown) {
sendError(
Error(`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(error)}`),
@ -429,22 +455,27 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
return;
}
if (playlist.items.length === 0) {
if (!playlist || !playlist.items || playlist.items.length === 0) {
sendError(new Error('Playlist is empty'));
}
if (playlist.items.length === 1) {
const items = playlist.items!.as(YTNodes.MusicResponsiveListItem);
if (items.length === 1) {
sendFeedback('Playlist has only one item, downloading it directly');
await downloadSong(playlist.items[0].url);
await downloadSongFromId(items.at(0)!.id!);
return;
}
const isAlbum = playlist.title.startsWith('Album - ');
let playlistTitle = playlist.header?.title?.text ?? '';
const isAlbum = playlistTitle?.startsWith('Album - ');
if (isAlbum) {
playlist.title = playlist.title.slice(8);
playlistTitle = playlistTitle.slice(8);
}
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
if (!is.macOS()) {
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
}
const folder = getFolder(config.get('downloadFolder') ?? '');
const playlistFolder = join(folder, safePlaylistTitle);
@ -461,47 +492,47 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
type: 'info',
buttons: ['OK'],
title: 'Started Download',
message: `Downloading Playlist "${playlist.title}"`,
detail: `(${playlist.items.length} songs)`,
message: `Downloading Playlist "${playlistTitle}"`,
detail: `(${items.length} songs)`,
});
if (is.dev()) {
console.log(
`Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`,
`Downloading playlist "${playlistTitle}" - ${items.length} songs (${playlistId})`,
);
}
win.setProgressBar(2); // Starts with indefinite bar
setBadge(playlist.items.length);
setBadge(items.length);
let counter = 1;
const progressStep = 1 / playlist.items.length;
const progressStep = 1 / items.length;
const increaseProgress = (itemPercentage: number) => {
const currentProgress = (counter - 1) / (playlist.items.length ?? 1);
const currentProgress = (counter - 1) / (items.length ?? 1);
const newProgress = currentProgress + (progressStep * itemPercentage);
win.setProgressBar(newProgress);
};
try {
for (const song of playlist.items) {
sendFeedback(`Downloading ${counter}/${playlist.items.length}...`);
for (const song of items) {
sendFeedback(`Downloading ${counter}/${items.length}...`);
const trackId = isAlbum ? counter : undefined;
await downloadSong(
song.url,
await downloadSongFromId(
song.id!,
playlistFolder,
trackId?.toString(),
increaseProgress,
).catch((error) =>
sendError(
new Error(`Error downloading "${song.author.name} - ${song.title}":\n ${error}`)
new Error(`Error downloading "${song.author!.name} - ${song.title!}":\n ${error}`)
),
);
win.setProgressBar(counter / playlist.items.length);
setBadge(playlist.items.length - counter);
win.setProgressBar(counter / items.length);
setBadge(items.length - counter);
counter++;
}
} catch (error: unknown) {