mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 11:01:45 +00:00
fix(downloader): private playlist download
This commit is contained in:
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user