support youtube

This commit is contained in:
Sayem Chowdhury 2021-03-28 01:17:27 +06:00
parent 9da80ee210
commit b0b32d6d20
5 changed files with 99 additions and 4 deletions

View File

@ -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));
}
});

View File

@ -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',
); );

View File

@ -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';

View File

@ -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);
} }

54
src/converter/youtube.ts Normal file
View File

@ -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);
};