@@ -3,23 +3,14 @@ import { join } from 'node:path';
3
3
import { randomBytes } from 'node:crypto' ;
4
4
5
5
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' ;
7
7
import is from 'electron-is' ;
8
- import ytpl from 'ytpl' ;
9
- // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
10
8
import filenamify from 'filenamify' ;
11
9
import { Mutex } from 'async-mutex' ;
12
10
import { createFFmpeg } from '@ffmpeg.wasm/main' ;
13
11
14
12
import NodeID3 , { TagConstants } from 'node-id3' ;
15
13
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
-
23
14
import { cropMaxWidth , getFolder , presets , sendFeedback as sendFeedback_ , setBadge } from './utils' ;
24
15
25
16
import config from './config' ;
@@ -32,8 +23,13 @@ import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
32
23
import { injectCSS } from '../utils' ;
33
24
import { cache } from '../../providers/decorators' ;
34
25
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' ;
36
31
32
+ import type { GetPlayerResponse } from '../../types/get-player-response' ;
37
33
38
34
type CustomSongInfo = SongInfo & { trackId ?: string } ;
39
35
@@ -69,16 +65,19 @@ const sendError = (error: Error, source?: string) => {
69
65
} ) ;
70
66
} ;
71
67
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
+
72
74
export default async ( win_ : BrowserWindow ) => {
73
75
win = win_ ;
74
76
injectCSS ( win . webContents , style ) ;
75
77
76
- const cookie = ( await win . webContents . session . cookies . get ( { url : 'https://music.youtube.com' } ) ) . map ( ( it ) =>
77
- it . name + '=' + it . value + ';'
78
- ) . join ( '' ) ;
79
78
yt = await Innertube . create ( {
80
79
cache : new UniversalCache ( false ) ,
81
- cookie,
80
+ cookie : await getCookieFromWindow ( win ) ,
82
81
generate_session_locally : true ,
83
82
fetch : async ( input : RequestInfo | URL , init ?: RequestInit ) => {
84
83
const url =
@@ -118,6 +117,7 @@ export async function downloadSong(
118
117
let resolvedName ;
119
118
try {
120
119
await downloadSongUnsafe (
120
+ false ,
121
121
url ,
122
122
( name : string ) => resolvedName = name ,
123
123
playlistFolder ,
@@ -129,8 +129,31 @@ export async function downloadSong(
129
129
}
130
130
}
131
131
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
+
132
154
async function downloadSongUnsafe (
133
- url : string ,
155
+ isId : boolean ,
156
+ idOrUrl : string ,
134
157
setName : ( name : string ) => void ,
135
158
playlistFolder : string | undefined = undefined ,
136
159
trackId : string | undefined = undefined ,
@@ -147,8 +170,13 @@ async function downloadSongUnsafe(
147
170
148
171
sendFeedback ( 'Downloading...' , 2 ) ;
149
172
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
+ }
152
180
153
181
let info : TrackInfo | VideoInfo = await yt . music . getInfo ( id ) ;
154
182
@@ -417,34 +445,37 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
417
445
418
446
console . log ( `trying to get playlist ID: '${ playlistId } '` ) ;
419
447
sendFeedback ( 'Getting playlist info…' ) ;
420
- let playlist : ytpl . Result ;
448
+ let playlist : Playlist ;
421
449
try {
422
- playlist = await ytpl ( playlistId , {
423
- limit : config . get ( 'playlistMaxItems' ) || Number . POSITIVE_INFINITY ,
424
- } ) ;
450
+ playlist = await yt . music . getPlaylist ( playlistId ) ;
425
451
} catch ( error : unknown ) {
426
452
sendError (
427
453
Error ( `Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${ String ( error ) } ` ) ,
428
454
) ;
429
455
return ;
430
456
}
431
457
432
- if ( playlist . items . length === 0 ) {
458
+ if ( ! playlist || ! playlist . items || playlist . items . length === 0 ) {
433
459
sendError ( new Error ( 'Playlist is empty' ) ) ;
434
460
}
435
461
436
- if ( playlist . items . length === 1 ) {
462
+ const items = playlist . items ! . as ( YTNodes . MusicResponsiveListItem ) ;
463
+ if ( items . length === 1 ) {
437
464
sendFeedback ( 'Playlist has only one item, downloading it directly' ) ;
438
- await downloadSong ( playlist . items [ 0 ] . url ) ;
465
+ await downloadSongFromId ( items . at ( 0 ) ! . id ! ) ;
439
466
return ;
440
467
}
441
468
442
- const isAlbum = playlist . title . startsWith ( 'Album - ' ) ;
469
+ let playlistTitle = playlist . header ?. title ?. text ?? '' ;
470
+ const isAlbum = playlistTitle ?. startsWith ( 'Album - ' ) ;
443
471
if ( isAlbum ) {
444
- playlist . title = playlist . title . slice ( 8 ) ;
472
+ playlistTitle = playlistTitle . slice ( 8 ) ;
445
473
}
446
474
447
- const safePlaylistTitle = filenamify ( playlist . title , { replacement : ' ' } ) ;
475
+ let safePlaylistTitle = filenamify ( playlistTitle , { replacement : ' ' } ) ;
476
+ if ( ! is . macOS ( ) ) {
477
+ safePlaylistTitle = safePlaylistTitle . normalize ( 'NFC' ) ;
478
+ }
448
479
449
480
const folder = getFolder ( config . get ( 'downloadFolder' ) ?? '' ) ;
450
481
const playlistFolder = join ( folder , safePlaylistTitle ) ;
@@ -461,47 +492,47 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
461
492
type : 'info' ,
462
493
buttons : [ 'OK' ] ,
463
494
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)` ,
466
497
} ) ;
467
498
468
499
if ( is . dev ( ) ) {
469
500
console . log (
470
- `Downloading playlist "${ playlist . title } " - ${ playlist . items . length } songs (${ playlistId } )` ,
501
+ `Downloading playlist "${ playlistTitle } " - ${ items . length } songs (${ playlistId } )` ,
471
502
) ;
472
503
}
473
504
474
505
win . setProgressBar ( 2 ) ; // Starts with indefinite bar
475
506
476
- setBadge ( playlist . items . length ) ;
507
+ setBadge ( items . length ) ;
477
508
478
509
let counter = 1 ;
479
510
480
- const progressStep = 1 / playlist . items . length ;
511
+ const progressStep = 1 / items . length ;
481
512
482
513
const increaseProgress = ( itemPercentage : number ) => {
483
- const currentProgress = ( counter - 1 ) / ( playlist . items . length ?? 1 ) ;
514
+ const currentProgress = ( counter - 1 ) / ( items . length ?? 1 ) ;
484
515
const newProgress = currentProgress + ( progressStep * itemPercentage ) ;
485
516
win . setProgressBar ( newProgress ) ;
486
517
} ;
487
518
488
519
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 } ...` ) ;
491
522
const trackId = isAlbum ? counter : undefined ;
492
- await downloadSong (
493
- song . url ,
523
+ await downloadSongFromId (
524
+ song . id ! ,
494
525
playlistFolder ,
495
526
trackId ?. toString ( ) ,
496
527
increaseProgress ,
497
528
) . catch ( ( error ) =>
498
529
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 } ` )
500
531
) ,
501
532
) ;
502
533
503
- win . setProgressBar ( counter / playlist . items . length ) ;
504
- setBadge ( playlist . items . length - counter ) ;
534
+ win . setProgressBar ( counter / items . length ) ;
535
+ setBadge ( items . length - counter ) ;
505
536
counter ++ ;
506
537
}
507
538
} catch ( error : unknown ) {
0 commit comments