#!/usr/bin/env node 'use strict'; 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(playListUrl){ const spotifyPlaylistPageContent = await fetch(playListUrl) .then(res => res.text()) 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 })) .then(items => _.filter(items, item => !_.isString(item))); } 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(playlistUrl){ const playlist = await getSpotifyPlaylist(playlistUrl); 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(process.argv[2]).catch(err => console.error(err)); async function test(query){ const response = await searchOnMyFreeMp3(query); console.log(response); } //test(process.argv[2]);