From 1ff01cc86846bbe79cabd566090c68b515415132 Mon Sep 17 00:00:00 2001 From: Lachie Date: Tue, 3 Sep 2024 06:57:00 +1000 Subject: [PATCH] feat: skip already downloaded songs extremely quickly --- deemix/src/downloader.ts | 473 ++++++------------------ deemix/src/index.ts | 4 +- deemix/src/types/Album.ts | 5 +- deemix/src/types/Artist.ts | 6 +- deemix/src/types/DownloadObjects.ts | 2 +- deemix/src/types/Track.ts | 54 +-- deemix/src/types/index.ts | 2 +- deemix/src/utils/downloadImage.ts | 95 +++++ deemix/src/utils/getPreferredBitrate.ts | 198 ++++++++++ deemix/src/utils/index.ts | 2 + deemix/src/utils/pathtemplates.ts | 204 ++++++---- deezer-js/src/api.ts | 68 +++- deezer-js/src/gw.ts | 37 +- deezer-js/src/utils.ts | 167 +++++---- server/src/app.ts | 4 +- 15 files changed, 750 insertions(+), 571 deletions(-) create mode 100644 deemix/src/utils/downloadImage.ts create mode 100644 deemix/src/utils/getPreferredBitrate.ts diff --git a/deemix/src/downloader.ts b/deemix/src/downloader.ts index daab508a..4a662537 100644 --- a/deemix/src/downloader.ts +++ b/deemix/src/downloader.ts @@ -1,38 +1,29 @@ import { each, queue } from "async"; import { exec } from "child_process"; -import { Deezer, TrackFormats, errors as _errors, utils } from "deezer-js"; -import { - createWriteStream, - existsSync, - mkdirSync, - readFileSync, - unlinkSync, - writeFileSync, -} from "fs"; -import { HTTPError, ReadError, TimeoutError, default as got } from "got"; +import { Deezer, TrackFormats, utils } from "deezer-js"; +import { APIAlbum, APITrack } from "deezer-js/src/api"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { HTTPError } from "got"; import { tmpdir } from "os"; -import { generateCryptedStreamURL, streamTrack } from "./decryption"; -import { - DownloadCanceled, - DownloadFailed, - ErrorMessages, - PreferredBitrateNotFound, - TrackNot360, -} from "./errors"; +import { streamTrack } from "./decryption"; +import { DownloadCanceled, DownloadFailed, ErrorMessages } from "./errors"; import { DEFAULTS, OverwriteOption, Settings } from "./settings"; import { IDownloadObject } from "./types/DownloadObjects"; import { StaticPicture } from "./types/Picture"; import Track, { formatsName } from "./types/Track"; -import { USER_AGENT_HEADER, pipeline, shellEscape } from "./utils"; +import { downloadImage, getPreferredBitrate, shellEscape } from "./utils"; import { checkShouldDownload, tagTrack } from "./utils/downloadUtils"; import { generateAlbumName, generateArtistName, generateDownloadObjectName, generatePath, + generatePathNew, } from "./utils/pathtemplates"; +import { Album, Playlist } from "./types"; + +const { performance } = require("perf_hooks"); -const { WrongLicense, WrongGeolocation } = _errors; const { map_track } = utils; const extensions = { @@ -46,285 +37,14 @@ const extensions = { [TrackFormats.MP4_RA1]: ".mp4", } as const; -const formats_non_360 = { - [TrackFormats.FLAC]: "FLAC", - [TrackFormats.MP3_320]: "MP3_320", - [TrackFormats.MP3_128]: "MP3_128", -}; -const formats_360 = { - [TrackFormats.MP4_RA3]: "MP4_RA3", - [TrackFormats.MP4_RA2]: "MP4_RA2", - [TrackFormats.MP4_RA1]: "MP4_RA1", -}; - const TEMPDIR = tmpdir() + "/deemix-imgs"; mkdirSync(TEMPDIR, { recursive: true }); -async function downloadImage( - url: string, - path: string, - overwrite = OverwriteOption.DONT_OVERWRITE -) { - if ( - existsSync(path) && - ![ - OverwriteOption.OVERWRITE, - OverwriteOption.ONLY_TAGS, - OverwriteOption.KEEP_BOTH, - ].includes(overwrite) - ) { - const file = readFileSync(path); - if (file.length !== 0) return path; - unlinkSync(path); - } - let timeout: NodeJS.Timeout | null = null; - let error = ""; - - const downloadStream = got - .stream(url, { - headers: { "User-Agent": USER_AGENT_HEADER }, - https: { rejectUnauthorized: false }, - }) - .on("data", function () { - clearTimeout(timeout); - timeout = setTimeout(() => { - error = "DownloadTimeout"; - downloadStream.destroy(); - }, 5000); - }); - const fileWriterStream = createWriteStream(path); - - timeout = setTimeout(() => { - error = "DownloadTimeout"; - downloadStream.destroy(); - }, 5000); - - try { - await pipeline(downloadStream, fileWriterStream); - } catch (e) { - unlinkSync(path); - if (e instanceof HTTPError) { - if (url.includes("images.dzcdn.net")) { - const urlBase = url.slice(0, url.lastIndexOf("/") + 1); - const pictureURL = url.slice(urlBase.length); - const pictureSize = parseInt( - pictureURL.slice(0, pictureURL.indexOf("x")) - ); - if (pictureSize > 1200) { - return downloadImage( - urlBase + - pictureURL.replace(`${pictureSize}x${pictureSize}`, "1200x1200"), - path, - overwrite - ); - } - } - return null; - } - if ( - e instanceof ReadError || - e instanceof TimeoutError || - [ - "ESOCKETTIMEDOUT", - "ERR_STREAM_PREMATURE_CLOSE", - "ETIMEDOUT", - "ECONNRESET", - ].includes(e.code) || - (downloadStream.destroyed && error === "DownloadTimeout") - ) { - return downloadImage(url, path, overwrite); - } - console.trace(e); - throw e; - } - return path; -} - -async function getPreferredBitrate( - dz: Deezer, - track: Track, - preferredBitrate: string, - shouldFallback: boolean, - feelingLucky: boolean, - uuid: string, - listener: any -) { - preferredBitrate = parseInt(preferredBitrate); - - let falledBack = false; - let hasAlternative = track.fallbackID !== "0"; - let isGeolocked = false; - let wrongLicense = false; - - async function testURL(track: Track, url: string, formatName: string) { - if (!url) return false; - let request; - try { - request = got - .get(url, { - headers: { "User-Agent": USER_AGENT_HEADER }, - https: { rejectUnauthorized: false }, - timeout: 7000, - }) - .on("response", (response) => { - track.filesizes[`${formatName.toLowerCase()}`] = - response.statusCode === 403 - ? 0 - : response.headers["content-length"]; - request.cancel(); - }); - - await request; - } catch (e) { - if (e.isCanceled) { - if (track.filesizes[`${formatName.toLowerCase()}`] === 0) return false; - return true; - } - if (e instanceof ReadError || e instanceof TimeoutError) { - return await testURL(track, url, formatName); - } - if (e instanceof HTTPError) return false; - console.trace(e); - throw e; - } - } - - async function getCorrectURL( - track: Track, - formatName: string, - formatNumber: number, - feelingLucky: boolean - ) { - // Check the track with the legit method - let url; - wrongLicense = - ((formatName === "FLAC" || formatName.startsWith("MP4_RA")) && - !dz.current_user.can_stream_lossless) || - (formatName === "MP3_320" && !dz.current_user.can_stream_hq); - if ( - track.filesizes[`${formatName.toLowerCase()}`] && - track.filesizes[`${formatName.toLowerCase()}`] !== "0" - ) { - try { - url = await dz.get_track_url(track.trackToken, formatName); - } catch (e) { - wrongLicense = e.name === "WrongLicense"; - isGeolocked = e.name === "WrongGeolocation"; - } - } - // Fallback to old method - if (!url && feelingLucky) { - url = generateCryptedStreamURL( - track.id, - track.MD5, - track.mediaVersion, - formatNumber - ); - if (await testURL(track, url, formatName)) return url; - url = undefined; - } - return url; - } - - if (track.local) { - const url = await getCorrectURL( - track, - "MP3_MISC", - TrackFormats.LOCAL, - feelingLucky - ); - track.urls.MP3_MISC = url; - return TrackFormats.LOCAL; - } - - const is360Format = Object.keys(formats_360).includes(preferredBitrate); - let formats: Record; - if (!shouldFallback) { - formats = { ...formats_360, ...formats_non_360 }; - } else if (is360Format) { - formats = { ...formats_360 }; - } else { - formats = { ...formats_non_360 }; - } - - // Check and renew trackToken before starting the check - await track.checkAndRenewTrackToken(dz); - for (let i = 0; i < Object.keys(formats).length; i++) { - // Check bitrates - const formatNumber = Object.keys(formats).reverse()[i]; - const formatName = formats[formatNumber]; - - // Current bitrate is higher than preferred bitrate; skip - if (formatNumber > preferredBitrate) { - continue; - } - - let currentTrack = track; - let url = await getCorrectURL( - currentTrack, - formatName, - formatNumber, - feelingLucky - ); - let newTrack; - do { - if (!url && hasAlternative) { - newTrack = await dz.gw.get_track_with_fallback(currentTrack.fallbackID); - newTrack = map_track(newTrack); - currentTrack = new Track(); - currentTrack.parseEssentialData(newTrack); - hasAlternative = currentTrack.fallbackID !== 0; - } - if (!url) - url = await getCorrectURL( - currentTrack, - formatName, - formatNumber, - feelingLucky - ); - } while (!url && hasAlternative); - - if (url) { - if (newTrack) track.parseEssentialData(newTrack); - track.urls[formatName] = url; - return formatNumber; - } - - if (!shouldFallback) { - if (wrongLicense) throw new WrongLicense(formatName); - if (isGeolocked) throw new WrongGeolocation(dz.current_user.country); - throw new PreferredBitrateNotFound(); - } else if (!falledBack) { - falledBack = true; - if (listener && uuid) { - listener.send("downloadInfo", { - uuid, - state: "bitrateFallback", - data: { - id: track.id, - title: track.title, - artist: track.mainArtist.name, - }, - }); - } - } - } - if (is360Format) throw new TrackNot360(); - const url = await getCorrectURL( - track, - "MP3_MISC", - TrackFormats.DEFAULT, - feelingLucky - ); - track.urls.MP3_MISC = url; - return TrackFormats.DEFAULT; -} - -class Downloader { +export class Downloader { dz: Deezer; downloadObject: IDownloadObject; settings: Settings; - bitrate: any; + bitrate: string; listener: any; playlistCovername: null; playlistURLs: any[]; @@ -415,12 +135,19 @@ class Downloader { } } - async download(extraData, track: Track) { + async download( + extraData: { trackAPI: APITrack; albumAPI?: APIAlbum; playlistAPI?: any }, + track?: Track + ) { + const startTime = performance.now(); + const returnData = {}; const { trackAPI, albumAPI, playlistAPI } = extraData; - trackAPI.size = this.downloadObject.size; + if (this.downloadObject.isCanceled) throw new DownloadCanceled(); - if (parseInt(trackAPI.id) === 0) throw new DownloadFailed("notOnDeezer"); + if (trackAPI.id === 0) throw new DownloadFailed("notOnDeezer"); + + trackAPI.size = this.downloadObject.size; let itemData = { id: trackAPI.id, @@ -428,28 +155,88 @@ class Downloader { artist: trackAPI.artist.name, }; - // Generate track object if (!track) { track = new Track(); - try { - await track.parseData( - this.dz, - trackAPI.id, - trackAPI, - albumAPI, - playlistAPI - ); - } catch (e) { - if (e.name === "AlbumDoesntExists") { - throw new DownloadFailed("albumDoesntExists"); - } - if (e.name === "MD5NotFound") { - throw new DownloadFailed("notLoggedIn"); - } - console.trace(e); - throw e; + track.parseTrack(trackAPI); + if (albumAPI) { + track.album = new Album(albumAPI.id, albumAPI.title); + track.album.parseAlbum(albumAPI); + } + if (playlistAPI) { + track.playlist = new Playlist(playlistAPI); } } + + const { filename, filepath, artistPath, coverPath, extrasPath } = + generatePath(track, this.downloadObject, this.settings); + + // Make sure the filepath exsists + mkdirSync(filepath, { recursive: true }); + const extension = extensions[track.bitrate]; + let writepath = `${filepath}/${filename}${extension}`; + + const shouldDownload = checkShouldDownload( + filename, + filepath, + extension, + writepath, + this.settings.overwriteFile, + track + ); + + // Adding tags + if ( + !shouldDownload && + [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE].includes( + this.settings.overwriteFile + ) + ) { + tagTrack(extension, writepath, track, this.settings.tags); + } + + if (!shouldDownload) { + if (this.listener) { + this.listener.send("updateQueue", { + uuid: this.downloadObject.uuid, + alreadyDownloaded: true, + downloadPath: writepath, + extrasPath: this.downloadObject.extrasPath, + }); + } + + returnData.filename = writepath.slice(extrasPath.length + 1); + returnData.data = itemData; + returnData.path = String(writepath); + + this.downloadObject.files.push(returnData); + + this.downloadObject.completeTrackProgress(this.listener); + this.downloadObject.downloaded += 1; + return returnData; + } + + // Enrich track with additional data + try { + await track.parseData( + this.dz, + trackAPI.id, + trackAPI, + albumAPI, + playlistAPI + ); + } catch (e) { + if (e.name === "AlbumDoesntExists") { + throw new DownloadFailed("albumDoesntExists"); + } + if (e.name === "MD5NotFound") { + throw new DownloadFailed("notLoggedIn"); + } + console.trace(e); + throw e; + } + + console.log(`Parsing: ${performance.now() - startTime}ms`); + if (this.downloadObject.isCanceled) throw new DownloadCanceled(); // Check if the track is encoded @@ -490,15 +277,10 @@ class Downloader { track.applySettings(this.settings); // Generate filename and filepath from metadata - const { filename, filepath, artistPath, coverPath, extrasPath } = - generatePath(track, this.downloadObject, this.settings); + // const { filename, filepath, artistPath, coverPath, extrasPath } = + // generatePath(track, this.downloadObject, this.settings); if (this.downloadObject.isCanceled) throw new DownloadCanceled(); - // Make sure the filepath exsists - mkdirSync(filepath, { recursive: true }); - const extension = extensions[track.bitrate]; - let writepath = `${filepath}/${filename}${extension}`; - if (this.settings.overwriteFile === OverwriteOption.KEEP_BOTH) { const baseFilename = `${filepath}/${filename}`; let currentFilename; @@ -510,46 +292,6 @@ class Downloader { writepath = currentFilename; } - const shouldDownload = checkShouldDownload( - filename, - filepath, - extension, - writepath, - this.settings.overwriteFile, - track - ); - - // Adding tags - if ( - !shouldDownload && - [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE].includes( - this.settings.overwriteFile - ) - ) { - tagTrack(extension, writepath, track, this.settings.tags); - } - - if (!shouldDownload) { - if (this.listener) { - this.listener.send("updateQueue", { - uuid: this.downloadObject.uuid, - alreadyDownloaded: true, - downloadPath: String(writepath), - extrasPath: String(this.downloadObject.extrasPath), - }); - } - - returnData.filename = writepath.slice(extrasPath.length + 1); - returnData.data = itemData; - returnData.path = String(writepath); - - this.downloadObject.files.push(returnData); - - this.downloadObject.completeTrackProgress(this.listener); - this.downloadObject.downloaded += 1; - return returnData; - } - itemData = { id: track.id, title: track.title, @@ -720,8 +462,9 @@ class Downloader { return returnData; } - async downloadWrapper(extraData, track?: Track) { + async downloadWrapper(extraData: { trackAPI: APITrack }, track?: Track) { const { trackAPI } = extraData; + // Temp metadata to generate logs const itemData = { id: trackAPI.id, @@ -1070,9 +813,3 @@ class Downloader { } } } - -export default { - Downloader, - downloadImage, - getPreferredBitrate, -}; diff --git a/deemix/src/index.ts b/deemix/src/index.ts index 00b4c923..2cbd0e62 100644 --- a/deemix/src/index.ts +++ b/deemix/src/index.ts @@ -1,6 +1,6 @@ import { Deezer } from "deezer-js"; import got from "got"; -import downloader from "./downloader"; +import { Downloader } from "./downloader"; import { LinkNotRecognized, LinkNotSupported } from "./errors"; import { generateAlbumItem, @@ -112,4 +112,4 @@ export * as types from "./types"; export * as utils from "./utils"; // Exporting the organized objects -export { downloader, generateDownloadObject, itemgen, parseLink }; +export { Downloader, generateDownloadObject, itemgen, parseLink }; diff --git a/deemix/src/types/Album.ts b/deemix/src/types/Album.ts index 306b6c6b..9a2d33f2 100644 --- a/deemix/src/types/Album.ts +++ b/deemix/src/types/Album.ts @@ -1,3 +1,4 @@ +import { APIAlbum } from "deezer-js/src/api"; import { removeDuplicateArtists, removeFeatures } from "../utils"; import { Artist } from "./Artist"; import { CustomDate } from "./CustomDate"; @@ -59,7 +60,7 @@ export class Album { this.isPlaylist = false; } - parseAlbum(albumAPI) { + parseAlbum(albumAPI: APIAlbum) { this.title = albumAPI.title; // Getting artist image ID @@ -85,7 +86,7 @@ export class Album { } albumAPI.contributors.forEach((artist) => { - const isVariousArtists = String(artist.id) === VARIOUS_ARTISTS; + const isVariousArtists = artist.id === VARIOUS_ARTISTS; const isMainArtist = artist.role === "Main"; if (isVariousArtists) { diff --git a/deemix/src/types/Artist.ts b/deemix/src/types/Artist.ts index 71b2cdf9..ac2af5bb 100644 --- a/deemix/src/types/Artist.ts +++ b/deemix/src/types/Artist.ts @@ -2,14 +2,14 @@ import { Picture } from "./Picture"; import { VARIOUS_ARTISTS } from "./index"; export class Artist { - id: string; + id: number; name: string; pic: Picture; role: string; save: boolean; - constructor(art_id = "0", name = "", role = "", pic_md5 = "") { - this.id = String(art_id); + constructor(art_id: number = 0, name = "", role = "", pic_md5 = "") { + this.id = art_id; this.name = name; this.pic = new Picture(pic_md5, "artist"); this.role = role; diff --git a/deemix/src/types/DownloadObjects.ts b/deemix/src/types/DownloadObjects.ts index 0c327e59..b94f55c9 100644 --- a/deemix/src/types/DownloadObjects.ts +++ b/deemix/src/types/DownloadObjects.ts @@ -3,7 +3,7 @@ import BasePlugin from "@/plugins/base"; export class IDownloadObject { type: any; id: any; - bitrate: any; + bitrate: string; title: any; artist: any; cover: any; diff --git a/deemix/src/types/Track.ts b/deemix/src/types/Track.ts index 645166e5..457be03b 100644 --- a/deemix/src/types/Track.ts +++ b/deemix/src/types/Track.ts @@ -15,7 +15,12 @@ import { VARIOUS_ARTISTS } from "./index"; import { Lyrics } from "./Lyrics"; import { Picture } from "./Picture"; import { Playlist } from "./Playlist"; -import { APITrack } from "deezer-js/src/api"; +import { + APIAlbum, + APIPlaylist, + APITrack, + EnrichedAPITrack, +} from "deezer-js/src/api"; const { map_track, map_album } = utils; export const formatsName = { @@ -32,12 +37,12 @@ export const formatsName = { class Track { id: string; title: string; - MD5: string; - mediaVersion: string; + MD5?: string; + mediaVersion?: number; trackToken: string; - trackTokenExpiration: number; + trackTokenExpiration?: string; duration: number; - fallbackID: string; + fallbackID: number; albumsFallback: any[]; filesizes: Record; local: boolean; @@ -45,8 +50,8 @@ class Track { artist: { Main: any[]; Featured?: any[] }; artists: any[]; album: Album | null; - trackNumber: string; - discNumber: string; + trackNumber: number; + discNumber: number; date: CustomDate; lyrics: Lyrics | null; bpm: number; @@ -73,11 +78,9 @@ class Track { this.id = "0"; this.title = ""; this.MD5 = ""; - this.mediaVersion = ""; this.trackToken = ""; - this.trackTokenExpiration = 0; this.duration = 0; - this.fallbackID = "0"; + this.fallbackID = 0; this.albumsFallback = []; this.filesizes = {}; this.local = false; @@ -85,8 +88,8 @@ class Track { this.artist = { Main: [] }; this.artists = []; this.album = null; - this.trackNumber = "0"; - this.discNumber = "0"; + this.trackNumber = 0; + this.discNumber = 0; this.date = new CustomDate(); this.lyrics = null; this.bpm = 0; @@ -107,7 +110,7 @@ class Track { this.urls = {}; } - parseEssentialData(trackAPI: APITrack) { + parseEssentialData(trackAPI: EnrichedAPITrack) { this.id = String(trackAPI.id); this.duration = trackAPI.duration; this.trackToken = trackAPI.track_token; @@ -115,8 +118,7 @@ class Track { this.MD5 = trackAPI.md5_origin; this.mediaVersion = trackAPI.media_version; this.filesizes = trackAPI.filesizes; - this.fallbackID = "0"; - if (trackAPI.fallback_id) this.fallbackID = trackAPI.fallback_id; + this.fallbackID = trackAPI.fallback_id ?? 0; this.local = parseInt(this.id) < 0; this.urls = {}; } @@ -124,22 +126,22 @@ class Track { async parseData( dz: Deezer, id, - existingTrack?: DeezerTrack, - albumAPI, - playlistAPI + existingTrack?: APITrack, + albumAPI: APIAlbum, + playlistAPI: APIPlaylist, + refetch: boolean = true ) { - if (id) { + if (id && refetch) { const gwTrack = await dz.gw.get_track_with_fallback(id); const newTrack = map_track(gwTrack); - if (!existingTrack) existingTrack = {}; + this.parseEssentialData(newTrack); + existingTrack = { ...existingTrack, ...newTrack }; } else if (!existingTrack) { throw new NoDataToParse(); } - this.parseEssentialData(existingTrack); - // only public api has bpm if (!existingTrack.bpm && !this.local) { try { @@ -256,7 +258,7 @@ class Track { this.album.mainArtist = this.mainArtist; } - parseTrack(trackAPI: any) { + parseTrack(trackAPI: APITrack) { this.title = trackAPI.title; this.discNumber = trackAPI.disk_number; @@ -266,7 +268,7 @@ class Track { this.replayGain = generateReplayGainString(trackAPI.gain); this.ISRC = trackAPI.isrc; this.trackNumber = trackAPI.track_position; - this.contributors = trackAPI.song_contributors; + this.contributors = trackAPI.contributors; this.rank = trackAPI.rank; this.bpm = trackAPI.bpm; @@ -286,8 +288,8 @@ class Track { this.date.fixDayMonth(); } - trackAPI.contributors.forEach((artist: any) => { - const isVariousArtists = String(artist.id) === VARIOUS_ARTISTS; + trackAPI.contributors?.forEach((artist: any) => { + const isVariousArtists = artist.id === VARIOUS_ARTISTS; const isMainArtist = artist.role === "Main"; if (trackAPI.contributors.length > 1 && isVariousArtists) return; diff --git a/deemix/src/types/index.ts b/deemix/src/types/index.ts index 1f9e9aba..5208adc3 100644 --- a/deemix/src/types/index.ts +++ b/deemix/src/types/index.ts @@ -1,4 +1,4 @@ -export const VARIOUS_ARTISTS = "5080"; +export const VARIOUS_ARTISTS = 5080; export * from "./Album"; export * from "./Artist"; diff --git a/deemix/src/utils/downloadImage.ts b/deemix/src/utils/downloadImage.ts new file mode 100644 index 00000000..7c1756fa --- /dev/null +++ b/deemix/src/utils/downloadImage.ts @@ -0,0 +1,95 @@ +import { TrackFormats, utils } from "deezer-js"; +import { + createWriteStream, + existsSync, + mkdirSync, + readFileSync, + unlinkSync, +} from "fs"; +import { HTTPError, ReadError, TimeoutError, default as got } from "got"; +import { tmpdir } from "os"; +import { OverwriteOption } from "../settings"; +import { USER_AGENT_HEADER, pipeline } from "../utils"; + +const TEMPDIR = tmpdir() + "/deemix-imgs"; +mkdirSync(TEMPDIR, { recursive: true }); + +export async function downloadImage( + url: string, + path: string, + overwrite = OverwriteOption.DONT_OVERWRITE +) { + if ( + existsSync(path) && + ![ + OverwriteOption.OVERWRITE, + OverwriteOption.ONLY_TAGS, + OverwriteOption.KEEP_BOTH, + ].includes(overwrite) + ) { + const file = readFileSync(path); + if (file.length !== 0) return path; + unlinkSync(path); + } + let timeout: NodeJS.Timeout | null = null; + let error = ""; + + const downloadStream = got + .stream(url, { + headers: { "User-Agent": USER_AGENT_HEADER }, + https: { rejectUnauthorized: false }, + }) + .on("data", function () { + clearTimeout(timeout); + timeout = setTimeout(() => { + error = "DownloadTimeout"; + downloadStream.destroy(); + }, 5000); + }); + const fileWriterStream = createWriteStream(path); + + timeout = setTimeout(() => { + error = "DownloadTimeout"; + downloadStream.destroy(); + }, 5000); + + try { + await pipeline(downloadStream, fileWriterStream); + } catch (e) { + unlinkSync(path); + if (e instanceof HTTPError) { + if (url.includes("images.dzcdn.net")) { + const urlBase = url.slice(0, url.lastIndexOf("/") + 1); + const pictureURL = url.slice(urlBase.length); + const pictureSize = parseInt( + pictureURL.slice(0, pictureURL.indexOf("x")) + ); + if (pictureSize > 1200) { + return downloadImage( + urlBase + + pictureURL.replace(`${pictureSize}x${pictureSize}`, "1200x1200"), + path, + overwrite + ); + } + } + return null; + } + if ( + e instanceof ReadError || + e instanceof TimeoutError || + [ + "ESOCKETTIMEDOUT", + "ERR_STREAM_PREMATURE_CLOSE", + "ETIMEDOUT", + "ECONNRESET", + ].includes(e.code) || + (downloadStream.destroyed && error === "DownloadTimeout") + ) { + return downloadImage(url, path, overwrite); + } + console.trace(e); + throw e; + } + return path; +} diff --git a/deemix/src/utils/getPreferredBitrate.ts b/deemix/src/utils/getPreferredBitrate.ts new file mode 100644 index 00000000..85d3857d --- /dev/null +++ b/deemix/src/utils/getPreferredBitrate.ts @@ -0,0 +1,198 @@ +import { Deezer, TrackFormats, errors as _errors, utils } from "deezer-js"; +import { HTTPError, ReadError, TimeoutError, default as got } from "got"; +import { generateCryptedStreamURL } from "../decryption"; +import { PreferredBitrateNotFound, TrackNot360 } from "../errors"; +import Track from "../types/Track"; +import { USER_AGENT_HEADER } from "../utils"; + +const { WrongLicense, WrongGeolocation } = _errors; +const { map_track } = utils; + +const formats_non_360 = { + [TrackFormats.FLAC]: "FLAC", + [TrackFormats.MP3_320]: "MP3_320", + [TrackFormats.MP3_128]: "MP3_128", +}; +const formats_360 = { + [TrackFormats.MP4_RA3]: "MP4_RA3", + [TrackFormats.MP4_RA2]: "MP4_RA2", + [TrackFormats.MP4_RA1]: "MP4_RA1", +}; + +export async function getPreferredBitrate( + dz: Deezer, + track: Track, + preferredBitrate: string, + shouldFallback: boolean, + feelingLucky: boolean, + uuid: string, + listener: any +) { + let falledBack = false; + let hasAlternative = track.fallbackID !== "0"; + let isGeolocked = false; + let wrongLicense = false; + + async function testURL(track: Track, url: string, formatName: string) { + if (!url) return false; + let request; + try { + request = got + .get(url, { + headers: { "User-Agent": USER_AGENT_HEADER }, + https: { rejectUnauthorized: false }, + timeout: 7000, + }) + .on("response", (response) => { + track.filesizes[`${formatName.toLowerCase()}`] = + response.statusCode === 403 + ? 0 + : response.headers["content-length"]; + request.cancel(); + }); + + await request; + } catch (e) { + if (e.isCanceled) { + if (track.filesizes[`${formatName.toLowerCase()}`] === 0) return false; + return true; + } + if (e instanceof ReadError || e instanceof TimeoutError) { + return await testURL(track, url, formatName); + } + if (e instanceof HTTPError) return false; + console.trace(e); + throw e; + } + } + + async function getCorrectURL( + track: Track, + formatName: string, + formatNumber: number, + feelingLucky: boolean + ) { + // Check the track with the legit method + let url; + wrongLicense = + ((formatName === "FLAC" || formatName.startsWith("MP4_RA")) && + !dz.current_user.can_stream_lossless) || + (formatName === "MP3_320" && !dz.current_user.can_stream_hq); + if ( + track.filesizes[`${formatName.toLowerCase()}`] && + track.filesizes[`${formatName.toLowerCase()}`] !== "0" + ) { + try { + url = await dz.get_track_url(track.trackToken, formatName); + } catch (e) { + wrongLicense = e.name === "WrongLicense"; + isGeolocked = e.name === "WrongGeolocation"; + } + } + // Fallback to old method + if (!url && feelingLucky) { + url = generateCryptedStreamURL( + track.id, + track.MD5, + track.mediaVersion, + formatNumber + ); + if (await testURL(track, url, formatName)) return url; + url = undefined; + } + return url; + } + + if (track.local) { + const url = await getCorrectURL( + track, + "MP3_MISC", + TrackFormats.LOCAL, + feelingLucky + ); + track.urls.MP3_MISC = url; + return TrackFormats.LOCAL; + } + + const is360Format = Object.keys(formats_360).includes(preferredBitrate); + let formats: Record; + if (!shouldFallback) { + formats = { ...formats_360, ...formats_non_360 }; + } else if (is360Format) { + formats = { ...formats_360 }; + } else { + formats = { ...formats_non_360 }; + } + + // Check and renew trackToken before starting the check + await track.checkAndRenewTrackToken(dz); + for (let i = 0; i < Object.keys(formats).length; i++) { + // Check bitrates + const formatNumber = Object.keys(formats).reverse()[i]; + const formatName = formats[formatNumber]; + + // Current bitrate is higher than preferred bitrate; skip + if (formatNumber > preferredBitrate) { + continue; + } + + let currentTrack = track; + let url = await getCorrectURL( + currentTrack, + formatName, + formatNumber, + feelingLucky + ); + let newTrack; + do { + if (!url && hasAlternative) { + newTrack = await dz.gw.get_track_with_fallback(currentTrack.fallbackID); + newTrack = map_track(newTrack); + currentTrack = new Track(); + currentTrack.parseEssentialData(newTrack); + hasAlternative = currentTrack.fallbackID !== 0; + } + if (!url) + url = await getCorrectURL( + currentTrack, + formatName, + formatNumber, + feelingLucky + ); + } while (!url && hasAlternative); + + if (url) { + if (newTrack) track.parseEssentialData(newTrack); + track.urls[formatName] = url; + return formatNumber; + } + + if (!shouldFallback) { + if (wrongLicense) throw new WrongLicense(formatName); + if (isGeolocked) throw new WrongGeolocation(dz.current_user.country); + throw new PreferredBitrateNotFound(); + } else if (!falledBack) { + falledBack = true; + if (listener && uuid) { + listener.send("downloadInfo", { + uuid, + state: "bitrateFallback", + data: { + id: track.id, + title: track.title, + artist: track.mainArtist.name, + }, + }); + } + } + } + if (is360Format) throw new TrackNot360(); + const url = await getCorrectURL( + track, + "MP3_MISC", + TrackFormats.DEFAULT, + feelingLucky + ); + track.urls.MP3_MISC = url; + return TrackFormats.DEFAULT; +} diff --git a/deemix/src/utils/index.ts b/deemix/src/utils/index.ts index 109bd700..3a4a427c 100644 --- a/deemix/src/utils/index.ts +++ b/deemix/src/utils/index.ts @@ -7,5 +7,7 @@ export * from "./deezer"; export * from "./downloadUtils"; export * from "./localpaths"; export * from "./pathtemplates"; +export * from "./getPreferredBitrate"; +export * from "./downloadImage"; export { blowfish, id3Writer }; diff --git a/deemix/src/utils/pathtemplates.ts b/deemix/src/utils/pathtemplates.ts index eb8e0e22..4a5a00ca 100644 --- a/deemix/src/utils/pathtemplates.ts +++ b/deemix/src/utils/pathtemplates.ts @@ -1,5 +1,9 @@ import { TrackFormats } from "deezer-js"; import { CustomDate } from "../types/CustomDate"; +import Track from "@/types/Track"; +import { IDownloadObject } from "@/types/DownloadObjects"; +import { Settings } from "@/settings"; +import { APITrack } from "deezer-js/src/api"; const bitrateLabels = { [TrackFormats.MP4_RA3]: "360 HQ", @@ -60,13 +64,68 @@ export function pad(num, max_val, settings) { return num + ""; } -export function generatePath(track, downloadObject, settings) { +const shouldCreatePlaylistFolder = (track: Track, settings: Settings) => { + return ( + settings.createPlaylistFolder && + track.playlist && + !settings.tags.savePlaylistAsCompilation + ); +}; + +const shouldCreateArtistFolder = (track: Track, settings: Settings) => { + return ( + (settings.createArtistFolder && !track.playlist) || + (settings.createArtistFolder && + track.playlist && + settings.tags.savePlaylistAsCompilation) || + (settings.createArtistFolder && + track.playlist && + settings.createStructurePlaylist) + ); +}; + +const shouldCreateAlbumFolder = ( + track: Track, + settings: Settings, + singleTrack: boolean +) => { + return ( + settings.createAlbumFolder && + (!singleTrack || (singleTrack && settings.createSingleFolder)) && + (!track.playlist || + (track.playlist && settings.tags.savePlaylistAsCompilation) || + (track.playlist && settings.createStructurePlaylist)) + ); +}; + +const shouldCreateCDFolder = ( + track: Track, + settings: Settings, + singleTrack: boolean +) => { + return ( + track.album && + parseInt(track.album?.discTotal) > 1 && + settings.createAlbumFolder && + settings.createCDFolder && + (!singleTrack || (singleTrack && settings.createSingleFolder)) && + (!track.playlist || + (track.playlist && settings.tags.savePlaylistAsCompilation) || + (track.playlist && settings.createStructurePlaylist)) + ); +}; + +export function generatePath( + track: Track, + downloadObject: IDownloadObject, + settings: Settings +) { let filenameTemplate = "%artist% - %title%"; let singleTrack = false; if (downloadObject.type === "track") { - if (settings.createSingleFolder) - filenameTemplate = settings.albumTracknameTemplate; - else filenameTemplate = settings.tracknameTemplate; + filenameTemplate = settings.createSingleFolder + ? settings.albumTracknameTemplate + : settings.tracknameTemplate; singleTrack = true; } else if (downloadObject.type === "album") { filenameTemplate = settings.albumTracknameTemplate; @@ -75,35 +134,19 @@ export function generatePath(track, downloadObject, settings) { } let filename = generateTrackName(filenameTemplate, track, settings); - let filepath, artistPath, coverPath, extrasPath; - filepath = settings.downloadLocation || "."; + let filepath = settings.downloadLocation || "."; + let artistPath: string, coverPath: string, extrasPath: string; - if ( - settings.createPlaylistFolder && - track.playlist && - !settings.tags.savePlaylistAsCompilation - ) { - filepath += `/${generatePlaylistName( - settings.playlistNameTemplate, - track.playlist, - settings - )}`; + if (shouldCreatePlaylistFolder(track, settings)) { + filepath += `/${generatePlaylistName(track, settings)}`; } if (track.playlist && !settings.tags.savePlaylistAsCompilation) { extrasPath = filepath; } - if ( - (settings.createArtistFolder && !track.playlist) || - (settings.createArtistFolder && - track.playlist && - settings.tags.savePlaylistAsCompilation) || - (settings.createArtistFolder && - track.playlist && - settings.createStructurePlaylist) - ) { + if (shouldCreateArtistFolder(track, settings)) { filepath += `/${generateArtistName( settings.artistNameTemplate, track.album.mainArtist, @@ -113,13 +156,7 @@ export function generatePath(track, downloadObject, settings) { artistPath = filepath; } - if ( - settings.createAlbumFolder && - (!singleTrack || (singleTrack && settings.createSingleFolder)) && - (!track.playlist || - (track.playlist && settings.tags.savePlaylistAsCompilation) || - (track.playlist && settings.createStructurePlaylist)) - ) { + if (shouldCreateAlbumFolder(track, settings, singleTrack)) { filepath += `/${generateAlbumName( settings.albumNameTemplate, track.album, @@ -131,15 +168,7 @@ export function generatePath(track, downloadObject, settings) { if (!extrasPath) extrasPath = filepath; - if ( - parseInt(track.album.discTotal) > 1 && - settings.createAlbumFolder && - settings.createCDFolder && - (!singleTrack || (singleTrack && settings.createSingleFolder)) && - (!track.playlist || - (track.playlist && settings.tags.savePlaylistAsCompilation) || - (track.playlist && settings.createStructurePlaylist)) - ) { + if (shouldCreateCDFolder(track, settings, singleTrack)) { filepath += `/CD${track.discNumber}`; } @@ -159,8 +188,13 @@ export function generatePath(track, downloadObject, settings) { }; } -export function generateTrackName(filename, track, settings) { +export function generateTrackName( + filename: string, + track: Track, + settings: Settings +) { const c = settings.illegalCharacterReplacer; + filename = filename.replaceAll("%title%", fixName(track.title, c)); filename = filename.replaceAll("%artist%", fixName(track.mainArtist.name, c)); filename = filename.replaceAll( @@ -188,37 +222,49 @@ export function generateTrackName(filename, track, settings) { filename = filename .replaceAll(" %featartists%", "") .replaceAll("%featartists%", ""); - filename = filename.replaceAll("%album%", fixName(track.album.title, c)); - filename = filename.replaceAll( - "%albumartist%", - fixName(track.album.mainArtist.name, c) - ); - filename = filename.replaceAll( - "%tracknumber%", - pad(track.trackNumber, track.album.trackTotal, settings) - ); - filename = filename.replaceAll("%tracktotal%", track.album.trackTotal); - filename = filename.replaceAll("%discnumber%", track.discNumber); - filename = filename.replaceAll("%disctotal%", track.album.discTotal); - if (track.album.genre.length) - filename = filename.replaceAll("%genre%", fixName(track.album.genre[0], c)); - else filename = filename.replaceAll("%genre%", "Unknown"); - filename = filename.replaceAll("%year%", track.date.year); + + if (track.album) { + filename = filename.replaceAll("%album%", fixName(track.album.title, c)); + filename = filename.replaceAll( + "%albumartist%", + fixName(track.album.mainArtist.name, c) + ); + filename = filename.replaceAll( + "%tracknumber%", + pad(track.trackNumber, track.album.trackTotal, settings) + ); + filename = filename.replaceAll("%tracktotal%", track.album.trackTotal); + + if (track.album.genre.length) { + filename = filename.replaceAll( + "%genre%", + fixName(track.album.genre[0], c) + ); + } else { + filename = filename.replaceAll("%genre%", "Unknown"); + } + + filename = filename.replaceAll("%disctotal%", track.album.discTotal); + filename = filename.replaceAll("%label%", fixName(track.album.label, c)); + filename = filename.replaceAll("%upc%", track.album.barcode); + filename = filename.replaceAll("%album_id%", track.album.id); + } + + filename = filename.replaceAll("%discnumber%", String(track.discNumber)); + filename = filename.replaceAll("%year%", String(track.date.year)); filename = filename.replaceAll("%date%", track.dateString); - filename = filename.replaceAll("%bpm%", track.bpm); - filename = filename.replaceAll("%label%", fixName(track.album.label, c)); + filename = filename.replaceAll("%bpm%", String(track.bpm)); filename = filename.replaceAll("%isrc%", track.ISRC); - filename = filename.replaceAll("%upc%", track.album.barcode); - if (track.explicit) + if (track.explicit) { filename = filename.replaceAll("%explicit%", "(Explicit)"); - else + } else { filename = filename .replaceAll(" %explicit%", "") .replaceAll("%explicit%", ""); + } filename = filename.replaceAll("%track_id%", track.id); - filename = filename.replaceAll("%album_id%", track.album.id); - filename = filename.replaceAll("%artist_id%", track.mainArtist.id); + filename = filename.replaceAll("%artist_id%", String(track.mainArtist.id)); if (track.playlist) { filename = filename.replaceAll("%playlist_id%", track.playlist.playlistID); filename = filename.replaceAll( @@ -227,16 +273,20 @@ export function generateTrackName(filename, track, settings) { ); } else { filename = filename.replaceAll("%playlist_id%", ""); - filename = filename.replaceAll( - "%position%", - pad(track.trackNumber, track.album.trackTotal, settings) - ); + if (track.album) { + filename = filename.replaceAll( + "%position%", + pad(track.trackNumber, track.album.trackTotal, settings) + ); + } } + filename = filename.replaceAll("\\", "/"); return antiDot(fixLongName(filename)); } export function generateAlbumName(foldername, album, settings, playlist) { + console.log("Create album"); const c = settings.illegalCharacterReplacer; if (playlist && settings.tags.savePlaylistAsCompilation) { foldername = foldername.replaceAll( @@ -320,14 +370,20 @@ export function generateArtistName(foldername, artist, settings, rootArtist) { return antiDot(fixLongName(foldername)); } -export function generatePlaylistName(foldername, playlist, settings) { - const c = settings.illegalCharacterReplacer; +export function generatePlaylistName( + { playlist }: Track, + { illegalCharacterReplacer, playlistNameTemplate, dateFormat }: Settings +) { + const c = illegalCharacterReplacer; const today = new Date(); const today_dz = new CustomDate( String(today.getDate()).padStart(2, "0"), String(today.getMonth() + 1).padStart(2, "0"), String(today.getFullYear()) ); + + let foldername = playlistNameTemplate; + foldername = foldername.replaceAll("%playlist%", fixName(playlist.title, c)); foldername = foldername.replaceAll( "%playlist_id%", @@ -344,11 +400,9 @@ export function generatePlaylistName(foldername, playlist, settings) { "%explicit%", playlist.explicit ? "(Explicit)" : "" ); - foldername = foldername.replaceAll( - "%today%", - today_dz.format(settings.dateFormat) - ); + foldername = foldername.replaceAll("%today%", today_dz.format(dateFormat)); foldername = foldername.replaceAll("\\", "/"); + return antiDot(fixLongName(foldername)); } diff --git a/deezer-js/src/api.ts b/deezer-js/src/api.ts index b5c05d6d..9e095221 100644 --- a/deezer-js/src/api.ts +++ b/deezer-js/src/api.ts @@ -33,16 +33,16 @@ export interface APIArtist { name: string; link: string; share: string; - picture: string; - picture_small: string; - picture_medium: string; - picture_big: string; - picture_xl: string; - nb_album: number; - nb_fan: number; + picture?: string; + picture_small?: string; + picture_medium?: string; + picture_big?: string; + picture_xl?: string; + nb_album?: number; + nb_fan?: number; radio: boolean; tracklist: string; - role: string; + role?: string; } export interface APIAlbum { @@ -80,7 +80,10 @@ export interface APITrack { gain: number; available_countries: string[]; // List of countries as strings alternative?: APITrack; // Assuming alternative is of type Track - contributors: APIContributor[]; // Assuming Contributor is an object + alternative_albums?: { + data: APIAlbum[]; + }; + contributors?: APIContributor[]; // Assuming Contributor is an object md5_image: string; track_token: string; artist: APIArtist; @@ -100,6 +103,51 @@ export interface APIContributor { role: string; } +export interface APIPlaylist {} + +// Contains additional information from GW +export interface EnrichedAPITrack + extends Omit { + type?: string; + md5_origin?: string; + filesizes?: Record; + media_version?: number; + track_token_expire?: string; + token: string; + user_id: string; + lyrics_id?: string; + physical_release_date?: string; + song_contributors?: any; + fallback_id?: number; + digital_release_date?: string; + genre_id?: number; + copyright?: string; + lyrics?: string; + alternative_albums?: any; + album?: EnrichedAPIAlbum; + artist: EnrichedAPIArtist; + contributors: EnrichedAPIContributor[]; +} + +export interface EnrichedAPIContributor extends APIContributor { + md5_image: string; + tracklist: string; + type: string; + order: string; + rank: any; +} + +export interface EnrichedAPIAlbum extends APIAlbum { + md5_image?: string; + tracklist?: string; + type?: string; +} + +export interface EnrichedAPIArtist extends APIArtist { + md5_image?: string; + type?: string; +} + type APIArgs = Record; export class API { @@ -495,7 +543,7 @@ export class API { return this.api_call("search/user", args); } - get_track(song_id: string): Promise { + get_track(song_id: string | number): Promise { return this.api_call(`track/${song_id}`) as Promise; } diff --git a/deezer-js/src/gw.ts b/deezer-js/src/gw.ts index f0189ae6..0905853b 100644 --- a/deezer-js/src/gw.ts +++ b/deezer-js/src/gw.ts @@ -16,6 +16,40 @@ export const PlaylistStatus = { }; export interface GWTrack { + ALB_ID: string; + TRACK_TOKEN_EXPIRE: string; + TOKEN: string; + USER_ID: any; + FILESIZE_MP3_MISC: any; + VERSION: string; + TRACK_NUMBER: number; + DISK_NUMBER: number; + RANK: any; + RANK_SNG: any; + PHYSICAL_RELEASE_DATE: string; + EXPLICIT_LYRICS(EXPLICIT_LYRICS: any): boolean; + EXPLICIT_TRACK_CONTENT: any; + MEDIA: any; + GAIN: number; + ARTISTS: any; + LYRICS_ID: any; + SNG_CONTRIBUTORS: any; + FALLBACK: any; + DIGITAL_RELEASE_DATE: any; + GENRE_ID: any; + COPYRIGHT: any; + LYRICS: any; + ALBUM_FALLBACK: any; + FILESIZE_AAC_64: any; + FILESIZE_MP3_64: any; + FILESIZE_MP3_128: any; + FILESIZE_MP3_256: any; + FILESIZE_MP3_320: any; + FILESIZE_MP4_RA1: any; + FILESIZE_MP4_RA2: any; + FILESIZE_MP4_RA3: any; + FILESIZE_FLAC: any; + TRACK_TOKEN: string; SNG_ID: number; SNG_TITLE: string; DURATION: number; @@ -26,6 +60,7 @@ export interface GWTrack { ALB_PICTURE: string; ART_ID: number; ART_NAME: string; + ISRC: string; } export const EMPTY_TRACK_OBJ = { @@ -460,7 +495,7 @@ export class GW { return result; } - async get_track_with_fallback(sng_id) { + async get_track_with_fallback(sng_id): Promise { let body; if (parseInt(sng_id) > 0) { try { diff --git a/deezer-js/src/utils.ts b/deezer-js/src/utils.ts index ba04e7a3..d0c07410 100644 --- a/deezer-js/src/utils.ts +++ b/deezer-js/src/utils.ts @@ -1,3 +1,6 @@ +import { APITrack, EnrichedAPITrack } from "./api"; +import { GWTrack } from "./gw"; + // Explicit Content Lyrics export const LyricsStatus = { NOT_EXPLICIT: 0, // Not Explicit @@ -413,8 +416,8 @@ export function map_playlist(playlist) { } // maps gw-light api tracks to standard api -export function map_track(track) { - let result: Record = { +export function map_track(track: GWTrack): EnrichedAPITrack { + let result: EnrichedAPITrack = { id: track.SNG_ID, readable: true, // not provided title: track.SNG_TITLE, @@ -461,91 +464,95 @@ export function map_track(track) { track_token: track.TRACK_TOKEN, track_token_expire: track.TRACK_TOKEN_EXPIRE, }; - if (parseInt(track.SNG_ID) > 0) { - result.title_version = (track.VERSION || "").trim(); - if ( - result.title_version && - result.title_short.includes(result.title_version) - ) { - result.title_short = result.title_short - .replace(result.title_version, "") - .trim(); - } - result.title = `${result.title_short} ${result.title_version}`.trim(); - result.track_position = track.TRACK_NUMBER; - result.disk_number = track.DISK_NUMBER; - result.rank = track.RANK || track.RANK_SNG; - result.release_date = track.PHYSICAL_RELEASE_DATE; - result.explicit_lyrics = is_explicit(track.EXPLICIT_LYRICS); - result.explicit_content_lyrics = - track.EXPLICIT_TRACK_CONTENT.EXPLICIT_LYRICS_STATUS; - result.explicit_content_cover = - track.EXPLICIT_TRACK_CONTENT.EXPLICIT_COVER_STATUS; - result.preview = track.MEDIA[0].HREF; - result.gain = track.GAIN; - if (track.ARTISTS) { - track.ARTISTS.forEach((contributor) => { - if (contributor.ART_ID === result.artist.id) { - result.artist = { - ...result.artist, - picture_small: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/56x56-000000-80-0-0.jpg`, - picture_medium: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/250x250-000000-80-0-0.jpg`, - picture_big: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/500x500-000000-80-0-0.jpg`, - picture_xl: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/1000x1000-000000-80-0-0.jpg`, - md5_image: contributor.ART_PICTURE, - }; - } - result.contributors.push({ - id: contributor.ART_ID, - name: contributor.ART_NAME, - link: `https://www.deezer.com/artist/${contributor.ART_ID}`, - share: `https://www.deezer.com/artist/${contributor.ART_ID}`, - picture: `https://www.deezer.com/artist/${contributor.ART_ID}/image`, + + if (track.SNG_ID <= 0) { + result.token = track.TOKEN; + result.user_id = track.USER_ID; + result.filesizes.mp3_misc = track.FILESIZE_MP3_MISC; + + return result; + } + + result.title_version = (track.VERSION || "").trim(); + if ( + result.title_version && + result.title_short.includes(result.title_version) + ) { + result.title_short = result.title_short + .replace(result.title_version, "") + .trim(); + } + result.title = `${result.title_short} ${result.title_version}`.trim(); + result.track_position = track.TRACK_NUMBER; + result.disk_number = track.DISK_NUMBER; + result.rank = track.RANK || track.RANK_SNG; + result.release_date = track.PHYSICAL_RELEASE_DATE; + result.explicit_lyrics = is_explicit(track.EXPLICIT_LYRICS); + result.explicit_content_lyrics = + track.EXPLICIT_TRACK_CONTENT.EXPLICIT_LYRICS_STATUS; + result.explicit_content_cover = + track.EXPLICIT_TRACK_CONTENT.EXPLICIT_COVER_STATUS; + result.preview = track.MEDIA[0].HREF; + result.gain = track.GAIN; + if (track.ARTISTS) { + track.ARTISTS.forEach((contributor) => { + if (contributor.ART_ID === result.artist.id) { + result.artist = { + ...result.artist, picture_small: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/56x56-000000-80-0-0.jpg`, picture_medium: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/250x250-000000-80-0-0.jpg`, picture_big: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/500x500-000000-80-0-0.jpg`, picture_xl: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/1000x1000-000000-80-0-0.jpg`, md5_image: contributor.ART_PICTURE, - tracklist: `https://api.deezer.com/artist/${contributor.ART_ID}/top?limit=50`, - type: "artist", - role: RoleID[contributor.ROLE_ID], - // Extras - order: contributor.ARTISTS_SONGS_ORDER, - rank: contributor.RANK, - }); + }; + } + result.contributors.push({ + id: contributor.ART_ID, + name: contributor.ART_NAME, + link: `https://www.deezer.com/artist/${contributor.ART_ID}`, + share: `https://www.deezer.com/artist/${contributor.ART_ID}`, + picture: `https://www.deezer.com/artist/${contributor.ART_ID}/image`, + picture_small: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/56x56-000000-80-0-0.jpg`, + picture_medium: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/250x250-000000-80-0-0.jpg`, + picture_big: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/500x500-000000-80-0-0.jpg`, + picture_xl: `https://e-cdns-images.dzcdn.net/images/artist/${contributor.ART_PICTURE}/1000x1000-000000-80-0-0.jpg`, + md5_image: contributor.ART_PICTURE, + tracklist: `https://api.deezer.com/artist/${contributor.ART_ID}/top?limit=50`, + type: "artist", + role: RoleID[contributor.ROLE_ID], + // Extras + order: contributor.ARTISTS_SONGS_ORDER, + rank: contributor.RANK, }); - } - // Extras - result = { - ...result, - lyrics_id: track.LYRICS_ID, - physical_release_date: track.PHYSICAL_RELEASE_DATE, - song_contributors: track.SNG_CONTRIBUTORS, - }; - if (track.FALLBACK) result.fallback_id = track.FALLBACK.SNG_ID; - if (track.DIGITAL_RELEASE_DATE) - result.digital_release_date = track.DIGITAL_RELEASE_DATE; - if (track.GENRE_ID) result.genre_id = track.GENRE_ID; - if (track.COPYRIGHT) result.copyright = track.COPYRIGHT; - if (track.LYRICS) result.lyrics = track.LYRICS; - if (track.ALBUM_FALLBACK) result.alternative_albums = track.ALBUM_FALLBACK; - result.filesizes = { - ...result.filesizes, - aac_64: track.FILESIZE_AAC_64, - mp3_64: track.FILESIZE_MP3_64, - mp3_128: track.FILESIZE_MP3_128, - mp3_256: track.FILESIZE_MP3_256, - mp3_320: track.FILESIZE_MP3_320, - mp4_ra1: track.FILESIZE_MP4_RA1, - mp4_ra2: track.FILESIZE_MP4_RA2, - mp4_ra3: track.FILESIZE_MP4_RA3, - flac: track.FILESIZE_FLAC, - }; - } else { - result.token = track.TOKEN; - result.user_id = track.USER_ID; - result.filesizes.mp3_misc = track.FILESIZE_MP3_MISC; + }); } + // Extras + result = { + ...result, + lyrics_id: track.LYRICS_ID, + physical_release_date: track.PHYSICAL_RELEASE_DATE, + song_contributors: track.SNG_CONTRIBUTORS, + }; + if (track.FALLBACK) result.fallback_id = track.FALLBACK.SNG_ID; + if (track.DIGITAL_RELEASE_DATE) + result.digital_release_date = track.DIGITAL_RELEASE_DATE; + if (track.GENRE_ID) result.genre_id = track.GENRE_ID; + if (track.COPYRIGHT) result.copyright = track.COPYRIGHT; + if (track.LYRICS) result.lyrics = track.LYRICS; + if (track.ALBUM_FALLBACK) result.alternative_albums = track.ALBUM_FALLBACK; + result.filesizes = { + ...result.filesizes, + aac_64: track.FILESIZE_AAC_64, + mp3_64: track.FILESIZE_MP3_64, + mp3_128: track.FILESIZE_MP3_128, + mp3_256: track.FILESIZE_MP3_256, + mp3_320: track.FILESIZE_MP3_320, + mp4_ra1: track.FILESIZE_MP4_RA1, + mp4_ra2: track.FILESIZE_MP4_RA2, + mp4_ra3: track.FILESIZE_MP4_RA3, + flac: track.FILESIZE_FLAC, + }; + return result; } diff --git a/server/src/app.ts b/server/src/app.ts index 461c758d..5b082158 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,5 +1,5 @@ import { - downloader, + Downloader, generateDownloadObject, plugins, settings, @@ -351,7 +351,7 @@ export class DeemixApp { if (typeof downloadObject === "undefined") return; - this.currentJob = new downloader.Downloader( + this.currentJob = new Downloader( dz, downloadObject, this.settings,