mirror of
https://github.com/d-fi/d-fi-core.git
synced 2025-07-22 13:14:57 +02:00
Initial commit
This commit is contained in:
commit
2b2ef342d3
6
.eslintrc.js
Normal file
6
.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
};
|
22
.github/workflows/test.yml
vendored
Normal file
22
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn install
|
||||
- run: yarn test
|
||||
env:
|
||||
CI: true
|
116
.gitignore
vendored
Normal file
116
.gitignore
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
8
.prettierrc.js
Normal file
8
.prettierrc.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
bracketSpacing: false,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
endOfLine: "lf",
|
||||
};
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 sayem314
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
||||
## d-fi-core [](https://github.com/d-fi/d-fi-core/actions)
|
||||
|
||||
d-fi is a streaming music downloader. This core module is designed to be used on future version of d-fi.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ yarn add d-fi-core
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Here's a simple example to download tracks.
|
||||
|
||||
```ts
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import * as api from 'd-fi-core';
|
||||
|
||||
// Init api with arl
|
||||
await api.initDeezerApi(arl_from_cookie);
|
||||
|
||||
// GET Track Object
|
||||
const track = await api.getTrackInfo(SNG_ID);
|
||||
|
||||
// Parse download URL for 128kbps
|
||||
const url = getTrackDownloadUrl(track, 1);
|
||||
|
||||
// Download encrypted track
|
||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||
|
||||
// Decrypt track
|
||||
const decryptedTrack = decryptDownload(data, track.SNG_ID);
|
||||
|
||||
// Add id3 metadata
|
||||
const trackWithMetadata = await api.addTrackTags(decryptedTrack, track, false, 500);
|
||||
|
||||
// Save file to disk
|
||||
fs.writeFileSync(track.SNG_TITLE + '.mp3', trackWithMetadata);
|
||||
```
|
||||
|
||||
### [Read FAQ](https://github.com/d-fi/d-fi-core/blob/master/docs/README.md)
|
||||
|
||||
## Methods
|
||||
|
||||
All method returns `Object` or throws `Error`. Make sure to catch error on your side.
|
||||
|
||||
### `.initDeezerApi(arl_cookie);`
|
||||
|
||||
> It is recommended that you first init the app with this method using your arl cookie.
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ------------ | :------: | -------: |
|
||||
| `arl_cookie` | Yes | `string` |
|
||||
|
||||
### `.getTrackInfo(track_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ---------- | :------: | -------: |
|
||||
| `track_id` | Yes | `string` |
|
||||
|
||||
### `.getLyrics(track_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ---------- | :------: | -------: |
|
||||
| `track_id` | Yes | `string` |
|
||||
|
||||
### `.getAlbumInfo(album_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ---------- | :------: | -------: |
|
||||
| `album_id` | Yes | `string` |
|
||||
|
||||
### `.getAlbumTracks(album_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ---------- | :------: | -------: |
|
||||
| `album_id` | Yes | `string` |
|
||||
|
||||
### `.getPlaylistInfo(playlist_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ------------- | :------: | -------: |
|
||||
| `playlist_id` | Yes | `string` |
|
||||
|
||||
### `.getPlaylistTracks(playlist_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ------------- | :------: | -------: |
|
||||
| `playlist_id` | Yes | `string` |
|
||||
|
||||
### `.getArtistInfo(artist_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ----------- | :------: | -------: |
|
||||
| `artist_id` | Yes | `string` |
|
||||
|
||||
### `.getDiscography(artist_id, limit);`
|
||||
|
||||
| Parameters | Required | Type | Default | Description |
|
||||
| ----------- | :------: | -------: | ------: | ----------------------: |
|
||||
| `artist_id` | Yes | `string` | - | artist id |
|
||||
| `limit` | No | `number` | 500 | maximum tracks to fetch |
|
||||
|
||||
### `.getProfile(user_id);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ---------- | :------: | -------: |
|
||||
| `user_id` | Yes | `string` |
|
||||
|
||||
### `.searchAlternative(artist_name, song_name);`
|
||||
|
||||
| Parameters | Required | Type |
|
||||
| ------------- | :------: | -------: |
|
||||
| `artist_name` | Yes | `string` |
|
||||
| `song_name` | Yes | `string` |
|
||||
|
||||
### `.searchMusic(query, types, limit);`
|
||||
|
||||
| Parameters | Required | Type | Default | Description |
|
||||
| ---------- | :------: | -------: | --------: | ------------------------------: |
|
||||
| `query` | Yes | `string` | - | search query |
|
||||
| `types` | No | `array` | ['TRACK'] | array of search types |
|
||||
| `limit` | No | `number` | 15 | maximum item to fetch per types |
|
||||
|
||||
### Donations
|
||||
|
||||
If you want to show your appreciation, you can donate me on [ko-fi](https://ko-fi.com/Z8Z5KDA6) or [buy me a coffee](https://www.buymeacoffee.com/sayem). Thanks!
|
||||
|
||||
> Made with :heart: & :coffee: by Sayem
|
17
docs/faq.md
Normal file
17
docs/faq.md
Normal file
@ -0,0 +1,17 @@
|
||||
## Frequently Asked Questions (FAQ)
|
||||
|
||||
- How to get arl cookie?
|
||||
|
||||
> Open Google Chrome in PC
|
||||
>
|
||||
> - Go to www.deezer.com and log into your account
|
||||
> - After logging in press F12 to open up Developer Tools
|
||||
> - Go under the Application tab (if you don't see it click the double arrow)
|
||||
> - Open the cookie dropdown
|
||||
> - Select www.deezer.com
|
||||
> - Find the arl cookie (It should be 192 chars long)
|
||||
> - That's your ARL, now you can use it in the app
|
||||
|
||||
Here is a sample gif tutorial.
|
||||
|
||||

|
42
package.json
Normal file
42
package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "d-fi-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Core module for d-fi",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"prepublish": "eslint . && tsc",
|
||||
"prebuild": "eslint .",
|
||||
"build": "tsc",
|
||||
"test": "ava"
|
||||
},
|
||||
"repository": "https://github.com/d-fi/d-fi-core",
|
||||
"author": "Sayem Chowdhury",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"browser-id3-writer": "^4.4.0",
|
||||
"delay": "^5.0.0",
|
||||
"metaflac-js2": "^1.0.7",
|
||||
"node-html-parser": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.31",
|
||||
"ava": "^3.15.0",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"prettier": "^2.2.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"ava": {
|
||||
"extensions": [
|
||||
"ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"timeout": "30s",
|
||||
"verbose": true
|
||||
}
|
||||
}
|
120
src/api.ts
Normal file
120
src/api.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import axios from './lib/request';
|
||||
import FastLRU from './lib/fast-lru';
|
||||
import type {
|
||||
albumType,
|
||||
trackType,
|
||||
lyricsType,
|
||||
albumTracksType,
|
||||
playlistInfo,
|
||||
playlistTracksType,
|
||||
artistInfoType,
|
||||
discographyType,
|
||||
profileType,
|
||||
searchType,
|
||||
} from './types';
|
||||
|
||||
// expire cache in 60 minutes
|
||||
const lru = new FastLRU({
|
||||
maxSize: 1000,
|
||||
ttl: 60 * 60000,
|
||||
});
|
||||
|
||||
/**
|
||||
* Make post requests to deezer api
|
||||
* @param {Object} body post body
|
||||
* @param {String} method request method
|
||||
*/
|
||||
export const request = async (body: object, method: string) => {
|
||||
const cacheKey = method + ':' + Object.entries(body).join(':');
|
||||
const cache = lru.get(cacheKey);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const {
|
||||
data: {error, results},
|
||||
} = await axios.post('/gateway.php', body, {params: {method}});
|
||||
|
||||
if (Object.keys(results).length > 0) {
|
||||
lru.set(cacheKey, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
const errorMessage = Object.entries(error).join(', ');
|
||||
throw new Error(errorMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} sng_id song id
|
||||
*/
|
||||
export const getTrackInfo = (sng_id: string): Promise<trackType> => request({sng_id}, 'song.getData');
|
||||
|
||||
/**
|
||||
* @param {String} sng_id song id
|
||||
*/
|
||||
export const getLyrics = (sng_id: string): Promise<lyricsType> => request({sng_id}, 'song.getLyrics');
|
||||
|
||||
/**
|
||||
* @param {String} alb_id album id
|
||||
*/
|
||||
export const getAlbumInfo = (alb_id: string): Promise<albumType> => request({alb_id}, 'album.getData');
|
||||
|
||||
/**
|
||||
* @param {String} alb_id user id
|
||||
*/
|
||||
export const getAlbumTracks = (alb_id: string): Promise<albumTracksType> =>
|
||||
request({alb_id, lang: 'us', nb: -1}, 'song.getListByAlbum');
|
||||
|
||||
/**
|
||||
* @param {String} playlist_id playlist id
|
||||
*/
|
||||
export const getPlaylistInfo = (playlist_id: string): Promise<playlistInfo> =>
|
||||
request({playlist_id, lang: 'en'}, 'playlist.getData');
|
||||
|
||||
/**
|
||||
* @param {String} playlist_id playlist id
|
||||
*/
|
||||
export const getPlaylistTracks = (playlist_id: string): Promise<playlistTracksType> =>
|
||||
request({playlist_id, lang: 'en', nb: -1, start: 0, tab: 0, tags: true, header: true}, 'playlist.getSongs');
|
||||
|
||||
/**
|
||||
* @param {String} art_id artist id
|
||||
*/
|
||||
export const getArtistInfo = (art_id: string): Promise<artistInfoType> =>
|
||||
request({art_id, filter_role_id: [0], lang: 'en', tab: 0, nb: -1, start: 0}, 'artist.getData');
|
||||
|
||||
/**
|
||||
* @param {String} art_id artist id
|
||||
* @param {String} nb number of total song to fetch
|
||||
*/
|
||||
export const getDiscography = (art_id: string, nb = 500): Promise<discographyType> =>
|
||||
request({art_id, filter_role_id: [0], lang: 'en', nb, nb_songs: -1, start: 0}, 'album.getDiscography');
|
||||
|
||||
/**
|
||||
* @param {String} user_id user id
|
||||
*/
|
||||
export const getProfile = (user_id: string): Promise<profileType> =>
|
||||
request({user_id, tab: 'loved', nb: -1}, 'mobile.pageUser');
|
||||
|
||||
/**
|
||||
* @param {String} artist artist name
|
||||
* @param {String} song song name
|
||||
*/
|
||||
export const searchAlternative = (artist: string, song: string): Promise<searchType> =>
|
||||
request(
|
||||
{
|
||||
query: `artist:'${artist}' track:'${song}'`,
|
||||
types: ['TRACK'],
|
||||
nb: 10,
|
||||
},
|
||||
'mobile_suggest',
|
||||
);
|
||||
|
||||
type searchTypesProp = 'ALBUM' | 'ARTIST' | 'TRACK' | 'PLAYLIST' | 'RADIO' | 'SHOW' | 'USER' | 'LIVESTREAM' | 'CHANNEL';
|
||||
/**
|
||||
* @param {String} query search query
|
||||
* @param {Array} types search types
|
||||
* @param {String} nb number of items to fetch
|
||||
*/
|
||||
export const searchMusic = (query: string, types: searchTypesProp[] = ['TRACK'], nb = 15): Promise<searchType> =>
|
||||
request({query, nb, types}, 'mobile_suggest');
|
4
src/index.ts
Normal file
4
src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export {initDeezerApi} from './lib/request';
|
||||
export * from './api';
|
||||
export * from './lib/decrypt';
|
||||
export * from './metadata-writer';
|
79
src/lib/decrypt.ts
Normal file
79
src/lib/decrypt.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import crypto from 'crypto';
|
||||
import type {trackType} from '../types';
|
||||
|
||||
const md5 = (data: string, type: crypto.Encoding = 'ascii') => {
|
||||
const md5sum = crypto.createHash('md5');
|
||||
md5sum.update(data, type);
|
||||
return md5sum.digest('hex');
|
||||
};
|
||||
|
||||
const getSongFileName = ({MD5_ORIGIN, SNG_ID, MEDIA_VERSION}: trackType, quality: number) => {
|
||||
const step1 = [MD5_ORIGIN, quality, SNG_ID, MEDIA_VERSION].join('¤');
|
||||
|
||||
let step2 = md5(step1) + '¤' + step1 + '¤';
|
||||
while (step2.length % 16 > 0) step2 += ' ';
|
||||
|
||||
return crypto.createCipheriv('aes-128-ecb', 'jo6aey6haid2Teih', '').update(step2, 'ascii', 'hex');
|
||||
};
|
||||
|
||||
const getBlowfishKey = (trackId: string) => {
|
||||
let SECRET = 'g4el58wc' + '0zvf9na1';
|
||||
let idMd5 = md5(trackId);
|
||||
let bfKey = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i));
|
||||
}
|
||||
return bfKey;
|
||||
};
|
||||
|
||||
const decryptChunk = (chunk: Buffer, blowFishKey: string) => {
|
||||
let cipher = crypto.createDecipheriv('bf-cbc', blowFishKey, Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]));
|
||||
cipher.setAutoPadding(false);
|
||||
// @ts-ignore
|
||||
return cipher.update(chunk, 'binary', 'binary') + cipher.final();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param source Downloaded song from `getTrackDownloadUrl`
|
||||
* @param trackId Song ID as string
|
||||
*/
|
||||
export const decryptDownload = (source: Buffer, trackId: string) => {
|
||||
// let part_size = 0x1800;
|
||||
let chunk_size = 2048;
|
||||
let blowFishKey = getBlowfishKey(trackId);
|
||||
let i = 0;
|
||||
let position = 0;
|
||||
|
||||
let destBuffer = Buffer.alloc(source.length);
|
||||
destBuffer.fill(0);
|
||||
|
||||
while (position < source.length) {
|
||||
let chunk;
|
||||
if (source.length - position >= 2048) chunk_size = 2048;
|
||||
else chunk_size = source.length - position;
|
||||
chunk = Buffer.alloc(chunk_size);
|
||||
|
||||
let chunkString;
|
||||
chunk.fill(0);
|
||||
source.copy(chunk, 0, position, position + chunk_size);
|
||||
if (i % 3 > 0 || chunk_size < 2048) chunkString = chunk.toString('binary');
|
||||
else chunkString = decryptChunk(chunk, blowFishKey);
|
||||
|
||||
destBuffer.write(chunkString, position, chunkString.length, 'binary');
|
||||
position += chunk_size;
|
||||
i++;
|
||||
}
|
||||
|
||||
return destBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param track Track info json returned from `getTrackInfo`
|
||||
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
||||
*/
|
||||
export const getTrackDownloadUrl = (track: trackType, quality: number) => {
|
||||
const cdn = track.MD5_ORIGIN[0]; // cdn destination
|
||||
const filename = getSongFileName(track, quality); // encrypted file name
|
||||
return `http://e-cdn-proxy-${cdn}.deezer.com/mobile/1/${filename}`;
|
||||
};
|
142
src/lib/fast-lru.ts
Normal file
142
src/lib/fast-lru.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Fast LRU & TTL cache
|
||||
* @param {Integer} options.max - Max entries in the cache. @default Infinity
|
||||
* @param {Integer} options.ttl - Timeout before removing entries. @default Infinity
|
||||
*/
|
||||
class FastLRU {
|
||||
_max: number;
|
||||
_ttl: number;
|
||||
_cache: Map<any, any>;
|
||||
_meta: {
|
||||
[key: string]: any;
|
||||
};
|
||||
constructor({maxSize = Infinity, ttl = 0}) {
|
||||
// Default options
|
||||
this._max = maxSize;
|
||||
this._ttl = ttl;
|
||||
this._cache = new Map();
|
||||
|
||||
// Metadata for entries
|
||||
this._meta = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new entry
|
||||
*/
|
||||
set(key: string, value: any, ttl: number = this._ttl) {
|
||||
// Execution time
|
||||
const time = Date.now();
|
||||
|
||||
// Remvove least recently used elements if exceeds max bytes
|
||||
if (this._cache.size >= this._max) {
|
||||
const items = Object.values(this._meta);
|
||||
if (this._ttl > 0) {
|
||||
for (const item of items) {
|
||||
if (item.expire < time) {
|
||||
this.delete(item.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._cache.size >= this._max) {
|
||||
const least = items.sort((a, b) => a.hits - b.hits)[0];
|
||||
this.delete(least.key);
|
||||
}
|
||||
}
|
||||
|
||||
// Override if key already set
|
||||
this._cache.set(key, value);
|
||||
this._meta[key] = {
|
||||
key,
|
||||
hits: 0,
|
||||
expire: time + ttl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry
|
||||
*/
|
||||
get(key: string) {
|
||||
if (this._cache.has(key)) {
|
||||
const item = this._cache.get(key);
|
||||
if (this._ttl > 0) {
|
||||
const time = Date.now();
|
||||
if (this._meta[key].expire < time) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this._meta[key].hits++;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get without hitting hits
|
||||
*/
|
||||
peek(key: string) {
|
||||
return this._cache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry
|
||||
*/
|
||||
delete(key: string) {
|
||||
delete this._meta[key];
|
||||
this._cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all entries
|
||||
*/
|
||||
clear() {
|
||||
this._cache.clear();
|
||||
this._meta = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check has entry
|
||||
*/
|
||||
has(key: string) {
|
||||
return this._cache.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all kies
|
||||
* @returns {Iterator} Iterator on all kies
|
||||
*/
|
||||
keys() {
|
||||
return this._cache.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over values
|
||||
*/
|
||||
values() {
|
||||
return this._cache.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over entries
|
||||
*/
|
||||
entries() {
|
||||
return this._cache.entries();
|
||||
}
|
||||
|
||||
/**
|
||||
* For each
|
||||
*/
|
||||
forEach(cb: any) {
|
||||
return this._cache.forEach(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entries total size
|
||||
*/
|
||||
get size() {
|
||||
return this._cache.size;
|
||||
}
|
||||
}
|
||||
|
||||
export default FastLRU;
|
65
src/lib/request.ts
Normal file
65
src/lib/request.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import axios, {AxiosRequestConfig} from 'axios';
|
||||
import delay from 'delay';
|
||||
|
||||
const ARL =
|
||||
'a62f40421c53479e411c78cd7420f7e40e7de97ab9e6436558145bcedf4557bc79946e96c02d7fef9a3166024a11c4a501236eca892b1ef989267244153af6efc4fec75d47a776129e971a4c68cef0b33b1633baf0eb0e8c08e170224e9527fc';
|
||||
const APIPayload = {
|
||||
version: '8.32.0',
|
||||
api_key: 'ZAIVAHCEISOHWAICUQUEXAEPICENGUAFAEZAIPHAELEEVAHPHUCUFONGUAPASUAY',
|
||||
output: 3,
|
||||
input: 3,
|
||||
buildId: 'ios12_universal',
|
||||
screenHeight: '480',
|
||||
screenWidth: '320',
|
||||
lang: 'en',
|
||||
};
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: 'https://api.deezer.com/1.0',
|
||||
withCredentials: true,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Accept-Language': 'en-US',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': 'Deezer/8.32.0.2 (iOS; 14.4; Mobile; en; iPhone10_5)',
|
||||
},
|
||||
params: APIPayload,
|
||||
data: APIPayload,
|
||||
});
|
||||
|
||||
export const initDeezerApi = async (arl: string): Promise<string> => {
|
||||
const options: AxiosRequestConfig = {
|
||||
params: {
|
||||
method: 'deezer.ping',
|
||||
api_version: '1.0',
|
||||
api_token: '',
|
||||
},
|
||||
};
|
||||
if (arl) {
|
||||
options.headers = {cookie: 'arl=' + arl};
|
||||
}
|
||||
|
||||
const {data} = await instance.get('https://www.deezer.com/ajax/gw-light.php', options);
|
||||
instance.defaults.params.sid = data.results.SESSION;
|
||||
return data.results.SESSION;
|
||||
};
|
||||
|
||||
// Add a request interceptor
|
||||
instance.interceptors.response.use(async (response) => {
|
||||
if (response.data.error && Object.keys(response.data.error).length > 0) {
|
||||
if (response.data.error.NEED_API_AUTH_REQUIRED) {
|
||||
await initDeezerApi(ARL);
|
||||
return await instance(response.config);
|
||||
} else if (response.data.error.code == 4) {
|
||||
await delay(1000);
|
||||
return await instance(response.config);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
export default instance;
|
36
src/metadata-writer/abumCover.ts
Normal file
36
src/metadata-writer/abumCover.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import axios from 'axios';
|
||||
import FastLRU from '../lib/fast-lru';
|
||||
import type {trackType} from '../types';
|
||||
|
||||
type coverSize = 56 | 250 | 500 | 1000 | 1500 | 1800 | number;
|
||||
|
||||
// expire cache in 30 minutes
|
||||
const lru = new FastLRU({
|
||||
maxSize: 50,
|
||||
ttl: 30 * 60000,
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} track track info json from deezer api
|
||||
* @param {Number} albumCoverSize in pixel, between 56-1800
|
||||
*/
|
||||
export const downloadAlbumCover = async (track: trackType, albumCoverSize: coverSize): Promise<Buffer | null> => {
|
||||
if (!track.ALB_PICTURE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cache = lru.get(track.ALB_PICTURE + albumCoverSize);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `https://e-cdns-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/${albumCoverSize}x${albumCoverSize}-000000-80-0-0.jpg`;
|
||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||
lru.set(track.ALB_PICTURE + albumCoverSize, data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
57
src/metadata-writer/flacmetata.ts
Normal file
57
src/metadata-writer/flacmetata.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// @ts-ignore
|
||||
import Metaflac from 'metaflac-js2';
|
||||
import type {trackType} from '../types';
|
||||
|
||||
export const writeMetadataFlac = (buffer: Buffer, track: trackType, cover?: Buffer | null): Buffer => {
|
||||
const flac = new Metaflac(buffer);
|
||||
flac.setTag('TITLE=' + track.SNG_TITLE);
|
||||
flac.setTag('ALBUM=' + track.ALB_TITLE);
|
||||
|
||||
const artists = track.ART_NAME.split(
|
||||
new RegExp(' featuring | feat. | Ft. | ft. | vs | vs. | x | - |, ', 'g'),
|
||||
).map((a) => a.trim());
|
||||
flac.setTag('ARTIST=' + artists.join(', '));
|
||||
|
||||
if (track.DISK_NUMBER) {
|
||||
flac.setTag('DISCNUMBER=' + track.DISK_NUMBER);
|
||||
}
|
||||
|
||||
flac.setTag('LENGTH=' + track.DURATION);
|
||||
flac.setTag('ISRC=' + track.ISRC);
|
||||
flac.setTag('MEDIA=Digital Media');
|
||||
|
||||
if (track.LYRICS) {
|
||||
flac.setTag('LYRICS=' + track.LYRICS.LYRICS_TEXT);
|
||||
}
|
||||
|
||||
if (track.SNG_CONTRIBUTORS.composer) {
|
||||
flac.setTag('COMPOSER=' + track.SNG_CONTRIBUTORS.composer.join(', '));
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.publisher) {
|
||||
flac.setTag('ORGANIZATION=' + track.SNG_CONTRIBUTORS.publisher.join(', '));
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.producer) {
|
||||
flac.setTag('PRODUCER=' + track.SNG_CONTRIBUTORS.producer.join(', '));
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.engineer) {
|
||||
flac.setTag('ENGINEER=' + track.SNG_CONTRIBUTORS.engineer.join(', '));
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.writer) {
|
||||
flac.setTag('WRITER=' + track.SNG_CONTRIBUTORS.writer.join(', '));
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.author) {
|
||||
flac.setTag('AUTHOR=' + track.SNG_CONTRIBUTORS.author.join(', '));
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.mixer) {
|
||||
flac.setTag('MIXER=' + track.SNG_CONTRIBUTORS.mixer.join(', '));
|
||||
}
|
||||
|
||||
if (cover) {
|
||||
flac.importPicture(cover);
|
||||
}
|
||||
|
||||
flac.setTag('SOURCE=Deezer');
|
||||
flac.setTag('SOURCEID=' + track.SNG_ID);
|
||||
|
||||
return Buffer.from(flac.save());
|
||||
};
|
24
src/metadata-writer/getTrackLyrics.ts
Normal file
24
src/metadata-writer/getTrackLyrics.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {getLyricsMusixmatch} from './musixmatchLyrics';
|
||||
import {getLyrics} from '../api';
|
||||
import type {lyricsType, trackType} from '../types';
|
||||
|
||||
const getTrackLyricsWeb = async (track: trackType): Promise<lyricsType | null> => {
|
||||
try {
|
||||
const LYRICS_TEXT = await getLyricsMusixmatch(`${track.ART_NAME} - ${track.SNG_TITLE}`);
|
||||
return {LYRICS_TEXT};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getTrackLyrics = async (track: trackType): Promise<lyricsType | null> => {
|
||||
if (track.LYRICS_ID > 0) {
|
||||
try {
|
||||
return await getLyrics(track.SNG_ID);
|
||||
} catch (err) {
|
||||
return await getTrackLyricsWeb(track);
|
||||
}
|
||||
}
|
||||
|
||||
return await getTrackLyricsWeb(track);
|
||||
};
|
88
src/metadata-writer/id3.ts
Normal file
88
src/metadata-writer/id3.ts
Normal file
@ -0,0 +1,88 @@
|
||||
// @ts-ignore
|
||||
import id3Writer from 'browser-id3-writer';
|
||||
import type {trackType} from '../types';
|
||||
|
||||
export const writeMetadataMp3 = (buffer: Buffer, track: trackType, cover?: Buffer | null): Buffer => {
|
||||
const writer = new id3Writer(buffer);
|
||||
writer.setFrame('TIT2', track.SNG_TITLE).setFrame('TALB', track.ALB_TITLE);
|
||||
|
||||
const artists = track.ART_NAME.split(
|
||||
new RegExp(' featuring | feat. | Ft. | ft. | vs | vs. | x | - |, ', 'g'),
|
||||
).map((a) => a.trim());
|
||||
writer.setFrame('TPE2', artists).setFrame('TPE1', [artists.join(', ')]);
|
||||
|
||||
writer
|
||||
.setFrame('TMED', 'Digital Media')
|
||||
.setFrame('TXXX', {
|
||||
description: 'Artists',
|
||||
value: artists.join(', '),
|
||||
})
|
||||
.setFrame('TXXX', {
|
||||
description: 'ISRC',
|
||||
value: track.ISRC,
|
||||
})
|
||||
.setFrame('TXXX', {
|
||||
description: 'SOURCE',
|
||||
value: 'Deezer',
|
||||
})
|
||||
.setFrame('TXXX', {
|
||||
description: 'SOURCEID',
|
||||
value: track.SNG_ID,
|
||||
});
|
||||
|
||||
if (track.DISK_NUMBER) {
|
||||
writer.setFrame('TPOS', track.DISK_NUMBER).setFrame('TXXX', {
|
||||
description: 'DISCNUMBER',
|
||||
value: track.DISK_NUMBER,
|
||||
});
|
||||
}
|
||||
|
||||
writer.setFrame('TXXX', {
|
||||
description: 'LENGTH',
|
||||
value: track.DURATION,
|
||||
});
|
||||
|
||||
if (track.SNG_CONTRIBUTORS.composer) {
|
||||
writer.setFrame('TXXX', {
|
||||
description: 'COMPOSER',
|
||||
value: track.SNG_CONTRIBUTORS.composer.join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
if (track.SNG_CONTRIBUTORS.writer) {
|
||||
writer.setFrame('TXXX', {
|
||||
description: 'LYRICIST',
|
||||
value: track.SNG_CONTRIBUTORS.writer.join(', '),
|
||||
});
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.mixer) {
|
||||
writer.setFrame('TXXX', {
|
||||
description: 'MIXARTIST',
|
||||
value: track.SNG_CONTRIBUTORS.mixer.join(', '),
|
||||
});
|
||||
}
|
||||
if (track.SNG_CONTRIBUTORS.producer && track.SNG_CONTRIBUTORS.engineer) {
|
||||
writer.setFrame('TXXX', {
|
||||
description: 'INVOLVEDPEOPLE',
|
||||
value: track.SNG_CONTRIBUTORS.producer.concat(track.SNG_CONTRIBUTORS.engineer).join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
if (track.LYRICS) {
|
||||
writer.setFrame('USLT', {
|
||||
description: '',
|
||||
lyrics: track.LYRICS.LYRICS_TEXT,
|
||||
});
|
||||
}
|
||||
|
||||
if (cover) {
|
||||
writer.setFrame('APIC', {
|
||||
type: 3,
|
||||
data: cover,
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
|
||||
writer.addTag();
|
||||
return Buffer.from(writer.arrayBuffer);
|
||||
};
|
28
src/metadata-writer/index.ts
Normal file
28
src/metadata-writer/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {downloadAlbumCover} from './abumCover';
|
||||
import {getTrackLyrics} from './getTrackLyrics';
|
||||
// @ts-ignore
|
||||
import {writeMetadataMp3} from './id3';
|
||||
import {writeMetadataFlac} from './flacmetata';
|
||||
import type {trackType} from '../types';
|
||||
|
||||
/**
|
||||
* Add metdata to the mp3
|
||||
* @param {Buffer} trackBuffer decrypted track buffer
|
||||
* @param {Object} track json containing track infos
|
||||
* @param {Boolean} fileType buffer type, mp3 or flac
|
||||
* @param {Number} albumCoverSize album cover size in pixel
|
||||
*/
|
||||
export const addTrackTags = async (
|
||||
trackBuffer: Buffer,
|
||||
track: trackType,
|
||||
isFlac: boolean,
|
||||
albumCoverSize = 1000,
|
||||
): Promise<Buffer> => {
|
||||
const [cover, lyrics] = await Promise.all([downloadAlbumCover(track, albumCoverSize), getTrackLyrics(track)]);
|
||||
|
||||
if (lyrics) {
|
||||
track.LYRICS = lyrics;
|
||||
}
|
||||
|
||||
return isFlac ? writeMetadataFlac(trackBuffer, track, cover) : writeMetadataMp3(trackBuffer, track, cover);
|
||||
};
|
37
src/metadata-writer/musixmatchLyrics.ts
Normal file
37
src/metadata-writer/musixmatchLyrics.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import axios from 'axios';
|
||||
import {parse} from 'node-html-parser';
|
||||
import {randomUseragent} from './useragents';
|
||||
|
||||
const baseUrl = 'https://musixmatch.com';
|
||||
|
||||
const getUrlMusixmatch = async (query: string) => {
|
||||
const {data} = await axios.get(`${baseUrl}/search/${encodeURI(query)}/tracks`, {
|
||||
headers: {
|
||||
'User-Agent': randomUseragent(),
|
||||
referer: 'https://l.facebook.com/',
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const url = parse(data).querySelector('h2').childNodes[0].attributes.href.replace('/add', '');
|
||||
if (url.includes('/lyrics/')) {
|
||||
return url.startsWith('/lyrics/') ? baseUrl + url : url;
|
||||
}
|
||||
|
||||
throw new Error('No song found!');
|
||||
};
|
||||
|
||||
export const getLyricsMusixmatch = async (query: string): Promise<string> => {
|
||||
const url = await getUrlMusixmatch(query);
|
||||
const {data} = await axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': randomUseragent(),
|
||||
referer: baseUrl + '/',
|
||||
},
|
||||
});
|
||||
|
||||
let lyrics = data.match(/("body":".*","language")/)[0];
|
||||
lyrics = lyrics.replace('"body":"', '').replace('","language"', '');
|
||||
|
||||
return lyrics.split('\\n').join('\n');
|
||||
};
|
83
src/metadata-writer/useragents.ts
Normal file
83
src/metadata-writer/useragents.ts
Normal file
@ -0,0 +1,83 @@
|
||||
const useragents = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 Edg/87.0.664.75',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Edg/88.0.705.56',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Edg/88.0.705.50',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 OPR/73.0.3856.344',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:85.0) Gecko/20100101 Firefox/85.0',
|
||||
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:85.0) Gecko/20100101 Firefox/85.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63',
|
||||
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.60 YaBrowser/20.12.0.963 Yowser/2.5 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 OPR/73.0.3856.329',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 OPR/73.0.3856.344 (Edition Yx 05)',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Edg/88.0.705.53',
|
||||
'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66',
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.3 Safari/605.1.15',
|
||||
'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0',
|
||||
];
|
||||
|
||||
export const randomUseragent = () => useragents[Math.floor(Math.random() * useragents.length)];
|
138
src/tests/api.ts
Normal file
138
src/tests/api.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import test from 'ava';
|
||||
import axios from 'axios';
|
||||
import * as api from '../';
|
||||
import {decryptDownload, getTrackDownloadUrl} from '../lib/decrypt';
|
||||
import {downloadAlbumCover} from '../metadata-writer/abumCover';
|
||||
import {getLyricsMusixmatch} from '../metadata-writer/musixmatchLyrics';
|
||||
|
||||
// Harder, Better, Faster, Stronger by Daft Punk
|
||||
const SNG_ID = '3135556';
|
||||
|
||||
// Discovery by Daft Punk
|
||||
const ALB_ID = '302127';
|
||||
|
||||
test.serial('GET TRACK INFO', async (t) => {
|
||||
const response = await api.getTrackInfo(SNG_ID);
|
||||
|
||||
t.is(response.SNG_ID, SNG_ID);
|
||||
t.is(response.ISRC, 'GBDUW0000059');
|
||||
t.is(response.__TYPE__, 'song');
|
||||
});
|
||||
|
||||
test('GET TRACK COVER', async (t) => {
|
||||
const track = await api.getTrackInfo(SNG_ID);
|
||||
const cover = await downloadAlbumCover(track, 500);
|
||||
|
||||
t.truthy(cover);
|
||||
t.true(Buffer.isBuffer(cover));
|
||||
t.is(cover ? cover.length : 0, 24646);
|
||||
});
|
||||
|
||||
test('GET TRACK LYRICS', async (t) => {
|
||||
const response = await api.getLyrics(SNG_ID);
|
||||
|
||||
t.is(response.LYRICS_ID, '2780622');
|
||||
t.is(response.LYRICS_TEXT.length, 1719);
|
||||
});
|
||||
|
||||
test('GET MUSIXMATCH LYRICS', async (t) => {
|
||||
const track = await api.getTrackInfo(SNG_ID);
|
||||
const lyrics = await getLyricsMusixmatch(`${track.ART_NAME} - ${track.SNG_TITLE}`);
|
||||
|
||||
t.truthy(lyrics);
|
||||
t.true(lyrics.length > 7000);
|
||||
t.true(lyrics.length < 8000);
|
||||
});
|
||||
|
||||
test('GET ALBUM INFO', async (t) => {
|
||||
const response = await api.getAlbumInfo(ALB_ID);
|
||||
|
||||
t.is(response.ALB_ID, ALB_ID);
|
||||
t.is(response.UPC, '724384960650');
|
||||
t.is(response.__TYPE__, 'album');
|
||||
});
|
||||
|
||||
test('GET ALBUM TRACKS', async (t) => {
|
||||
const response = await api.getAlbumTracks(ALB_ID);
|
||||
|
||||
t.is(response.count, 14);
|
||||
t.is(response.data.length, response.count);
|
||||
});
|
||||
|
||||
test('GET PLAYLIST INFO', async (t) => {
|
||||
const PLAYLIST_ID = '4523119944';
|
||||
const response = await api.getPlaylistInfo(PLAYLIST_ID);
|
||||
|
||||
t.truthy(response.NB_SONG > 0);
|
||||
t.is(response.PARENT_USERNAME, 'sayem314');
|
||||
t.is(response.__TYPE__, 'playlist');
|
||||
});
|
||||
|
||||
test('GET PLAYLIST TRACKS', async (t) => {
|
||||
const PLAYLIST_ID = '4523119944';
|
||||
const response = await api.getPlaylistTracks(PLAYLIST_ID);
|
||||
|
||||
t.truthy(response.count > 0);
|
||||
t.is(response.data.length, response.count);
|
||||
});
|
||||
|
||||
test('GET ARTIST INFO', async (t) => {
|
||||
const ART_ID = '13';
|
||||
const response = await api.getArtistInfo(ART_ID);
|
||||
|
||||
t.is(response.ART_NAME, 'Eminem');
|
||||
t.is(response.__TYPE__, 'artist');
|
||||
});
|
||||
|
||||
test('GET ARTIST TRACKS', async (t) => {
|
||||
const ART_ID = '13';
|
||||
const response = await api.getDiscography(ART_ID, 10);
|
||||
|
||||
t.is(response.count, 10);
|
||||
t.is(response.data.length, response.count);
|
||||
});
|
||||
|
||||
test('GET USER PROFILE', async (t) => {
|
||||
const USER_ID = '2064440442';
|
||||
const response = await api.getProfile(USER_ID);
|
||||
|
||||
t.is(response.USER.BLOG_NAME, 'sayem314');
|
||||
t.is(response.USER.__TYPE__, 'user');
|
||||
});
|
||||
|
||||
test('GET TRACK ALTERNATIVE', async (t) => {
|
||||
const ARTIST = 'Eminem';
|
||||
const TRACK = 'The Real Slim Shady';
|
||||
const response = await api.searchAlternative(ARTIST, TRACK);
|
||||
|
||||
t.is(response.QUERY, `artist:'${ARTIST.toLowerCase()}' track:'${TRACK.toLowerCase()}'`);
|
||||
t.is(response.TRACK.data.length, response.TRACK.count);
|
||||
});
|
||||
|
||||
test('SEARCH TRACK, ALBUM & ARTIST', async (t) => {
|
||||
const QUERY = 'Eminem';
|
||||
const response = await api.searchMusic(QUERY, ['TRACK', 'ALBUM', 'ARTIST'], 1);
|
||||
|
||||
t.is(response.QUERY, QUERY.toLowerCase());
|
||||
t.truthy(response.TRACK.count > 0);
|
||||
t.truthy(response.ALBUM.count > 0);
|
||||
t.truthy(response.ARTIST.count > 0);
|
||||
});
|
||||
|
||||
test('DOWNLOAD TRACK & ADD METADATA', async (t) => {
|
||||
const track = await api.getTrackInfo(SNG_ID);
|
||||
const url = getTrackDownloadUrl(track, 1);
|
||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||
|
||||
t.truthy(data);
|
||||
t.true(Buffer.isBuffer(data));
|
||||
t.is(data.length, 3596119);
|
||||
|
||||
const decryptedTrack = decryptDownload(data, track.SNG_ID);
|
||||
t.true(Buffer.isBuffer(decryptedTrack));
|
||||
t.is(data.length, 3596119);
|
||||
|
||||
const trackWithMetadata = await api.addTrackTags(decryptedTrack, track, false, 500);
|
||||
t.true(Buffer.isBuffer(trackWithMetadata));
|
||||
t.is(trackWithMetadata.length, 3628837);
|
||||
});
|
244
src/types.ts
Normal file
244
src/types.ts
Normal file
@ -0,0 +1,244 @@
|
||||
interface localesType {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface artistType {
|
||||
ART_ID: string; // '27'
|
||||
ROLE_ID: string; // '0'
|
||||
ARTISTS_SONGS_ORDER: string; // '0'
|
||||
ART_NAME: string; // 'Daft Punk'
|
||||
ARTIST_IS_DUMMY: boolean; // false
|
||||
ART_PICTURE: string; // 'f2bc007e9133c946ac3c3907ddc5d2ea'
|
||||
RANK: string; // '836071'
|
||||
LOCALES?: localesType;
|
||||
__TYPE__: 'artist';
|
||||
}
|
||||
|
||||
interface mediaType {
|
||||
TYPE: 'preview';
|
||||
HREF: string; // 'https://cdns-preview-d.dzcdn.net/stream/c-deda7fa9316d9e9e880d2c6207e92260-8.mp3';
|
||||
}
|
||||
|
||||
interface lyricsSync {
|
||||
lrc_timestamp: string; //'[00:03.58]',
|
||||
milliseconds: string; // '3580',
|
||||
duration: string; // '8660',
|
||||
line: string; // "Hey brother! There's an endless road to rediscover"
|
||||
}
|
||||
|
||||
export interface lyricsType {
|
||||
LYRICS_ID?: string; // '2310758',
|
||||
LYRICS_SYNC_JSON?: lyricsSync[];
|
||||
LYRICS_TEXT: string;
|
||||
LYRICS_COPYRIGHTS?: string;
|
||||
LYRICS_WRITERS?: string;
|
||||
}
|
||||
|
||||
interface songType {
|
||||
ALB_ID: string; // '302127'
|
||||
ALB_TITLE: string; // 'Discovery'
|
||||
ALB_PICTURE: string; // '2e018122cb56986277102d2041a592c8'
|
||||
ARTISTS: artistType[];
|
||||
ART_ID: '27';
|
||||
ART_NAME: 'Daft Punk';
|
||||
ARTIST_IS_DUMMY: boolean; // false
|
||||
ART_PICTURE: string; //'f2bc007e9133c946ac3c3907ddc5d2ea'
|
||||
DATE_START: string; // '0000-00-00'
|
||||
DISK_NUMBER?: string; // '1'
|
||||
DURATION: string; // '224'
|
||||
EXPLICIT_TRACK_CONTENT: {
|
||||
EXPLICIT_LYRICS_STATUS: number; // 0
|
||||
EXPLICIT_COVER_STATUS: number; // 0
|
||||
};
|
||||
ISRC: string; // 'GBDUW0000059'
|
||||
LYRICS_ID: number; // 2780622
|
||||
LYRICS?: lyricsType;
|
||||
EXPLICIT_LYRICS?: string;
|
||||
RANK: string; // '787708'
|
||||
SMARTRADIO: string; // 0
|
||||
SNG_ID: string; // '3135556'
|
||||
SNG_TITLE: string; // 'Harder, Better, Faster, Stronger'
|
||||
SNG_CONTRIBUTORS: {
|
||||
main_artist: string[]; //['Daft Punk']
|
||||
author?: string[]; // ['Edwin Birdsong', 'Guy-Manuel de Homem-Christo', 'Thomas Bangalter']
|
||||
composer?: string[];
|
||||
musicpublisher?: string[];
|
||||
producer?: string[];
|
||||
publisher: string[];
|
||||
engineer?: string[];
|
||||
writer?: string[];
|
||||
mixer?: string[];
|
||||
};
|
||||
STATUS: number; // 3
|
||||
S_MOD: number; // 0
|
||||
S_PREMIUM: number; // 0
|
||||
TRACK_NUMBER: number; // '4'
|
||||
URL_REWRITING: string; // 'daft-punk'
|
||||
VERSION: string; // ''
|
||||
MD5_ORIGIN: string; // '51afcde9f56a132096c0496cc95eb24b'
|
||||
FILESIZE_AAC_64: '0';
|
||||
FILESIZE_MP3_64: string; // '1798059'
|
||||
FILESIZE_MP3_128: string; // '3596119'
|
||||
FILESIZE_MP3_256: '0';
|
||||
FILESIZE_MP3_320: '0';
|
||||
FILESIZE_MP4_RA1: '0';
|
||||
FILESIZE_MP4_RA2: '0';
|
||||
FILESIZE_MP4_RA3: '0';
|
||||
FILESIZE_FLAC: '0';
|
||||
FILESIZE: string; //'3596119'
|
||||
GAIN: string; // '-12.4'
|
||||
MEDIA_VERSION: string; // '8'
|
||||
TRACK_TOKEN: string; // 'AAAAAWAzlaRgNK7kyEh8dI3tpyObkIpy15hgDXr4GGiFTJakRmh5F7rMVf6-cYTWZNUIq4TLZj6x68mFstAqp9bml_eUzbfFbvIkpmx_hhDRZJhqLsHe-aBRZ9VdHEBr7LYSE3qKpmpTdDp6Odkrw3f-pNQW'
|
||||
TRACK_TOKEN_EXPIRE: number; // 1614065380
|
||||
MEDIA: [mediaType];
|
||||
RIGHTS: {
|
||||
STREAM_ADS_AVAILABLE?: boolean;
|
||||
STREAM_ADS?: string; // '2000-01-01'
|
||||
STREAM_SUB_AVAILABLE?: boolean; // true,
|
||||
STREAM_SUB?: string; // '2000-01-01'
|
||||
};
|
||||
PROVIDER_ID: string; // '3'
|
||||
__TYPE__: 'song';
|
||||
}
|
||||
|
||||
export interface trackType extends songType {
|
||||
FALLBACK?: songType;
|
||||
}
|
||||
|
||||
export interface albumType {
|
||||
ALB_CONTRIBUTORS: {
|
||||
main_artist: string[]; // ['Avicii']
|
||||
};
|
||||
ALB_ID: string; // '9188269'
|
||||
ALB_PICTURE: string; // '6e58a99f59a150e9b4aefbeb2d6fc856'
|
||||
EXPLICIT_ALBUM_CONTENT: {
|
||||
EXPLICIT_LYRICS_STATUS: number; // 0
|
||||
EXPLICIT_COVER_STATUS: number; // 0
|
||||
};
|
||||
ALB_TITLE: string; // 'The Days / Nights'
|
||||
ARTISTS: artistType[];
|
||||
ART_ID: string; // '293585'
|
||||
ART_NAME: string; // 'Avicii'
|
||||
ARTIST_IS_DUMMY: boolean;
|
||||
DIGITAL_RELEASE_DATE: string; //'2014-12-01'
|
||||
EXPLICIT_LYRICS?: string; // '0'
|
||||
NB_FAN: number; // 36285
|
||||
NUMBER_DISK: string; // '1'
|
||||
NUMBER_TRACK: string; // '4'
|
||||
PHYSICAL_RELEASE_DATE?: string; // '2014-01-01'
|
||||
PRODUCER_LINE: string; // '℗ 2014 Avicii Music AB'
|
||||
PROVIDER_ID: string; // '427'
|
||||
RANK: string; // '601128'
|
||||
RANK_ART: string; // '861905'
|
||||
STATUS: string; // '1'
|
||||
TYPE: string; // '1'
|
||||
UPC: string; // '602547151544'
|
||||
__TYPE__: 'album';
|
||||
}
|
||||
|
||||
export interface albumTracksType {
|
||||
data: songType[];
|
||||
count: number;
|
||||
total: number;
|
||||
filtered_count: number;
|
||||
filtered_items?: number[];
|
||||
next?: number;
|
||||
}
|
||||
|
||||
export interface playlistInfo {
|
||||
PLAYLIST_ID: string; // '4523119944'
|
||||
DESCRIPTION?: string; // ''
|
||||
PARENT_USERNAME: string; // 'sayem314'
|
||||
PARENT_USER_PICTURE?: string; // ''
|
||||
PARENT_USER_ID: string; // '2064440442'
|
||||
PICTURE_TYPE: string; // 'cover'
|
||||
PLAYLIST_PICTURE: string; // 'e206dafb59a3d378d7ffacc989bc4e35'
|
||||
TITLE: string; // 'wtf playlist '
|
||||
TYPE: string; // '0'
|
||||
STATUS: string; // 0
|
||||
USER_ID: string; // '2064440442'
|
||||
DATE_ADD: string; // '2018-09-08 19:13:57'
|
||||
DATE_MOD: string; //'2018-09-08 19:14:11'
|
||||
DATE_CREATE: string; // '2018-05-31 00:01:05'
|
||||
NB_SONG: number; // 3
|
||||
NB_FAN: number; // 0
|
||||
CHECKSUM: string; // 'c185d123834444e3c8869e235dd6f0a6'
|
||||
HAS_ARTIST_LINKED: boolean;
|
||||
IS_SPONSORED: boolean;
|
||||
IS_EDITO: boolean;
|
||||
__TYPE__: 'playlist';
|
||||
}
|
||||
|
||||
export interface playlistTracksType extends albumTracksType {
|
||||
checksum: string;
|
||||
}
|
||||
|
||||
export interface artistInfoType {
|
||||
ART_ID: string; // "293585",
|
||||
ART_NAME: string; // "Avicii",
|
||||
ARTIST_IS_DUMMY: boolean;
|
||||
ART_PICTURE: string; // "82e214b0cb39316f4a12a082fded54f6",
|
||||
FACEBOOK?: string; // "https://www.facebook.com/avicii?fref=ts",
|
||||
NB_FAN: number; // 7140516,
|
||||
TWITTER?: string; // "https://twitter.com/Avicii",
|
||||
__TYPE__: 'artist';
|
||||
}
|
||||
|
||||
export interface discographyType extends albumTracksType {
|
||||
cache_version: number; // 2,
|
||||
art_id: number; // 293585,
|
||||
start: number; // 0,
|
||||
nb: number; // 2
|
||||
}
|
||||
|
||||
export interface profileType {
|
||||
IS_FOLLOW: boolean;
|
||||
NB_ARTISTS: number;
|
||||
NB_FOLLOWERS: number;
|
||||
NB_FOLLOWINGS: number;
|
||||
NB_MP3S: number;
|
||||
TOP_TRACK: albumTracksType;
|
||||
USER: {
|
||||
USER_ID: string; // '2064440442'
|
||||
BLOG_NAME: string; // 'sayem314'
|
||||
SEX?: string; // ''
|
||||
COUNTRY: string; // 'BD'
|
||||
USER_PICTURE?: string; // ''
|
||||
COUNTRY_NAME: string; // 'Bangladesh'
|
||||
PRIVATE: boolean;
|
||||
DISPLAY_NAME: string; // 'sayem314'
|
||||
__TYPE__: 'user';
|
||||
};
|
||||
}
|
||||
|
||||
export interface searchType {
|
||||
QUERY: string; //;
|
||||
FUZZINNESS: boolean;
|
||||
AUTOCORRECT: boolean;
|
||||
TOP_RESULT: [albumType | artistType | trackType | playlistInfo | artistType | unknown] | [];
|
||||
ORDER: [
|
||||
'TOP_RESULT',
|
||||
'TRACK',
|
||||
'PLAYLIST',
|
||||
'ALBUM',
|
||||
'ARTIST',
|
||||
'LIVESTREAM',
|
||||
'EPISODE',
|
||||
'SHOW',
|
||||
'CHANNEL',
|
||||
'RADIO',
|
||||
'USER',
|
||||
'LYRICS',
|
||||
];
|
||||
ALBUM: albumTracksType;
|
||||
ARTIST: albumTracksType;
|
||||
TRACK: albumTracksType;
|
||||
PLAYLIST: albumTracksType;
|
||||
RADIO: albumTracksType;
|
||||
SHOW: albumTracksType;
|
||||
USER: albumTracksType;
|
||||
LIVESTREAM: albumTracksType;
|
||||
CHANNEL: albumTracksType;
|
||||
}
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user