@@ -7,14 +7,20 @@ module.exports = class VoiceConnection {
7
7
this . fresh = true
8
8
this . textChannel = opts . textChannel
9
9
10
- this . _encoder . on ( 'trackEnd' , this . next . bind ( this ) )
10
+ this . _encoder . on ( 'trackEnd' , ( ) => {
11
+ if ( this . playlist . length === 0 ) {
12
+ this . textChannel . createMessage ( 'The queue is empty, disconnecting' )
13
+ this . destroy ( )
14
+ } else this . next ( )
15
+ } )
11
16
this . _encoder . on ( 'trackError' , x => {
12
17
this . textChannel . createMessage ( `The track I'm trying to play broke! \`${ x . error } \`` )
13
- this . next . bind ( this )
18
+ this . next ( )
14
19
} )
15
20
this . _encoder . on ( 'trackStuck' , x => {
21
+ logger . debug ( x )
16
22
this . textChannel . createMessage ( 'Seems the track got stuck, automatically skipping it...' )
17
- this . next . bind ( this )
23
+ this . next ( )
18
24
} )
19
25
this . _encoder . on ( 'trackStart' , ctx => {
20
26
const index = this . playlist . findIndex ( x => x . track === ctx . track )
@@ -27,13 +33,17 @@ module.exports = class VoiceConnection {
27
33
url : this . nowPlaying . info . uri ,
28
34
title : this . nowPlaying . info . title || '[Unknown!]' ,
29
35
author : {
30
- name : this . nowPlaying . info . author || '[Unknown!]'
31
- }
36
+ name : this . nowPlaying . info . author || '[Unknown!]' ,
37
+ ...( this . nowPlaying . info . authorImage ? { icon_url : this . nowPlaying . info . authorImage } : { } ) ,
38
+ ...( this . nowPlaying . info . authorURL ? { url : this . nowPlaying . info . authorURL } : { } )
39
+ } ,
40
+ ...( this . nowPlaying . info . image ? { thumbnail : { url : this . nowPlaying . info . image } } : { } )
32
41
}
33
42
} )
34
43
} )
35
- this . _encoder . once ( 'disconnected' , ( ) => {
36
- this . textChannel . createMessage ( 'I got disconnected from the voice channel, ending playback' )
44
+ this . _encoder . once ( 'disconnected' , x => {
45
+ if ( x . byRemote && x . code !== 4014 ) this . textChannel . createMessage ( 'I got disconnected from the voice channel, ending playback' )
46
+ this . destroy ( )
37
47
} )
38
48
}
39
49
@@ -61,13 +71,22 @@ module.exports = class VoiceConnection {
61
71
return this . _encoder . disconnect ( )
62
72
}
63
73
64
- resolve ( ctx ) {
74
+ async resolve ( ctx ) {
75
+ const SA = require ( 'superagent' )
76
+ ctx = ctx . trim ( )
65
77
try {
66
- // eslint-disable-next-line no-new
67
- new URL ( ctx )
78
+ const url = new URL ( ctx )
79
+ if ( process . env . INVIDIOUS_HOST && / (?: h t t p s ? : \/ \/ ) ? (?: w w w \. ) ? y o u t u ( .b e | b e \. c o m ) / . test ( url . hostname ) ) {
80
+ if ( url . hostname === 'youtu.be' ) return this . _invidiousResolve ( url . pathname . slice ( 1 ) )
81
+ if ( url . searchParams . has ( 'list' ) ) return this . _invidiousPlaylist ( url . searchParams . get ( 'list' ) )
82
+ if ( url . searchParams . has ( 'v' ) ) return this . _invidiousResolve ( url . searchParams . get ( 'v' ) )
83
+ }
68
84
return this . _encoder . node . loadTracks ( ctx )
69
85
} catch ( _ ) {
70
- return this . _encoder . node . loadTracks ( `scsearch:${ ctx } ` )
86
+ if ( process . env . INVIDIOUS_HOST ) {
87
+ const resp = await SA . get ( `${ process . env . INVIDIOUS_HOST } /api/v1/search?q=${ encodeURIComponent ( ctx ) } ` )
88
+ return this . _invidiousResolve ( resp . body [ 0 ] . videoId )
89
+ } else return this . _encoder . node . loadTracks ( `scsearch:${ ctx } ` )
71
90
}
72
91
}
73
92
@@ -92,4 +111,55 @@ module.exports = class VoiceConnection {
92
111
addDJs ( ctx ) {
93
112
return this . controllers . push ( ctx )
94
113
}
114
+
115
+ async _invidiousResolve ( videoId ) {
116
+ const authorImg = ( data ) => {
117
+ if ( ! data . authorThumbnails [ 0 ] . url || data . authorThumbnails [ 0 ] . url . length < 1 ) return undefined // this can happen sometimes
118
+ else if ( ! data . authorThumbnails [ 0 ] . url . startsWith ( 'https:' ) ) return `https:${ data . authorThumbnails [ 0 ] . url } `
119
+ return data . authorThumbnails [ 0 ] . url
120
+ }
121
+ const SA = require ( 'superagent' )
122
+ const info = await SA . get ( `${ process . env . INVIDIOUS_HOST } /api/v1/videos/${ videoId } ` )
123
+ let lavaresp
124
+ if ( info . body . liveNow ) {
125
+ // lavaresp = await this._encoder.node.loadTracks(`${info.body.hlsUrl}?local=true`)
126
+ return { loadType : 'LOAD_FAILED' , exception : { severity : 'COMMON' , message : 'Unable to play livestreams at the moment' } }
127
+ } else {
128
+ const itags = [ '251' , '250' , '249' , '171' , '141' , '140' , '139' ]
129
+ const tag = info . body . adaptiveFormats . find ( x => itags . includes ( x . itag ) )
130
+ lavaresp = await this . _encoder . node . loadTracks ( `${ process . env . INVIDIOUS_HOST } /latest_version?id=${ videoId } &itag=${ tag . itag } ${ process . env . INVIDIOUS_PROXY ? '&local=true' : '' } ` )
131
+ }
132
+ if ( lavaresp . loadType !== 'TRACK_LOADED' ) return lavaresp
133
+ lavaresp . tracks [ 0 ] . info = {
134
+ ...lavaresp . tracks [ 0 ] . info ,
135
+ author : info . body . author ,
136
+ title : info . body . title ,
137
+ uri : `https://youtu.be/${ videoId } ` ,
138
+ image : `https://i.ytimg.com/vi/${ videoId } /hqdefault.jpg` ,
139
+ authorImage : authorImg ( info . body ) ,
140
+ authorURL : `https://youtube.com${ info . body . authorUrl } `
141
+ }
142
+ return lavaresp
143
+ }
144
+
145
+ async _invidiousPlaylist ( id ) {
146
+ const authorImg = ( data ) => {
147
+ if ( ! data . authorThumbnails [ 0 ] . url || data . authorThumbnails [ 0 ] . url . length < 1 ) return undefined // this can happen sometimes
148
+ else if ( ! data . authorThumbnails [ 0 ] . url . startsWith ( 'https:' ) ) return `https:${ data . authorThumbnails [ 0 ] . url } `
149
+ return data . authorThumbnails [ 0 ] . url
150
+ }
151
+ const SA = require ( 'superagent' )
152
+ const resp = await SA . get ( `${ process . env . INVIDIOUS_HOST } /api/v1/playlists/${ id } ` )
153
+ const result = await Promise . allSettled ( resp . body . videos . map ( x => this . _invidiousResolve ( x . videoId ) ) )
154
+ this . addMany ( result . filter ( x => x . status === 'fulfilled' ) . map ( x => x . value . tracks ) . flat ( 1 ) )
155
+ return {
156
+ loadType : 'IV_PLAYLIST_LOADED' ,
157
+ uri : `https://youtube.com/playlist?list=${ resp . body . playlistId } ` ,
158
+ title : resp . body . title ,
159
+ author : resp . body . author ,
160
+ authorImage : authorImg ( resp . body ) ,
161
+ authorURL : `https://youtube.com${ resp . body . authorUrl } ` ,
162
+ image : resp . body . playlistThumbnail
163
+ }
164
+ }
95
165
}
0 commit comments