2020-09-27 10:39:34 +02:00
'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' ) ;
const promisepipe = require ( 'promisepipe' ) ;
2020-09-27 11:22:10 +02:00
const path = require ( 'path' ) ;
2020-09-27 10:39:34 +02:00
require ( 'dotenv' ) . config ( ) ;
2020-09-27 11:22:10 +02:00
const userAgentString = 'Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0' ;
2020-09-27 10:39:34 +02:00
async function getSpotifyPlaylist ( playListUrl ) {
const spotifyPlaylistPageContent = await fetch ( playListUrl , {
headers : {
2020-09-27 11:22:10 +02:00
'User-Agent' : userAgentString ,
2020-09-27 10:39:34 +02:00
}
} )
. then ( res => res . text ( ) )
const $ = Cheerio . load ( spotifyPlaylistPageContent ) ;
let config ;
$ ( 'script#config' ) . each ( function ( i , elem ) {
config = JSON . parse ( $ ( this ) . html ( ) ) ;
} ) ;
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 : {
2020-09-27 11:22:10 +02:00
'User-Agent' : userAgentString ,
2020-09-27 10:39:34 +02:00
Authorization : ` Bearer ${ config . accessToken } `
}
} ) . then ( res => res . json ( ) ) ;
return playlist ;
}
exports . getSpotifyPlaylist = getSpotifyPlaylist ;
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 ;
}
} ) ;
}
exports . searchOnVkMusic = searchOnVkMusic ;
async function searchOnMyFreeMp3 ( query ) {
const url = new URL ( 'https://myfreemp3.vip/api/search.php?callback=jQuery666' ) ;
return await fetch ( url , {
method : 'POST' ,
2020-09-27 11:22:10 +02:00
headers : {
'User-Agent' : userAgentString ,
'X-Requested-With' : 'XMLHttpRequest' ,
Accept : 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01' ,
'Accept-Language' : 'fr-FR,fr;q=0.5' ,
Referer : 'https://myfreemp3.vip/' ,
Origin : 'https://myfreemp3.vip' ,
//Cookie: '__cfduid=d1821f08faee82ed9d4bf93b1b5fb9d3e1601196911; musicLang=en'
} ,
2020-09-27 10:39:34 +02:00
body : new URLSearchParams ( {
q : query ,
page : 0 ,
} ) ,
} )
. then ( res => res . text ( ) )
. then ( page => {
debug ( 'page=%O' , page ) ;
return page ;
} )
. then ( jsonp => vm . runInNewContext ( jsonp , { jQuery666 : ( payload ) => payload . response } ) )
. then ( items => _ . filter ( items , item => ! _ . isString ( item ) ) ) ;
}
exports . searchOnMyFreeMp3 = searchOnMyFreeMp3 ;
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 ;
}
exports . matchScore = matchScore ;
function generateM3U8Playlist ( filesWithMetas ) {
const m3uWriter = m3u . extendedWriter ( ) ;
filesWithMetas . forEach ( fileWithMetas => m3uWriter . file ( fileWithMetas . path , fileWithMetas . duration , ` ${ fileWithMetas . artist } - ${ fileWithMetas . title } ` ) ) ;
return m3uWriter . toString ( ) ;
}
exports . generateM3U8Playlist = generateM3U8Playlist ;
exports . main = async function main ( playlistUrl ) {
const playlist = await getSpotifyPlaylist ( playlistUrl ) ;
2020-09-27 11:22:10 +02:00
await Promise . resolve ( fs . mkdir ( playlist . name ) ) . catch ( { code : 'EEXIST' } , ( ) => { } ) ;
2020-09-27 10:39:34 +02:00
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 ) ;
try {
2020-09-27 11:22:10 +02:00
await fs . access ( path . join ( playlist . name , bestMatch . path ) ) . catch ( async ( ) => { // TODO find a proper way to not re-download, lower/uppercase problems
2020-09-27 10:39:34 +02:00
await fetch ( bestMatch . url ) . then ( async res => {
2020-09-27 11:22:10 +02:00
await promisepipe ( res . body , createWriteStream ( path . join ( playlist . name , bestMatch . path ) ) ) ;
2020-09-27 10:39:34 +02:00
} ) ;
debug ( 'Done downloading' ) ;
} ) ;
} catch ( err ) {
console . error ( 'Download failed for ' , bestMatch ) ;
console . error ( err ) ;
return null ;
}
return bestMatch ;
} , { concurrency : 1 } ) ;
2020-09-27 11:22:10 +02:00
await fs . writeFile ( path . join ( playlist . name , ` playlist.m3u8 ` ) , generateM3U8Playlist ( _ . compact ( vkPlaylist ) ) ) ;
2020-09-27 10:39:34 +02:00
}