Skip to content

Commit

Permalink
fix(downloader): private playlist download
Browse files Browse the repository at this point in the history
  • Loading branch information
JellyBrick committed Oct 11, 2023
1 parent 572a023 commit 1d5b299
Showing 1 changed file with 72 additions and 41 deletions.
113 changes: 72 additions & 41 deletions plugins/downloader/back.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 };

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -118,6 +117,7 @@ export async function downloadSong(
let resolvedName;
try {
await downloadSongUnsafe(
false,
url,
(name: string) => resolvedName = name,
playlistFolder,
Expand All @@ -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,
Expand All @@ -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);

Expand Down Expand Up @@ -417,34 +445,37 @@ 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)}`),
);
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);
Expand All @@ -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) {
Expand Down

0 comments on commit 1d5b299

Please sign in to comment.