From d2e71033a61ccb88e09ba48b2b573f0ed6c676e8 Mon Sep 17 00:00:00 2001 From: Lachie Date: Mon, 2 Sep 2024 13:06:08 +1000 Subject: [PATCH 1/5] chore: significant rewrite to typescript --- deemix/package.json | 1 + deemix/src/{decryption.js => decryption.ts} | 33 +-- deemix/src/{downloader.js => downloader.ts} | 207 ++++++++++-------- deemix/src/{errors.js => errors.ts} | 109 +++++---- deemix/src/index.ts | 59 ++--- deemix/src/{itemgen.js => itemgen.ts} | 171 +++++++++++---- deemix/src/plugins/{index.js => base.ts} | 10 +- deemix/src/plugins/index.ts | 1 + deemix/src/plugins/{spotify.js => spotify.ts} | 65 +++--- deemix/src/{settings.js => settings.ts} | 118 ++++++++-- deemix/src/{tagger.js => tagger.ts} | 23 +- deemix/src/types/{Album.js => Album.ts} | 44 +++- deemix/src/types/{Artist.js => Artist.ts} | 16 +- deemix/src/types/{Date.js => CustomDate.ts} | 10 +- ...{DownloadObjects.js => DownloadObjects.ts} | 67 +++--- deemix/src/types/{Lyrics.js => Lyrics.ts} | 13 +- deemix/src/types/{Picture.js => Picture.ts} | 16 +- deemix/src/types/{Playlist.js => Playlist.ts} | 32 ++- deemix/src/types/{Track.js => Track.ts} | 94 ++++++-- deemix/src/types/index.ts | 17 +- deemix/src/utils/{index.js => core.ts} | 45 ++-- deemix/src/utils/deezer.js | 70 ------ deemix/src/utils/deezer.ts | 64 ++++++ .../{download-utils.js => downloadUtils.ts} | 6 +- deemix/src/utils/index.ts | 11 + .../utils/{localpaths.js => localpaths.ts} | 19 +- .../{pathtemplates.js => pathtemplates.ts} | 35 ++- deemix/tsconfig.json | 9 +- deezer-js/src/api.js | 35 ++- deezer-js/src/gw.js | 4 +- deezer-js/src/index.ts | 48 ++-- deezer-js/src/{utils.js => utils.ts} | 42 ++-- gui/index.js | 2 +- pnpm-lock.yaml | 8 + server/src/app.ts | 69 +++--- server/src/helpers/logger.ts | 5 +- server/src/helpers/loginStorage.ts | 2 +- server/src/middlewares.ts | 3 +- server/src/routes/api/get/albumSearch.ts | 2 +- server/src/routes/api/get/newReleases.ts | 2 +- server/src/routes/api/post/loginArl.ts | 16 +- webui/tests/unit/utils/dates.spec.js | 2 +- 42 files changed, 917 insertions(+), 688 deletions(-) rename deemix/src/{decryption.js => decryption.ts} (87%) rename deemix/src/{downloader.js => downloader.ts} (88%) rename deemix/src/{errors.js => errors.ts} (61%) rename deemix/src/{itemgen.js => itemgen.ts} (73%) rename deemix/src/plugins/{index.js => base.ts} (68%) create mode 100644 deemix/src/plugins/index.ts rename deemix/src/plugins/{spotify.js => spotify.ts} (90%) rename deemix/src/{settings.js => settings.ts} (62%) rename deemix/src/{tagger.js => tagger.ts} (96%) rename deemix/src/types/{Album.js => Album.ts} (83%) rename deemix/src/types/{Artist.js => Artist.ts} (60%) rename deemix/src/types/{Date.js => CustomDate.ts} (87%) rename deemix/src/types/{DownloadObjects.js => DownloadObjects.ts} (78%) rename deemix/src/types/{Lyrics.js => Lyrics.ts} (87%) rename deemix/src/types/{Picture.js => Picture.ts} (81%) rename deemix/src/types/{Playlist.js => Playlist.ts} (72%) rename deemix/src/types/{Track.js => Track.ts} (84%) rename deemix/src/utils/{index.js => core.ts} (87%) delete mode 100644 deemix/src/utils/deezer.js create mode 100644 deemix/src/utils/deezer.ts rename deemix/src/utils/{download-utils.js => downloadUtils.ts} (91%) create mode 100644 deemix/src/utils/index.ts rename deemix/src/utils/{localpaths.js => localpaths.ts} (90%) rename deemix/src/utils/{pathtemplates.js => pathtemplates.ts} (93%) rename deezer-js/src/{utils.js => utils.ts} (96%) diff --git a/deemix/package.json b/deemix/package.json index c528c9c6..220c5366 100644 --- a/deemix/package.json +++ b/deemix/package.json @@ -19,6 +19,7 @@ "tough-cookie": "^4.0.0" }, "devDependencies": { + "@types/async": "^3.2.24", "tsup": "^8.2.4" } } diff --git a/deemix/src/decryption.js b/deemix/src/decryption.ts similarity index 87% rename from deemix/src/decryption.js rename to deemix/src/decryption.ts index 7d4cbe21..7684acb7 100644 --- a/deemix/src/decryption.js +++ b/deemix/src/decryption.ts @@ -1,17 +1,17 @@ -const got = require("got"); -const fs = require("fs"); -const { +import got from "got"; +import fs from "fs"; +import { _md5, _ecbCrypt, _ecbDecrypt, generateBlowfishKey, decryptChunk, -} = require("./utils/crypto.js"); -const { DownloadCanceled, DownloadEmpty } = require("./errors.js"); +} from "./utils/crypto"; +import { DownloadCanceled, DownloadEmpty } from "./errors"; -const { USER_AGENT_HEADER, pipeline } = require("./utils/index.js"); +import { USER_AGENT_HEADER, pipeline } from "./utils/index"; -function generateStreamPath(sngID, md5, mediaVersion, format) { +export function generateStreamPath(sngID, md5, mediaVersion, format) { let urlPart = md5 + "¤" + format + "¤" + sngID + "¤" + mediaVersion; const md5val = _md5(urlPart); let step2 = md5val + "¤" + urlPart + "¤"; @@ -20,28 +20,28 @@ function generateStreamPath(sngID, md5, mediaVersion, format) { return urlPart; } -function reverseStreamPath(urlPart) { +export function reverseStreamPath(urlPart) { const step2 = _ecbDecrypt("jo6aey6haid2Teih", urlPart); const [, md5, format, sngID, mediaVersion] = step2.split("¤"); return [sngID, md5, mediaVersion, format]; } -function generateCryptedStreamURL(sngID, md5, mediaVersion, format) { +export function generateCryptedStreamURL(sngID, md5, mediaVersion, format) { const urlPart = generateStreamPath(sngID, md5, mediaVersion, format); return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/mobile/1/" + urlPart; } -function generateStreamURL(sngID, md5, mediaVersion, format) { +export function generateStreamURL(sngID, md5, mediaVersion, format) { const urlPart = generateStreamPath(sngID, md5, mediaVersion, format); return "https://cdns-proxy-" + md5[0] + ".dzcdn.net/api/1/" + urlPart; } -function reverseStreamURL(url) { +export function reverseStreamURL(url) { const urlPart = url.slice(url.find("/1/") + 3); return reverseStreamPath(urlPart); } -async function streamTrack(writepath, track, downloadObject, listener) { +export async function streamTrack(writepath, track, downloadObject, listener) { if (downloadObject && downloadObject.isCanceled) throw new DownloadCanceled(); const headers = { "User-Agent": USER_AGENT_HEADER }; let chunkLength = 0; @@ -214,12 +214,3 @@ async function streamTrack(writepath, track, downloadObject, listener) { } } } - -module.exports = { - generateStreamPath, - generateStreamURL, - generateCryptedStreamURL, - reverseStreamPath, - reverseStreamURL, - streamTrack, -}; diff --git a/deemix/src/downloader.js b/deemix/src/downloader.ts similarity index 88% rename from deemix/src/downloader.js rename to deemix/src/downloader.ts index a82e342c..a7558616 100644 --- a/deemix/src/downloader.js +++ b/deemix/src/downloader.ts @@ -1,34 +1,39 @@ -const { Track } = require("./types/Track.js"); -const { StaticPicture } = require("./types/Picture.js"); -const { streamTrack, generateCryptedStreamURL } = require("./decryption.js"); -const { - USER_AGENT_HEADER, - pipeline, - shellEscape, -} = require("./utils/index.js"); -const { DEFAULTS, OverwriteOption } = require("./settings.js"); -const { - generatePath, +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 { tmpdir } from "os"; +import { generateCryptedStreamURL, streamTrack } from "./decryption"; +import { + DownloadCanceled, + DownloadFailed, + ErrorMessages, + PreferredBitrateNotFound, + TrackNot360, +} 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 { checkShouldDownload, tagTrack } from "./utils/downloadUtils"; +import { generateAlbumName, generateArtistName, generateDownloadObjectName, -} = require("./utils/pathtemplates.js"); -const { - PreferredBitrateNotFound, - TrackNot360, - DownloadFailed, - ErrorMessages, - DownloadCanceled, -} = require("./errors.js"); -const { TrackFormats } = require("deezer-js"); -const { WrongLicense, WrongGeolocation } = require("deezer-js").errors; -const { map_track } = require("deezer-js").utils; -const got = require("got"); -const fs = require("fs"); -const { tmpdir } = require("os"); -const { queue, each } = require("async"); -const { exec } = require("child_process"); -const { checkShouldDownload, tagTrack } = require("./utils/download-utils"); + generatePath, +} from "./utils/pathtemplates"; + +const { WrongLicense, WrongGeolocation } = _errors; +const { map_track } = utils; const extensions = { [TrackFormats.FLAC]: ".flac", @@ -39,40 +44,40 @@ const extensions = { [TrackFormats.MP4_RA3]: ".mp4", [TrackFormats.MP4_RA2]: ".mp4", [TrackFormats.MP4_RA1]: ".mp4", -}; +} as const; -const formatsName = { +const formats_non_360 = { [TrackFormats.FLAC]: "FLAC", - [TrackFormats.LOCAL]: "MP3_MISC", [TrackFormats.MP3_320]: "MP3_320", [TrackFormats.MP3_128]: "MP3_128", - [TrackFormats.DEFAULT]: "MP3_MISC", +}; +const formats_360 = { [TrackFormats.MP4_RA3]: "MP4_RA3", [TrackFormats.MP4_RA2]: "MP4_RA2", [TrackFormats.MP4_RA1]: "MP4_RA1", }; const TEMPDIR = tmpdir() + "/deemix-imgs"; -fs.mkdirSync(TEMPDIR, { recursive: true }); +mkdirSync(TEMPDIR, { recursive: true }); async function downloadImage( - url, - path, + url: string, + path: string, overwrite = OverwriteOption.DONT_OVERWRITE ) { if ( - fs.existsSync(path) && + existsSync(path) && ![ OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS, OverwriteOption.KEEP_BOTH, ].includes(overwrite) ) { - const file = fs.readFileSync(path); + const file = readFileSync(path); if (file.length !== 0) return path; - fs.unlinkSync(path); + unlinkSync(path); } - let timeout = null; + let timeout: NodeJS.Timeout | null = null; let error = ""; const downloadStream = got @@ -87,7 +92,7 @@ async function downloadImage( downloadStream.destroy(); }, 5000); }); - const fileWriterStream = fs.createWriteStream(path); + const fileWriterStream = createWriteStream(path); timeout = setTimeout(() => { error = "DownloadTimeout"; @@ -97,8 +102,8 @@ async function downloadImage( try { await pipeline(downloadStream, fileWriterStream); } catch (e) { - fs.unlinkSync(path); - if (e instanceof got.HTTPError) { + 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); @@ -117,8 +122,8 @@ async function downloadImage( return null; } if ( - e instanceof got.ReadError || - e instanceof got.TimeoutError || + e instanceof ReadError || + e instanceof TimeoutError || [ "ESOCKETTIMEDOUT", "ERR_STREAM_PREMATURE_CLOSE", @@ -136,22 +141,22 @@ async function downloadImage( } async function getPreferredBitrate( - dz, - track, - preferredBitrate, - shouldFallback, - feelingLucky, - uuid, - listener + 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 hasAlternative = track.fallbackID !== "0"; let isGeolocked = false; let wrongLicense = false; - async function testURL(track, url, formatName) { + async function testURL(track: Track, url: string, formatName: string) { if (!url) return false; let request; try { @@ -175,16 +180,21 @@ async function getPreferredBitrate( if (track.filesizes[`${formatName.toLowerCase()}`] === 0) return false; return true; } - if (e instanceof got.ReadError || e instanceof got.TimeoutError) { + if (e instanceof ReadError || e instanceof TimeoutError) { return await testURL(track, url, formatName); } - if (e instanceof got.HTTPError) return false; + if (e instanceof HTTPError) return false; console.trace(e); throw e; } } - async function getCorrectURL(track, formatName, formatNumber, feelingLucky) { + async function getCorrectURL( + track: Track, + formatName: string, + formatNumber: number, + feelingLucky: boolean + ) { // Check the track with the legit method let url; wrongLicense = @@ -210,7 +220,7 @@ async function getPreferredBitrate( track.mediaVersion, formatNumber ); - if (await testURL(track, url, formatName, formatNumber)) return url; + if (await testURL(track, url, formatName)) return url; url = undefined; } return url; @@ -227,17 +237,6 @@ async function getPreferredBitrate( return TrackFormats.LOCAL; } - 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 is360Format = Object.keys(formats_360).includes(preferredBitrate); let formats; if (!shouldFallback) { @@ -322,7 +321,21 @@ async function getPreferredBitrate( } class Downloader { - constructor(dz, downloadObject, settings, listener) { + dz: Deezer; + downloadObject: IDownloadObject; + settings: Settings; + bitrate: any; + listener: any; + playlistCovername: null; + playlistURLs: any[]; + coverQueue: {}; + + constructor( + dz: Deezer, + downloadObject: IDownloadObject, + settings: Settings, + listener + ) { this.dz = dz; this.downloadObject = downloadObject; this.settings = settings || DEFAULTS; @@ -367,18 +380,23 @@ class Downloader { } else if (this.downloadObject.__type__ === "Collection") { const tracks = []; - const q = queue(async (data) => { - const { track, pos } = data; - tracks[pos] = await this.downloadWrapper({ - trackAPI: track, - albumAPI: this.downloadObject.collection.albumAPI, - playlistAPI: this.downloadObject.collection.playlistAPI, - }); - }, this.settings.queueConcurrency); + const q = queue( + async (data: { track: Track; pos: number }, callback) => { + const { track, pos } = data; + tracks[pos] = await this.downloadWrapper({ + trackAPI: track, + albumAPI: this.downloadObject.collection.albumAPI, + playlistAPI: this.downloadObject.collection.playlistAPI, + }); + + callback(); + }, + this.settings.queueConcurrency + ); if (this.downloadObject.collection.tracks.length) { this.downloadObject.collection.tracks.forEach((track, pos) => { - q.push({ track, pos }); + q.push({ track, pos }, () => {}); }); await q.drain(); @@ -397,7 +415,7 @@ class Downloader { } } - async download(extraData, track) { + async download(extraData, track: Track) { const returnData = {}; const { trackAPI, albumAPI, playlistAPI } = extraData; trackAPI.size = this.downloadObject.size; @@ -477,7 +495,7 @@ class Downloader { if (this.downloadObject.isCanceled) throw new DownloadCanceled(); // Make sure the filepath exsists - fs.mkdirSync(filepath, { recursive: true }); + mkdirSync(filepath, { recursive: true }); const extension = extensions[track.bitrate]; let writepath = `${filepath}/${filename}${extension}`; @@ -488,7 +506,7 @@ class Downloader { do { c++; currentFilename = `${baseFilename} (${c})${extension}`; - } while (fs.existsSync(currentFilename)); + } while (existsSync(currentFilename)); writepath = currentFilename; } @@ -660,12 +678,12 @@ class Downloader { // Save lyrics in lrc file if (this.settings.syncedLyrics && track.lyrics.sync) { if ( - !fs.existsSync(`${filepath}/${filename}.lrc`) || + !existsSync(`${filepath}/${filename}.lrc`) || [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS].includes( this.settings.overwriteFile ) ) { - fs.writeFileSync(`${filepath}/${filename}.lrc`, track.lyrics.sync); + writeFileSync(`${filepath}/${filename}.lrc`, track.lyrics.sync); } } @@ -675,7 +693,7 @@ class Downloader { try { await streamTrack(writepath, track, this.downloadObject, this.listener); } catch (e) { - if (e instanceof got.HTTPError) + if (e instanceof HTTPError) throw new DownloadFailed("notAvailable", track); throw e; } @@ -702,7 +720,7 @@ class Downloader { return returnData; } - async downloadWrapper(extraData, track) { + async downloadWrapper(extraData, track: Track) { const { trackAPI } = extraData; // Temp metadata to generate logs const itemData = { @@ -871,16 +889,16 @@ class Downloader { const filename = `${track.data.artist} - ${track.data.title}`; let searchedFile; try { - searchedFile = fs - .readFileSync(`${this.downloadObject.extrasPath}/searched.txt`) - .toString(); + searchedFile = readFileSync( + `${this.downloadObject.extrasPath}/searched.txt` + ).toString(); } catch { searchedFile = ""; } if (searchedFile.indexOf(filename) === -1) { if (searchedFile !== "") searchedFile += "\r\n"; searchedFile += filename + "\r\n"; - fs.writeFileSync( + writeFileSync( `${this.downloadObject.extrasPath}/searched.txt`, searchedFile ); @@ -973,10 +991,7 @@ class Downloader { // Create errors logfile try { if (this.settings.logErrors && errors !== "") { - fs.writeFileSync( - `${this.downloadObject.extrasPath}/errors.txt`, - errors - ); + writeFileSync(`${this.downloadObject.extrasPath}/errors.txt`, errors); } } catch (e) { this.afterDownloadErrorReport("CreateErrorLog", e); @@ -985,7 +1000,7 @@ class Downloader { // Create searched logfile try { if (this.settings.logSearched && searched !== "") { - fs.writeFileSync( + writeFileSync( `${this.downloadObject.extrasPath}/searched.txt`, searched ); @@ -1022,7 +1037,7 @@ class Downloader { this.downloadObject, this.settings ) || "playlist"; - fs.writeFileSync( + writeFileSync( `${this.downloadObject.extrasPath}/${filename}.m3u8`, playlist.join("\n") ); @@ -1056,7 +1071,7 @@ class Downloader { } } -module.exports = { +export default { Downloader, downloadImage, getPreferredBitrate, diff --git a/deemix/src/errors.js b/deemix/src/errors.ts similarity index 61% rename from deemix/src/errors.js rename to deemix/src/errors.ts index ecb33e62..501e3328 100644 --- a/deemix/src/errors.js +++ b/deemix/src/errors.ts @@ -1,82 +1,96 @@ -class DeemixError extends Error { - constructor(message) { +import Track from "./types/Track"; + +export class DeemixError extends Error { + constructor(message?: string) { super(message); this.name = "DeemixError"; } } -class GenerationError extends DeemixError { - constructor(link, message) { +export class GenerationError extends DeemixError { + link: string; + + constructor(link: string, message: string) { super(message); this.link = link; this.name = "GenerationError"; } } -class ISRCnotOnDeezer extends GenerationError { - constructor(link) { +export class ISRCnotOnDeezer extends GenerationError { + errid: string; + + constructor(link: string) { super(link, "Track ISRC is not available on deezer"); this.name = "ISRCnotOnDeezer"; this.errid = "ISRCnotOnDeezer"; } } -class NotYourPrivatePlaylist extends GenerationError { - constructor(link) { +export class NotYourPrivatePlaylist extends GenerationError { + errid: string; + constructor(link: string) { super(link, "You can't download others private playlists."); this.name = "NotYourPrivatePlaylist"; this.errid = "notYourPrivatePlaylist"; } } -class TrackNotOnDeezer extends GenerationError { - constructor(link) { +export class TrackNotOnDeezer extends GenerationError { + errid: string; + constructor(link: string) { super(link, "Track not found on deezer!"); this.name = "TrackNotOnDeezer"; this.errid = "trackNotOnDeezer"; } } -class AlbumNotOnDeezer extends GenerationError { - constructor(link) { +export class AlbumNotOnDeezer extends GenerationError { + errid: string; + constructor(link: string) { super(link, "Album not found on deezer!"); this.name = "AlbumNotOnDeezer"; this.errid = "albumNotOnDeezer"; } } -class InvalidID extends GenerationError { - constructor(link) { +export class InvalidID extends GenerationError { + errid: string; + constructor(link: string) { super(link, "Link ID is invalid!"); this.name = "InvalidID"; this.errid = "invalidID"; } } -class LinkNotSupported extends GenerationError { - constructor(link) { +export class LinkNotSupported extends GenerationError { + errid: string; + + constructor(link: string) { super(link, "Link is not supported."); this.name = "LinkNotSupported"; this.errid = "unsupportedURL"; } } -class LinkNotRecognized extends GenerationError { - constructor(link) { +export class LinkNotRecognized extends GenerationError { + errid: string; + + constructor(link: string) { super(link, "Link is not recognized."); this.name = "LinkNotRecognized"; this.errid = "invalidURL"; } } -class DownloadError extends DeemixError { +export class DownloadError extends DeemixError { constructor() { super(); this.name = "DownloadError"; } } -const ErrorMessages = { +export const ErrorMessages = { notOnDeezer: "Track not available on Deezer!", notEncoded: "Track not yet encoded!", notEncodedNoAlternative: "Track not yet encoded and no alternative found!", @@ -98,9 +112,13 @@ const ErrorMessages = { "Your account can't stream the track from your current country and no alternative found.", }; -class DownloadFailed extends DownloadError { - constructor(errid, track) { +export class DownloadFailed extends DownloadError { + errid: keyof typeof ErrorMessages; + track: Track; + + constructor(errid: keyof typeof ErrorMessages, track: Track) { super(); + this.errid = errid; this.message = ErrorMessages[errid]; this.name = "DownloadFailed"; @@ -108,81 +126,58 @@ class DownloadFailed extends DownloadError { } } -class TrackNot360 extends DownloadError { +export class TrackNot360 extends DownloadError { constructor() { super(); this.name = "TrackNot360"; } } -class PreferredBitrateNotFound extends DownloadError { +export class PreferredBitrateNotFound extends DownloadError { constructor() { super(); this.name = "PreferredBitrateNotFound"; } } -class DownloadEmpty extends DeemixError { +export class DownloadEmpty extends DeemixError { constructor() { super(); this.name = "DownloadEmpty"; } } -class DownloadCanceled extends DeemixError { +export class DownloadCanceled extends DeemixError { constructor() { super(); this.name = "DownloadCanceled"; } } -class TrackError extends DeemixError { - constructor(message) { +export class TrackError extends DeemixError { + constructor(message: string) { super(message); this.name = "TrackError"; } } -class MD5NotFound extends TrackError { - constructor(message) { +export class MD5NotFound extends TrackError { + constructor(message: any) { super(message); this.name = "MD5NotFound"; } } -class NoDataToParse extends TrackError { - constructor(message) { +export class NoDataToParse extends TrackError { + constructor(message: any) { super(message); this.name = "NoDataToParse"; } } -class AlbumDoesntExists extends TrackError { - constructor(message) { +export class AlbumDoesntExists extends TrackError { + constructor(message: any) { super(message); this.name = "AlbumDoesntExists"; } } - -module.exports = { - DeemixError, - GenerationError, - ISRCnotOnDeezer, - NotYourPrivatePlaylist, - TrackNotOnDeezer, - AlbumNotOnDeezer, - InvalidID, - LinkNotSupported, - LinkNotRecognized, - ErrorMessages, - DownloadError, - DownloadFailed, - TrackNot360, - PreferredBitrateNotFound, - DownloadEmpty, - DownloadCanceled, - TrackError, - MD5NotFound, - NoDataToParse, - AlbumDoesntExists, -}; diff --git a/deemix/src/index.ts b/deemix/src/index.ts index 5765e3e5..00b4c923 100644 --- a/deemix/src/index.ts +++ b/deemix/src/index.ts @@ -1,25 +1,15 @@ +import { Deezer } from "deezer-js"; import got from "got"; +import downloader from "./downloader"; +import { LinkNotRecognized, LinkNotSupported } from "./errors"; import { - generateTrackItem, generateAlbumItem, - generatePlaylistItem, generateArtistItem, generateArtistTopItem, -} from "./itemgen.js"; -import { LinkNotSupported, LinkNotRecognized } from "./errors.js"; -import { Deezer } from "deezer-js"; - -import * as typesIndex from "./types"; -import settings from "./settings.js"; -import downloader from "./downloader.js"; -import decryption from "./decryption.js"; -import tagger from "./tagger.js"; -import * as utilsIndex from "./utils/index.js"; -import localpaths from "./utils/localpaths.js"; -import pathtemplates from "./utils/pathtemplates.js"; -import deezer from "./utils/deezer.js"; -import Plugin from "./plugins/index.js"; -import Spotify from "./plugins/spotify.js"; + generatePlaylistItem, + generateTrackItem, +} from "./itemgen"; +import BasePlugin from "@/plugins/base"; async function parseLink(link: string) { if (link.includes("deezer.page.link")) { @@ -66,7 +56,7 @@ async function generateDownloadObject( dz: Deezer, link: string, bitrate: number, - plugins: Record = {}, + plugins: Record = {}, listener: any ) { let link_type, link_id; @@ -106,11 +96,6 @@ async function generateDownloadObject( throw new LinkNotSupported(link); } -// Aggregating the exports into organized objects -export const types = { - ...typesIndex, -}; - const itemgen = { generateTrackItem, generateAlbumItem, @@ -119,26 +104,12 @@ const itemgen = { generateArtistTopItem, }; -const utils = { - ...utilsIndex, - localpaths, - pathtemplates, - deezer, -}; - -const plugins = { - Spotify, -}; +export * as decryption from "./decryption"; +export * as plugins from "./plugins"; +export * as settings from "./settings"; +export * as tagger from "./tagger"; +export * as types from "./types"; +export * as utils from "./utils"; // Exporting the organized objects -export { - parseLink, - generateDownloadObject, - settings, - downloader, - decryption, - tagger, - itemgen, - utils, - plugins, -}; +export { downloader, generateDownloadObject, itemgen, parseLink }; diff --git a/deemix/src/itemgen.js b/deemix/src/itemgen.ts similarity index 73% rename from deemix/src/itemgen.js rename to deemix/src/itemgen.ts index 77848c9f..077c8b16 100644 --- a/deemix/src/itemgen.js +++ b/deemix/src/itemgen.ts @@ -1,14 +1,30 @@ -const { Single, Collection } = require("./types/DownloadObjects.js"); -const { +import { Single, Collection } from "./types/DownloadObjects"; +import { GenerationError, ISRCnotOnDeezer, InvalidID, NotYourPrivatePlaylist, -} = require("./errors.js"); +} from "./errors"; const { map_user_playlist, map_track, map_album } = require("deezer-js").utils; -const { each } = require("async"); +import { each } from "async"; +import { Deezer } from "deezer-js"; +import { Album } from "./types/Album"; -async function generateTrackItem(dz, id, bitrate, trackAPI, albumAPI) { +export async function generateTrackItem( + dz: Deezer, + id: string, + bitrate: number, + trackAPI?: { + id: any; + title: any; + album: { cover_small: string }; + md5_image: any; + track_token: any; + artist: { name: any }; + explicit_lyrics: any; + }, + albumAPI?: any +) { // Get essential track info if (!trackAPI) { if (String(id).startsWith("isrc") || parseInt(id) > 0) { @@ -33,7 +49,7 @@ async function generateTrackItem(dz, id, bitrate, trackAPI, albumAPI) { if (!/^-?\d+$/.test(id)) throw new InvalidID(`https://deezer.com/track/${id}`); - let cover; + let cover: string; if (trackAPI.album.cover_small) { cover = trackAPI.album.cover_small.slice(0, -24) + "/75x75-000000-80-0-0.jpg"; @@ -58,13 +74,28 @@ async function generateTrackItem(dz, id, bitrate, trackAPI, albumAPI) { }); } -async function generateAlbumItem(dz, id, bitrate, rootArtist) { +export async function generateAlbumItem( + dz: Deezer, + id: string | any[], + bitrate: number, + rootArtist?: { id: any; name: any; picture_small: any } +) { // Get essential album info - let albumAPI; + let albumAPI: { + id: any; + root_artist: any; + nb_tracks: number; + tracks: { data: string | any[] }; + cover_small: string; + md5_image: any; + title: any; + artist: { name: any }; + explicit_lyrics: any; + }; if (String(id).startsWith("upc")) { - const upcs = [id.slice(4)]; - upcs.push(parseInt(upcs[0])); // Try UPC without leading zeros as well - let lastError; + const upcs = [id.slice(4).toString()]; + upcs.push(parseInt(upcs[0], 10).toString()); // Try UPC without leading zeros as well + let lastError: { message: string }; await each(upcs, async (upc) => { try { albumAPI = await dz.api.get_album(`upc:${upc}`); @@ -100,7 +131,8 @@ async function generateAlbumItem(dz, id, bitrate, rootArtist) { throw new GenerationError(`https://deezer.com/album/${id}`, e.message); } } - if (!/^\d+$/.test(id)) throw new InvalidID(`https://deezer.com/album/${id}`); + if (!/^\d+$/.test(String(id))) + throw new InvalidID(`https://deezer.com/album/${id}`); // Get extra info about album // This saves extra api calls when downloading @@ -128,7 +160,7 @@ async function generateAlbumItem(dz, id, bitrate, rootArtist) { const tracksArray = await dz.gw.get_album_tracks(id); - let cover; + let cover: string; if (albumAPI.cover_small) { cover = albumAPI.cover_small.slice(0, -24) + "/75x75-000000-80-0-0.jpg"; } else { @@ -138,12 +170,14 @@ async function generateAlbumItem(dz, id, bitrate, rootArtist) { const totalSize = tracksArray.length; albumAPI.nb_tracks = totalSize; const collection = []; - tracksArray.forEach((trackAPI, pos) => { - trackAPI = map_track(trackAPI); - delete trackAPI.track_token; - trackAPI.position = pos + 1; - collection.push(trackAPI); - }); + tracksArray.forEach( + (trackAPI: { track_token: any; position: any }, pos: number) => { + trackAPI = map_track(trackAPI); + delete trackAPI.track_token; + trackAPI.position = pos + 1; + collection.push(trackAPI); + } + ); return new Collection({ type: "album", @@ -161,12 +195,36 @@ async function generateAlbumItem(dz, id, bitrate, rootArtist) { }); } -async function generatePlaylistItem( - dz, - id, - bitrate, - playlistAPI, - playlistTracksAPI +export async function generatePlaylistItem( + dz: Deezer, + id: string, + bitrate: number, + playlistAPI?: { + id?: string; + title: any; + description?: string; + duration?: number; + public: any; + is_loved_track?: boolean; + collaborative?: boolean; + nb_tracks: any; + fans?: any; + link?: string; + share?: any; + picture?: any; + picture_small: any; + picture_medium?: any; + picture_big?: any; + picture_xl?: any; + checksum?: any; + tracklist?: string; + creation_date?: string; + creator: any; + type?: string; + various_artist?: any; + explicit?: any; + }, + playlistTracksAPI?: any[] ) { if (!playlistAPI) { if (!/^\d+$/.test(id)) @@ -205,15 +263,20 @@ async function generatePlaylistItem( const totalSize = playlistTracksAPI.length; playlistAPI.nb_tracks = totalSize; const collection = []; - playlistTracksAPI.forEach((trackAPI, pos) => { - trackAPI = map_track(trackAPI); - if (trackAPI.explicit_lyrics) { - playlistAPI.explicit = true; + playlistTracksAPI.forEach( + ( + trackAPI: { explicit_lyrics: any; track_token: any; position: any }, + pos: number + ) => { + trackAPI = map_track(trackAPI); + if (trackAPI.explicit_lyrics) { + playlistAPI.explicit = true; + } + delete trackAPI.track_token; + trackAPI.position = pos + 1; + collection.push(trackAPI); } - delete trackAPI.track_token; - trackAPI.position = pos + 1; - collection.push(trackAPI); - }); + ); if (!playlistAPI.explicit) playlistAPI.explicit = false; @@ -233,14 +296,25 @@ async function generatePlaylistItem( }); } -async function generateArtistItem(dz, id, bitrate, listener, tab = "all") { +export async function generateArtistItem( + dz: Deezer, + id: string, + bitrate: number, + listener: { + send: ( + arg0: string, + arg1: { id: any; name: any; picture_small: any } + ) => void; + }, + tab = "all" +) { let path = ""; if (tab !== "all") path = "/" + tab; if (!/^\d+$/.test(id)) throw new InvalidID(`https://deezer.com/artist/${id}${path}`); // Get essential artist info - let artistAPI; + let artistAPI: { id: any; name: any; picture_small: any }; try { artistAPI = await dz.api.get_artist(id); } catch (e) { @@ -280,7 +354,7 @@ async function generateArtistItem(dz, id, bitrate, listener, tab = "all") { }); } else { const tabReleases = artistDiscographyAPI[tab] || []; - await each(tabReleases, async (album) => { + await each(tabReleases, async (album: Album) => { try { const albumData = await generateAlbumItem( dz, @@ -301,11 +375,24 @@ async function generateArtistItem(dz, id, bitrate, listener, tab = "all") { return albumList; } -async function generateArtistTopItem(dz, id, bitrate) { +export async function generateArtistTopItem( + dz: Deezer, + id: string, + bitrate: number +) { if (!/^\d+$/.test(id)) throw new InvalidID(`https://deezer.com/artist/${id}/top_track`); // Get essential artist info - let artistAPI; + let artistAPI: { + id: string; + name: string; + nb_fan: any; + picture: any; + picture_small: any; + picture_medium: any; + picture_big: any; + picture_xl: any; + }; try { artistAPI = await dz.api.get_artist(id); } catch (e) { @@ -355,11 +442,3 @@ async function generateArtistTopItem(dz, id, bitrate) { artistTopTracksAPI_gw ); } - -module.exports = { - generateTrackItem, - generateAlbumItem, - generatePlaylistItem, - generateArtistItem, - generateArtistTopItem, -}; diff --git a/deemix/src/plugins/index.js b/deemix/src/plugins/base.ts similarity index 68% rename from deemix/src/plugins/index.js rename to deemix/src/plugins/base.ts index 4ee6dacf..3d1c2003 100644 --- a/deemix/src/plugins/index.js +++ b/deemix/src/plugins/base.ts @@ -1,8 +1,10 @@ -class Plugin { +export default class BasePlugin { /* constructor () {} */ - async setup() {} + setup() { + return this; + } - async parseLink(link) { + async parseLink(link: string) { return [link, undefined, undefined]; } @@ -11,5 +13,3 @@ class Plugin { return null; } } - -module.exports = Plugin; diff --git a/deemix/src/plugins/index.ts b/deemix/src/plugins/index.ts new file mode 100644 index 00000000..80640f9a --- /dev/null +++ b/deemix/src/plugins/index.ts @@ -0,0 +1 @@ +export { default as Spotify } from "./spotify"; diff --git a/deemix/src/plugins/spotify.js b/deemix/src/plugins/spotify.ts similarity index 90% rename from deemix/src/plugins/spotify.js rename to deemix/src/plugins/spotify.ts index fb80a0e8..521c90b5 100644 --- a/deemix/src/plugins/spotify.js +++ b/deemix/src/plugins/spotify.ts @@ -1,20 +1,24 @@ -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 { SpotifyApi } = require("@spotify/web-api-ts-sdk"); -const got = require("got"); -const { queue } = require("async"); - -class Spotify extends Plugin { +import BasePlugin from "./base"; +import { getConfigFolder } from "../utils/localpaths"; +import { generateTrackItem, generateAlbumItem } from "../itemgen"; +import { TrackNotOnDeezer, AlbumNotOnDeezer, InvalidID } from "../errors"; +import { Convertable, Collection } from "../types/DownloadObjects"; +import { sep } from "path"; +import fs from "fs"; +import { SpotifyApi } from "@spotify/web-api-ts-sdk"; +import got from "got"; +import { queue } from "async"; +import { Deezer } from "deezer-js"; +import { Settings } from "@/settings"; +import Track from "@/types/Track"; + +export default class Spotify extends BasePlugin { + credentials: { clientId: string; clientSecret: string }; + settings: { fallbackSearch: boolean }; + enabled: boolean; + configFolder: any; + sp: any; + constructor(configFolder = undefined) { super(); this.credentials = { clientId: "", clientSecret: "" }; @@ -79,7 +83,7 @@ class Spotify extends Plugin { } } - async generateTrackItem(dz, link_id, bitrate) { + async generateTrackItem(dz: Deezer, link_id: string, bitrate: number) { const cache = this.loadCache(); let cachedTrack; @@ -117,7 +121,7 @@ class Spotify extends Plugin { throw new TrackNotOnDeezer(`https://open.spotify.com/track/${link_id}`); } - async generateAlbumItem(dz, link_id, bitrate) { + async generateAlbumItem(dz: Deezer, link_id, bitrate) { const cache = this.loadCache(); let cachedAlbum; @@ -242,7 +246,12 @@ class Spotify extends Plugin { return cachedAlbum; } - async convert(dz, downloadObject, settings, listener = null) { + async convert( + dz: Deezer, + downloadObject: Convertable, + settings: Settings, + listener: any = null + ): Promise { const cache = this.loadCache(); let conversion = 0; @@ -250,7 +259,7 @@ class Spotify extends Plugin { const collection = []; if (listener) listener.send("startConversion", downloadObject.uuid); - const q = queue(async (data) => { + const q = queue(async (data: { track: Track; pos: number }, callback) => { const { track, pos } = data; if (downloadObject.isCanceled) return; @@ -310,6 +319,7 @@ class Spotify extends Plugin { collection[pos] = trackAPI; conversionNext += (1 / downloadObject.size) * 100; + if ( Math.round(conversionNext) !== conversion && Math.round(conversionNext) % 2 === 0 @@ -321,22 +331,25 @@ class Spotify extends Plugin { conversion, }); } + + callback(); }, settings.queueConcurrency); downloadObject.conversion_data.forEach((track, pos) => { - q.push({ track, pos }); + q.push({ track, pos }, () => {}); }); await q.drain(); downloadObject.collection.tracks = collection; downloadObject.size = collection.length; - downloadObject = new Collection(downloadObject.toDict()); + + const returnCollection = new Collection(downloadObject.toDict()); if (listener) - listener.send("finishConversion", downloadObject.getSlimmedDict()); + listener.send("finishConversion", returnCollection.getSlimmedDict()); fs.writeFileSync(this.configFolder + "cache.json", JSON.stringify(cache)); - return downloadObject; + return returnCollection; } _convertPlaylistStructure(spotifyPlaylist) { @@ -514,5 +527,3 @@ class Spotify extends Plugin { this.saveSettings(); } } - -module.exports = Spotify; diff --git a/deemix/src/settings.js b/deemix/src/settings.ts similarity index 62% rename from deemix/src/settings.js rename to deemix/src/settings.ts index b5d87c57..9713c214 100644 --- a/deemix/src/settings.js +++ b/deemix/src/settings.ts @@ -1,9 +1,9 @@ -const { TrackFormats } = require("deezer-js"); -const { getMusicFolder, getConfigFolder } = require("./utils/localpaths.js"); -const fs = require("fs"); +import { TrackFormats } from "deezer-js"; +import { getMusicFolder, getConfigFolder } from "./utils/localpaths"; +import fs from "fs"; // Should the lib overwrite files? -const OverwriteOption = { +export const OverwriteOption = { OVERWRITE: "y", // Yes, overwrite the file DONT_OVERWRITE: "n", // No, don't overwrite the file DONT_CHECK_EXT: "e", // No, and don't check for extensions @@ -13,14 +13,100 @@ const OverwriteOption = { }; // What should I do with featured artists? -const FeaturesOption = { +export const FeaturesOption = { NO_CHANGE: "0", // Do nothing REMOVE_TITLE: "1", // Remove from track title REMOVE_TITLE_ALBUM: "3", // Remove from track title and album title MOVE_TITLE: "2", // Move to track title }; -const DEFAULTS = { +export interface Tags { + title: boolean; + artist: boolean; + artists: boolean; + album: boolean; + cover: boolean; + trackNumber: boolean; + trackTotal: boolean; + discNumber: boolean; + discTotal: boolean; + albumArtist: boolean; + genre: boolean; + year: boolean; + date: boolean; + explicit: boolean; + isrc: boolean; + length: boolean; + barcode: boolean; + bpm: boolean; + replayGain: boolean; + label: boolean; + lyrics: boolean; + syncedLyrics: boolean; + copyright: boolean; + composer: boolean; + involvedPeople: boolean; + source: boolean; + rating: boolean; + savePlaylistAsCompilation: boolean; + useNullSeparator: boolean; + saveID3v1: boolean; + multiArtistSeparator: string; + singleAlbumArtist: boolean; + coverDescriptionUTF8: boolean; +} + +export interface Settings { + downloadLocation: string; + tracknameTemplate: string; + albumTracknameTemplate: string; + playlistTracknameTemplate: string; + createPlaylistFolder: boolean; + playlistNameTemplate: string; + createArtistFolder: boolean; + artistNameTemplate: string; + createAlbumFolder: boolean; + albumNameTemplate: string; + createCDFolder: boolean; + createStructurePlaylist: boolean; + createSingleFolder: boolean; + padTracks: boolean; + padSingleDigit: boolean; + paddingSize: string; + illegalCharacterReplacer: string; + queueConcurrency: number; + maxBitrate: string; + feelingLucky: boolean; + fallbackBitrate: boolean; + fallbackSearch: boolean; + fallbackISRC: boolean; + logErrors: boolean; + logSearched: boolean; + overwriteFile: string; + createM3U8File: boolean; + playlistFilenameTemplate: string; + syncedLyrics: boolean; + embeddedArtworkSize: number; + embeddedArtworkPNG: boolean; + localArtworkSize: number; + localArtworkFormat: string; + saveArtwork: boolean; + coverImageTemplate: string; + saveArtworkArtist: boolean; + artistImageTemplate: string; + jpegImageQuality: number; + dateFormat: string; + albumVariousArtists: boolean; + removeAlbumVersion: boolean; + removeDuplicateArtists: boolean; + featuredToTitle: string; + titleCasing: string; + artistCasing: string; + executeCommand: string; + tags: Tags; +} + +export const DEFAULTS: Settings = { downloadLocation: getMusicFolder(), tracknameTemplate: "%artist% - %title%", albumTracknameTemplate: "%tracknumber% - %title%", @@ -104,7 +190,7 @@ const DEFAULTS = { }, }; -function save(settings, configFolder) { +export function save(settings: Settings, configFolder) { configFolder = configFolder || getConfigFolder(); if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder); @@ -114,16 +200,18 @@ function save(settings, configFolder) { ); } -function load(configFolder) { +export function load(configFolder: string) { configFolder = configFolder || getConfigFolder(); if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder); if (!fs.existsSync(configFolder + "config.json")) save(DEFAULTS, configFolder); - let settings; + let settings: Settings; try { - settings = JSON.parse(fs.readFileSync(configFolder + "config.json")); + settings = JSON.parse( + fs.readFileSync(configFolder + "config.json").toString() + ); } catch (e) { if (e.name === "SyntaxError") save(DEFAULTS, configFolder); settings = JSON.parse(JSON.stringify(DEFAULTS)); @@ -132,7 +220,7 @@ function load(configFolder) { return settings; } -function check(settings) { +function check(settings: Settings) { let changes = 0; Object.keys(DEFAULTS).forEach((_iSet) => { if ( @@ -175,11 +263,3 @@ function check(settings) { }); return changes; } - -module.exports = { - OverwriteOption, - FeaturesOption, - DEFAULTS, - save, - load, -}; diff --git a/deemix/src/tagger.js b/deemix/src/tagger.ts similarity index 96% rename from deemix/src/tagger.js rename to deemix/src/tagger.ts index 1e1c2dfd..97536ebf 100644 --- a/deemix/src/tagger.js +++ b/deemix/src/tagger.ts @@ -1,10 +1,11 @@ -const ID3Writer = require("./utils/id3-writer.js"); -const Metaflac = require("metaflac-js2"); -const fs = require("fs"); +import ID3Writer from "./utils/id3-writer"; +import Metaflac from "metaflac-js2"; +import fs from "fs"; +import Track from "./types/Track"; -function tagID3(path, track, save) { +function tagID3(path: string, track: Track, save: any) { const songBuffer = fs.readFileSync(path); - const tag = new ID3Writer(songBuffer); + const tag: any = new ID3Writer(songBuffer); tag.separateWithNull = String.fromCharCode(0); if (save.title) tag.setFrame("TIT2", track.title); @@ -58,7 +59,7 @@ function tagID3(path, track, save) { // The 'Date' frame is a numeric string in the DDMM format. if (save.date) tag.setFrame("TDAT", "" + track.date.day + track.date.month); - if (save.length) tag.setFrame("TLEN", parseInt(track.duration) * 1000); + if (save.length) tag.setFrame("TLEN", track.duration * 1000); if (save.bpm && track.bpm) tag.setFrame("TBPM", track.bpm); if (save.label) tag.setFrame("TPUB", track.album.label); if (save.isrc) tag.setFrame("TSRC", track.ISRC); @@ -515,8 +516,8 @@ function tagID3v1(taggedSongBuffer, track, save) { if (save.trackNumber) { if (track.trackNumber <= 65535) { if (track.trackNumber > 255) { - tagBuffer.writeUInt8(parseInt(track.trackNumber >> 8), 125); - tagBuffer.writeUInt8(parseInt(track.trackNumber & 255), 126); + tagBuffer.writeUInt8(track.trackNumber >> 8, 125); + tagBuffer.writeUInt8(track.trackNumber & 255, 126); } else { tagBuffer.writeUInt8(parseInt(track.trackNumber), 126); } @@ -539,8 +540,4 @@ function tagID3v1(taggedSongBuffer, track, save) { return Buffer.from(buffer); } -module.exports = { - tagID3, - tagFLAC, - tagID3v1, -}; +export { tagID3, tagFLAC, tagID3v1 }; diff --git a/deemix/src/types/Album.js b/deemix/src/types/Album.ts similarity index 83% rename from deemix/src/types/Album.js rename to deemix/src/types/Album.ts index aefbb9bc..306b6c6b 100644 --- a/deemix/src/types/Album.js +++ b/deemix/src/types/Album.ts @@ -1,10 +1,36 @@ -const { removeDuplicateArtists, removeFeatures } = require("../utils/index.js"); -const { Artist } = require("./Artist.js"); -const { Date } = require("./Date.js"); -const { Picture } = require("./Picture.js"); -const { VARIOUS_ARTISTS } = require("./index.js"); +import { removeDuplicateArtists, removeFeatures } from "../utils"; +import { Artist } from "./Artist"; +import { CustomDate } from "./CustomDate"; +import { Picture } from "./Picture"; +import { VARIOUS_ARTISTS } from "./index"; + +export class Album { + id: string; + title: string; + pic: any; + artist: { Main: any[] }; + artists: any[]; + mainArtist: Artist | null; + date: CustomDate; + dateString: string; + trackTotal: string; + discTotal: string; + embeddedCoverPath: string; + embeddedCoverURL: string; + explicit: boolean; + genre: any[]; + barcode: string; + label: string; + copyright: string; + recordType: string; + bitrate: number; + rootArtist: Artist | null; + variousArtists: Artist | null; + playlistId: null; + owner: null; + isPlaylist: boolean; + playlistID: any; -class Album { constructor(alb_id = "0", title = "", pic_md5 = "") { this.id = alb_id; this.title = title; @@ -12,7 +38,7 @@ class Album { this.artist = { Main: [] }; this.artists = []; this.mainArtist = null; - this.date = new Date(); + this.date = new CustomDate(); this.dateString = ""; this.trackTotal = "0"; this.discTotal = "0"; @@ -152,7 +178,3 @@ class Album { return removeFeatures(this.title); } } - -module.exports = { - Album, -}; diff --git a/deemix/src/types/Artist.js b/deemix/src/types/Artist.ts similarity index 60% rename from deemix/src/types/Artist.js rename to deemix/src/types/Artist.ts index 90066fc4..71b2cdf9 100644 --- a/deemix/src/types/Artist.js +++ b/deemix/src/types/Artist.ts @@ -1,7 +1,13 @@ -const { Picture } = require("./Picture.js"); -const { VARIOUS_ARTISTS } = require("./index.js"); +import { Picture } from "./Picture"; +import { VARIOUS_ARTISTS } from "./index"; + +export class Artist { + id: string; + name: string; + pic: Picture; + role: string; + save: boolean; -class Artist { constructor(art_id = "0", name = "", role = "", pic_md5 = "") { this.id = String(art_id); this.name = name; @@ -14,7 +20,3 @@ class Artist { return this.id === VARIOUS_ARTISTS; } } - -module.exports = { - Artist, -}; diff --git a/deemix/src/types/Date.js b/deemix/src/types/CustomDate.ts similarity index 87% rename from deemix/src/types/Date.js rename to deemix/src/types/CustomDate.ts index 5ad92662..aa21bef1 100644 --- a/deemix/src/types/Date.js +++ b/deemix/src/types/CustomDate.ts @@ -1,4 +1,8 @@ -class Date { +export class CustomDate { + day: string; + month: string; + year: string; + constructor(day = "00", month = "00", year = "XXXX") { this.day = day; this.month = month; @@ -21,7 +25,3 @@ class Date { return template; } } - -module.exports = { - Date, -}; diff --git a/deemix/src/types/DownloadObjects.js b/deemix/src/types/DownloadObjects.ts similarity index 78% rename from deemix/src/types/DownloadObjects.js rename to deemix/src/types/DownloadObjects.ts index b99ed50b..0c327e59 100644 --- a/deemix/src/types/DownloadObjects.js +++ b/deemix/src/types/DownloadObjects.ts @@ -1,4 +1,25 @@ -class IDownloadObject { +import BasePlugin from "@/plugins/base"; + +export class IDownloadObject { + type: any; + id: any; + bitrate: any; + title: any; + artist: any; + cover: any; + explicit: any; + size: any; + downloaded: any; + failed: any; + progress: any; + errors: any; + files: any; + extrasPath: any; + progressNext: number; + uuid: string; + isCanceled: boolean; + __type__: "Single" | "Collection" | "Convertable"; + constructor(obj) { this.type = obj.type; this.id = obj.id; @@ -41,16 +62,6 @@ class IDownloadObject { }; } - getResettedDict() { - const item = this.toDict(); - item.downloaded = 0; - item.failed = 0; - item.progress = 0; - item.errors = []; - item.files = []; - return item; - } - getSlimmedDict() { const light = this.toDict(); const propertiesToDelete = [ @@ -97,7 +108,8 @@ class IDownloadObject { } } -class Single extends IDownloadObject { +export class Single extends IDownloadObject { + single: any; constructor(obj) { super(obj); this.size = 1; @@ -107,8 +119,8 @@ class Single extends IDownloadObject { toDict() { const item = super.toDict(); - item.single = this.single; - return item; + + return { ...item, single: this.single }; } completeTrackProgress(listener) { @@ -122,7 +134,8 @@ class Single extends IDownloadObject { } } -class Collection extends IDownloadObject { +export class Collection extends IDownloadObject { + collection: any; constructor(obj) { super(obj); this.collection = obj.collection; @@ -131,8 +144,8 @@ class Collection extends IDownloadObject { toDict() { const item = super.toDict(); - item.collection = this.collection; - return item; + + return { ...item, collection: this.collection }; } completeTrackProgress(listener) { @@ -146,7 +159,9 @@ class Collection extends IDownloadObject { } } -class Convertable extends Collection { +export class Convertable extends Collection { + plugin: BasePlugin; + conversion_data: any; constructor(obj) { super(obj); this.plugin = obj.plugin; @@ -156,15 +171,11 @@ class Convertable extends Collection { toDict() { const item = super.toDict(); - item.plugin = this.plugin; - item.conversion_data = this.conversion_data; - return item; + + return { + ...item, + plugin: this.plugin, + conversion_data: this.conversion_data, + }; } } - -module.exports = { - IDownloadObject, - Single, - Collection, - Convertable, -}; diff --git a/deemix/src/types/Lyrics.js b/deemix/src/types/Lyrics.ts similarity index 87% rename from deemix/src/types/Lyrics.js rename to deemix/src/types/Lyrics.ts index be2de816..240861fd 100644 --- a/deemix/src/types/Lyrics.js +++ b/deemix/src/types/Lyrics.ts @@ -1,6 +1,11 @@ -const { decode } = require("html-entities"); +import { decode } from "html-entities"; + +export class Lyrics { + id: string; + sync: string; + unsync: string; + syncID3: any[]; -class Lyrics { constructor(lyr_id = "0") { this.id = lyr_id; this.sync = ""; @@ -30,7 +35,3 @@ class Lyrics { } } } - -module.exports = { - Lyrics, -}; diff --git a/deemix/src/types/Picture.js b/deemix/src/types/Picture.ts similarity index 81% rename from deemix/src/types/Picture.js rename to deemix/src/types/Picture.ts index f4c5b63d..f646aec7 100644 --- a/deemix/src/types/Picture.js +++ b/deemix/src/types/Picture.ts @@ -1,4 +1,7 @@ -class Picture { +export class Picture { + md5: string; + type: string; + constructor(md5 = "", pic_type = "") { this.md5 = md5; this.type = pic_type; @@ -21,8 +24,10 @@ class Picture { } } -class StaticPicture { - constructor(url) { +export class StaticPicture { + staticURL: string; + + constructor(url: string) { this.staticURL = url; } @@ -30,8 +35,3 @@ class StaticPicture { return this.staticURL; } } - -module.exports = { - Picture, - StaticPicture, -}; diff --git a/deemix/src/types/Playlist.js b/deemix/src/types/Playlist.ts similarity index 72% rename from deemix/src/types/Playlist.js rename to deemix/src/types/Playlist.ts index 7996c7a3..6442fbf5 100644 --- a/deemix/src/types/Playlist.js +++ b/deemix/src/types/Playlist.ts @@ -1,8 +1,26 @@ -const { Artist } = require("./Artist.js"); -const { Date } = require("./Date.js"); -const { Picture, StaticPicture } = require("./Picture.js"); +import { Artist } from "./Artist"; +import { CustomDate } from "./CustomDate"; +import { Picture, StaticPicture } from "./Picture"; + +export class Playlist { + id: string; + title: any; + artist: { Main: any[] }; + artists: any[]; + trackTotal: any; + recordType: string; + barcode: string; + label: string; + explicit: any; + genre: string[]; + date: any; + discTotal: string; + playlistID: any; + owner: any; + pic: Picture | StaticPicture; + variousArtists: Artist; + mainArtist: any; -class Playlist { constructor(playlistAPI) { this.id = `pl_${playlistAPI.id}`; this.title = playlistAPI.title; @@ -18,7 +36,7 @@ class Playlist { const year = playlistAPI.creation_date.slice(0, 4); const month = playlistAPI.creation_date.slice(5, 7); const day = playlistAPI.creation_date.slice(8, 10); - this.date = new Date(day, month, year); + this.date = new CustomDate(day, month, year); this.discTotal = "1"; this.playlistID = playlistAPI.id; @@ -50,7 +68,3 @@ class Playlist { } } } - -module.exports = { - Playlist, -}; diff --git a/deemix/src/types/Track.js b/deemix/src/types/Track.ts similarity index 84% rename from deemix/src/types/Track.js rename to deemix/src/types/Track.ts index 5fe64425..648e192a 100644 --- a/deemix/src/types/Track.js +++ b/deemix/src/types/Track.ts @@ -1,23 +1,73 @@ -const { map_track, map_album } = require("deezer-js").utils; -const { Artist } = require("./Artist.js"); -const { Album } = require("./Album.js"); -const { Playlist } = require("./Playlist.js"); -const { Picture } = require("./Picture.js"); -const { Lyrics } = require("./Lyrics.js"); -const { Date: dzDate } = require("./Date.js"); -const { VARIOUS_ARTISTS } = require("./index.js"); -const { changeCase } = require("../utils/index.js"); -const { FeaturesOption } = require("../settings.js"); -const { NoDataToParse, AlbumDoesntExists } = require("../errors.js"); - -const { +import { Deezer, TrackFormats, utils } from "deezer-js"; +import { AlbumDoesntExists, NoDataToParse } from "../errors"; +import { FeaturesOption } from "../settings"; +import { + andCommaConcat, + changeCase, generateReplayGainString, removeDuplicateArtists, removeFeatures, - andCommaConcat, -} = require("../utils/index.js"); +} from "../utils/index"; +import { Album } from "./Album"; +import { Artist } from "./Artist"; +import { CustomDate } from "./CustomDate"; +import { VARIOUS_ARTISTS } from "./index"; +import { Lyrics } from "./Lyrics"; +import { Picture } from "./Picture"; +import { Playlist } from "./Playlist"; +const { map_track, map_album } = utils; + +export const formatsName = { + [TrackFormats.FLAC]: "FLAC", + [TrackFormats.LOCAL]: "MP3_MISC", + [TrackFormats.MP3_320]: "MP3_320", + [TrackFormats.MP3_128]: "MP3_128", + [TrackFormats.DEFAULT]: "MP3_MISC", + [TrackFormats.MP4_RA3]: "MP4_RA3", + [TrackFormats.MP4_RA2]: "MP4_RA2", + [TrackFormats.MP4_RA1]: "MP4_RA1", +} as const; class Track { + id: string; + title: string; + MD5: string; + mediaVersion: string; + trackToken: string; + trackTokenExpiration: number; + duration: number; + fallbackID: string; + albumsFallback: any[]; + filesizes: Record; + local: boolean; + mainArtist: Artist | null; + artist: { Main: any[] }; + artists: any[]; + album: Album | null; + trackNumber: string; + discNumber: string; + date: CustomDate; + lyrics: Lyrics | null; + bpm: number; + contributors: Record; + copyright: string; + explicit: boolean; + ISRC: string; + replayGain: string; + playlist: null; + position: null; + searched: boolean; + bitrate: keyof typeof formatsName; + dateString: string; + artistsString: string; + mainArtistsString: string; + featArtistsString: string; + fullArtistsString: string; + urls: Record<(typeof formatsName)[keyof typeof formatsName], string>; + downloadURL: string; + rank: any; + artistString: any; + constructor() { this.id = "0"; this.title = ""; @@ -36,7 +86,7 @@ class Track { this.album = null; this.trackNumber = "0"; this.discNumber = "0"; - this.date = new dzDate(); + this.date = new CustomDate(); this.lyrics = null; this.bpm = 0; this.contributors = {}; @@ -70,7 +120,7 @@ class Track { this.urls = {}; } - async parseData(dz, id, trackAPI, albumAPI, playlistAPI) { + async parseData(dz: Deezer, id, trackAPI, albumAPI, playlistAPI) { if (id) { let trackAPI_new = await dz.gw.get_track_with_fallback(id); trackAPI_new = map_track(trackAPI_new); @@ -198,7 +248,7 @@ class Track { this.album.mainArtist = this.mainArtist; } - parseTrack(trackAPI) { + parseTrack(trackAPI: any) { this.title = trackAPI.title; this.discNumber = trackAPI.disk_number; @@ -228,7 +278,7 @@ class Track { this.date.fixDayMonth(); } - trackAPI.contributors.forEach((artist) => { + trackAPI.contributors.forEach((artist: any) => { const isVariousArtists = String(artist.id) === VARIOUS_ARTISTS; const isMainArtist = artist.role === "Main"; @@ -250,7 +300,7 @@ class Track { }); if (trackAPI.alternative_albums) { - trackAPI.alternative_albums.data.forEach((album) => { + trackAPI.alternative_albums.data.forEach((album: any) => { if ( album.RIGHTS.STREAM_ADS_AVAILABLE || album.RIGHTS.STREAM_SUB_AVAILABLE @@ -404,6 +454,4 @@ class Track { } } -module.exports = { - Track, -}; +export default Track; diff --git a/deemix/src/types/index.ts b/deemix/src/types/index.ts index 2a2b7ed6..1f9e9aba 100644 --- a/deemix/src/types/index.ts +++ b/deemix/src/types/index.ts @@ -1,11 +1,10 @@ export const VARIOUS_ARTISTS = "5080"; -export * from "./Album.js"; -export * from "./Artist.js"; -export * from "./Date.js"; -export * from "./Lyrics.js"; -export * from "./Picture.js"; -export * from "./Playlist.js"; -export * from "./Track.js"; - -export { default as downloadObjects } from "./DownloadObjects.js"; +export * from "./Album"; +export * from "./Artist"; +export * from "./CustomDate"; +export * from "./Lyrics"; +export * from "./Picture"; +export * from "./Playlist"; +export * from "./Track"; +export * as downloadObjects from "./DownloadObjects"; diff --git a/deemix/src/utils/index.js b/deemix/src/utils/core.ts similarity index 87% rename from deemix/src/utils/index.js rename to deemix/src/utils/core.ts index a9adcc2b..68115d3d 100644 --- a/deemix/src/utils/index.js +++ b/deemix/src/utils/core.ts @@ -1,13 +1,14 @@ -const stream = require("stream"); -const { promisify } = require("util"); -const pipeline = promisify(stream.pipeline); -const { accessSync, constants } = require("fs"); -const { ErrorMessages } = require("../errors.js"); +import { accessSync, constants } from "fs"; +import stream from "stream"; +import { promisify } from "util"; +import { ErrorMessages } from "../errors"; -const USER_AGENT_HEADER = +export const pipeline = promisify(stream.pipeline); + +export const USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"; -function canWrite(path) { +export function canWrite(path) { try { accessSync(path, constants.R_OK | constants.W_OK); } catch { @@ -16,11 +17,11 @@ function canWrite(path) { return true; } -function generateReplayGainString(trackGain) { +export function generateReplayGainString(trackGain) { return `${Math.round((parseFloat(trackGain) + 18.4) * -100) / 100} dB`; } -function changeCase(txt, type) { +export function changeCase(txt, type) { switch (type) { case "lower": return txt.toLowerCase(); @@ -52,7 +53,7 @@ function changeCase(txt, type) { } } -function removeFeatures(title) { +export function removeFeatures(title) { let clean = title; let found = false; let pos; @@ -80,7 +81,7 @@ function removeFeatures(title) { return clean; } -function andCommaConcat(lst) { +export function andCommaConcat(lst) { const tot = lst.length; let result = ""; lst.forEach((art, i) => { @@ -96,7 +97,7 @@ function andCommaConcat(lst) { return result; } -function uniqueArray(arr) { +export function uniqueArray(arr) { arr.forEach((namePrinc, iPrinc) => { arr.forEach((nameRest, iRest) => { if ( @@ -110,13 +111,13 @@ function uniqueArray(arr) { return arr; } -function shellEscape(s) { +export function shellEscape(s) { if (typeof s !== "string") return ""; if (!/[^\w@%+=:,./-]/g.test(s)) return s; return "'" + s.replaceAll("'", "'\"'\"'") + "'"; } -function removeDuplicateArtists(artist, artists) { +export function removeDuplicateArtists(artist, artists) { artists = uniqueArray(artists); Object.keys(artist).forEach((role) => { artist[role] = uniqueArray(artist[role]); @@ -124,7 +125,7 @@ function removeDuplicateArtists(artist, artists) { return [artist, artists]; } -function formatListener(key, data) { +export function formatListener(key, data) { let message = ""; switch (key) { @@ -221,17 +222,3 @@ function formatListener(key, data) { return message; } } - -module.exports = { - USER_AGENT_HEADER, - generateReplayGainString, - removeFeatures, - andCommaConcat, - uniqueArray, - removeDuplicateArtists, - pipeline, - canWrite, - changeCase, - shellEscape, - formatListener, -}; diff --git a/deemix/src/utils/deezer.js b/deemix/src/utils/deezer.js deleted file mode 100644 index 7d6f119f..00000000 --- a/deemix/src/utils/deezer.js +++ /dev/null @@ -1,70 +0,0 @@ -const got = require("got"); -const { CookieJar } = require("tough-cookie"); -const { _md5 } = require("./crypto.js"); -const { USER_AGENT_HEADER } = require("./index.js"); - -const CLIENT_ID = "172365"; -const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"; - -async function getAccessToken(email, password) { - let accessToken = null; - password = _md5(password, "utf8"); - const hash = _md5( - [CLIENT_ID, email, password, CLIENT_SECRET].join(""), - "utf8" - ); - try { - const response = await got - .get("https://api.deezer.com/auth/token", { - searchParams: { - app_id: CLIENT_ID, - login: email, - password, - hash, - }, - https: { rejectUnauthorized: false }, - headers: { "User-Agent": USER_AGENT_HEADER }, - }) - .json(); - accessToken = response.access_token; - if (accessToken === "undefined") accessToken = null; - } catch { - /* empty */ - } - return accessToken; -} - -async function getArlFromAccessToken(accessToken) { - if (!accessToken) return null; - let arl = null; - const cookieJar = new CookieJar(); - try { - await got.get("https://api.deezer.com/platform/generic/track/3135556", { - headers: { - Authorization: `Bearer ${accessToken}`, - "User-Agent": USER_AGENT_HEADER, - }, - https: { rejectUnauthorized: false }, - cookieJar, - }); - const response = await got - .get( - "https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null", - { - headers: { "User-Agent": USER_AGENT_HEADER }, - https: { rejectUnauthorized: false }, - cookieJar, - } - ) - .json(); - arl = response.results; - } catch { - /* empty */ - } - return arl; -} - -module.exports = { - getAccessToken, - getArlFromAccessToken, -}; diff --git a/deemix/src/utils/deezer.ts b/deemix/src/utils/deezer.ts new file mode 100644 index 00000000..b9132d1e --- /dev/null +++ b/deemix/src/utils/deezer.ts @@ -0,0 +1,64 @@ +import got from "got"; +import { CookieJar } from "tough-cookie"; +import { USER_AGENT_HEADER } from "."; +import { _md5 } from "./crypto"; + +const CLIENT_ID = "172365"; +const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"; + +export async function getDeezerAccessTokenFromEmailPassword(email, password) { + let accessToken = null; + password = _md5(password, "utf8"); + const hash = _md5( + [CLIENT_ID, email, password, CLIENT_SECRET].join(""), + "utf8" + ); + try { + const response = got.get("https://api.deezer.com/auth/token", { + searchParams: { + app_id: CLIENT_ID, + login: email, + password, + hash, + }, + https: { rejectUnauthorized: false }, + headers: { "User-Agent": USER_AGENT_HEADER }, + }); + const responseData = (await response.json()) as { access_token?: string }; + + accessToken = responseData.access_token ?? null; + } catch { + /* empty */ + } + return accessToken; +} + +export async function getDeezerArlFromAccessToken(accessToken) { + if (!accessToken) return null; + let arl = null; + const cookieJar = new CookieJar(); + try { + await got.get("https://api.deezer.com/platform/generic/track/3135556", { + headers: { + Authorization: `Bearer ${accessToken}`, + "User-Agent": USER_AGENT_HEADER, + }, + https: { rejectUnauthorized: false }, + cookieJar, + }); + const response = got.get( + "https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null", + { + headers: { "User-Agent": USER_AGENT_HEADER }, + https: { rejectUnauthorized: false }, + cookieJar, + } + ); + const responseData = (await response.json()) as { results?: string }; + + arl = responseData.results ?? null; + } catch { + /* empty */ + } + return arl; +} diff --git a/deemix/src/utils/download-utils.js b/deemix/src/utils/downloadUtils.ts similarity index 91% rename from deemix/src/utils/download-utils.js rename to deemix/src/utils/downloadUtils.ts index 17c38390..900d20b2 100644 --- a/deemix/src/utils/download-utils.js +++ b/deemix/src/utils/downloadUtils.ts @@ -1,6 +1,6 @@ -const { OverwriteOption } = require("../settings.js"); -const fs = require("fs"); -const { tagID3, tagFLAC } = require("../tagger.js"); +import { OverwriteOption } from "../settings"; +import fs from "fs"; +import { tagID3, tagFLAC } from "../tagger"; export const checkShouldDownload = ( filename, diff --git a/deemix/src/utils/index.ts b/deemix/src/utils/index.ts new file mode 100644 index 00000000..109bd700 --- /dev/null +++ b/deemix/src/utils/index.ts @@ -0,0 +1,11 @@ +import blowfish from "./blowfish"; +import id3Writer from "./id3-writer"; + +export * from "./core"; +export * from "./crypto"; +export * from "./deezer"; +export * from "./downloadUtils"; +export * from "./localpaths"; +export * from "./pathtemplates"; + +export { blowfish, id3Writer }; diff --git a/deemix/src/utils/localpaths.js b/deemix/src/utils/localpaths.ts similarity index 90% rename from deemix/src/utils/localpaths.js rename to deemix/src/utils/localpaths.ts index 06295d9a..59c63aa2 100644 --- a/deemix/src/utils/localpaths.js +++ b/deemix/src/utils/localpaths.ts @@ -1,20 +1,20 @@ -const { sep } = require("path"); -const { homedir } = require("os"); -const fs = require("fs"); -const { canWrite } = require("./index.js"); +import { canWrite } from "../utils"; +import fs from "fs"; +import { homedir } from "os"; +import { sep } from "path"; const homedata = homedir(); let userdata = ""; let musicdata = ""; -function checkPath(path) { +function checkPath(path: string) { if (path === "") return ""; if (!fs.existsSync(path)) return ""; if (!canWrite(path)) return ""; return path; } -function getConfigFolder() { +export function getConfigFolder() { if (userdata !== "") return userdata; if (process.env.XDG_CONFIG_HOME && userdata === "") { userdata = `${process.env.XDG_CONFIG_HOME}${sep}`; @@ -40,7 +40,7 @@ function getConfigFolder() { return userdata; } -function getMusicFolder() { +export function getMusicFolder() { if (musicdata !== "") return musicdata; if (process.env.XDG_MUSIC_DIR && musicdata === "") { musicdata = `${process.env.XDG_MUSIC_DIR}${sep}`; @@ -93,8 +93,3 @@ function getMusicFolder() { if (process.env.DEEMIX_MUSIC_DIR) musicdata = process.env.DEEMIX_MUSIC_DIR; return musicdata; } - -module.exports = { - getConfigFolder, - getMusicFolder, -}; diff --git a/deemix/src/utils/pathtemplates.js b/deemix/src/utils/pathtemplates.ts similarity index 93% rename from deemix/src/utils/pathtemplates.js rename to deemix/src/utils/pathtemplates.ts index 2930eaf6..eb8e0e22 100644 --- a/deemix/src/utils/pathtemplates.js +++ b/deemix/src/utils/pathtemplates.ts @@ -1,5 +1,5 @@ -const { TrackFormats } = require("deezer-js"); -const { Date: dzDate } = require("../types/Date.js"); +import { TrackFormats } from "deezer-js"; +import { CustomDate } from "../types/CustomDate"; const bitrateLabels = { [TrackFormats.MP4_RA3]: "360 HQ", @@ -12,13 +12,13 @@ const bitrateLabels = { [TrackFormats.LOCAL]: "MP3", }; -function fixName(txt, char = "_") { +export function fixName(txt, char = "_") { txt = txt + ""; txt = txt.replace(/[\0/\\:*?"<>|]/g, char); return txt.normalize("NFC"); } -function fixLongName(name) { +export function fixLongName(name) { if (name.includes("/")) { const sepName = name.split("/"); name = ""; @@ -33,7 +33,7 @@ function fixLongName(name) { return name; } -function antiDot(str) { +export function antiDot(str) { while ( str[str.length - 1] === "." || str[str.length - 1] === " " || @@ -47,7 +47,7 @@ function antiDot(str) { return str; } -function pad(num, max_val, settings) { +export function pad(num, max_val, settings) { let paddingSize; if (parseInt(settings.paddingSize) === 0) { paddingSize = (max_val + "").length; @@ -60,7 +60,7 @@ function pad(num, max_val, settings) { return num + ""; } -function generatePath(track, downloadObject, settings) { +export function generatePath(track, downloadObject, settings) { let filenameTemplate = "%artist% - %title%"; let singleTrack = false; if (downloadObject.type === "track") { @@ -159,7 +159,7 @@ function generatePath(track, downloadObject, settings) { }; } -function generateTrackName(filename, track, settings) { +export function generateTrackName(filename, track, settings) { const c = settings.illegalCharacterReplacer; filename = filename.replaceAll("%title%", fixName(track.title, c)); filename = filename.replaceAll("%artist%", fixName(track.mainArtist.name, c)); @@ -236,7 +236,7 @@ function generateTrackName(filename, track, settings) { return antiDot(fixLongName(filename)); } -function generateAlbumName(foldername, album, settings, playlist) { +export function generateAlbumName(foldername, album, settings, playlist) { const c = settings.illegalCharacterReplacer; if (playlist && settings.tags.savePlaylistAsCompilation) { foldername = foldername.replaceAll( @@ -299,7 +299,7 @@ function generateAlbumName(foldername, album, settings, playlist) { return antiDot(fixLongName(foldername)); } -function generateArtistName(foldername, artist, settings, rootArtist) { +export function generateArtistName(foldername, artist, settings, rootArtist) { const c = settings.illegalCharacterReplacer; foldername = foldername.replaceAll("%artist%", fixName(artist.name, c)); foldername = foldername.replaceAll("%artist_id%", artist.id); @@ -320,10 +320,10 @@ function generateArtistName(foldername, artist, settings, rootArtist) { return antiDot(fixLongName(foldername)); } -function generatePlaylistName(foldername, playlist, settings) { +export function generatePlaylistName(foldername, playlist, settings) { const c = settings.illegalCharacterReplacer; const today = new Date(); - const today_dz = new dzDate( + const today_dz = new CustomDate( String(today.getDate()).padStart(2, "0"), String(today.getMonth() + 1).padStart(2, "0"), String(today.getFullYear()) @@ -352,7 +352,7 @@ function generatePlaylistName(foldername, playlist, settings) { return antiDot(fixLongName(foldername)); } -function generateDownloadObjectName(foldername, queueItem, settings) { +export function generateDownloadObjectName(foldername, queueItem, settings) { const c = settings.illegalCharacterReplacer; foldername = foldername.replaceAll("%title%", fixName(queueItem.title, c)); foldername = foldername.replaceAll("%artist%", fixName(queueItem.artist, c)); @@ -366,12 +366,3 @@ function generateDownloadObjectName(foldername, queueItem, settings) { foldername = foldername.replaceAll("\\", "/").replace("/", c); return antiDot(fixLongName(foldername)); } - -module.exports = { - generatePath, - generateTrackName, - generateAlbumName, - generateArtistName, - generatePlaylistName, - generateDownloadObjectName, -}; diff --git a/deemix/tsconfig.json b/deemix/tsconfig.json index 20226040..6c19e687 100644 --- a/deemix/tsconfig.json +++ b/deemix/tsconfig.json @@ -4,6 +4,7 @@ "target": "es2016", "module": "commonjs", + "lib": ["es2021"], "allowJs": true, // "checkJs": true, @@ -14,7 +15,11 @@ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, "strict": false /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + + "paths": { + "@/*": ["./src/*"] + } } } diff --git a/deezer-js/src/api.js b/deezer-js/src/api.js index a6f1d46b..0b5794a9 100644 --- a/deezer-js/src/api.js +++ b/deezer-js/src/api.js @@ -1,5 +1,5 @@ -const got = require("got"); -const { +import { get } from "got"; +import { APIError, ItemsLimitExceededException, PermissionException, @@ -9,10 +9,10 @@ const { InvalidQueryException, DataException, IndividualAccountChangedNotAllowedException, -} = require("./errors.js"); +} from "./errors"; // Possible values for order parameter in search -const SearchOrder = { +export const SearchOrder = { RANKING: "RANKING", TRACK_ASC: "TRACK_ASC", TRACK_DESC: "TRACK_DESC", @@ -26,7 +26,7 @@ const SearchOrder = { DURATION_DESC: "DURATION_DESC", }; -class API { +export class API { constructor(cookie_jar, headers) { this.http_headers = headers; this.cookie_jar = cookie_jar; @@ -38,17 +38,15 @@ class API { if (this.access_token) args.access_token = this.access_token; let result_json; try { - result_json = await got - .get("https://api.deezer.com/" + method, { - searchParams: args, - cookieJar: this.cookie_jar, - headers: this.http_headers, - https: { - rejectUnauthorized: false, - }, - timeout: 30000, - }) - .json(); + result_json = await get("https://api.deezer.com/" + method, { + searchParams: args, + cookieJar: this.cookie_jar, + headers: this.http_headers, + https: { + rejectUnauthorized: false, + }, + timeout: 30000, + }).json(); } catch (e) { console.debug("[ERROR] deezer.api", method, args, e.name, e.message); if ( @@ -517,8 +515,3 @@ class API { return "0"; } } - -module.exports = { - SearchOrder, - API, -}; diff --git a/deezer-js/src/gw.js b/deezer-js/src/gw.js index fa589cb3..f46f8b49 100644 --- a/deezer-js/src/gw.js +++ b/deezer-js/src/gw.js @@ -5,8 +5,8 @@ const { map_user_artist, map_user_album, map_user_playlist, -} = require("./utils.js"); -const { GWAPIError } = require("./errors.js"); +} = require("./utils"); +const { GWAPIError } = require("./errors"); const PlaylistStatus = { PUBLIC: 0, diff --git a/deezer-js/src/index.ts b/deezer-js/src/index.ts index f2a431b6..390731ad 100644 --- a/deezer-js/src/index.ts +++ b/deezer-js/src/index.ts @@ -1,8 +1,8 @@ import got from "got"; import { CookieJar, Cookie } from "tough-cookie"; -import { API } from "./api.js"; -import { GW } from "./gw.js"; -import { DeezerError, WrongLicense, WrongGeolocation } from "./errors.js"; +import { API } from "./api"; +import { GW } from "./gw"; +import { DeezerError, WrongLicense, WrongGeolocation } from "./errors"; // Number associtation for formats const TrackFormats = { @@ -14,7 +14,7 @@ const TrackFormats = { MP4_RA1: 13, DEFAULT: 8, LOCAL: 0, -}; +} as const; interface User { id?: number; @@ -54,15 +54,22 @@ class Deezer { this.gw = new GW(this.cookie_jar, this.http_headers); } - async login(email, password, re_captcha_token, child = 0) { - if (child) child = parseInt(child); + async login( + email: string, + password: string, + reCaptchaToken: string, + child: string | number = 0 + ) { + if (child && typeof child === "string") child = parseInt(child); + // Check if user already logged in let user_data = await this.gw.get_user_data(); - if (!user_data || (user_data && Object.keys(user_data).length === 0)) + if (!user_data || (user_data && Object.keys(user_data).length === 0)) { return (this.logged_in = false); + } + if (user_data.USER.USER_ID === 0) return (this.logged_in = false); - // Get the checkFormLogin - const check_form_login = user_data.checkFormLogin; + const login = await got .post("https://www.deezer.com/ajax/action.php", { headers: this.http_headers, @@ -74,13 +81,14 @@ class Deezer { type: "login", mail: email, password, - checkFormLogin: check_form_login, - reCaptchaToken: re_captcha_token, + checkFormLogin: user_data.checkFormLogin, + reCaptchaToken, }, }) .text(); + // Check if user logged in - if (login.text.indexOf("success") === -1) { + if (login.indexOf("success") === -1) { this.logged_in = false; return false; } @@ -91,13 +99,13 @@ class Deezer { return true; } - async login_via_arl(arl, child = 0) { - arl = arl.trim(); - if (child) child = parseInt(child); + async login_via_arl(arl: string, child: string | number = 0) { + if (child && typeof child === "string") child = parseInt(child); + // Create cookie const cookie_obj = new Cookie({ key: "arl", - value: arl, + value: arl.trim(), domain: ".deezer.com", path: "/", httpOnly: true, @@ -244,7 +252,7 @@ class Deezer { export { TrackFormats, Deezer }; -export * as api from "./api.js"; -export * as gw from "./gw.js"; -export * as utils from "./utils.js"; -export * as errors from "./errors.js"; +export * as api from "./api"; +export * as gw from "./gw"; +export * as utils from "./utils"; +export * as errors from "./errors"; diff --git a/deezer-js/src/utils.js b/deezer-js/src/utils.ts similarity index 96% rename from deezer-js/src/utils.js rename to deezer-js/src/utils.ts index da17e051..7277eff7 100644 --- a/deezer-js/src/utils.js +++ b/deezer-js/src/utils.ts @@ -1,5 +1,5 @@ // Explicit Content Lyrics -const LyricsStatus = { +export const LyricsStatus = { NOT_EXPLICIT: 0, // Not Explicit EXPLICIT: 1, // Explicit UNKNOWN: 2, // Unknown @@ -10,20 +10,20 @@ const LyricsStatus = { PARTIALLY_NO_ADVICE: 7, // Partially No Advice Available (Album "lyrics" only) }; -const ReleaseType = ["single", "album", "compile", "ep", "bundle"]; +export const ReleaseType = ["single", "album", "compile", "ep", "bundle"]; // TODO: add missing role ids -const RoleID = ["Main", null, null, null, null, "Featured"]; +export const RoleID = ["Main", null, null, null, null, "Featured"]; // get explicit lyrics boolean from explicit_content_lyrics -function is_explicit(explicit_content_lyrics) { +export function is_explicit(explicit_content_lyrics) { return [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT].includes( parseInt(explicit_content_lyrics) || LyricsStatus.UNKNOWN ); } // maps gw-light api user/tracks to standard api -function map_user_track(track) { +export function map_user_track(track) { const result = { id: track.SNG_ID, title: track.SNG_TITLE, @@ -108,7 +108,7 @@ function map_user_track(track) { } // maps gw-light api user/artists to standard api -function map_user_artist(artist) { +export function map_user_artist(artist) { return { id: artist.ART_ID, name: artist.ART_NAME, @@ -138,7 +138,7 @@ function map_user_artist(artist) { } // maps gw-light api user/albums to standard api -function map_user_album(album) { +export function map_user_album(album) { return { id: album.ALB_ID, title: album.ALB_TITLE, @@ -176,7 +176,7 @@ function map_user_album(album) { } // maps gw-light api user/playlists to standard api -function map_user_playlist(playlist, default_user_name = "") { +export function map_user_playlist(playlist, default_user_name = "") { return { id: playlist.PLAYLIST_ID, title: playlist.TITLE, @@ -221,7 +221,7 @@ function map_user_playlist(playlist, default_user_name = "") { } // maps gw-light api albums to standard api -function map_album(album) { +export function map_album(album) { const result = { id: album.ALB_ID, title: album.ALB_TITLE, @@ -320,7 +320,7 @@ function map_album(album) { } // maps gw-light api artist/albums to standard api -function map_artist_album(album) { +export function map_artist_album(album) { return { id: album.ALB_ID, title: album.ALB_TITLE, @@ -354,7 +354,7 @@ function map_artist_album(album) { } // maps gw-light api playlists to standard api -function map_playlist(playlist) { +export function map_playlist(playlist) { return { id: playlist.PLAYLIST_ID, title: playlist.TITLE, @@ -411,7 +411,7 @@ function map_playlist(playlist) { } // maps gw-light api tracks to standard api -function map_track(track) { +export function map_track(track) { let result = { id: track.SNG_ID, readable: true, // not provided @@ -548,7 +548,7 @@ function map_track(track) { } // Cleanup terms that can hurt search results -function clean_search_query(term) { +export function clean_search_query(term) { term = term.replaceAll(/ feat[\.]? /g, " "); term = term.replaceAll(/ ft[\.]? /g, " "); term = term.replaceAll(/\(feat[\.]? /g, " "); @@ -556,19 +556,3 @@ function clean_search_query(term) { term = term.replace(" & ", " ").replace("–", "-").replace("—", "-"); return term; } - -module.exports = { - LyricsStatus, - ReleaseType, - RoleID, - is_explicit, - map_user_track, - map_user_artist, - map_user_album, - map_user_playlist, - map_album, - map_artist_album, - map_playlist, - map_track, - clean_search_query, -}; diff --git a/gui/index.js b/gui/index.js index 970e2407..a3b7d4bf 100644 --- a/gui/index.js +++ b/gui/index.js @@ -18,7 +18,7 @@ const argv = yargs(hideBin(process.argv)).options({ host: { type: "string", default: "0.0.0.0" }, dev: { type: "boolean", default: false }, }).argv; -const { DeemixServer } = require("../server/dist/app.js"); +const { DeemixServer } = require("../server/dist/app"); const PORT = process.env.DEEMIX_SERVER_PORT || argv.port; process.env.DEEMIX_SERVER_PORT = PORT; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66abeb80..418d9fb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: specifier: ^4.0.0 version: 4.1.4 devDependencies: + '@types/async': + specifier: ^3.2.24 + version: 3.2.24 tsup: specifier: ^8.2.4 version: 8.2.4(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0) @@ -1233,6 +1236,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/async@3.2.24': + resolution: {integrity: sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -7083,6 +7089,8 @@ snapshots: '@trysound/sax@0.2.0': {} + '@types/async@3.2.24': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.25.4 diff --git a/server/src/app.ts b/server/src/app.ts index 624aa31f..461c758d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,25 +1,32 @@ +import { + downloader, + generateDownloadObject, + plugins, + settings, + types, + utils, +} from "deemix"; +import { Deezer } from "deezer-js"; import fs from "fs"; +import got from "got"; import { sep } from "path"; import { v4 as uuidv4 } from "uuid"; -import * as deemix from "deemix"; -import got from "got"; -import { Settings, Listener } from "./types"; -import { NotLoggedIn, CantStream } from "./helpers/errors"; - -import { GUI_PACKAGE } from "./helpers/paths"; +import { CantStream, NotLoggedIn } from "./helpers/errors"; import { logger } from "./helpers/logger"; +import { GUI_PACKAGE } from "./helpers/paths"; +import { Listener } from "./types"; +import { Spotify } from "deemix/src/plugins"; // Types -const Downloader = deemix.downloader.Downloader; -const { Single, Collection, Convertable } = deemix.types.downloadObjects; +const { Single, Collection, Convertable } = types.downloadObjects; // Functions -export const getAccessToken = deemix.utils.deezer.getAccessToken; -export const getArlFromAccessToken = deemix.utils.deezer.getArlFromAccessToken; +export const getAccessToken = utils.getDeezerAccessTokenFromEmailPassword; +export const getArlFromAccessToken = utils.getDeezerArlFromAccessToken; // Constants -export const configFolder: string = deemix.utils.localpaths.getConfigFolder(); -export const defaultSettings: Settings = deemix.settings.DEFAULTS; +export const configFolder: string = utils.getConfigFolder(); +export const defaultSettings: settings.Settings = settings.DEFAULTS; export const deemixVersion = require("../node_modules/deemix/package.json").version; const currentVersionTemp = JSON.parse( @@ -42,20 +49,20 @@ export class DeemixApp { deezerAvailable: string | null; latestVersion: string | null; - plugins: any; + plugins: Record; settings: any; listener: Listener; constructor(listener: Listener) { - this.settings = deemix.settings.load(configFolder); + this.settings = settings.load(configFolder); this.queueOrder = []; this.queue = {}; this.currentJob = null; this.plugins = { - spotify: new deemix.plugins.Spotify(), + spotify: new plugins.Spotify(), }; this.deezerAvailable = null; this.latestVersion = null; @@ -170,7 +177,7 @@ export class DeemixApp { saveSettings(newSettings: any, newSpotifySettings: any) { newSettings.executeCommand = this.settings.executeCommand; - deemix.settings.save(newSettings, configFolder); + settings.save(newSettings, configFolder); this.settings = newSettings; this.plugins.spotify.saveSettings(newSpotifySettings); } @@ -187,7 +194,7 @@ export class DeemixApp { } async addToQueue( - dz: any, + dz: Deezer, url: string[], bitrate: number, retry: boolean = false @@ -217,7 +224,7 @@ export class DeemixApp { logger.info(`Adding ${link} to queue`); let downloadObj; try { - downloadObj = await deemix.generateDownloadObject( + downloadObj = await generateDownloadObject( dz, link, bitrate, @@ -288,7 +295,7 @@ export class DeemixApp { return slimmedObjects; } - async startQueue(dz: any): Promise { + async startQueue(dz: Deezer): Promise { do { if (this.currentJob !== null || this.queueOrder.length === 0) { // Should not start another download @@ -314,7 +321,12 @@ export class DeemixApp { .readFileSync(configFolder + `queue${sep}${currentUUID}.json`) .toString() ); - let downloadObject: any; + let downloadObject: + | types.downloadObjects.Single + | types.downloadObjects.Collection + | types.downloadObjects.Convertable + | undefined = undefined; + switch (currentItem.__type__) { case "Single": downloadObject = new Single(currentItem); @@ -323,10 +335,10 @@ export class DeemixApp { downloadObject = new Collection(currentItem); break; case "Convertable": - downloadObject = new Convertable(currentItem); - downloadObject = await this.plugins[downloadObject.plugin].convert( + const convertable = new Convertable(currentItem); + downloadObject = await this.plugins[convertable.plugin].convert( dz, - downloadObject, + convertable, this.settings, this.listener ); @@ -336,7 +348,10 @@ export class DeemixApp { ); break; } - this.currentJob = new Downloader( + + if (typeof downloadObject === "undefined") return; + + this.currentJob = new downloader.Downloader( dz, downloadObject, this.settings, @@ -358,8 +373,10 @@ export class DeemixApp { this.queue[currentUUID].status = "completed"; } - const savedObject = downloadObject.getSlimmedDict(); - savedObject.status = this.queue[currentUUID].status; + const savedObject = { + ...downloadObject.getSlimmedDict(), + status: this.queue[currentUUID].status, + }; // Save queue status this.queue[currentUUID] = savedObject; diff --git a/server/src/helpers/logger.ts b/server/src/helpers/logger.ts index 69876b3c..315767f9 100644 --- a/server/src/helpers/logger.ts +++ b/server/src/helpers/logger.ts @@ -7,10 +7,7 @@ import * as deemix from "deemix"; const { combine, timestamp, errors, colorize, printf } = format; -const logFolder: string = joinPath( - deemix.utils.localpaths.getConfigFolder(), - "logs" -); +const logFolder: string = joinPath(deemix.utils.getConfigFolder(), "logs"); const logFilename = joinPath( logFolder, diff --git a/server/src/helpers/loginStorage.ts b/server/src/helpers/loginStorage.ts index f71ab278..7e3d87de 100644 --- a/server/src/helpers/loginStorage.ts +++ b/server/src/helpers/loginStorage.ts @@ -2,7 +2,7 @@ import * as deemix from "deemix"; import fs from "fs"; import { LoginFile } from "../types"; -const configFolder = deemix.utils.localpaths.getConfigFolder(); +const configFolder = deemix.utils.getConfigFolder(); const DEFAULTS: LoginFile = { accessToken: null, diff --git a/server/src/middlewares.ts b/server/src/middlewares.ts index 65d8002b..9fa0f1c7 100644 --- a/server/src/middlewares.ts +++ b/server/src/middlewares.ts @@ -5,12 +5,13 @@ import cookieParser from "cookie-parser"; import session from "express-session"; import { WEBUI_DIR } from "./helpers/paths"; +import { Deezer } from "deezer-js"; const MemoryStore = require("memorystore")(session); declare module "express-session" { export interface SessionData { - dz: any; + dz: Deezer; } } diff --git a/server/src/routes/api/get/albumSearch.ts b/server/src/routes/api/get/albumSearch.ts index a1801972..035b6055 100644 --- a/server/src/routes/api/get/albumSearch.ts +++ b/server/src/routes/api/get/albumSearch.ts @@ -74,7 +74,7 @@ function parseQuery(query: RawAlbumQuery): AlbumSearchParams { }; } -async function getAlbumDetails(dz: any, albumId: string): Promise { +async function getAlbumDetails(dz: Deezer, albumId: string): Promise { const result = await dz.gw.get_album_page(albumId); const output = result.DATA; diff --git a/server/src/routes/api/get/newReleases.ts b/server/src/routes/api/get/newReleases.ts index 0df0085c..c9a63b35 100644 --- a/server/src/routes/api/get/newReleases.ts +++ b/server/src/routes/api/get/newReleases.ts @@ -65,7 +65,7 @@ const apiHandler: ApiHandler = { path, handler }; export default apiHandler; async function channelNewReleases( - dz: any, + dz: Deezer, channelName: string ): Promise { const channelData = await dz.gw.get_page(channelName); diff --git a/server/src/routes/api/post/loginArl.ts b/server/src/routes/api/post/loginArl.ts index 852113dc..ae39b704 100644 --- a/server/src/routes/api/post/loginArl.ts +++ b/server/src/routes/api/post/loginArl.ts @@ -1,6 +1,6 @@ import { Deezer } from "deezer-js"; import { RequestHandler } from "express"; -import { sessionDZ } from "../../../app"; +import { DeemixApp, sessionDZ } from "../../../app"; import { logger } from "../../../helpers/logger"; import { resetLoginCredentials, @@ -29,9 +29,9 @@ const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async ( _ ) => { if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer(); - const deemix = req.app.get("deemix"); - const dz = sessionDZ[req.session.id]; - const isSingleUser = req.app.get("isSingleUser"); + const deemix: DeemixApp = req.app.get("deemix"); + const dz: Deezer = sessionDZ[req.session.id]; + const isSingleUser: boolean = req.app.get("isSingleUser"); if (!req.body) { return res.status(400).send(); @@ -41,12 +41,12 @@ const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async ( return res.status(400).send(); } - const loginParams: (string | number)[] = [req.body.arl]; + const loginParams: { arl: string; child?: number } = { arl: req.body.arl }; // TODO Handle the child === 0 case, don't want to rely on the login_via_arl default param (it may change in the // future) if (req.body.child) { - loginParams.push(req.body.child); + loginParams.child = req.body.child; } let response; @@ -54,7 +54,7 @@ const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async ( if (process.env.NODE_ENV !== "test") { if (!dz.logged_in) { try { - response = await dz.login_via_arl(...loginParams); + response = await dz.login_via_arl(loginParams.arl, loginParams.child); } catch (e) { logger.error(e); response = false; @@ -65,7 +65,7 @@ const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async ( } } else { const testDz = new Deezer(); - response = await testDz.login_via_arl(...loginParams); + response = await testDz.login_via_arl(loginParams.arl, loginParams.child); } if (response === LoginStatus.FAILED) sessionDZ[req.session.id] = new Deezer(); if (!(await deemix.isDeezerAvailable())) response = LoginStatus.NOT_AVAILABLE; diff --git a/webui/tests/unit/utils/dates.spec.js b/webui/tests/unit/utils/dates.spec.js index 706bba14..735adf00 100644 --- a/webui/tests/unit/utils/dates.spec.js +++ b/webui/tests/unit/utils/dates.spec.js @@ -1,4 +1,4 @@ -import { checkNewRelease } from "../../../src/utils/dates.js"; +import { checkNewRelease } from "../../../src/utils/dates"; describe("date utils", () => { describe("checkNewRelease", () => { From 71479c0b727a2745d69c85ef8450770d1fa12108 Mon Sep 17 00:00:00 2001 From: Lachie Underhill Date: Mon, 2 Sep 2024 16:52:41 +1000 Subject: [PATCH 2/5] chore: refactor deezer-js to ts --- deemix/src/errors.ts | 2 +- deezer-js/src/{api.js => api.ts} | 135 ++++++++++++++++--------------- deezer-js/src/errors.js | 103 ----------------------- deezer-js/src/errors.ts | 89 ++++++++++++++++++++ deezer-js/src/{gw.js => gw.ts} | 56 ++++++------- deezer-js/src/index.ts | 8 ++ 6 files changed, 198 insertions(+), 195 deletions(-) rename deezer-js/src/{api.js => api.ts} (79%) delete mode 100644 deezer-js/src/errors.js create mode 100644 deezer-js/src/errors.ts rename deezer-js/src/{gw.js => gw.ts} (91%) diff --git a/deemix/src/errors.ts b/deemix/src/errors.ts index 501e3328..c1d94dea 100644 --- a/deemix/src/errors.ts +++ b/deemix/src/errors.ts @@ -116,7 +116,7 @@ export class DownloadFailed extends DownloadError { errid: keyof typeof ErrorMessages; track: Track; - constructor(errid: keyof typeof ErrorMessages, track: Track) { + constructor(errid: keyof typeof ErrorMessages, track?: Track) { super(); this.errid = errid; diff --git a/deezer-js/src/api.js b/deezer-js/src/api.ts similarity index 79% rename from deezer-js/src/api.js rename to deezer-js/src/api.ts index 0b5794a9..a3f660de 100644 --- a/deezer-js/src/api.js +++ b/deezer-js/src/api.ts @@ -1,4 +1,4 @@ -import { get } from "got"; +import got from "got"; import { APIError, ItemsLimitExceededException, @@ -10,6 +10,7 @@ import { DataException, IndividualAccountChangedNotAllowedException, } from "./errors"; +import { APIOptions } from "."; // Possible values for order parameter in search export const SearchOrder = { @@ -26,27 +27,35 @@ export const SearchOrder = { DURATION_DESC: "DURATION_DESC", }; +type APIArgs = Record; + export class API { + http_headers: any; + cookie_jar: any; + access_token: string; + constructor(cookie_jar, headers) { this.http_headers = headers; this.cookie_jar = cookie_jar; this.access_token = null; } - async api_call(method, args) { - if (typeof args === "undefined") args = {}; - if (this.access_token) args.access_token = this.access_token; + async api_call(method: string, args: APIArgs = {}) { + if (this.access_token) args["access_token"] = this.access_token; + let result_json; try { - result_json = await get("https://api.deezer.com/" + method, { - searchParams: args, - cookieJar: this.cookie_jar, - headers: this.http_headers, - https: { - rejectUnauthorized: false, - }, - timeout: 30000, - }).json(); + result_json = await got + .get("https://api.deezer.com/" + method, { + searchParams: args, + cookieJar: this.cookie_jar, + headers: this.http_headers, + https: { + rejectUnauthorized: false, + }, + timeout: 30000, + }) + .json(); } catch (e) { console.debug("[ERROR] deezer.api", method, args, e.name, e.message); if ( @@ -125,19 +134,19 @@ export class API { return this.get_album(`upc:${upc}`); } - get_album_comments(album_id, options = {}) { + get_album_comments(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`album/${album_id}/comments`, { index, limit }); } - get_album_fans(album_id, options = {}) { + get_album_fans(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 100; return this.api_call(`album/${album_id}/fans`, { index, limit }); } - get_album_tracks(album_id, options = {}) { + get_album_tracks(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || -1; return this.api_call(`album/${album_id}/tracks`, { index, limit }); @@ -147,79 +156,79 @@ export class API { return this.api_call(`artist/${artist_id}`); } - get_artist_top(artist_id, options = {}) { + get_artist_top(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`artist/${artist_id}/top`, { index, limit }); } - get_artist_albums(artist_id, options = {}) { + get_artist_albums(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || -1; return this.api_call(`artist/${artist_id}/albums`, { index, limit }); } - get_artist_comments(artist_id, options = {}) { + get_artist_comments(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`artist/${artist_id}/comments`, { index, limit }); } - get_artist_fans(artist_id, options = {}) { + get_artist_fans(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 100; return this.api_call(`artist/${artist_id}/fans`, { index, limit }); } - get_artist_related(artist_id, options = {}) { + get_artist_related(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 20; return this.api_call(`artist/${artist_id}/related`, { index, limit }); } - get_artist_radio(artist_id, options = {}) { + get_artist_radio(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`artist/${artist_id}/radio`, { index, limit }); } - get_artist_playlists(artist_id, options = {}) { + get_artist_playlists(artist_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || -1; return this.api_call(`artist/${artist_id}/playlists`, { index, limit }); } - get_chart(genre_id = 0, options = {}) { + get_chart(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`chart/${genre_id}`, { index, limit }); } - get_chart_tracks(genre_id = 0, options = {}) { + get_chart_tracks(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`chart/${genre_id}/tracks`, { index, limit }); } - get_chart_albums(genre_id = 0, options = {}) { + get_chart_albums(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`chart/${genre_id}/albums`, { index, limit }); } - get_chart_artists(genre_id = 0, options = {}) { + get_chart_artists(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`chart/${genre_id}/artists`, { index, limit }); } - get_chart_playlists(genre_id = 0, options = {}) { + get_chart_playlists(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`chart/${genre_id}/playlists`, { index, limit }); } - get_chart_podcasts(genre_id = 0, options = {}) { + get_chart_podcasts(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`chart/${genre_id}/podcasts`, { index, limit }); @@ -229,7 +238,7 @@ export class API { return this.api_call(`comment/${comment_id}`); } - get_editorials(options = {}) { + get_editorials(options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call("editorial", { index, limit }); @@ -239,25 +248,25 @@ export class API { return this.api_call(`editorial/${genre_id}`); } - get_editorial_selection(genre_id = 0, options = {}) { + get_editorial_selection(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`editorial/${genre_id}/selection`, { index, limit }); } - get_editorial_charts(genre_id = 0, options = {}) { + get_editorial_charts(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`editorial/${genre_id}/charts`, { index, limit }); } - get_editorial_releases(genre_id = 0, options = {}) { + get_editorial_releases(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`editorial/${genre_id}/releases`, { index, limit }); } - get_genres(options = {}) { + get_genres(options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call("genre", { index, limit }); @@ -267,13 +276,13 @@ export class API { return this.api_call(`genre/${genre_id}`); } - get_genre_artists(genre_id = 0, options = {}) { + get_genre_artists(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`genre/${genre_id}/artists`, { index, limit }); } - get_genre_radios(genre_id = 0, options = {}) { + get_genre_radios(genre_id = 0, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`genre/${genre_id}/radios`, { index, limit }); @@ -291,49 +300,49 @@ export class API { return this.api_call(`playlist/${playlist_id}`); } - get_playlist_comments(album_id, options = {}) { + get_playlist_comments(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call(`playlist/${album_id}/comments`, { index, limit }); } - get_playlist_fans(album_id, options = {}) { + get_playlist_fans(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 100; return this.api_call(`playlist/${album_id}/fans`, { index, limit }); } - get_playlist_tracks(album_id, options = {}) { + get_playlist_tracks(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || -1; return this.api_call(`playlist/${album_id}/tracks`, { index, limit }); } - get_playlist_radio(album_id, options = {}) { + get_playlist_radio(album_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 100; return this.api_call(`playlist/${album_id}/radio`, { index, limit }); } - get_radios(options = {}) { + get_radios(options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call("radio", { index, limit }); } - get_radios_genres(options = {}) { + get_radios_genres(options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call("radio/genres", { index, limit }); } - get_radios_top(options = {}) { + get_radios_top(options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 50; return this.api_call("radio/top", { index, limit }); } - get_radios_lists(options = {}) { + get_radios_lists(options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call("radio/lists", { index, limit }); @@ -343,7 +352,7 @@ export class API { return this.api_call(`radio/${radio_id}`); } - get_radio_tracks(radio_id, options = {}) { + get_radio_tracks(radio_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 40; return this.api_call(`radio/${radio_id}/tracks`, { index, limit }); @@ -362,53 +371,53 @@ export class API { return query.trim(); } - _generate_search_args(query, options = {}) { + _generate_search_args(query, options: APIOptions = {}) { const strict = options.strict || false; const order = options.order || SearchOrder.RANKING; const index = options.index || 0; const limit = options.limit || 25; - const args = { q: query, index, limit }; + const args: APIArgs = { q: query, index, limit }; if (strict) args.strict = "on"; if (order) args.order = order; return args; } - search(query, options = {}) { + search(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search", args); } - advanced_search(filters, options = {}) { + advanced_search(filters, options: APIOptions = {}) { const query = this._generate_search_advanced_query(filters); return this.search(query, options); } - search_album(query, options = {}) { + search_album(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search/album", args); } - search_artist(query, options = {}) { + search_artist(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search/artist", args); } - search_playlist(query, options = {}) { + search_playlist(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search/playlist", args); } - search_radio(query, options = {}) { + search_radio(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search/radio", args); } - search_track(query, options = {}) { + search_track(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search/track", args); } - search_user(query, options = {}) { + search_user(query, options: APIOptions = {}) { const args = this._generate_search_args(query, options); return this.api_call("search/user", args); } @@ -425,49 +434,49 @@ export class API { return this.api_call(`user/${user_id}`); } - get_user_albums(user_id, options = {}) { + get_user_albums(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/albums`, { index, limit }); } - get_user_artists(user_id, options = {}) { + get_user_artists(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/artists`, { index, limit }); } - get_user_flow(user_id, options = {}) { + get_user_flow(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/flow`, { index, limit }); } - get_user_following(user_id, options = {}) { + get_user_following(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/followings`, { index, limit }); } - get_user_followers(user_id, options = {}) { + get_user_followers(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/followers`, { index, limit }); } - get_user_playlists(user_id, options = {}) { + get_user_playlists(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/playlists`, { index, limit }); } - get_user_radios(user_id, options = {}) { + get_user_radios(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/radios`, { index, limit }); } - get_user_tracks(user_id, options = {}) { + get_user_tracks(user_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call(`user/${user_id}/tracks`, { index, limit }); diff --git a/deezer-js/src/errors.js b/deezer-js/src/errors.js deleted file mode 100644 index 561be17c..00000000 --- a/deezer-js/src/errors.js +++ /dev/null @@ -1,103 +0,0 @@ -class DeezerError extends Error { - constructor(message) { - super(message); - this.name = "DeezerError"; - } -} - -class WrongLicense extends DeezerError { - constructor(format) { - super(); - this.name = "WrongLicense"; - this.message = `Your account can't request urls for ${format} tracks`; - this.format = format; - } -} - -class WrongGeolocation extends DeezerError { - constructor(country) { - super(); - this.name = "WrongGeolocation"; - this.message = `The track you requested can't be streamed in country ${country}`; - this.country = country; - } -} - -// APIError -class APIError extends DeezerError { - constructor(message) { - super(message); - this.name = "APIError"; - } -} -class ItemsLimitExceededException extends APIError { - constructor(message) { - super(message); - this.name = "ItemsLimitExceededException"; - } -} -class PermissionException extends APIError { - constructor(message) { - super(message); - this.name = "PermissionException"; - } -} -class InvalidTokenException extends APIError { - constructor(message) { - super(message); - this.name = "InvalidTokenException"; - } -} -class WrongParameterException extends APIError { - constructor(message) { - super(message); - this.name = "WrongParameterException"; - } -} -class MissingParameterException extends APIError { - constructor(message) { - super(message); - this.name = "MissingParameterException"; - } -} -class InvalidQueryException extends APIError { - constructor(message) { - super(message); - this.name = "InvalidQueryException"; - } -} -class DataException extends APIError { - constructor(message) { - super(message); - this.name = "DataException"; - } -} -class IndividualAccountChangedNotAllowedException extends APIError { - constructor(message) { - super(message); - this.name = "IndividualAccountChangedNotAllowedException"; - } -} -class GWAPIError extends DeezerError { - constructor(message) { - super(message); - this.name = "GWAPIError"; - this.message = "Track unavailable on Deezer"; - } -} - -module.exports = { - DeezerError, - WrongLicense, - WrongGeolocation, - APIError, - ItemsLimitExceededException, - PermissionException, - InvalidTokenException, - WrongParameterException, - MissingParameterException, - InvalidQueryException, - DataException, - IndividualAccountChangedNotAllowedException, - GWAPIError, -}; diff --git a/deezer-js/src/errors.ts b/deezer-js/src/errors.ts new file mode 100644 index 00000000..e29a3234 --- /dev/null +++ b/deezer-js/src/errors.ts @@ -0,0 +1,89 @@ +export class DeezerError extends Error { + constructor(message: string) { + super(message); + this.name = "DeezerError"; + } +} + +export class WrongLicense extends DeezerError { + format: string; + + constructor(format: string) { + super(`Your account can't request urls for ${format} tracks`); + this.name = "WrongLicense"; + this.format = format; + } +} + +export class WrongGeolocation extends DeezerError { + country: string; + + constructor(country: string) { + super(`The track you requested can't be streamed in country ${country}`); + this.name = "WrongGeolocation"; + this.country = country; + } +} + +// APIError +export class APIError extends DeezerError { + constructor(message: string) { + super(message); + this.name = "APIError"; + } +} +export class ItemsLimitExceededException extends APIError { + constructor(message: string) { + super(message); + this.name = "ItemsLimitExceededException"; + } +} +export class PermissionException extends APIError { + constructor(message: string) { + super(message); + this.name = "PermissionException"; + } +} +export class InvalidTokenException extends APIError { + constructor(message: string) { + super(message); + this.name = "InvalidTokenException"; + } +} +export class WrongParameterException extends APIError { + constructor(message: string) { + super(message); + this.name = "WrongParameterException"; + } +} +export class MissingParameterException extends APIError { + constructor(message: string) { + super(message); + this.name = "MissingParameterException"; + } +} +export class InvalidQueryException extends APIError { + constructor(message: string) { + super(message); + this.name = "InvalidQueryException"; + } +} +export class DataException extends APIError { + constructor(message: string) { + super(message); + this.name = "DataException"; + } +} +export class IndividualAccountChangedNotAllowedException extends APIError { + constructor(message: string) { + super(message); + this.name = "IndividualAccountChangedNotAllowedException"; + } +} +export class GWAPIError extends DeezerError { + constructor(message: string) { + super(message); + this.name = "GWAPIError"; + this.message = "Track unavailable on Deezer"; + } +} diff --git a/deezer-js/src/gw.js b/deezer-js/src/gw.ts similarity index 91% rename from deezer-js/src/gw.js rename to deezer-js/src/gw.ts index f46f8b49..574e8242 100644 --- a/deezer-js/src/gw.js +++ b/deezer-js/src/gw.ts @@ -1,20 +1,21 @@ -const got = require("got"); -const { +import got from "got"; +import { map_artist_album, map_user_track, map_user_artist, map_user_album, map_user_playlist, -} = require("./utils"); -const { GWAPIError } = require("./errors"); +} from "./utils"; +import { GWAPIError } from "./errors"; +import { APIOptions } from "."; -const PlaylistStatus = { +export const PlaylistStatus = { PUBLIC: 0, PRIVATE: 1, COLLABORATIVE: 2, }; -const EMPTY_TRACK_OBJ = { +export const EMPTY_TRACK_OBJ = { SNG_ID: 0, SNG_TITLE: "", DURATION: 0, @@ -27,14 +28,18 @@ const EMPTY_TRACK_OBJ = { ART_NAME: "", }; -class GW { +export class GW { + http_headers: any; + cookie_jar: any; + api_token: any; + constructor(cookie_jar, headers) { this.http_headers = headers; this.cookie_jar = cookie_jar; this.api_token = null; } - async api_call(method, args, params) { + async api_call(method: string, args?: any, params?: any) { if (typeof args === undefined) args = {}; if (typeof params === undefined) params = {}; if (!this.api_token && method !== "deezer.getUserData") @@ -108,7 +113,7 @@ class GW { return this.api_call("deezer.getUserData"); } - get_user_profile_page(user_id, tab, options = {}) { + get_user_profile_page(user_id, tab, options: APIOptions = {}) { const limit = options.limit || 10; return this.api_call("deezer.pageProfile", { USER_ID: user_id, @@ -117,7 +122,7 @@ class GW { }); } - get_user_favorite_ids(checksum = null, options = {}) { + get_user_favorite_ids(checksum = null, options: APIOptions = {}) { const limit = options.limit || 10000; const start = options.start || 0; return this.api_call("song.getFavoriteIds", { nb: limit, start, checksum }); @@ -194,7 +199,7 @@ class GW { }); } - async get_artist_top_tracks(art_id, options = {}) { + async get_artist_top_tracks(art_id, options: APIOptions = {}) { const limit = options.limit || 100; const tracks_array = []; const body = await this.api_call("artist.getTopTrack", { @@ -208,7 +213,7 @@ class GW { return tracks_array; } - get_artist_discography(art_id, options = {}) { + get_artist_discography(art_id, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 25; return this.api_call("album.getDiscography", { @@ -316,6 +321,9 @@ class GW { add_song_to_favorites(sng_id) { return this.gw_api_call("favorite_song.add", { SNG_ID: sng_id }); } + gw_api_call(arg0: string, arg1: {}) { + throw new Error("Method not implemented."); + } remove_song_from_favorites(sng_id) { return this.gw_api_call("favorite_song.remove", { SNG_ID: sng_id }); @@ -382,7 +390,7 @@ class GW { }); } - search_music(query, type, options = {}) { + search_music(query, type, options: APIOptions = {}) { const index = options.index || 0; const limit = options.limit || 10; return this.api_call("search.music", { @@ -396,11 +404,11 @@ class GW { // Extra calls - async get_artist_discography_tabs(art_id, options = {}) { + async get_artist_discography_tabs(art_id, options: APIOptions = {}) { const limit = options.limit || 100; let index = 0; let releases = []; - const result = { all: [] }; + const result = { all: [], featured: [], more: [] }; const ids = []; // Get all releases @@ -427,11 +435,9 @@ class GW { } else { if (release.ROLE_ID === 5) { // Handle albums where the artist is featured - if (!result.featured) result.featured = []; result.featured.push(obj); } else if (release.ROLE_ID === 0) { // Handle "more" albums - if (!result.more) result.more = []; result.more.push(obj); result.all.push(obj); } @@ -461,7 +467,7 @@ class GW { return body; } - async get_user_playlists(user_id, options = {}) { + async get_user_playlists(user_id, options: APIOptions = {}) { const limit = options.limit || 25; const user_profile_page = await this.get_user_profile_page( user_id, @@ -477,7 +483,7 @@ class GW { return result; } - async get_user_albums(user_id, options = {}) { + async get_user_albums(user_id, options: APIOptions = {}) { const limit = options.limit || 25; let data = await this.get_user_profile_page(user_id, "albums", { limit }); data = data.TAB.albums.data; @@ -488,7 +494,7 @@ class GW { return result; } - async get_user_artists(user_id, options = {}) { + async get_user_artists(user_id, options: APIOptions = {}) { const limit = options.limit || 25; let data = await this.get_user_profile_page(user_id, "artists", { limit }); data = data.TAB.artists.data; @@ -499,7 +505,7 @@ class GW { return result; } - async get_user_tracks(user_id, options = {}) { + async get_user_tracks(user_id, options: APIOptions = {}) { const user_data = await this.get_user_data(); if (user_data.USER.USER_ID === user_id) return this.get_my_favorite_tracks(options); @@ -514,7 +520,7 @@ class GW { } // TODO: Optimise this function - async get_my_favorite_tracks(options = {}) { + async get_my_favorite_tracks(options: APIOptions = {}) { const limit = options.limit || 25; const ids_raw = await this.get_user_favorite_ids(null, { limit }); const ids = ids_raw.data.map((x) => x.SNG_ID); @@ -532,9 +538,3 @@ class GW { return result; } } - -module.exports = { - PlaylistStatus, - EMPTY_TRACK_OBJ, - GW, -}; diff --git a/deezer-js/src/index.ts b/deezer-js/src/index.ts index 390731ad..65e68cd5 100644 --- a/deezer-js/src/index.ts +++ b/deezer-js/src/index.ts @@ -28,6 +28,14 @@ interface User { loved_tracks?: number; } +export interface APIOptions { + index?: number; + limit?: number; + start?: number; + strict?: boolean; + order?: string; +} + class Deezer { logged_in: boolean; http_headers: { "User-Agent": string }; From faa3041f9007973e88c8ca276f321bbd4a7e77c6 Mon Sep 17 00:00:00 2001 From: Lachie Underhill Date: Mon, 2 Sep 2024 17:00:54 +1000 Subject: [PATCH 3/5] chore(deezer-js): fix final ts errors --- .changeset/smooth-feet-pull.md | 5 +++++ deemix/src/downloader.ts | 4 ++-- deezer-js/src/api.ts | 9 +++++---- deezer-js/src/errors.ts | 4 ++-- deezer-js/src/index.ts | 10 +++++----- deezer-js/src/utils.ts | 8 +++++--- deezer-js/tsconfig.json | 5 ++--- server/src/routes/api/get/getUserFavorites.ts | 2 +- 8 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 .changeset/smooth-feet-pull.md diff --git a/.changeset/smooth-feet-pull.md b/.changeset/smooth-feet-pull.md new file mode 100644 index 00000000..a928f396 --- /dev/null +++ b/.changeset/smooth-feet-pull.md @@ -0,0 +1,5 @@ +--- +"deezer-js": minor +--- + +Refactor deezer-js to typescript diff --git a/deemix/src/downloader.ts b/deemix/src/downloader.ts index a7558616..daab508a 100644 --- a/deemix/src/downloader.ts +++ b/deemix/src/downloader.ts @@ -238,7 +238,7 @@ async function getPreferredBitrate( } const is360Format = Object.keys(formats_360).includes(preferredBitrate); - let formats; + let formats: Record; if (!shouldFallback) { formats = { ...formats_360, ...formats_non_360 }; } else if (is360Format) { @@ -720,7 +720,7 @@ class Downloader { return returnData; } - async downloadWrapper(extraData, track: Track) { + async downloadWrapper(extraData, track?: Track) { const { trackAPI } = extraData; // Temp metadata to generate logs const itemData = { diff --git a/deezer-js/src/api.ts b/deezer-js/src/api.ts index a3f660de..04f87f64 100644 --- a/deezer-js/src/api.ts +++ b/deezer-js/src/api.ts @@ -11,6 +11,7 @@ import { IndividualAccountChangedNotAllowedException, } from "./errors"; import { APIOptions } from "."; +import { CookieJar } from "tough-cookie"; // Possible values for order parameter in search export const SearchOrder = { @@ -30,11 +31,11 @@ export const SearchOrder = { type APIArgs = Record; export class API { - http_headers: any; - cookie_jar: any; - access_token: string; + http_headers: { "User-Agent": string }; + cookie_jar: CookieJar; + access_token: string | null; - constructor(cookie_jar, headers) { + constructor(cookie_jar: CookieJar, headers: { "User-Agent": string }) { this.http_headers = headers; this.cookie_jar = cookie_jar; this.access_token = null; diff --git a/deezer-js/src/errors.ts b/deezer-js/src/errors.ts index e29a3234..36c8b05f 100644 --- a/deezer-js/src/errors.ts +++ b/deezer-js/src/errors.ts @@ -16,9 +16,9 @@ export class WrongLicense extends DeezerError { } export class WrongGeolocation extends DeezerError { - country: string; + country?: string; - constructor(country: string) { + constructor(country?: string) { super(`The track you requested can't be streamed in country ${country}`); this.name = "WrongGeolocation"; this.country = country; diff --git a/deezer-js/src/index.ts b/deezer-js/src/index.ts index 65e68cd5..203cb692 100644 --- a/deezer-js/src/index.ts +++ b/deezer-js/src/index.ts @@ -183,12 +183,12 @@ class Deezer { this.current_user = this.childs[child_n]; this.selected_account = child_n; let lang = this.current_user.language - .toString() + ?.toString() .replace(/[^0-9A-Za-z *,-.;=]/g, ""); - if (lang.slice(2, 1) === "-") { + if (lang?.slice(2, 1) === "-") { lang = lang.slice(0, 5); } else { - lang = lang.slice(0, 2); + lang = lang?.slice(0, 2); } this.http_headers["Accept-Language"] = lang; @@ -215,7 +215,7 @@ class Deezer { throw new WrongLicense(format); let response; - const result = []; + const result: (DeezerError | null)[] = []; try { response = await got @@ -237,7 +237,7 @@ class Deezer { }, }) .json(); - } catch (e) { + } catch { return []; } diff --git a/deezer-js/src/utils.ts b/deezer-js/src/utils.ts index 7277eff7..ba04e7a3 100644 --- a/deezer-js/src/utils.ts +++ b/deezer-js/src/utils.ts @@ -24,7 +24,7 @@ export function is_explicit(explicit_content_lyrics) { // maps gw-light api user/tracks to standard api export function map_user_track(track) { - const result = { + const result: Record = { id: track.SNG_ID, title: track.SNG_TITLE, link: "https://www.deezer.com/track/" + track.SNG_ID, @@ -71,6 +71,7 @@ export function map_user_track(track) { }, type: "track", }; + if (parseInt(track.SNG_ID) >= 0) { let art_picture = track.ART_PICTURE; if (!art_picture) { @@ -104,6 +105,7 @@ export function map_user_track(track) { art_picture + "/1000x1000-000000-80-0-0.jpg"; } + return result; } @@ -222,7 +224,7 @@ export function map_user_playlist(playlist, default_user_name = "") { // maps gw-light api albums to standard api export function map_album(album) { - const result = { + const result: Record = { id: album.ALB_ID, title: album.ALB_TITLE, title_short: album.ALB_TITLE, @@ -412,7 +414,7 @@ export function map_playlist(playlist) { // maps gw-light api tracks to standard api export function map_track(track) { - let result = { + let result: Record = { id: track.SNG_ID, readable: true, // not provided title: track.SNG_TITLE, diff --git a/deezer-js/tsconfig.json b/deezer-js/tsconfig.json index 7d41f2c2..740e66bc 100644 --- a/deezer-js/tsconfig.json +++ b/deezer-js/tsconfig.json @@ -1,16 +1,15 @@ { "compilerOptions": { /* https://aka.ms/tsconfig */ - "target": "es2016", + "target": "es2021", "module": "commonjs", "rootDir": "./src", - "allowJs": true, - "checkJs": true, "outDir": "./dist", "noEmit": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, + "noImplicitAny": false, "skipLibCheck": true } } diff --git a/server/src/routes/api/get/getUserFavorites.ts b/server/src/routes/api/get/getUserFavorites.ts index 39f0ec0f..6c222c94 100644 --- a/server/src/routes/api/get/getUserFavorites.ts +++ b/server/src/routes/api/get/getUserFavorites.ts @@ -17,7 +17,7 @@ const handler: ApiHandler["handler"] = async (req, res) => { result.albums = await dz.gw.get_user_albums(userID, { limit: -1 }); result.artists = await dz.gw.get_user_artists(userID, { limit: -1 }); // TODO: Lazy load favourites when navigating to relevant tab - // result.tracks = await dz.gw.get_my_favorite_tracks({ limit: -1 }) // Reenable once this isn't so slow + result.tracks = await dz.gw.get_my_favorite_tracks({ limit: 100 }); result.lovedTracks = `https://deezer.com/playlist/${dz.current_user.loved_tracks}`; } else { result = { error: "notLoggedIn" }; From 5c5ac4766482103a629afc5cb13fcd41a3d49f83 Mon Sep 17 00:00:00 2001 From: Lachie Underhill Date: Mon, 2 Sep 2024 17:27:41 +1000 Subject: [PATCH 4/5] chore: fix various ts errors --- deemix/src/errors.ts | 8 ++++---- deemix/src/types/CustomDate.ts | 2 +- deemix/src/types/Playlist.ts | 3 ++- server/src/routes/api/register.ts | 1 - server/tsconfig.json | 8 +++++--- webui/src/utils/forms.ts | 8 +++++++- webui/src/utils/toasts.ts | 5 ++++- webui/src/utils/utils.ts | 31 +++++++++---------------------- 8 files changed, 32 insertions(+), 34 deletions(-) diff --git a/deemix/src/errors.ts b/deemix/src/errors.ts index c1d94dea..aab43b6d 100644 --- a/deemix/src/errors.ts +++ b/deemix/src/errors.ts @@ -155,28 +155,28 @@ export class DownloadCanceled extends DeemixError { } export class TrackError extends DeemixError { - constructor(message: string) { + constructor(message?: string) { super(message); this.name = "TrackError"; } } export class MD5NotFound extends TrackError { - constructor(message: any) { + constructor(message?: string) { super(message); this.name = "MD5NotFound"; } } export class NoDataToParse extends TrackError { - constructor(message: any) { + constructor(message?: string) { super(message); this.name = "NoDataToParse"; } } export class AlbumDoesntExists extends TrackError { - constructor(message: any) { + constructor(message?: string) { super(message); this.name = "AlbumDoesntExists"; } diff --git a/deemix/src/types/CustomDate.ts b/deemix/src/types/CustomDate.ts index aa21bef1..ad727705 100644 --- a/deemix/src/types/CustomDate.ts +++ b/deemix/src/types/CustomDate.ts @@ -18,7 +18,7 @@ export class CustomDate { } } - format(template) { + format(template: string) { template = template.replaceAll(/D+/g, this.day); template = template.replaceAll(/M+/g, this.month); template = template.replaceAll(/Y+/g, this.year); diff --git a/deemix/src/types/Playlist.ts b/deemix/src/types/Playlist.ts index 6442fbf5..405372d6 100644 --- a/deemix/src/types/Playlist.ts +++ b/deemix/src/types/Playlist.ts @@ -13,7 +13,8 @@ export class Playlist { label: string; explicit: any; genre: string[]; - date: any; + date: CustomDate; + dateString?: string; discTotal: string; playlistID: any; owner: any; diff --git a/server/src/routes/api/register.ts b/server/src/routes/api/register.ts index 172d655d..99cb35cc 100644 --- a/server/src/routes/api/register.ts +++ b/server/src/routes/api/register.ts @@ -34,7 +34,6 @@ const methods: Method[] = [ export function registerApis(app: Application) { methods.forEach(({ method, endpoints }) => { endpoints.forEach((endpoint) => { - // @ts-expect-error app[method](prependApiPath(endpoint.path), endpoint.handler); }); }); diff --git a/server/tsconfig.json b/server/tsconfig.json index 92db8e90..50cd0df4 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,16 +1,18 @@ { "compilerOptions": { - "target": "ES2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "target": "ES2021" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "sourceMap": true /* Generates corresponding '.map' file. */, "outDir": "./dist" /* Redirect output structure to the directory. */, "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, - "strict": true /* Enable all strict type-checking options. */, + "strict": false /* Enable all strict type-checking options. */, "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + "noImplicitAny": false }, + "include": ["./**/*"], "exclude": ["node_modules", "**/*.spec.ts", "tests/**"] } diff --git a/webui/src/utils/forms.ts b/webui/src/utils/forms.ts index a9744270..5c4e37ea 100644 --- a/webui/src/utils/forms.ts +++ b/webui/src/utils/forms.ts @@ -2,6 +2,12 @@ export const getFormItem = (formEl: HTMLFormElement) => (item: string) => { const element = formEl.elements.namedItem(item); return { - [item]: element?.value, + [item]: + element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element instanceof HTMLSelectElement || + element instanceof RadioNodeList + ? element.value + : element, }; }; diff --git a/webui/src/utils/toasts.ts b/webui/src/utils/toasts.ts index d7eb9c13..dfe7f320 100644 --- a/webui/src/utils/toasts.ts +++ b/webui/src/utils/toasts.ts @@ -121,7 +121,10 @@ export const toast = function ( x: localStorage.getItem("slimSidebar") === "true" ? "3rem" : "14rem", y: undefined, }, - }).showToast(); + }); + + toastObj.showToast(); + if (id) { toastsWithId[id] = toastObj; diff --git a/webui/src/utils/utils.ts b/webui/src/utils/utils.ts index 5d61acc9..a6fa5cbb 100644 --- a/webui/src/utils/utils.ts +++ b/webui/src/utils/utils.ts @@ -74,28 +74,15 @@ export function copyToClipboard(text: string) { ghostInput.remove(); } -// On scroll event, returns currentTarget = null -// Probably on other events too -export function debounce( - func: { apply: (arg0: any, arg1: IArguments) => void }, - wait: number | undefined, - immediate: any -) { - let timeout: NodeJS.Timeout | null; - return function () { - const context = this; - const args = arguments; - const later = function () { - timeout = null; - if (!immediate) func.apply(context, args); - }; - const callNow = immediate && !timeout; - - if (timeout) clearTimeout(timeout); - - timeout = setTimeout(later, wait); - - if (callNow) func.apply(context, args); +export function debounce) => ReturnType>( + func: F, + waitFor: number +): (...args: Parameters) => void { + let timeout: NodeJS.Timeout; + + return (...args: Parameters): void => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), waitFor); }; } From 5fc3dd42fc243480041d31d06a6402cc4d85373f Mon Sep 17 00:00:00 2001 From: Lachie Underhill Date: Mon, 2 Sep 2024 17:48:43 +1000 Subject: [PATCH 5/5] chore: add types to some api and gw calls --- deemix/src/itemgen.ts | 27 +++---------- deemix/src/types/Track.ts | 66 +++++++++++++++++-------------- deezer-js/src/api.ts | 82 ++++++++++++++++++++++++++++++++++++--- deezer-js/src/gw.ts | 21 ++++++++-- 4 files changed, 136 insertions(+), 60 deletions(-) diff --git a/deemix/src/itemgen.ts b/deemix/src/itemgen.ts index 077c8b16..39dfbffd 100644 --- a/deemix/src/itemgen.ts +++ b/deemix/src/itemgen.ts @@ -9,21 +9,14 @@ const { map_user_playlist, map_track, map_album } = require("deezer-js").utils; import { each } from "async"; import { Deezer } from "deezer-js"; import { Album } from "./types/Album"; +import { APIAlbum, APITrack } from "deezer-js/src/api"; export async function generateTrackItem( dz: Deezer, id: string, bitrate: number, - trackAPI?: { - id: any; - title: any; - album: { cover_small: string }; - md5_image: any; - track_token: any; - artist: { name: any }; - explicit_lyrics: any; - }, - albumAPI?: any + trackAPI?: APITrack, + albumAPI?: APIAlbum ) { // Get essential track info if (!trackAPI) { @@ -76,22 +69,12 @@ export async function generateTrackItem( export async function generateAlbumItem( dz: Deezer, - id: string | any[], + id: string, bitrate: number, rootArtist?: { id: any; name: any; picture_small: any } ) { // Get essential album info - let albumAPI: { - id: any; - root_artist: any; - nb_tracks: number; - tracks: { data: string | any[] }; - cover_small: string; - md5_image: any; - title: any; - artist: { name: any }; - explicit_lyrics: any; - }; + let albumAPI: APIAlbum; if (String(id).startsWith("upc")) { const upcs = [id.slice(4).toString()]; upcs.push(parseInt(upcs[0], 10).toString()); // Try UPC without leading zeros as well diff --git a/deemix/src/types/Track.ts b/deemix/src/types/Track.ts index 648e192a..645166e5 100644 --- a/deemix/src/types/Track.ts +++ b/deemix/src/types/Track.ts @@ -1,6 +1,6 @@ import { Deezer, TrackFormats, utils } from "deezer-js"; import { AlbumDoesntExists, NoDataToParse } from "../errors"; -import { FeaturesOption } from "../settings"; +import { FeaturesOption, Settings } from "../settings"; import { andCommaConcat, changeCase, @@ -15,6 +15,7 @@ import { VARIOUS_ARTISTS } from "./index"; import { Lyrics } from "./Lyrics"; import { Picture } from "./Picture"; import { Playlist } from "./Playlist"; +import { APITrack } from "deezer-js/src/api"; const { map_track, map_album } = utils; export const formatsName = { @@ -41,7 +42,7 @@ class Track { filesizes: Record; local: boolean; mainArtist: Artist | null; - artist: { Main: any[] }; + artist: { Main: any[]; Featured?: any[] }; artists: any[]; album: Album | null; trackNumber: string; @@ -54,7 +55,7 @@ class Track { explicit: boolean; ISRC: string; replayGain: string; - playlist: null; + playlist: Playlist | null; position: null; searched: boolean; bitrate: keyof typeof formatsName; @@ -63,7 +64,7 @@ class Track { mainArtistsString: string; featArtistsString: string; fullArtistsString: string; - urls: Record<(typeof formatsName)[keyof typeof formatsName], string>; + urls: Partial>; downloadURL: string; rank: any; artistString: any; @@ -106,7 +107,7 @@ class Track { this.urls = {}; } - parseEssentialData(trackAPI) { + parseEssentialData(trackAPI: APITrack) { this.id = String(trackAPI.id); this.duration = trackAPI.duration; this.trackToken = trackAPI.track_token; @@ -120,51 +121,58 @@ class Track { this.urls = {}; } - async parseData(dz: Deezer, id, trackAPI, albumAPI, playlistAPI) { + async parseData( + dz: Deezer, + id, + existingTrack?: DeezerTrack, + albumAPI, + playlistAPI + ) { if (id) { - let trackAPI_new = await dz.gw.get_track_with_fallback(id); - trackAPI_new = map_track(trackAPI_new); - if (!trackAPI) trackAPI = {}; - trackAPI = { ...trackAPI, ...trackAPI_new }; - } else if (!trackAPI) { + const gwTrack = await dz.gw.get_track_with_fallback(id); + const newTrack = map_track(gwTrack); + + if (!existingTrack) existingTrack = {}; + existingTrack = { ...existingTrack, ...newTrack }; + } else if (!existingTrack) { throw new NoDataToParse(); } - this.parseEssentialData(trackAPI); + this.parseEssentialData(existingTrack); // only public api has bpm - if (!trackAPI.bpm && !this.local) { + if (!existingTrack.bpm && !this.local) { try { - const trackAPI_new = await dz.api.get_track(trackAPI.id); - trackAPI_new.release_date = trackAPI.release_date; - trackAPI = { ...trackAPI, ...trackAPI_new }; + const trackAPI_new = await dz.api.get_track(existingTrack.id); + trackAPI_new.release_date = existingTrack.release_date; + existingTrack = { ...existingTrack, ...trackAPI_new }; } catch { /* empty */ } } if (this.local) { - this.parseLocalTrackData(trackAPI); + this.parseLocalTrackData(existingTrack); } else { - this.parseTrack(trackAPI); + this.parseTrack(existingTrack); // Get Lyrics Data - if (!trackAPI.lyrics && this.lyrics.id !== "0") { + if (!existingTrack.lyrics && this.lyrics.id !== "0") { try { - trackAPI.lyrics = await dz.gw.get_track_lyrics(this.id); + existingTrack.lyrics = await dz.gw.get_track_lyrics(this.id); } catch { this.lyrics.id = "0"; } } if (this.lyrics.id !== "0") { - this.lyrics.parseLyrics(trackAPI.lyrics); + this.lyrics.parseLyrics(existingTrack.lyrics); } // Parse Album Data this.album = new Album( - trackAPI.album.id, - trackAPI.album.title, - trackAPI.album.md5_origin || "" + existingTrack.album.id, + existingTrack.album.title, + existingTrack.album.md5_origin || "" ); // Get album Data @@ -206,8 +214,8 @@ class Track { // Fill missing data if (this.album.date && !this.date) this.date = this.album.date; - if (trackAPI.genres) { - trackAPI.genres.forEach((genre) => { + if (existingTrack.genres) { + existingTrack.genres.forEach((genre) => { if (!this.album.genre.includes(genre)) this.album.genre.push(genre); }); } @@ -221,7 +229,7 @@ class Track { if (!this.artist.Main.length) { this.artist.Main = [this.mainArtist.name]; } - this.position = trackAPI.position; + this.position = existingTrack.position; if (playlistAPI) { this.playlist = new Playlist(playlistAPI); @@ -339,7 +347,7 @@ class Track { } } - async checkAndRenewTrackToken(dz) { + async checkAndRenewTrackToken(dz: Deezer) { const now = new Date(); const expiration = new Date(this.trackTokenExpiration * 1000); if (now > expiration) { @@ -349,7 +357,7 @@ class Track { } } - applySettings(settings) { + applySettings(settings: Settings) { // Check if should save the playlist as a compilation if (settings.tags.savePlaylistAsCompilation && this.playlist) { this.trackNumber = this.position; diff --git a/deezer-js/src/api.ts b/deezer-js/src/api.ts index 04f87f64..b5c05d6d 100644 --- a/deezer-js/src/api.ts +++ b/deezer-js/src/api.ts @@ -28,6 +28,78 @@ export const SearchOrder = { DURATION_DESC: "DURATION_DESC", }; +export interface APIArtist { + id: number; + 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; + radio: boolean; + tracklist: string; + role: string; +} + +export interface APIAlbum { + id: string; + title: string; + link: string; + cover: string; + cover_small: string; + cover_medium: string; + cover_big: string; + cover_xl: string; + release_date: string; // Assuming the date is in string format (e.g., "YYYY-MM-DD") +} + +export interface APITrack { + id: number; + readable: boolean; + title: string; + title_short: string; + title_version: string; + unseen: boolean; + isrc: string; + link: string; + share: string; + duration: number; + track_position: number; + disk_number: number; + rank: number; + release_date: string; // Assuming the date is in string format (e.g., "YYYY-MM-DD") + explicit_lyrics: boolean; + explicit_content_lyrics: number; + explicit_content_cover: number; + preview: string; + bpm: number; + 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 + md5_image: string; + track_token: string; + artist: APIArtist; + album: APIAlbum; +} + +export interface APIContributor { + id: number; + name: string; + link: string; + share: string; + picture: string; + picture_small: string; + picture_medium: string; + picture_big: string; + picture_xl: string; + role: string; +} + type APIArgs = Record; export class API { @@ -41,7 +113,7 @@ export class API { this.access_token = null; } - async api_call(method: string, args: APIArgs = {}) { + async api_call(method: string, args: APIArgs = {}): Promise { if (this.access_token) args["access_token"] = this.access_token; let result_json; @@ -127,8 +199,8 @@ export class API { return result_json; } - get_album(album_id) { - return this.api_call(`album/${album_id}`); + get_album(album_id: string): Promise { + return this.api_call(`album/${album_id}`) as Promise; } get_album_by_UPC(upc) { @@ -423,8 +495,8 @@ export class API { return this.api_call("search/user", args); } - get_track(song_id) { - return this.api_call(`track/${song_id}`); + get_track(song_id: string): Promise { + return this.api_call(`track/${song_id}`) as Promise; } get_track_by_ISRC(isrc) { diff --git a/deezer-js/src/gw.ts b/deezer-js/src/gw.ts index 574e8242..f0189ae6 100644 --- a/deezer-js/src/gw.ts +++ b/deezer-js/src/gw.ts @@ -15,6 +15,19 @@ export const PlaylistStatus = { COLLABORATIVE: 2, }; +export interface GWTrack { + SNG_ID: number; + SNG_TITLE: string; + DURATION: number; + MD5_ORIGIN: number; + MEDIA_VERSION: number; + FILESIZE: number; + ALB_TITLE: string; + ALB_PICTURE: string; + ART_ID: number; + ART_NAME: string; +} + export const EMPTY_TRACK_OBJ = { SNG_ID: 0, SNG_TITLE: "", @@ -26,7 +39,7 @@ export const EMPTY_TRACK_OBJ = { ALB_PICTURE: "", ART_ID: 0, ART_NAME: "", -}; +} satisfies GWTrack; export class GW { http_headers: any; @@ -39,7 +52,7 @@ export class GW { this.api_token = null; } - async api_call(method: string, args?: any, params?: any) { + async api_call(method: string, args?: any, params?: any): Promise { if (typeof args === undefined) args = {}; if (typeof params === undefined) params = {}; if (!this.api_token && method !== "deezer.getUserData") @@ -132,7 +145,7 @@ export class GW { return this.api_call("deezer.getChildAccounts"); } - get_track(sng_id) { + get_track(sng_id): Promise { return this.api_call("song.getData", { SNG_ID: sng_id }); } @@ -510,7 +523,7 @@ export class GW { if (user_data.USER.USER_ID === user_id) return this.get_my_favorite_tracks(options); const limit = options.limit || 25; - let data = this.get_user_profile_page(user_id, "loved", { limit }); + let data = await this.get_user_profile_page(user_id, "loved", { limit }); data = data.TAB.loved.data; const result = []; data.forEach((track) => {