Skip to content

Commit 9b21566

Browse files
authored
Merge pull request #671 from TheSharks/feat/invidious
Change YT service from Lavalink to Invidious
2 parents 58194e2 + e01f8a4 commit 9b21566

File tree

3 files changed

+125
-32
lines changed

3 files changed

+125
-32
lines changed

src/classes/VoiceConnection.js

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ module.exports = class VoiceConnection {
77
this.fresh = true
88
this.textChannel = opts.textChannel
99

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+
})
1116
this._encoder.on('trackError', x => {
1217
this.textChannel.createMessage(`The track I'm trying to play broke! \`${x.error}\``)
13-
this.next.bind(this)
18+
this.next()
1419
})
1520
this._encoder.on('trackStuck', x => {
21+
logger.debug(x)
1622
this.textChannel.createMessage('Seems the track got stuck, automatically skipping it...')
17-
this.next.bind(this)
23+
this.next()
1824
})
1925
this._encoder.on('trackStart', ctx => {
2026
const index = this.playlist.findIndex(x => x.track === ctx.track)
@@ -27,13 +33,17 @@ module.exports = class VoiceConnection {
2733
url: this.nowPlaying.info.uri,
2834
title: this.nowPlaying.info.title || '[Unknown!]',
2935
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 } } : {})
3241
}
3342
})
3443
})
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()
3747
})
3848
}
3949

@@ -61,13 +71,22 @@ module.exports = class VoiceConnection {
6171
return this._encoder.disconnect()
6272
}
6373

64-
resolve (ctx) {
74+
async resolve (ctx) {
75+
const SA = require('superagent')
76+
ctx = ctx.trim()
6577
try {
66-
// eslint-disable-next-line no-new
67-
new URL(ctx)
78+
const url = new URL(ctx)
79+
if (process.env.INVIDIOUS_HOST && /(?:https?:\/\/)?(?:www\.)?youtu(.be|be\.com)/.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+
}
6884
return this._encoder.node.loadTracks(ctx)
6985
} 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}`)
7190
}
7291
}
7392

@@ -92,4 +111,55 @@ module.exports = class VoiceConnection {
92111
addDJs (ctx) {
93112
return this.controllers.push(ctx)
94113
}
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+
}
95165
}

src/commands/music/play.js

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,50 @@ module.exports = new Command(async function (msg, suffix) {
55
const player = client.voiceConnectionManager.get(msg.channel.guild.id)
66
if (player) {
77
const m = await this.safeSendMessage(msg.channel, 'Working on it...')
8-
const x = await player.resolve(suffix)
9-
switch (x.loadType) {
10-
case 'PLAYLIST_LOADED': {
11-
if (x.tracks && x.tracks.length > 0) {
12-
player.addMany(x.tracks)
13-
return m.edit(`Playlist ${x.playlistInfo.name} has been added`)
14-
} else return m.edit('Nothing found with your search query')
8+
try {
9+
const x = await player.resolve(suffix)
10+
switch (x.loadType) {
11+
case 'PLAYLIST_LOADED': {
12+
if (x.tracks && x.tracks.length > 0) {
13+
player.addMany(x.tracks)
14+
return m.edit(`Playlist ${x.playlistInfo.name} has been added`)
15+
} else return m.edit('Nothing found with your search query')
16+
}
17+
case 'IV_PLAYLIST_LOADED': {
18+
return m.edit({
19+
content: 'Your playlist has finished loading',
20+
embed: {
21+
url: x.uri,
22+
title: x.title,
23+
author: {
24+
name: x.author,
25+
icon_url: x.authorImage,
26+
url: x.authorURL
27+
},
28+
thumbnail: {
29+
url: x.image
30+
}
31+
}
32+
})
33+
}
34+
case 'SEARCH_RESULT':
35+
case 'TRACK_LOADED': {
36+
if (x.tracks && x.tracks.length > 0) {
37+
player.add(x.tracks[0])
38+
return m.edit('Your track has been added')
39+
} else return m.edit('Nothing found with your search query')
40+
}
41+
case 'LOAD_FAILED': {
42+
if (x.exception.severity === 'COMMON') return m.edit(`I'm unable to play that track: \`${x.exception.message}\``)
43+
else return m.edit("I'm unable to play that track for unknown reasons")
44+
}
45+
case 'NO_MATCHES': return m.edit('Nothing found with your search query')
46+
default: return m.edit('Something went wrong while adding this track, try again later')
1547
}
16-
case 'SEARCH_RESULT':
17-
case 'TRACK_LOADED': {
18-
if (x.tracks && x.tracks.length > 0) {
19-
player.add(x.tracks[0])
20-
return m.edit('Your track has been added')
21-
} else return m.edit('Nothing found with your search query')
22-
}
23-
case 'LOAD_FAILED': {
24-
if (x.exception.severity === 'COMMON') return m.edit(`I'm unable to play that track: \`${x.exception.message}\``)
25-
else return m.edit("I'm unable to play that track for unknown reasons")
26-
}
27-
case 'NO_MATCHES': return m.edit('Nothing found with your search query')
28-
default: return m.edit('Something went wrong while adding this track, try again later')
48+
} catch (e) {
49+
logger.error('CMD', e)
50+
if (!(m instanceof require('eris').Message)) await this.safeSendMessage(msg.channel, 'Something went wrong, try again?')
51+
else await m.edit('Something went wrong, try again?')
2952
}
3053
} else return this.safeSendMessage(msg.channel, "I'm not streaming in this server")
3154
}, {

src/commands/music/queue.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const createEmbed = (ctx) => {
2323
fields: ctx.slice(0, 10).map(x => {
2424
return {
2525
name: ctx.indexOf(x) + 1,
26-
value: `${x.info.title} - ${x.info.author}`
26+
value: `[${x.info.title} - ${x.info.author}](${x.info.uri})`
2727
}
2828
})
2929
}

0 commit comments

Comments
 (0)