mirror of
https://github.com/d-fi/d-fi-core.git
synced 2025-07-27 07:34:42 +02:00
Merge pull request #4 from forkbomb9/flac-v2
Rewrite track URL generation
This commit is contained in:
commit
1dc9b37c99
11
README.md
11
README.md
@ -34,13 +34,16 @@ try {
|
|||||||
const track = await api.getTrackInfo(song_id);
|
const track = await api.getTrackInfo(song_id);
|
||||||
|
|
||||||
// Parse download URL for 128kbps
|
// Parse download URL for 128kbps
|
||||||
const url = api.getTrackDownloadUrl(track, 1);
|
const url = await api.getTrackDownloadUrl(track, 1);
|
||||||
|
|
||||||
// Download encrypted track
|
// Download track
|
||||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||||
|
|
||||||
// Decrypt track
|
// Decrypt track if needed
|
||||||
const decryptedTrack = api.decryptDownload(data, track.SNG_ID);
|
let decryptedTrack = data;
|
||||||
|
if (api.isTrackEncrypted(url)) {
|
||||||
|
decryptedTrack = api.decryptDownload(data, track.SNG_ID);
|
||||||
|
}
|
||||||
|
|
||||||
// Add id3 metadata
|
// Add id3 metadata
|
||||||
const trackWithMetadata = await api.addTrackTags(decryptedTrack, track, 500);
|
const trackWithMetadata = await api.addTrackTags(decryptedTrack, track, 500);
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as api from '../src';
|
import * as api from '../src';
|
||||||
import {decryptDownload, getTrackDownloadUrl} from '../src/lib/decrypt';
|
import {decryptDownload, trackIsEncrypted} from '../src/lib/decrypt';
|
||||||
import {downloadAlbumCover} from '../src/metadata-writer/abumCover';
|
import {downloadAlbumCover} from '../src/metadata-writer/abumCover';
|
||||||
import {getLyricsMusixmatch} from '../src/metadata-writer/musixmatchLyrics';
|
import {getLyricsMusixmatch} from '../src/metadata-writer/musixmatchLyrics';
|
||||||
|
import {getTrackDownloadUrl} from '../src/lib/get-url';
|
||||||
|
|
||||||
// Harder, Better, Faster, Stronger by Daft Punk
|
// Harder, Better, Faster, Stronger by Daft Punk
|
||||||
const SNG_ID = '3135556';
|
const SNG_ID = '3135556';
|
||||||
@ -139,14 +140,20 @@ test('SEARCH TRACK, ALBUM & ARTIST', async (t) => {
|
|||||||
if (process.env.CI) {
|
if (process.env.CI) {
|
||||||
test('DOWNLOAD TRACK128 & ADD METADATA', async (t) => {
|
test('DOWNLOAD TRACK128 & ADD METADATA', async (t) => {
|
||||||
const track = await api.getTrackInfo(SNG_ID);
|
const track = await api.getTrackInfo(SNG_ID);
|
||||||
const url = getTrackDownloadUrl(track, 1);
|
const urlGen = await getTrackDownloadUrl(track, 1);
|
||||||
|
const url = urlGen ? urlGen : '';
|
||||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||||
|
|
||||||
t.truthy(data);
|
t.truthy(data);
|
||||||
t.true(Buffer.isBuffer(data));
|
t.true(Buffer.isBuffer(data));
|
||||||
t.is(data.length, 3596119);
|
t.is(data.length, 3596119);
|
||||||
|
|
||||||
const decryptedTrack = decryptDownload(data, track.SNG_ID);
|
let decryptedTrack: Buffer;
|
||||||
|
if (trackIsEncrypted(url)) {
|
||||||
|
decryptedTrack = decryptDownload(data, track.SNG_ID);
|
||||||
|
} else {
|
||||||
|
decryptedTrack = data;
|
||||||
|
}
|
||||||
t.true(Buffer.isBuffer(decryptedTrack));
|
t.true(Buffer.isBuffer(decryptedTrack));
|
||||||
t.is(decryptedTrack.length, 3596119);
|
t.is(decryptedTrack.length, 3596119);
|
||||||
|
|
||||||
@ -157,14 +164,20 @@ if (process.env.CI) {
|
|||||||
|
|
||||||
test('TRACK128 WITHOUT ALBUM INFO', async (t) => {
|
test('TRACK128 WITHOUT ALBUM INFO', async (t) => {
|
||||||
const track = await api.getTrackInfo('912254892');
|
const track = await api.getTrackInfo('912254892');
|
||||||
const url = getTrackDownloadUrl(track, 1);
|
const urlGen = await getTrackDownloadUrl(track, 1);
|
||||||
|
const url = urlGen ? urlGen : '';
|
||||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||||
|
|
||||||
t.truthy(data);
|
t.truthy(data);
|
||||||
t.true(Buffer.isBuffer(data));
|
t.true(Buffer.isBuffer(data));
|
||||||
t.is(data.length, 3262170);
|
t.is(data.length, 3262170);
|
||||||
|
|
||||||
const decryptedTrack = decryptDownload(data, track.SNG_ID);
|
let decryptedTrack: Buffer;
|
||||||
|
if (trackIsEncrypted(url)) {
|
||||||
|
decryptedTrack = decryptDownload(data, track.SNG_ID);
|
||||||
|
} else {
|
||||||
|
decryptedTrack = data;
|
||||||
|
}
|
||||||
t.true(Buffer.isBuffer(decryptedTrack));
|
t.true(Buffer.isBuffer(decryptedTrack));
|
||||||
t.is(decryptedTrack.length, 3262170);
|
t.is(decryptedTrack.length, 3262170);
|
||||||
|
|
||||||
@ -177,14 +190,20 @@ if (process.env.CI) {
|
|||||||
|
|
||||||
test('DOWNLOAD TRACK320 & ADD METADATA', async (t) => {
|
test('DOWNLOAD TRACK320 & ADD METADATA', async (t) => {
|
||||||
const track = await api.getTrackInfo(SNG_ID);
|
const track = await api.getTrackInfo(SNG_ID);
|
||||||
const url = getTrackDownloadUrl(track, 3);
|
const urlGen = await getTrackDownloadUrl(track, 3);
|
||||||
|
const url = urlGen ? urlGen : '';
|
||||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||||
|
|
||||||
t.truthy(data);
|
t.truthy(data);
|
||||||
t.true(Buffer.isBuffer(data));
|
t.true(Buffer.isBuffer(data));
|
||||||
t.is(data.length, 8990301);
|
t.is(data.length, 8990301);
|
||||||
|
|
||||||
const decryptedTrack = decryptDownload(data, track.SNG_ID);
|
let decryptedTrack: Buffer;
|
||||||
|
if (trackIsEncrypted(url)) {
|
||||||
|
decryptedTrack = decryptDownload(data, track.SNG_ID);
|
||||||
|
} else {
|
||||||
|
decryptedTrack = data;
|
||||||
|
}
|
||||||
t.true(Buffer.isBuffer(decryptedTrack));
|
t.true(Buffer.isBuffer(decryptedTrack));
|
||||||
t.is(decryptedTrack.length, 8990301);
|
t.is(decryptedTrack.length, 8990301);
|
||||||
|
|
||||||
@ -195,14 +214,20 @@ if (process.env.CI) {
|
|||||||
|
|
||||||
test('DOWNLOAD TRACK1411 & ADD METADATA', async (t) => {
|
test('DOWNLOAD TRACK1411 & ADD METADATA', async (t) => {
|
||||||
const track = await api.getTrackInfo(SNG_ID);
|
const track = await api.getTrackInfo(SNG_ID);
|
||||||
const url = getTrackDownloadUrl(track, 9);
|
const urlGen = await getTrackDownloadUrl(track, 9);
|
||||||
|
const url = urlGen ? urlGen : '';
|
||||||
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
const {data} = await axios.get(url, {responseType: 'arraybuffer'});
|
||||||
|
|
||||||
t.truthy(data);
|
t.truthy(data);
|
||||||
t.true(Buffer.isBuffer(data));
|
t.true(Buffer.isBuffer(data));
|
||||||
t.is(data.length, 25418289);
|
t.is(data.length, 25418289);
|
||||||
|
|
||||||
const decryptedTrack = decryptDownload(data, track.SNG_ID);
|
let decryptedTrack: Buffer;
|
||||||
|
if (trackIsEncrypted(url)) {
|
||||||
|
decryptedTrack = decryptDownload(data, track.SNG_ID);
|
||||||
|
} else {
|
||||||
|
decryptedTrack = data;
|
||||||
|
}
|
||||||
t.true(Buffer.isBuffer(decryptedTrack));
|
t.true(Buffer.isBuffer(decryptedTrack));
|
||||||
t.is(data.length, 25418289);
|
t.is(data.length, 25418289);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ const md5 = (data: string, type: crypto.Encoding = 'ascii') => {
|
|||||||
return md5sum.digest('hex');
|
return md5sum.digest('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongFileName = ({MD5_ORIGIN, SNG_ID, MEDIA_VERSION}: trackType, quality: number) => {
|
export const getSongFileName = ({MD5_ORIGIN, SNG_ID, MEDIA_VERSION}: trackType, quality: number) => {
|
||||||
const step1 = [MD5_ORIGIN, quality, SNG_ID, MEDIA_VERSION].join('¤');
|
const step1 = [MD5_ORIGIN, quality, SNG_ID, MEDIA_VERSION].join('¤');
|
||||||
|
|
||||||
let step2 = md5(step1) + '¤' + step1 + '¤';
|
let step2 = md5(step1) + '¤' + step1 + '¤';
|
||||||
@ -68,12 +68,6 @@ export const decryptDownload = (source: Buffer, trackId: string) => {
|
|||||||
return destBuffer;
|
return destBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const trackIsEncrypted = (url: string) => {
|
||||||
* @param track Track info json returned from `getTrackInfo`
|
return url.includes("/mobile/") || url.includes("/media/");
|
||||||
* @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/api/1/${filename}`;
|
|
||||||
};
|
|
||||||
|
@ -1,52 +1,49 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import {getSongFileName} from '../lib/decrypt';
|
||||||
|
import instance from '../lib/request';
|
||||||
|
import type {trackType} from '../types'
|
||||||
|
|
||||||
interface getUrlType {
|
interface userData {
|
||||||
data: [
|
license_token: string,
|
||||||
{
|
can_stream_lossless: boolean,
|
||||||
media: [
|
can_stream_hq: boolean,
|
||||||
{
|
|
||||||
cipher: {
|
|
||||||
type: 'BF_CBC_STRIPE';
|
|
||||||
};
|
|
||||||
exp: number;
|
|
||||||
format: string;
|
|
||||||
media_type: 'FULL';
|
|
||||||
nbf: number;
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
provider: 'ec';
|
|
||||||
url: string;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: 'ak';
|
|
||||||
url: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let license_token: string | null = null;
|
class WrongLicense extends Error {
|
||||||
|
constructor(format: string) {
|
||||||
|
super()
|
||||||
|
this.name = "WrongLicense"
|
||||||
|
this.message = `Your account can't stream ${format} tracks`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getLicenseToken = async (): Promise<string> => {
|
let user_data: userData | null = null;
|
||||||
const {data} = await axios.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
||||||
|
const dzAuthenticate = async (): Promise<userData> => {
|
||||||
|
const {data} = await instance.get('https://www.deezer.com/ajax/gw-light.php', {
|
||||||
params: {
|
params: {
|
||||||
method: 'deezer.getUserData',
|
method: 'deezer.getUserData',
|
||||||
api_version: '1.0',
|
api_version: '1.0',
|
||||||
api_token: '',
|
api_token: 'null',
|
||||||
cid: Math.floor(1e9 * Math.random()),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
license_token = data.results.USER.OPTIONS.license_token;
|
user_data = {
|
||||||
return data.results.USER.OPTIONS.license_token;
|
license_token: data.results.USER.OPTIONS.license_token,
|
||||||
|
can_stream_lossless: data.results.USER.OPTIONS.web_lossless || data.results.USER.OPTIONS.mobile_loseless,
|
||||||
|
can_stream_hq: data.results.USER.OPTIONS.web_hq || data.results.USER.OPTIONS.mobile_hq,
|
||||||
|
}
|
||||||
|
return user_data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTrackUrlFromServer = async (track_token: string, format: string): Promise<getUrlType> => {
|
const getTrackUrlFromServer = async (track_token: string, format: string): Promise<string | null> => {
|
||||||
const token = license_token ? license_token : await getLicenseToken();
|
const user = user_data ? user_data : await dzAuthenticate();
|
||||||
const {data} = await axios.post('https://media.deezer.com/v1/get_url', {
|
if (
|
||||||
license_token: token,
|
format === "FLAC" && !user.can_stream_lossless ||
|
||||||
|
format === "MP3_320" && !user.can_stream_hq
|
||||||
|
) throw new WrongLicense(format);
|
||||||
|
|
||||||
|
const {data} = await instance.post('https://media.deezer.com/v1/get_url', {
|
||||||
|
license_token: user.license_token,
|
||||||
media: [
|
media: [
|
||||||
{
|
{
|
||||||
type: 'FULL',
|
type: 'FULL',
|
||||||
@ -56,5 +53,66 @@ export const getTrackUrlFromServer = async (track_token: string, format: string)
|
|||||||
track_tokens: [track_token],
|
track_tokens: [track_token],
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
if (data.data.length) {
|
||||||
|
if (data.data[0].errors) {
|
||||||
|
throw new Error(`Deezer error: ${JSON.stringify(data)}`)
|
||||||
|
}
|
||||||
|
return data.data[0].media.length > 0 ? data.data[0].media[0].sources[0].url : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param track Track info json returned from `getTrackInfo`
|
||||||
|
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
||||||
|
*/
|
||||||
|
export const getTrackDownloadUrl = async (track: trackType, quality: number): Promise<string | null> => {
|
||||||
|
let url: string | null = null;
|
||||||
|
let wrongLicense;
|
||||||
|
let formatName;
|
||||||
|
if (quality === 9) {
|
||||||
|
formatName = 'FLAC';
|
||||||
|
} else if (quality === 3) {
|
||||||
|
formatName = 'MP3_320';
|
||||||
|
} else {
|
||||||
|
formatName = 'MP3_128';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get URL with the official API
|
||||||
|
try {
|
||||||
|
url = await getTrackUrlFromServer(track.TRACK_TOKEN, formatName)
|
||||||
|
if (url) {
|
||||||
|
let isUrlOk = await testUrl(url)
|
||||||
|
if (isUrlOk) return url;
|
||||||
|
}
|
||||||
|
url = null;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof WrongLicense) {
|
||||||
|
wrongLicense = true;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
// Fallback to the old method
|
||||||
|
if (!url) {
|
||||||
|
const cdn = track.MD5_ORIGIN[0]; // cdn destination
|
||||||
|
const filename = getSongFileName(track, quality); // encrypted file name
|
||||||
|
url = `https://e-cdns-proxy-${cdn}.dzcdn.net/mobile/1/${filename}`;
|
||||||
|
let isUrlOk = await testUrl(url)
|
||||||
|
if (isUrlOk) return url
|
||||||
|
url = null
|
||||||
|
}
|
||||||
|
if (wrongLicense) throw new Error(`Your account can't stream ${formatName} tracks`);
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const testUrl = async (url: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
let response = await axios.head(url);
|
||||||
|
if (Number(response.headers["content-length"]) > 0) return true;
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user