mirror of
https://gitlab.com/RemixDev/deemix-js.git
synced 2024-12-29 10:56:18 +00:00
456 lines
14 KiB
JavaScript
456 lines
14 KiB
JavaScript
const Plugin = require('./index.js')
|
|
const { getConfigFolder } = require('../utils/localpaths.js')
|
|
const {
|
|
generateTrackItem,
|
|
generateAlbumItem,
|
|
TrackNotOnDeezer,
|
|
AlbumNotOnDeezer,
|
|
InvalidID
|
|
} = require('../itemgen.js')
|
|
const { Convertable, Collection } = require('../types/DownloadObjects.js')
|
|
const { sep } = require('path')
|
|
const fs = require('fs')
|
|
const SpotifyWebApi = require('spotify-web-api-node')
|
|
const got = require('got')
|
|
const { queue } = require('async')
|
|
|
|
class Spotify extends Plugin {
|
|
constructor(configFolder = undefined){
|
|
super()
|
|
this.credentials = {clientId: "", clientSecret: ""}
|
|
this.settings = {
|
|
fallbackSearch: false
|
|
}
|
|
this.enabled = false
|
|
this.sp
|
|
this.configFolder = configFolder || getConfigFolder()
|
|
this.configFolder += `spotify${sep}`
|
|
return this
|
|
}
|
|
|
|
setup(){
|
|
fs.mkdirSync(this.configFolder, { recursive: true })
|
|
|
|
this.loadSettings()
|
|
return this
|
|
}
|
|
|
|
async parseLink(link){
|
|
if (link.includes('link.tospotify.com')){
|
|
link = await got.get(link, {https: {rejectUnauthorized: false}}) // Resolve URL shortner
|
|
link = link.url
|
|
}
|
|
// Remove extra stuff
|
|
if (link.includes('?')) link = link.slice(0, link.indexOf('?'))
|
|
if (link.includes('&')) link = link.slice(0, link.indexOf('&'))
|
|
if (link.endsWith('/')) link = link.slice(0, -1) // Remove last slash if present
|
|
|
|
let link_type, link_id
|
|
|
|
if (!link.includes('spotify')) return [link, link_type, link_id] // return if not a spotify link
|
|
|
|
if (link.search(/[/:]track[/:](.+)/g) != -1){
|
|
link_type = 'track'
|
|
link_id = /[/:]track[/:](.+)/g.exec(link)[1]
|
|
}else if (link.search(/[/:]album[/:](.+)/g) != -1){
|
|
link_type = 'album'
|
|
link_id = /[/:]album[/:](.+)/g.exec(link)[1]
|
|
}else if (link.search(/[/:]playlist[/:](\d+)/g) != -1){
|
|
link_type = 'playlist'
|
|
link_id = /[/:]playlist[/:](.+)/g.exec(link)[1]
|
|
}
|
|
|
|
return [link, link_type, link_id]
|
|
}
|
|
|
|
async generateDownloadObject(dz, link, bitrate){
|
|
let link_type, link_id
|
|
[link, link_type, link_id] = await this.parseLink(link)
|
|
|
|
if (link_type == null || link_id == null) return null
|
|
|
|
switch (link_type) {
|
|
case 'track':
|
|
return this.generateTrackItem(dz, link_id, bitrate)
|
|
case 'album':
|
|
return this.generateAlbumItem(dz, link_id, bitrate)
|
|
case 'playlist':
|
|
return this.generatePlaylistItem(dz, link_id, bitrate)
|
|
}
|
|
}
|
|
|
|
async generateTrackItem(dz, link_id, bitrate){
|
|
let cache = this.loadCache()
|
|
|
|
let cachedTrack
|
|
if (cache.tracks[link_id]){
|
|
cachedTrack = cache.tracks[link_id]
|
|
} else {
|
|
cachedTrack = await this.getTrack(link_id)
|
|
cache.tracks[link_id] = cachedTrack
|
|
this.saveCache(cache)
|
|
}
|
|
|
|
if (cachedTrack.isrc){
|
|
try { return generateTrackItem(dz, `isrc:${cachedTrack.isrc}`, bitrate) }
|
|
catch (e){ /* empty */ }
|
|
}
|
|
if (this.settings.fallbackSearch){
|
|
if (!cachedTrack.id || cachedTrack.id === "0"){
|
|
let trackID = await dz.api.get_track_id_from_metadata(
|
|
cachedTrack.data.artist,
|
|
cachedTrack.data.title,
|
|
cachedTrack.data.album
|
|
)
|
|
if (trackID !== "0"){
|
|
cachedTrack.id = trackID
|
|
cache.tracks[link_id] = cachedTrack
|
|
this.saveCache(cache)
|
|
}
|
|
}
|
|
if (cachedTrack.id !== "0") return generateTrackItem(dz, cachedTrack.id, bitrate)
|
|
}
|
|
throw new TrackNotOnDeezer(`https://open.spotify.com/track/${link_id}`)
|
|
}
|
|
|
|
async generateAlbumItem(dz, link_id, bitrate){
|
|
let cache = this.loadCache()
|
|
|
|
let cachedAlbum
|
|
if (cache.albums[link_id]){
|
|
cachedAlbum = cache.albums[link_id]
|
|
} else {
|
|
cachedAlbum = await this.getAlbum(link_id)
|
|
cache.albums[link_id] = cachedAlbum
|
|
this.saveCache(cache)
|
|
}
|
|
|
|
try {
|
|
return generateAlbumItem(dz, `upc:${cachedAlbum.upc}`, bitrate)
|
|
} catch (e){
|
|
throw new AlbumNotOnDeezer(`https://open.spotify.com/album/${link_id}`)
|
|
}
|
|
}
|
|
|
|
async generatePlaylistItem(dz, link_id, bitrate){
|
|
if (!this.enabled) throw new Error("Spotify plugin not enabled")
|
|
let spotifyPlaylist = await this.sp.getPlaylist(link_id)
|
|
spotifyPlaylist = spotifyPlaylist.body
|
|
|
|
let playlistAPI = this._convertPlaylistStructure(spotifyPlaylist)
|
|
playlistAPI.various_artist = await dz.api.get_artist(5080) // Useful for save as compilation
|
|
|
|
let tracklistTemp = spotifyPlaylist.tracks.items
|
|
while (spotifyPlaylist.tracks.next) {
|
|
let regExec = /offset=(\d+)&limit=(\d+)/g.exec(spotifyPlaylist.tracks.next)
|
|
let offset = regExec[1]
|
|
let limit = regExec[2]
|
|
let playlistTracks = await this.sp.getPlaylistTracks(link_id, { offset, limit })
|
|
spotifyPlaylist.tracks = playlistTracks.body
|
|
tracklistTemp = tracklistTemp.concat(spotifyPlaylist.tracks.items)
|
|
}
|
|
|
|
let tracklist = []
|
|
tracklistTemp.forEach((item) => {
|
|
if (!item.track) return // Skip everything that isn't a track
|
|
if (item.track.explicit && !playlistAPI.explicit) playlistAPI.explicit = true
|
|
tracklist.push(item.track)
|
|
})
|
|
if (!playlistAPI.explicit) playlistAPI.explicit = false
|
|
|
|
return new Convertable({
|
|
type: 'spotify_playlist',
|
|
id: link_id,
|
|
bitrate,
|
|
title: spotifyPlaylist.name,
|
|
artist: spotifyPlaylist.owner.display_name,
|
|
cover: playlistAPI.picture_thumbnail,
|
|
explicit: playlistAPI.explicit,
|
|
size: tracklist.length,
|
|
collection: {
|
|
tracks: [],
|
|
playlistAPI: playlistAPI
|
|
},
|
|
plugin: 'spotify',
|
|
conversion_data: tracklist
|
|
})
|
|
}
|
|
|
|
async getTrack(track_id, spotifyTrack=null){
|
|
if (!this.enabled) throw new Error("Spotify plugin not enabled")
|
|
let cachedTrack = {
|
|
isrc: null,
|
|
data: null
|
|
}
|
|
|
|
if (!spotifyTrack){
|
|
try{
|
|
spotifyTrack = await this.sp.getTrack(track_id)
|
|
} catch (e){
|
|
if (e.body.error.message === "invalid id") throw new InvalidID(`https://open.spotify.com/track/${track_id}`)
|
|
throw e
|
|
}
|
|
spotifyTrack = spotifyTrack.body
|
|
}
|
|
if (spotifyTrack.external_ids && spotifyTrack.external_ids.isrc) cachedTrack.isrc = spotifyTrack.external_ids.isrc
|
|
cachedTrack.data = {
|
|
title: spotifyTrack.name,
|
|
artist: spotifyTrack.artists[0].name,
|
|
album: spotifyTrack.album.name
|
|
}
|
|
return cachedTrack
|
|
}
|
|
|
|
async getAlbum(album_id, spotifyAlbum=null){
|
|
if (!this.enabled) throw new Error("Spotify plugin not enabled")
|
|
let cachedAlbum = {
|
|
upc: null,
|
|
data: null
|
|
}
|
|
|
|
if (!spotifyAlbum){
|
|
try{
|
|
spotifyAlbum = await this.sp.getAlbum(album_id)
|
|
} catch (e){
|
|
if (e.body.error.message === "invalid id") throw new InvalidID(`https://open.spotify.com/album/${album_id}`)
|
|
throw e
|
|
}
|
|
spotifyAlbum = spotifyAlbum.body
|
|
}
|
|
if (spotifyAlbum.external_ids && spotifyAlbum.external_ids.upc) cachedAlbum.upc = spotifyAlbum.external_ids.upc
|
|
cachedAlbum.data = {
|
|
title: spotifyAlbum.name,
|
|
artist: spotifyAlbum.artists[0].name
|
|
}
|
|
return cachedAlbum
|
|
}
|
|
|
|
async convert(dz, downloadObject, settings, listener = null){
|
|
let cache = this.loadCache()
|
|
|
|
let conversion = 0
|
|
let conversionNext = 0
|
|
|
|
let collection = []
|
|
if (listener) listener.send("startConversion", downloadObject.uuid)
|
|
let q = queue(async (data) => {
|
|
let {track, pos} = data
|
|
if (downloadObject.isCanceled) return
|
|
|
|
let cachedTrack, trackAPI
|
|
if (cache.tracks[track.id]){
|
|
cachedTrack = cache.tracks[track.id]
|
|
} else {
|
|
cachedTrack = await this.getTrack(track.id, track)
|
|
cache.tracks[track.id] = cachedTrack
|
|
this.saveCache(cache)
|
|
}
|
|
|
|
if (cachedTrack.isrc){
|
|
try {
|
|
trackAPI = await dz.api.get_track_by_ISRC(cachedTrack.isrc)
|
|
if (!trackAPI.id || !trackAPI.title) trackAPI = null
|
|
} catch { /* Empty */ }
|
|
}
|
|
if (this.settings.fallbackSearch && !trackAPI){
|
|
if (!cachedTrack.id || cachedTrack.id === "0"){
|
|
let trackID = await dz.api.get_track_id_from_metadata(
|
|
cachedTrack.data.artist,
|
|
cachedTrack.data.title,
|
|
cachedTrack.data.album
|
|
)
|
|
if (trackID !== "0"){
|
|
cachedTrack.id = trackID
|
|
cache.tracks[track.id] = cachedTrack
|
|
this.saveCache(cache)
|
|
}
|
|
}
|
|
if (cachedTrack.id !== "0") trackAPI = await dz.api.get_track(cachedTrack.id)
|
|
}
|
|
if (!trackAPI){
|
|
trackAPI = {
|
|
id: "0",
|
|
title: track.name,
|
|
duration: 0,
|
|
md5_origin: 0,
|
|
media_version: 0,
|
|
filesizes: {},
|
|
album: {
|
|
title: track.album.name,
|
|
md5_image: ""
|
|
},
|
|
artist: {
|
|
id: 0,
|
|
name: track.artists[0].name,
|
|
md5_image: ""
|
|
}
|
|
}
|
|
}
|
|
trackAPI.position = pos+1
|
|
collection[pos] = trackAPI
|
|
|
|
conversionNext += (1 / downloadObject.size) * 100
|
|
if (Math.round(conversionNext) != conversion && Math.round(conversionNext) % 2 == 0){
|
|
conversion = Math.round(conversionNext)
|
|
if (listener) listener.send('updateQueue', {uuid: downloadObject.uuid, conversion})
|
|
}
|
|
}, settings.queueConcurrency)
|
|
|
|
downloadObject.conversion_data.forEach((track, pos) => {
|
|
q.push({track, pos: pos})
|
|
});
|
|
|
|
await q.drain()
|
|
|
|
downloadObject.collection.tracks = collection
|
|
downloadObject.size = collection.length
|
|
downloadObject = new Collection(downloadObject.toDict())
|
|
if (listener) listener.send("finishConversion", downloadObject.getSlimmedDict())
|
|
|
|
fs.writeFileSync(this.configFolder+'cache.json', JSON.stringify(cache))
|
|
return downloadObject
|
|
}
|
|
|
|
_convertPlaylistStructure(spotifyPlaylist){
|
|
let cover = null
|
|
if (spotifyPlaylist.images.length) cover = spotifyPlaylist.images[0].url
|
|
|
|
let deezerPlaylist = {
|
|
checksum: spotifyPlaylist.snapshot_id,
|
|
collaborative: spotifyPlaylist.collaborative,
|
|
creation_date: "XXXX-00-00",
|
|
creator: {
|
|
id: spotifyPlaylist.owner.id,
|
|
name: spotifyPlaylist.owner.display_name,
|
|
tracklist: spotifyPlaylist.owner.href,
|
|
type: "user"
|
|
},
|
|
description: spotifyPlaylist.description,
|
|
duration: 0,
|
|
fans: spotifyPlaylist.followers ? spotifyPlaylist.followers.total : 0,
|
|
id: spotifyPlaylist.id,
|
|
is_loved_track: false,
|
|
link: spotifyPlaylist.external_urls.spotify,
|
|
nb_tracks: spotifyPlaylist.tracks.total,
|
|
picture: cover,
|
|
picture_small: cover || "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/56x56-000000-80-0-0.jpg",
|
|
picture_medium: cover || "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/250x250-000000-80-0-0.jpg",
|
|
picture_big: cover || "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/500x500-000000-80-0-0.jpg",
|
|
picture_xl: cover || "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg",
|
|
picture_thumbnail: cover || "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg",
|
|
public: spotifyPlaylist.public,
|
|
share: spotifyPlaylist.external_urls.spotify,
|
|
title: spotifyPlaylist.name,
|
|
tracklist: spotifyPlaylist.tracks.href,
|
|
type: "playlist"
|
|
}
|
|
|
|
return deezerPlaylist
|
|
}
|
|
|
|
loadSettings(){
|
|
if (!fs.existsSync(this.configFolder+'config.json'))
|
|
fs.writeFileSync(this.configFolder+'config.json', JSON.stringify({
|
|
...this.credentials,
|
|
...this.settings
|
|
}, null, 2))
|
|
let settings
|
|
try {
|
|
settings = JSON.parse(fs.readFileSync(this.configFolder+'config.json'))
|
|
} catch (e){
|
|
if (e.name === "SyntaxError"){
|
|
fs.writeFileSync(this.configFolder+'config.json', JSON.stringify({
|
|
...this.credentials,
|
|
...this.settings
|
|
}, null, 2))
|
|
}
|
|
settings = JSON.parse(JSON.stringify({
|
|
...this.credentials,
|
|
...this.settings
|
|
}))
|
|
}
|
|
this.setSettings(settings)
|
|
this.checkCredentials()
|
|
}
|
|
|
|
saveSettings(newSettings){
|
|
if (newSettings) this.setSettings(newSettings)
|
|
this.checkCredentials()
|
|
fs.writeFileSync(this.configFolder+'config.json', JSON.stringify({
|
|
...this.credentials,
|
|
...this.settings
|
|
}, null, 2))
|
|
}
|
|
|
|
getSettings(){
|
|
return {
|
|
...this.credentials,
|
|
...this.settings
|
|
}
|
|
}
|
|
|
|
setSettings(newSettings){
|
|
this.credentials = { clientId: newSettings.clientId, clientSecret: newSettings.clientSecret }
|
|
let settings = {...newSettings}
|
|
delete settings.clientId
|
|
delete settings.clientSecret
|
|
this.settings = settings
|
|
}
|
|
|
|
loadCache(){
|
|
let cache
|
|
try {
|
|
cache = JSON.parse(fs.readFileSync(this.configFolder+'cache.json'))
|
|
} catch (e){
|
|
if (e.name === "SyntaxError"){
|
|
fs.writeFileSync(this.configFolder+'cache.json', JSON.stringify(
|
|
{tracks: {}, albums: {}},
|
|
null, 2
|
|
))
|
|
}
|
|
cache = {tracks: {}, albums: {}}
|
|
}
|
|
return cache
|
|
}
|
|
|
|
saveCache(newCache){
|
|
fs.writeFileSync(this.configFolder+'cache.json', JSON.stringify(newCache))
|
|
}
|
|
|
|
checkCredentials(){
|
|
if (this.credentials.clientId === "" || this.credentials.clientSecret === ""){
|
|
this.enabled = false
|
|
return
|
|
}
|
|
this.sp = new SpotifyWebApi(this.credentials)
|
|
this.sp.clientCredentialsGrant().then(
|
|
(creds)=>{
|
|
this.sp.setAccessToken(creds.body.access_token)
|
|
// Need to get a new access_token when it expires
|
|
setTimeout(()=>{
|
|
this.checkCredentials()
|
|
}, creds.body.expires_in*1000-10)
|
|
this.enabled = true
|
|
},
|
|
()=>{
|
|
this.enabled = false
|
|
this.sp = undefined
|
|
}
|
|
)
|
|
}
|
|
|
|
getCredentials(){
|
|
return this.credentials
|
|
}
|
|
|
|
setCredentials(clientId, clientSecret){
|
|
clientId = clientId.trim()
|
|
clientSecret = clientSecret.trim()
|
|
|
|
this.credentials = {clientId, clientSecret}
|
|
this.saveSettings()
|
|
}
|
|
}
|
|
|
|
module.exports = Spotify
|