tidal converter
This commit is contained in:
parent
03983dfcde
commit
a6f9ef4bef
|
@ -17,7 +17,8 @@
|
|||
"axios": "^0.21.1",
|
||||
"browser-id3-writer": "^4.4.0",
|
||||
"delay": "^5.0.0",
|
||||
"node-html-parser": "^3.1.0"
|
||||
"node-html-parser": "^3.1.0",
|
||||
"p-queue": "^6.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.35",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * as deezerConverter from './deezer';
|
||||
export * as tidalConverter from './tidal';
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
import axios from 'axios';
|
||||
import PQueue from 'p-queue';
|
||||
import {isrc2deezer} from './deezer';
|
||||
import type {playlistInfo, trackType} from '../types';
|
||||
|
||||
interface commonType {
|
||||
id: number;
|
||||
title: string;
|
||||
duration: number;
|
||||
premiumStreamingOnly: boolean;
|
||||
trackNumber: number;
|
||||
copyright: string;
|
||||
url: string;
|
||||
explicit: boolean;
|
||||
audioQuality: string;
|
||||
artist: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
album: {
|
||||
id: number;
|
||||
title: string;
|
||||
cover: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface tidalTrackType extends commonType {
|
||||
isrc: string;
|
||||
editable: boolean;
|
||||
audioQuality: string;
|
||||
album: {
|
||||
id: number;
|
||||
title: string;
|
||||
cover: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface tidalAlbumType extends commonType {
|
||||
cover: string;
|
||||
videoCover: null | string;
|
||||
upc: string;
|
||||
audioQuality: string;
|
||||
}
|
||||
|
||||
interface tidalPlaylistType {
|
||||
uuid: string;
|
||||
title: string;
|
||||
numberOfTracks: number;
|
||||
numberOfVideos: number;
|
||||
creator: {
|
||||
id: number;
|
||||
};
|
||||
description: string;
|
||||
duration: number;
|
||||
lastUpdated: string;
|
||||
created: string;
|
||||
type: string;
|
||||
publicPlaylist: boolean;
|
||||
url: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface listType {
|
||||
limit: number;
|
||||
offset: number;
|
||||
totalNumberOfItems: number;
|
||||
}
|
||||
|
||||
interface tidalArtistTopTracksType extends listType {
|
||||
items: tidalTrackType[];
|
||||
}
|
||||
|
||||
interface tidalAlbumsTracksType extends listType {
|
||||
items: tidalAlbumType[];
|
||||
}
|
||||
|
||||
interface tidalPlaylistTracksType extends listType {
|
||||
items: tidalTrackType[];
|
||||
}
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: 'https://api.tidal.com/v1/',
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'user-agent': 'TIDAL/3704 CFNetwork/1220.1 Darwin/20.3.0',
|
||||
'x-tidal-token': 'i4ZDjcyhed7Mu47q',
|
||||
},
|
||||
params: {limit: 500, countryCode: 'US'},
|
||||
});
|
||||
|
||||
const queue = new PQueue({concurrency: 25});
|
||||
|
||||
/**
|
||||
* Get a track by its id
|
||||
* @param {string} id - track id
|
||||
* @example tidal.getTrack('64975224')
|
||||
*/
|
||||
export const getTrack = async (id: string): Promise<tidalTrackType> => {
|
||||
const {data} = await client(`tracks/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an album by its id
|
||||
* @param {string} id - album id
|
||||
* @example tidal.getAlbum('80216363')
|
||||
*/
|
||||
export const getAlbum = async (id: string): Promise<tidalAlbumType> => {
|
||||
const {data} = await client(`albums/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get album tracks by album id
|
||||
* @param {string} id - album id
|
||||
* @example tidal.getAlbumTracks('80216363')
|
||||
*/
|
||||
export const getAlbumTracks = async (id: string): Promise<tidalAlbumsTracksType> => {
|
||||
const {data} = await client(`albums/${id}/tracks`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get artist albums by artist id
|
||||
* @param {string} id - artist id
|
||||
* @example tidal.getArtistAlbums('3575680')
|
||||
*/
|
||||
export const getArtistAlbums = async (id: string): Promise<tidalAlbumsTracksType> => {
|
||||
const {data} = await client(`artists/${id}/albums`);
|
||||
data.items = data.items.filter((item: any) => item.artist.id.toString() === id);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get top tracks by artist
|
||||
* @param {string} id - artist id
|
||||
* @example tidal.getArtistTopTracks('3575680')
|
||||
*/
|
||||
export const getArtistTopTracks = async (id: string): Promise<tidalArtistTopTracksType> => {
|
||||
const {body}: any = await client(`artists/${id}/toptracks`);
|
||||
body.items = (body as tidalArtistTopTracksType).items.filter((item) => item.artist.id.toString() === id);
|
||||
return body;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a playlist by its uuid
|
||||
* @param {string} uuid - playlist uuid
|
||||
* @example tidal.getPlaylist('1c5d01ed-4f05-40c4-bd28-0f73099e9648')
|
||||
*/
|
||||
export const getPlaylist = async (uuid: string): Promise<tidalPlaylistType> => {
|
||||
const {data} = await client(`playlists/${uuid}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get playlist tracks by playlist uuid
|
||||
* @param {string} uuid - playlist uuid
|
||||
* @example tidal.getPlaylistTracks('1c5d01ed-4f05-40c4-bd28-0f73099e9648')
|
||||
*/
|
||||
export const getPlaylistTracks = async (uuid: string): Promise<tidalPlaylistTracksType> => {
|
||||
const {data} = await client(`playlists/${uuid}/tracks`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get valid urls to album art
|
||||
* @param {string} uuid - album art uuid (can be found as cover property in album object)
|
||||
* @example tidal.albumArtToUrl('9a56f482-e9cf-46c3-bb21-82710e7854d4')
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const albumArtToUrl = (uuid: string) => {
|
||||
const baseUrl = `https://resources.tidal.com/images/${uuid.replace(/-/g, '/')}`;
|
||||
return {
|
||||
sm: `${baseUrl}/160x160.jpg`,
|
||||
md: `${baseUrl}/320x320.jpg`,
|
||||
lg: `${baseUrl}/640x640.jpg`,
|
||||
xl: `${baseUrl}/1280x1280.jpg`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Find tidal artists tracks on deezer
|
||||
* @param {string} id - artist id
|
||||
* @example tidal.artist2Deezer('3575680')
|
||||
*/
|
||||
export const artist2Deezer = async (
|
||||
id: string,
|
||||
onError?: (item: tidalTrackType, index: number, err: Error) => void,
|
||||
): Promise<trackType[]> => {
|
||||
const {items} = await getArtistTopTracks(id);
|
||||
const tracks: trackType[] = [];
|
||||
|
||||
await queue.addAll(
|
||||
items.map((item, index) => {
|
||||
return async () => {
|
||||
try {
|
||||
const track = await isrc2deezer(item.title, item.isrc);
|
||||
// console.log(signale.success(`Track #${index}: ${item.name}`));
|
||||
tracks.push(track);
|
||||
} catch (err) {
|
||||
if (onError) {
|
||||
onError(item, index, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return tracks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find same set of playlist tracks on deezer
|
||||
* @param {string} uuid - playlist uuid
|
||||
* @example tidal.playlist2Deezer('1c5d01ed-4f05-40c4-bd28-0f73099e9648')
|
||||
*/
|
||||
export const playlist2Deezer = async (
|
||||
uuid: string,
|
||||
onError?: (item: tidalTrackType, index: number, err: Error) => void,
|
||||
): Promise<[playlistInfo, trackType[]]> => {
|
||||
const body = await getPlaylist(uuid);
|
||||
const {items} = await getPlaylistTracks(uuid);
|
||||
const tracks: trackType[] = [];
|
||||
|
||||
await queue.addAll(
|
||||
items.map((item, index) => {
|
||||
return async () => {
|
||||
try {
|
||||
const track = await isrc2deezer(item.title, item.isrc);
|
||||
// console.log(signale.success(`Track #${index}: ${item.track.name}`));
|
||||
track.TRACK_POSITION = index + 1;
|
||||
tracks.push(track);
|
||||
} catch (err) {
|
||||
if (onError) {
|
||||
onError(item, index, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const userId = body.creator.id.toString();
|
||||
const playlistInfoData: playlistInfo = {
|
||||
PLAYLIST_ID: body.uuid,
|
||||
PARENT_USERNAME: userId,
|
||||
PARENT_USER_ID: userId,
|
||||
PICTURE_TYPE: 'cover',
|
||||
PLAYLIST_PICTURE: body.image,
|
||||
TITLE: body.title,
|
||||
TYPE: '0',
|
||||
STATUS: '0',
|
||||
USER_ID: userId,
|
||||
DATE_ADD: body.created,
|
||||
DATE_MOD: body.lastUpdated,
|
||||
DATE_CREATE: body.created,
|
||||
NB_SONG: body.numberOfTracks,
|
||||
NB_FAN: 0,
|
||||
CHECKSUM: body.created,
|
||||
HAS_ARTIST_LINKED: false,
|
||||
IS_SPONSORED: false,
|
||||
IS_EDITO: false,
|
||||
__TYPE__: 'playlist',
|
||||
};
|
||||
|
||||
return [playlistInfoData, tracks];
|
||||
};
|
15
yarn.lock
15
yarn.lock
|
@ -943,6 +943,11 @@ esutils@^2.0.2, esutils@^2.0.3:
|
|||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
eventemitter3@^4.0.4:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
|
@ -1732,7 +1737,15 @@ p-map@^4.0.0:
|
|||
dependencies:
|
||||
aggregate-error "^3.0.0"
|
||||
|
||||
p-timeout@^3.1.0:
|
||||
p-queue@^6.6.2:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
|
||||
integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.4"
|
||||
p-timeout "^3.2.0"
|
||||
|
||||
p-timeout@^3.1.0, p-timeout@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
|
||||
integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
|
||||
|
|
Loading…
Reference in New Issue