2020-07-04 18:04:10 +02:00
#!/usr/bin/env node
'use strict' ;
2020-07-01 22:22:11 +02:00
const Cheerio = require ( 'cheerio' ) ;
2020-07-02 00:30:07 +02:00
const { promises : fs , createWriteStream } = require ( 'fs' ) ;
2020-07-01 22:22:11 +02:00
const debug = require ( 'debug' ) ( 'vk-music-dl' ) ;
const vm = require ( 'vm' ) ;
2020-07-01 22:23:59 +02:00
const Promise = require ( 'bluebird' ) ;
const _ = require ( 'lodash' ) ;
const leven = require ( 'leven' ) ;
2020-07-04 16:28:49 +02:00
const { URL , URLSearchParams } = require ( 'url' ) ;
2020-07-02 00:30:07 +02:00
const fetch = require ( 'node-fetch' ) ;
2020-07-04 16:28:49 +02:00
const m3u = require ( 'm3u' ) ;
2020-07-06 13:11:27 +02:00
const promisepipe = require ( 'promisepipe' ) ;
2020-07-01 22:22:11 +02:00
require ( 'dotenv' ) . config ( ) ;
2020-07-04 17:40:20 +02:00
async function getSpotifyPlaylist ( playListUrl ) {
2020-09-06 16:54:02 +02:00
const spotifyPlaylistPageContent = await fetch ( playListUrl , {
headers : {
'User-Agent' : 'Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0' ,
}
} )
2020-07-04 17:40:20 +02:00
. then ( res => res . text ( ) )
2020-07-01 22:22:11 +02:00
const $ = Cheerio . load ( spotifyPlaylistPageContent ) ;
2020-09-06 16:54:02 +02:00
let config ;
$ ( 'script#config' ) . each ( function ( i , elem ) {
config = JSON . parse ( $ ( this ) . html ( ) ) ;
2020-07-01 22:22:11 +02:00
} ) ;
2020-09-06 16:54:02 +02:00
debug ( 'config=%O' , config ) ;
const playlistSpotifyId = /\/playlist\/([^\/]+)\/?$/ . exec ( playListUrl ) [ 1 ] ;
const playlist = await fetch ( ` https://api.spotify.com/v1/playlists/ ${ playlistSpotifyId } ?type=track%2Cepisode ` , {
headers : {
'User-Agent' : 'Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0' ,
Authorization : ` Bearer ${ config . accessToken } `
}
} ) . then ( res => res . json ( ) ) ;
2020-07-01 22:22:11 +02:00
return playlist ;
}
2020-07-01 22:23:59 +02:00
async function searchOnVkMusic ( query ) {
2020-07-04 16:28:49 +02:00
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 , {
2020-07-01 22:22:11 +02:00
headers : {
'User-Agent' : 'KateMobileAndroid/48.2 lite-433 (Android 8.1.0; SDK 27; arm64-v8a; Google Pixel 2 XL; en)' ,
2020-07-04 16:28:49 +02:00
}
} )
. then ( res => res . json ( ) )
. then ( responseBody => {
if ( responseBody . error ) {
throw _ . assign ( responseBody . error , new Error ( responseBody . error . error _msg ) ) ;
} else {
2020-07-04 17:30:25 +02:00
return responseBody . response . items ;
2020-07-04 16:28:49 +02:00
}
2020-07-01 22:22:11 +02:00
} ) ;
}
2020-07-04 17:30:25 +02:00
async function searchOnMyFreeMp3 ( query ) {
2020-09-06 16:54:02 +02:00
const url = new URL ( 'https://myfreemp3cc.com/api/search.php?callback=jQuery666' ) ;
2020-07-04 17:30:25 +02:00
return await fetch ( url , {
method : 'POST' ,
body : new URLSearchParams ( {
q : query ,
page : 0 ,
} ) ,
} )
. then ( res => res . text ( ) )
2020-09-06 16:54:02 +02:00
. then ( page => {
debug ( 'page=%O' , page ) ;
return page ;
} )
. then ( jsonp => vm . runInNewContext ( jsonp , { jQuery666 : ( payload ) => payload . response } ) )
2020-07-04 18:11:14 +02:00
. then ( items => _ . filter ( items , item => ! _ . isString ( item ) ) ) ;
2020-07-04 17:30:25 +02:00
}
2020-07-01 22:23:59 +02:00
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 ) ;
2020-07-04 17:30:25 +02:00
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
2020-07-01 22:23:59 +02:00
debug ( 'matchArtistScore=%f matchTitleScore=%f matchDurationScore=%f' , matchArtistScore , matchTitleScore , matchDurationScore ) ;
return matchArtistScore + matchTitleScore + matchDurationScore ;
}
2020-07-04 16:28:49 +02:00
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 ( ) ) ;
}
2020-07-04 17:40:20 +02:00
async function main ( playlistUrl ) {
const playlist = await getSpotifyPlaylist ( playlistUrl ) ;
2020-07-04 16:28:49 +02:00
const vkPlaylist = await Promise . map ( playlist . tracks . items , async ( { track } ) => {
2020-07-01 22:23:59 +02:00
const artistNames = _ . map ( track . artists , 'name' ) . join ( ', ' ) ;
debug ( '%s - %s' , track . name , artistNames ) ;
2020-07-04 17:30:25 +02:00
const items = await searchOnMyFreeMp3 ( ` ${ artistNames } ${ track . name } ` ) ;
debug ( 'items=%O' , items ) ;
const bestMatch = _ . chain ( items ) . map ( ( item ) => {
2020-07-01 22:23:59 +02:00
item . score = matchScore ( track , item ) ;
return item ;
2020-07-04 17:30:25 +02:00
} ) . filter ( item => item . score && item . url ) . sortBy ( 'score' ) . last ( ) . value ( ) ;
2020-07-04 16:28:49 +02:00
if ( ! bestMatch ) {
console . log ( ` You are on your own for ${ track . name } - ${ artistNames } ` ) ;
return ;
}
bestMatch . path = ` ${ bestMatch . artist } - ${ bestMatch . title } .mp3 ` ;
2020-07-04 17:30:25 +02:00
debug ( 'bestMatch=%O' , bestMatch ) ;
2020-07-06 13:11:27 +02:00
try {
await fs . access ( bestMatch . path ) . catch ( async ( ) => { // TODO find a proper way to not re-download, lower/uppercase problems
await fetch ( bestMatch . url ) . then ( async res => {
await promisepipe ( res . body , createWriteStream ( bestMatch . path ) ) ;
} ) ;
debug ( 'Done downloading' ) ;
2020-07-04 16:28:49 +02:00
} ) ;
2020-07-06 13:11:27 +02:00
} catch ( err ) {
console . error ( 'Download failed for ' , bestMatch ) ;
console . error ( err ) ;
return null ;
}
2020-07-04 16:28:49 +02:00
return bestMatch ;
} , { concurrency : 1 } ) ;
await generateM3U8Playlist ( _ . compact ( vkPlaylist ) ) ;
}
2020-07-04 17:40:20 +02:00
main ( process . argv [ 2 ] ) . catch ( err => console . error ( err ) ) ;
2020-07-04 16:28:49 +02:00
async function test ( query ) {
2020-07-04 17:30:25 +02:00
const response = await searchOnMyFreeMp3 ( query ) ;
2020-07-04 16:28:49 +02:00
console . log ( response ) ;
2020-07-01 22:22:11 +02:00
}
2020-07-04 17:30:25 +02:00
//test(process.argv[2]);