support youtube
This commit is contained in:
parent
9da80ee210
commit
b0b32d6d20
|
@ -0,0 +1,26 @@
|
||||||
|
import test from 'ava';
|
||||||
|
import {youtube} from '../../src';
|
||||||
|
|
||||||
|
// The Weeknd - I Feel It Coming ft. Daft Punk (Official Video)
|
||||||
|
const VALID_VIDEO = 'qFLhGq0060w';
|
||||||
|
|
||||||
|
// youtube-dl test video "'/\ä↭𝕐
|
||||||
|
const INVALID_VIDEO = 'BaW_jenozKc';
|
||||||
|
|
||||||
|
test('GET TRACK INFO', async (t) => {
|
||||||
|
const response = await youtube.track2deezer(VALID_VIDEO);
|
||||||
|
|
||||||
|
t.is(response.SNG_ID, '136889434');
|
||||||
|
t.is(response.SNG_TITLE, 'I Feel It Coming');
|
||||||
|
t.is(response.ALB_TITLE, 'Starboy');
|
||||||
|
t.is(response.ISRC, 'USUG11601012');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FAIL INVALID VIDEO', async (t) => {
|
||||||
|
try {
|
||||||
|
await youtube.track2deezer(INVALID_VIDEO);
|
||||||
|
t.fail();
|
||||||
|
} catch (err) {
|
||||||
|
t.true(err.message.includes('No track found for youtube video ' + INVALID_VIDEO));
|
||||||
|
}
|
||||||
|
});
|
|
@ -145,13 +145,14 @@ export const getProfile = (user_id: string): Promise<profileType> =>
|
||||||
/**
|
/**
|
||||||
* @param {String} artist artist name
|
* @param {String} artist artist name
|
||||||
* @param {String} song song name
|
* @param {String} song song name
|
||||||
|
* @param {String} nb number of items to fetch
|
||||||
*/
|
*/
|
||||||
export const searchAlternative = (artist: string, song: string): Promise<searchType> =>
|
export const searchAlternative = (artist: string, song: string, nb = 10): Promise<searchType> =>
|
||||||
request(
|
request(
|
||||||
{
|
{
|
||||||
query: `artist:'${artist}' track:'${song}'`,
|
query: `artist:'${artist}' track:'${song}'`,
|
||||||
types: ['TRACK'],
|
types: ['TRACK'],
|
||||||
nb: 10,
|
nb,
|
||||||
},
|
},
|
||||||
'mobile_suggest',
|
'mobile_suggest',
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,3 +2,4 @@ export * from './parse';
|
||||||
export * from './deezer';
|
export * from './deezer';
|
||||||
export * as tidal from './tidal';
|
export * as tidal from './tidal';
|
||||||
export * as spotify from './spotify';
|
export * as spotify from './spotify';
|
||||||
|
export * as youtube from './youtube';
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import spotifyUri from 'spotify-uri';
|
import spotifyUri from 'spotify-uri';
|
||||||
import * as spotify from './spotify';
|
import * as spotify from './spotify';
|
||||||
import * as tidal from './tidal';
|
import * as tidal from './tidal';
|
||||||
|
import * as youtube from './tidal';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import type {albumType, artistInfoType, playlistInfo, trackType} from '../types';
|
import type {albumType, artistInfoType, playlistInfo, trackType} from '../types';
|
||||||
|
|
||||||
|
@ -30,7 +31,8 @@ export type urlPartsType = {
|
||||||
| 'tidal-track'
|
| 'tidal-track'
|
||||||
| 'tidal-album'
|
| 'tidal-album'
|
||||||
| 'tidal-playlist'
|
| 'tidal-playlist'
|
||||||
| 'tidal-artist';
|
| 'tidal-artist'
|
||||||
|
| 'youtube-track';
|
||||||
};
|
};
|
||||||
|
|
||||||
const queue = new PQueue({concurrency: 10});
|
const queue = new PQueue({concurrency: 10});
|
||||||
|
@ -41,7 +43,7 @@ export const getUrlParts = async (url: string, setToken = false): Promise<urlPar
|
||||||
url = 'https://open.spotify.com/' + spotify[1] + '/' + spotify[2];
|
url = 'https://open.spotify.com/' + spotify[1] + '/' + spotify[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
const site = url.match(/deezer|spotify|tidal/);
|
const site = url.match(/deezer|spotify|tidal|youtube/);
|
||||||
if (!site) {
|
if (!site) {
|
||||||
throw new Error('Unknown URL: ' + url);
|
throw new Error('Unknown URL: ' + url);
|
||||||
}
|
}
|
||||||
|
@ -62,6 +64,13 @@ export const getUrlParts = async (url: string, setToken = false): Promise<urlPar
|
||||||
const tidalUrlParts = url.split(/\/(\w+)\/(\d+|\w+-\w+-\w+-\w+-\w+)/);
|
const tidalUrlParts = url.split(/\/(\w+)\/(\d+|\w+-\w+-\w+-\w+-\w+)/);
|
||||||
return {type: ('tidal-' + tidalUrlParts[1]) as any, id: tidalUrlParts[2]};
|
return {type: ('tidal-' + tidalUrlParts[1]) as any, id: tidalUrlParts[2]};
|
||||||
|
|
||||||
|
case 'youtube':
|
||||||
|
let yotubeId = url.split('v=')[1];
|
||||||
|
if (yotubeId.includes('&')) {
|
||||||
|
yotubeId = yotubeId.split('&')[0];
|
||||||
|
}
|
||||||
|
return {type: 'youtube-track', id: yotubeId};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error('Unable to parse URL: ' + url);
|
throw new Error('Unable to parse URL: ' + url);
|
||||||
}
|
}
|
||||||
|
@ -163,6 +172,10 @@ export const parseInfo = async (url: string) => {
|
||||||
linktype = 'artist';
|
linktype = 'artist';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'youtube-track':
|
||||||
|
tracks.push(await youtube.track2deezer(info.id));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown type: ' + info.type);
|
throw new Error('Unknown type: ' + info.type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import {parse} from 'node-html-parser';
|
||||||
|
import {searchAlternative} from '../api';
|
||||||
|
|
||||||
|
const getTrack = async (id: string) => {
|
||||||
|
const response = await axios.get(`https://www.youtube.com/watch?v=${id}&hl=en`);
|
||||||
|
const script = parse(response.data)
|
||||||
|
.querySelectorAll('script')
|
||||||
|
.find((script) => script.childNodes.find((node) => node.rawText.includes('responseText')));
|
||||||
|
|
||||||
|
if (script) {
|
||||||
|
const info = script.text.split('= ');
|
||||||
|
info.shift();
|
||||||
|
if (info) {
|
||||||
|
let json = info.join('= ').trim();
|
||||||
|
if (json.endsWith(';')) {
|
||||||
|
json = json.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(json).contents.twoColumnWatchNextResults.results.results.contents[1]
|
||||||
|
.videoSecondaryInfoRenderer.metadataRowContainer.metadataRowContainerRenderer;
|
||||||
|
const song = data.rows?.find(
|
||||||
|
(row: any) => row.metadataRowRenderer && row.metadataRowRenderer.title.simpleText === 'Song',
|
||||||
|
);
|
||||||
|
const artist = data.rows?.find(
|
||||||
|
(row: any) => row.metadataRowRenderer && row.metadataRowRenderer.title.simpleText === 'Artist',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (song && artist) {
|
||||||
|
const {TRACK} = await searchAlternative(
|
||||||
|
artist.metadataRowRenderer.contents[0].runs[0].text,
|
||||||
|
song.metadataRowRenderer.contents[0].simpleText,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
if (TRACK.data[0]) {
|
||||||
|
return TRACK.data[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a youtube video to track by video id
|
||||||
|
* @param {String} id - video id
|
||||||
|
*/
|
||||||
|
export const track2deezer = async (id: string) => {
|
||||||
|
const track = await getTrack(id);
|
||||||
|
if (track) {
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No track found for youtube video ' + id);
|
||||||
|
};
|
Loading…
Reference in New Issue