@@ -3,23 +3,14 @@ import { join } from 'node:path';
33import { randomBytes } from 'node:crypto' ;
44
55import { 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' ;
77import is from 'electron-is' ;
8- import ytpl from 'ytpl' ;
9- // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
108import filenamify from 'filenamify' ;
119import { Mutex } from 'async-mutex' ;
1210import { createFFmpeg } from '@ffmpeg.wasm/main' ;
1311
1412import 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-
2314import { cropMaxWidth , getFolder , presets , sendFeedback as sendFeedback_ , setBadge } from './utils' ;
2415
2516import config from './config' ;
@@ -32,8 +23,13 @@ import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
3223import { injectCSS } from '../utils' ;
3324import { 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
3834type 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+
7274export 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+
132154async 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