121 lines
4.4 KiB
JavaScript
121 lines
4.4 KiB
JavaScript
const Cheerio = require('cheerio');
|
|
const { promises: fs, createWriteStream } = require('fs');
|
|
const debug = require('debug')('vk-music-dl');
|
|
const vm = require('vm');
|
|
const Promise = require('bluebird');
|
|
const _ = require('lodash');
|
|
const leven = require('leven');
|
|
const { URL, URLSearchParams } = require('url');
|
|
const fetch = require('node-fetch');
|
|
const m3u = require('m3u');
|
|
require('dotenv').config();
|
|
|
|
|
|
async function getSpotifyPlaylist(){
|
|
const spotifyPlaylistPageContent = await fs.readFile('./test.html');
|
|
const $ = Cheerio.load(spotifyPlaylistPageContent);
|
|
|
|
let playlist;
|
|
$('script').each(function(i, elem){
|
|
const content = $(this).html();
|
|
if(/Spotify.Entity/.test(content)){
|
|
playlist = vm.runInNewContext(content);
|
|
}
|
|
});
|
|
return playlist;
|
|
}
|
|
|
|
async function searchOnVkMusic(query){
|
|
const url = new URL('https://api.vk.com/method/audio.search');
|
|
url.search = new URLSearchParams({
|
|
v: '5.71',
|
|
access_token: process.env.ACCESS_TOKEN,
|
|
q: query,
|
|
count: 200,
|
|
}).toString();
|
|
|
|
return await fetch(url, {
|
|
headers: {
|
|
'User-Agent': 'KateMobileAndroid/48.2 lite-433 (Android 8.1.0; SDK 27; arm64-v8a; Google Pixel 2 XL; en)',
|
|
}
|
|
})
|
|
.then(res => res.json())
|
|
.then(responseBody => {
|
|
if(responseBody.error){
|
|
throw _.assign(responseBody.error, new Error(responseBody.error.error_msg));
|
|
} else {
|
|
return responseBody.response.items;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function searchOnMyFreeMp3(query){
|
|
const url = new URL('https://myfreemp3cc.com/api/search.php?callback=callback');
|
|
|
|
return await fetch(url, {
|
|
method: 'POST',
|
|
body: new URLSearchParams({
|
|
q: query,
|
|
page: 0,
|
|
}),
|
|
})
|
|
.then(res => res.text())
|
|
.then(jsonp => vm.runInNewContext(jsonp, { callback: (payload) => payload.response }));
|
|
}
|
|
|
|
function matchScore(spotifyMetas, vkmusicMetas){
|
|
const originalArtistNames = _.map(spotifyMetas.artists, 'name').join(', ').toLowerCase();
|
|
const originalTitle = spotifyMetas.name.toLowerCase();
|
|
const originalDuration = Math.round(spotifyMetas.duration_ms/1000);
|
|
|
|
const matchArtistScore = 1 - (leven(originalArtistNames, _.get(vkmusicMetas, 'artist', '').toLowerCase()) / Math.max(originalArtistNames.length, _.get(vkmusicMetas, 'artist', '').length));
|
|
const matchTitleScore = 1 - (leven(originalTitle, _.get(vkmusicMetas, 'title', '').toLowerCase()) / Math.max(originalTitle.length, _.get(vkmusicMetas, 'title', '').length));
|
|
const matchDurationScore = 1 - (Math.abs(originalDuration - _.get(vkmusicMetas, 'duration', 0)) / originalDuration); // TODO this can return more than 1 or less than 0
|
|
debug('matchArtistScore=%f matchTitleScore=%f matchDurationScore=%f', matchArtistScore, matchTitleScore, matchDurationScore);
|
|
return matchArtistScore + matchTitleScore + matchDurationScore;
|
|
}
|
|
|
|
async function generateM3U8Playlist(filesWithMetas){
|
|
const m3uWriter = m3u.extendedWriter();
|
|
filesWithMetas.forEach(fileWithMetas => m3uWriter.file(fileWithMetas.path, fileWithMetas.duration, `${fileWithMetas.artist} - ${fileWithMetas.title}`));
|
|
await fs.writeFile(`playlist.m3u8`, m3uWriter.toString());
|
|
}
|
|
|
|
async function main(){
|
|
const playlist = await getSpotifyPlaylist();
|
|
const vkPlaylist = await Promise.map(playlist.tracks.items, async ({track}) => {
|
|
const artistNames = _.map(track.artists, 'name').join(', ');
|
|
debug('%s - %s', track.name, artistNames);
|
|
const items = await searchOnMyFreeMp3(`${artistNames} ${track.name}`);
|
|
debug('items=%O', items);
|
|
const bestMatch = _.chain(items).map((item) => {
|
|
item.score = matchScore(track, item);
|
|
return item;
|
|
}).filter(item => item.score && item.url).sortBy('score').last().value();
|
|
if(!bestMatch){
|
|
console.log(`You are on your own for ${track.name} - ${artistNames}`);
|
|
return;
|
|
}
|
|
bestMatch.path = `${bestMatch.artist} - ${bestMatch.title}.mp3`;
|
|
debug('bestMatch=%O', bestMatch);
|
|
await fs.access(bestMatch.path).catch(async () => { // TODO find a proper way to not re-download, lower/uppercase problems
|
|
await fetch(bestMatch.url).then(res => {
|
|
res.body.pipe(createWriteStream(bestMatch.path));
|
|
});
|
|
debug('Done downloading');
|
|
});
|
|
return bestMatch;
|
|
}, {concurrency: 1});
|
|
|
|
await generateM3U8Playlist(_.compact(vkPlaylist));
|
|
}
|
|
|
|
main().catch(err => console.error(err));
|
|
|
|
async function test(query){
|
|
const response = await searchOnMyFreeMp3(query);
|
|
console.log(response);
|
|
}
|
|
|
|
//test(process.argv[2]);
|