From a9c92b850a2d71968cb8cc021f8cc4b0c222fcd9 Mon Sep 17 00:00:00 2001 From: paxt82 Date: Fri, 18 Jun 2021 17:06:29 +0200 Subject: [PATCH] Upload files to 'paxt002' const expect = require('chai').expect; const alltomp3 = require('..'); const _ = require('lodash'); function testTracks(tracks, artistName) { let queries = []; _.forEach(tracks, t => { t.song.artistName = artistName; let q = alltomp3.findVideoForSong(t.song).then(v => { if (t.videos.indexOf(v[0].url) === -1) { console.log(t.song.title, v); } expect(t.videos).to.include(v[0].url); }); queries.push(q); }); return Promise.all(queries); } describe('findVideo', function() { it('should find YouTube videos for Broken Back by Broken Back', function () { this.timeout(20000); let artistName = "Broken Back"; let tracks = [ { song: { title: "Excuses", duration: 224 }, videos: ['https://www.youtube.com/watch?v=aBp7s6BpmrM', 'https://www.youtube.com/watch?v=Ogsfvvwu-wU', 'https://www.youtube.com/watch?v=upUWTD6x3dw'] }, { song: { title: "Halcyon Birds (Radio Edit)", duration: 197 }, videos: ['https://www.youtube.com/watch?v=xWlXEGIol9E', 'https://www.youtube.com/watch?v=EGSqjTixIyk', 'https://www.youtube.com/watch?v=gzZ43IEJ8S0', 'https://www.youtube.com/watch?v=2Ggu0m0a8WA', 'https://www.youtube.com/watch?v=5eS1WxW4GM4'] }, { song: { title: "Better Run", duration: 176 }, videos: ['https://www.youtube.com/watch?v=oEugd8BV_Bs', 'https://www.youtube.com/watch?v=5QP8K42-wA8'], }, { song: { title: "Happiest Man on Earth (Radio Edit)", duration: 183 }, videos: ['https://www.youtube.com/watch?v=j01T8N7wVK0', 'https://www.youtube.com/watch?v=TqpM_0_H6Qc'], }, { song: { title: "Got to Go", duration: 218 }, videos: ['https://www.youtube.com/watch?v=A2iBEZBxT3s', 'https://www.youtube.com/watch?v=MXABmCs5Fkc'] }, // This last one is currently too hard // { // title: "Young Souls (Album Edit) - Broken Back", // videos: ['https://www.youtube.com/watch?v=LT9kwbunzWc'] // } ]; return testTracks(tracks, artistName); }); it('should find YouTube videos for Night Visions by Imagine Dragons', function () { this.timeout(20000); let artistName = "Imagine Dragons"; let tracks = [ { "song": { "title": "Radioactive", "duration": 186 }, "videos": ['https://www.youtube.com/watch?v=ktvTqknDobU', 'https://www.youtube.com/watch?v=iO_WxYC34eM', 'https://www.youtube.com/watch?v=Thbsg9i2mZ0', 'https://www.youtube.com/watch?v=y_8Mgn30xRU'] }, { "song": { "title": "Tiptoe", "duration": 194 }, "videos": ['https://www.youtube.com/watch?v=ajjj4pLnjz8', 'https://www.youtube.com/watch?v=UB96k1arlTk', 'https://www.youtube.com/watch?v=211bk6ctXM4', 'https://www.youtube.com/watch?v=zmKv2Aok1Mw'] }, { "song": { "title": "It's Time", "duration": 240 }, "videos": ['https://www.youtube.com/watch?v=sENM2wA_FTg', 'https://www.youtube.com/watch?v=IOatp-OCw3E', 'https://www.youtube.com/watch?v=qFqMy0ewYFQ'] }, { "song": { "title": "Demons", "duration": 177 }, "videos": ['https://www.youtube.com/watch?v=mWRsgZuwf_8', 'https://www.youtube.com/watch?v=GFQYaoiIFh8', 'https://www.youtube.com/watch?v=LqI78S14Wgg', 'https://www.youtube.com/watch?v=lxUXvOUKM_Q'] }, { "song": { "title": "On Top Of The World", "duration": 192 }, "videos": ['https://www.youtube.com/watch?v=w5tWYmIOWGk', 'https://www.youtube.com/watch?v=g8PrTzLaLHc', 'https://www.youtube.com/watch?v=Nwvil057g-g', 'https://www.youtube.com/watch?v=e74VMNgARvY'] }, { "song": { "title": "Amsterdam", "duration": 241 }, "videos": ['https://www.youtube.com/watch?v=TKtPXO5iEnA', 'https://www.youtube.com/watch?v=s6Nc4qEI3k4', 'https://www.youtube.com/watch?v=dboQYGddEC8', 'https://www.youtube.com/watch?v=j-tfNaBcyes'] }, { "song": { "title": "Hear Me", "duration": 235 }, "videos": ['https://www.youtube.com/watch?v=1Yr683VLxes', 'https://www.youtube.com/watch?v=EkB2eJfF3W8', 'https://www.youtube.com/watch?v=u0Q3r4ywA34'] }, { "song": { "title": "Every Night", "duration": 217 }, "videos": ['https://www.youtube.com/watch?v=6RVxzeBiBJU', 'https://www.youtube.com/watch?v=kuijhOvKyYg', 'https://www.youtube.com/watch?v=f6CQ3ATP_6Y', 'https://www.youtube.com/watch?v=k4ESylzBW4M'] }, { "song": { "title": "Bleeding Out", "duration": 223 }, "videos": ['https://www.youtube.com/watch?v=jNFgynmVmx0', 'https://www.youtube.com/watch?v=gJEoxeW7JvQ', 'https://www.youtube.com/watch?v=Hq6kn87NpKw', 'https://www.youtube.com/watch?v=Tyjt1Ff4m7k'] }, { "song": { "title": "Underdog", "duration": 209 }, "videos": ['https://www.youtube.com/watch?v=JCUV43T7HZ0', 'https://www.youtube.com/watch?v=USeEyhodZqk', 'https://www.youtube.com/watch?v=KoX80w5b8ps', 'https://www.youtube.com/watch?v=m4SBGLtJvPo', 'https://www.youtube.com/watch?v=2RQRoTvXXiw'] }, { "song": { "title": "Nothing Left To Say / Rocks (Medley)", "duration": 537 }, "videos": ['https://www.youtube.com/watch?v=Bn7eYibzmTs', 'https://www.youtube.com/watch?v=bSBkYqbdOKc', 'https://www.youtube.com/watch?v=B4z7loNm_kw', 'https://www.youtube.com/watch?v=Q6zqH6qKaTU', 'https://www.youtube.com/watch?v=zCqHNBleR6M'] }, { "song": { "title": "Cha-Ching (Till We Grow Older)", "duration": 249 }, "videos": ['https://www.youtube.com/watch?v=rmqyXQf8jkU', 'https://www.youtube.com/watch?v=vhSvfxtlUQc', 'https://www.youtube.com/watch?v=wO0ohGn-x3E', 'https://www.youtube.com/watch?v=IgC8yZpMRqc', 'https://www.youtube.com/watch?v=zTingWDAAts'] }, { "song": { "title": "Working Man", "duration": 235 }, "videos": ['https://www.youtube.com/watch?v=m4SBGLtJvPo', 'https://www.youtube.com/watch?v=2d-GIw-pBMs', 'https://www.youtube.com/watch?v=NARf6QW3KYA', 'https://www.youtube.com/watch?v=31YTMhD4NYs', 'https://www.youtube.com/watch?v=mhXoXYbmCz4', 'https://www.youtube.com/watch?v=2d-GIw-pBMs', 'https://www.youtube.com/watch?v=aW8_7M8e5CQ'] } ]; return testTracks(tracks, artistName); }); // Failed, but the videos it finds are unavailable // Should update testTracks() so it checks if the video is available or not // it('should find YouTube videos for Night Shift (Original Mix) by Overwerk', function () { // this.timeout(10000); // // let artistName = "Overwerk"; // let tracks = [ // { // song: { // title: "Night Shift (Original Mix)", // duration: 541 // }, // videos: ['https://www.youtube.com/watch?v=SI2wnEvrepM', 'https://www.youtube.com/watch?v=87bCbQHez9k', 'https://www.youtube.com/watch?v=dtq9NNLa1O8'] // }, // { // song: { // title: "Last Call (Original Mix)", // duration: 600 // }, // videos: ['https://www.youtube.com/watch?v=4C9LACMJFT4', 'https://www.youtube.com/watch?v=eQMXBaB0Ehs', 'https://www.youtube.com/watch?v=RzKCHDgQZ9g'] // } // ]; // // return testTracks(tracks, artistName); // }); it('should find YouTube videos for single letter artist M', function () { this.timeout(10000); // It's a French song alltomp3.regionCode = 'FR'; let artistName = "M"; let tracks = [ { song: { title: "Je dis aime", duration: 236 }, videos: ['https://www.youtube.com/watch?v=6hV-UnrC9tU', 'https://www.youtube.com/watch?v=QYWV67qgHvg', 'https://www.youtube.com/watch?v=ORam68OtcmY'] }, { song: { title: "Mama Sam", duration: 195 }, videos: ['https://www.youtube.com/watch?v=avZTCTR6e9k', 'https://www.youtube.com/watch?v=HRez3YiXxJw'] } ]; return testTracks(tracks, artistName); }); }); --- paxt002/findLyrics.js | 109 ++ paxt002/findVideo.js | 224 +++ paxt002/guessTrackFromString.js | 40 + paxt002/index.js | 2395 +++++++++++++++++++++++++++++++ paxt002/test.js | 211 +++ 5 files changed, 2979 insertions(+) create mode 100644 paxt002/findLyrics.js create mode 100644 paxt002/findVideo.js create mode 100644 paxt002/guessTrackFromString.js create mode 100644 paxt002/index.js create mode 100644 paxt002/test.js diff --git a/paxt002/findLyrics.js b/paxt002/findLyrics.js new file mode 100644 index 0000000..37755bc --- /dev/null +++ b/paxt002/findLyrics.js @@ -0,0 +1,109 @@ +const expect = require('chai').expect; +const alltomp3 = require('..'); +const _ = require('lodash'); + +/** + * Lyrics uses 5 or 6 different websites, + * and the first to answer is the one chosen. + * So it is absolutely non determinist, and + * these tests are repeated 5 times. + * Moreover lyrics varies depending on the website, + * and can sometimes be wrong... +*/ + +describe('findLyrics', () => { + + for (let i = 0; i < 5; i += 1) { + it('should find lyrics for Imagine Dragons - On Top of the World', (done) => { + alltomp3.findLyrics('On Top of the World', 'Imagine Dragons').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/^if-you-love-somebody-better-tell-them-while-theyre-here/); + expect(lyrics).to.match(/(when-you-hit-the-ground-get-up-now-get-up-get-up-now)|(i-can-been-dreaming-of-this-since-a-child-im-on-top-of-the-world)$/); + done(); + }); + }); + it('should find lyrics for Pauline Croze - T\'es Beau', (done) => { + alltomp3.findLyrics('T\'es beau', 'Pauline Croze').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/tes-beau-parce-que-tes-courageux-de-regarder-dans-le-fond-des-yeux-celui-qui-te-defie-detre-heureux/); + expect(lyrics).to.match(/jai-peur-doublier-jai-peur-daccepter-jai-peur-des-vivants-a-present-tes-beau$/); + done(); + }); + }); + it('should find lyrics for M - Mama Sam', (done) => { + alltomp3.findLyrics('Mama Sam', 'M').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/^quand-je-te-revois-mama-sam-je-retrouve-les-vraies-valeurs/); + expect(lyrics).to.match(/non-je-ne-connais-pas-l-afrique-aigrie-est-ma-couleur-de-peau-la-vie-est-une-machine-a-fric-ou-les-affreux-non(t?)-pas-dafro$/); + done(); + }); + }); + it('should find lyrics for Coldplay - Viva la Vida', (done) => { + alltomp3.findLyrics('Viva la Vida', 'Coldplay').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/^i-used-to-rule-the-world-seas-would-rise-when-i-gave-the-word/); + expect(lyrics).to.match(/for-some-reason-i-cant-explain-i-know-saint-peter-wont-call-my-name-never-an-honest-word-but-that-was-when-i-ruled-the-world/); + done(); + }); + }); + it('should find lyrics for HYPHEN HYPHEN - Cause I Got A Chance', (done) => { + alltomp3.findLyrics('Cause I Got A Chance', 'HYPHEN HYPHEN').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/^tonight-i-dont-wanna-cry-tonight-i-want-you-to-dance-with-me-tonight/); + expect(lyrics).to.match(/ive-been-waiting-for-so-long-knew-you-always-thought-about-always-thought-about-me$/); + done(); + }); + }); + it('should find lyrics for Calvin Harris - The Rain', (done) => { + alltomp3.findLyrics('The Rain', 'Calvin Harris').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/^shes-the-type-of-girl-that-makes-you-feel-better/); + expect(lyrics).to.match(/these-are-the-good-times-in-your-life-so-put-on-a-smile-and-itll-be-((alright)|(all-right))/); + done(); + }); + }); + it('should find lyrics for Sam Smith - Writing\'s on the Wall', (done) => { + alltomp3.findLyrics('Writing\'s on the Wall', 'Sam Smith').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/^ive-been-here-before-but-always-hit-the-floor-ive-spent-a-lifetime-running-and-i-always-get-away/); + expect(lyrics).to.match(/where-i-give-it-all-up-for-you-i-have-to-risk-it-all-cause-the-writings-on-the-wall$/); + done(); + }); + }); + it('should find lyrics for C2C - Down The Road', (done) => { + alltomp3.findLyrics('Down The Road', 'C2C').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/have-no-place-to-go-have-no-place-to-go-darling/); + expect(lyrics).to.match(/when-that-train-rolls-up-and-i-come-walking-out/); + done(); + }); + }); + it('should find lyrics for Galantis - Runaway (U & I)', (done) => { + alltomp3.findLyrics('Runaway (U & I)', 'Galantis').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/think-i-can-fly-think-i-can-fly-when-im-with-u/); + expect(lyrics).to.match(/i-know-that-im-rich-enough-for-pride-i-see-a-billion-dollars-in-your-eyes-even-if-were-strangers/); + done(); + }); + }); + it('should find lyrics for Mika - Elle me dit', (done) => { + alltomp3.findLyrics('Elle me dit', 'Mika').then((lyrics) => { + lyrics = _.kebabCase(lyrics); + + expect(lyrics).to.match(/elle-me-dit-ecris-une-chanson-contente-pas-une-chanson-deprimante-une-chanson-que-tout-l((e-)?)monde-aime/); + expect(lyrics).to.match(/pourquoi-tu-gaches-ta-vie-pourquoi-tu-gaches-ta-vie/); + expect(lyrics).to.match(/regarde-un-peu-tes-amis-quest-c((e-)?)quils-vont-faire-de-leur-vie/); + done(); + }); + }); + } +}); diff --git a/paxt002/findVideo.js b/paxt002/findVideo.js new file mode 100644 index 0000000..60b6879 --- /dev/null +++ b/paxt002/findVideo.js @@ -0,0 +1,224 @@ +const expect = require('chai').expect; +const alltomp3 = require('..'); +const _ = require('lodash'); + +function testTracks(tracks, artistName) { + let queries = []; + _.forEach(tracks, t => { + t.song.artistName = artistName; + let q = alltomp3.findVideoForSong(t.song).then(v => { + if (t.videos.indexOf(v[0].url) === -1) { + console.log(t.song.title, v); + } + expect(t.videos).to.include(v[0].url); + }); + queries.push(q); + }); + + return Promise.all(queries); +} + +describe('findVideo', function() { + it('should find YouTube videos for Broken Back by Broken Back', function () { + this.timeout(20000); + let artistName = "Broken Back"; + let tracks = [ + { + song: { + title: "Excuses", + duration: 224 + }, + videos: ['https://www.youtube.com/watch?v=aBp7s6BpmrM', 'https://www.youtube.com/watch?v=Ogsfvvwu-wU', 'https://www.youtube.com/watch?v=upUWTD6x3dw'] + }, + { + song: { + title: "Halcyon Birds (Radio Edit)", + duration: 197 + }, + videos: ['https://www.youtube.com/watch?v=xWlXEGIol9E', 'https://www.youtube.com/watch?v=EGSqjTixIyk', 'https://www.youtube.com/watch?v=gzZ43IEJ8S0', 'https://www.youtube.com/watch?v=2Ggu0m0a8WA', 'https://www.youtube.com/watch?v=5eS1WxW4GM4'] + }, + { + song: { + title: "Better Run", + duration: 176 + }, + videos: ['https://www.youtube.com/watch?v=oEugd8BV_Bs', 'https://www.youtube.com/watch?v=5QP8K42-wA8'], + }, + { + song: { + title: "Happiest Man on Earth (Radio Edit)", + duration: 183 + }, + videos: ['https://www.youtube.com/watch?v=j01T8N7wVK0', 'https://www.youtube.com/watch?v=TqpM_0_H6Qc'], + }, + { + song: { + title: "Got to Go", + duration: 218 + }, + videos: ['https://www.youtube.com/watch?v=A2iBEZBxT3s', 'https://www.youtube.com/watch?v=MXABmCs5Fkc'] + }, + // This last one is currently too hard + // { + // title: "Young Souls (Album Edit) - Broken Back", + // videos: ['https://www.youtube.com/watch?v=LT9kwbunzWc'] + // } + ]; + + return testTracks(tracks, artistName); + }); + + it('should find YouTube videos for Night Visions by Imagine Dragons', function () { + this.timeout(20000); + let artistName = "Imagine Dragons"; + + let tracks = [ + { + "song": { + "title": "Radioactive", + "duration": 186 + }, + "videos": ['https://www.youtube.com/watch?v=ktvTqknDobU', 'https://www.youtube.com/watch?v=iO_WxYC34eM', 'https://www.youtube.com/watch?v=Thbsg9i2mZ0', 'https://www.youtube.com/watch?v=y_8Mgn30xRU'] + }, + { + "song": { + "title": "Tiptoe", + "duration": 194 + }, + "videos": ['https://www.youtube.com/watch?v=ajjj4pLnjz8', 'https://www.youtube.com/watch?v=UB96k1arlTk', 'https://www.youtube.com/watch?v=211bk6ctXM4', 'https://www.youtube.com/watch?v=zmKv2Aok1Mw'] + }, + { + "song": { + "title": "It's Time", + "duration": 240 + }, + "videos": ['https://www.youtube.com/watch?v=sENM2wA_FTg', 'https://www.youtube.com/watch?v=IOatp-OCw3E', 'https://www.youtube.com/watch?v=qFqMy0ewYFQ'] + }, + { + "song": { + "title": "Demons", + "duration": 177 + }, + "videos": ['https://www.youtube.com/watch?v=mWRsgZuwf_8', 'https://www.youtube.com/watch?v=GFQYaoiIFh8', 'https://www.youtube.com/watch?v=LqI78S14Wgg', 'https://www.youtube.com/watch?v=lxUXvOUKM_Q'] + }, + { + "song": { + "title": "On Top Of The World", + "duration": 192 + }, + "videos": ['https://www.youtube.com/watch?v=w5tWYmIOWGk', 'https://www.youtube.com/watch?v=g8PrTzLaLHc', 'https://www.youtube.com/watch?v=Nwvil057g-g', 'https://www.youtube.com/watch?v=e74VMNgARvY'] + }, + { + "song": { + "title": "Amsterdam", + "duration": 241 + }, + "videos": ['https://www.youtube.com/watch?v=TKtPXO5iEnA', 'https://www.youtube.com/watch?v=s6Nc4qEI3k4', 'https://www.youtube.com/watch?v=dboQYGddEC8', 'https://www.youtube.com/watch?v=j-tfNaBcyes'] + }, + { + "song": { + "title": "Hear Me", + "duration": 235 + }, + "videos": ['https://www.youtube.com/watch?v=1Yr683VLxes', 'https://www.youtube.com/watch?v=EkB2eJfF3W8', 'https://www.youtube.com/watch?v=u0Q3r4ywA34'] + }, + { + "song": { + "title": "Every Night", + "duration": 217 + }, + "videos": ['https://www.youtube.com/watch?v=6RVxzeBiBJU', 'https://www.youtube.com/watch?v=kuijhOvKyYg', 'https://www.youtube.com/watch?v=f6CQ3ATP_6Y', 'https://www.youtube.com/watch?v=k4ESylzBW4M'] + }, + { + "song": { + "title": "Bleeding Out", + "duration": 223 + }, + "videos": ['https://www.youtube.com/watch?v=jNFgynmVmx0', 'https://www.youtube.com/watch?v=gJEoxeW7JvQ', 'https://www.youtube.com/watch?v=Hq6kn87NpKw', 'https://www.youtube.com/watch?v=Tyjt1Ff4m7k'] + }, + { + "song": { + "title": "Underdog", + "duration": 209 + }, + "videos": ['https://www.youtube.com/watch?v=JCUV43T7HZ0', 'https://www.youtube.com/watch?v=USeEyhodZqk', 'https://www.youtube.com/watch?v=KoX80w5b8ps', 'https://www.youtube.com/watch?v=m4SBGLtJvPo', 'https://www.youtube.com/watch?v=2RQRoTvXXiw'] + }, + { + "song": { + "title": "Nothing Left To Say / Rocks (Medley)", + "duration": 537 + }, + "videos": ['https://www.youtube.com/watch?v=Bn7eYibzmTs', 'https://www.youtube.com/watch?v=bSBkYqbdOKc', 'https://www.youtube.com/watch?v=B4z7loNm_kw', 'https://www.youtube.com/watch?v=Q6zqH6qKaTU', 'https://www.youtube.com/watch?v=zCqHNBleR6M'] + }, + { + "song": { + "title": "Cha-Ching (Till We Grow Older)", + "duration": 249 + }, + "videos": ['https://www.youtube.com/watch?v=rmqyXQf8jkU', 'https://www.youtube.com/watch?v=vhSvfxtlUQc', 'https://www.youtube.com/watch?v=wO0ohGn-x3E', 'https://www.youtube.com/watch?v=IgC8yZpMRqc', 'https://www.youtube.com/watch?v=zTingWDAAts'] + }, + { + "song": { + "title": "Working Man", + "duration": 235 + }, + "videos": ['https://www.youtube.com/watch?v=m4SBGLtJvPo', 'https://www.youtube.com/watch?v=2d-GIw-pBMs', 'https://www.youtube.com/watch?v=NARf6QW3KYA', 'https://www.youtube.com/watch?v=31YTMhD4NYs', 'https://www.youtube.com/watch?v=mhXoXYbmCz4', 'https://www.youtube.com/watch?v=2d-GIw-pBMs', 'https://www.youtube.com/watch?v=aW8_7M8e5CQ'] + } + ]; + + return testTracks(tracks, artistName); + }); + + // Failed, but the videos it finds are unavailable + // Should update testTracks() so it checks if the video is available or not + // it('should find YouTube videos for Night Shift (Original Mix) by Overwerk', function () { + // this.timeout(10000); + // + // let artistName = "Overwerk"; + // let tracks = [ + // { + // song: { + // title: "Night Shift (Original Mix)", + // duration: 541 + // }, + // videos: ['https://www.youtube.com/watch?v=SI2wnEvrepM', 'https://www.youtube.com/watch?v=87bCbQHez9k', 'https://www.youtube.com/watch?v=dtq9NNLa1O8'] + // }, + // { + // song: { + // title: "Last Call (Original Mix)", + // duration: 600 + // }, + // videos: ['https://www.youtube.com/watch?v=4C9LACMJFT4', 'https://www.youtube.com/watch?v=eQMXBaB0Ehs', 'https://www.youtube.com/watch?v=RzKCHDgQZ9g'] + // } + // ]; + // + // return testTracks(tracks, artistName); + // }); + + it('should find YouTube videos for single letter artist M', function () { + this.timeout(10000); + + // It's a French song + alltomp3.regionCode = 'FR'; + + let artistName = "M"; + let tracks = [ + { + song: { + title: "Je dis aime", + duration: 236 + }, + videos: ['https://www.youtube.com/watch?v=6hV-UnrC9tU', 'https://www.youtube.com/watch?v=QYWV67qgHvg', 'https://www.youtube.com/watch?v=ORam68OtcmY'] + }, + { + song: { + title: "Mama Sam", + duration: 195 + }, + videos: ['https://www.youtube.com/watch?v=avZTCTR6e9k', 'https://www.youtube.com/watch?v=HRez3YiXxJw'] + } + ]; + + return testTracks(tracks, artistName); + }); +}); diff --git a/paxt002/guessTrackFromString.js b/paxt002/guessTrackFromString.js new file mode 100644 index 0000000..6559a40 --- /dev/null +++ b/paxt002/guessTrackFromString.js @@ -0,0 +1,40 @@ +const expect = require('chai').expect; +const alltomp3 = require('..'); +const _ = require('lodash'); + +describe('guessTrackFromString', function() { + it('should guess the right track', function () { + this.timeout(20000); + let queries = [ + { + q: "Kungs vs Cookin’ on 3 Burners - This Girl", + title: "This Girl (Kungs Vs. Cookin' On 3 Burners)", + a: "Kungs" + }, + { + q: "'City of Stars' (Duet ft. Ryan Gosling, Emma Stone) - La La Land Original Motion Picture Soundtrack", + t: "City of Stars", + a: "Piano Dreamers" + }, + { + q: "Imagine Dragons - On Top Of The World (Official Music Video)", + t: "On Top Of The World", + a: "Imagine Dragons" + }, + { + q: "Mika - Elle Me Dit (clip officiel)", + t: "Elle Me Dit", + a: "Mika" + } + ]; + let promises = []; + _.forEach(queries, q => { + let p = alltomp3.guessTrackFromString(q.q).then(a => { + expect(a.title).to.be(q.t); + expect(a.artistName).to.be(q.a); + }); + promises.push(p); + }); + return promises; + }); +}); diff --git a/paxt002/index.js b/paxt002/index.js new file mode 100644 index 0000000..d2f1111 --- /dev/null +++ b/paxt002/index.js @@ -0,0 +1,2395 @@ +const ytdl = require('ytdl-core'); +const ffmpeg = require('fluent-ffmpeg'); +const path = require('path'); +const fs = require('fs-extra'); +const EventEmitter = require('events'); +const request = require('request-promise'); +const requestNoPromise = require('request'); +const _ = require('lodash'); +const acoustid = require('acoustid'); +const EyeD3 = require('eyed3'); +let eyed3 = new EyeD3({ eyed3_path: 'eyeD3' }); +eyed3.metaHook = (m) => m; +const levenshtein = require('fast-levenshtein'); +const randomstring = require('randomstring'); +const cheerio = require('cheerio'); +const Promise = require('bluebird'); +const sharp = require('sharp'); +const smartcrop = require('smartcrop-sharp'); +const ytsr = require('ytsr'); +const ytpl = require('ytpl'); +const lcs = require('longest-common-substring'); + +// API keys +const API_ACOUSTID = 'lm59lNN597'; +const API_SOUNDCLOUD = 'dba290d84e6ca924414c91ac12fc3c8f'; +const API_SPOTIFY = 'ODNiZjMzMmQ4MDI1NGNlNzhkNjNkOWM2ZWM2N2M5ZTU6Mzg4OTIxY2M0ZjEyNGEwYWFjM2NiMzIzYTNiZGVlYmU='; + +const at3 = {}; + +// ISO 3166-1 alpha-2 country code of the user (ex: US, FR) +at3.regionCode; + +// ISO 639-1 two-letter language code of the user (ex: en, fr) +at3.relevanceLanguage; + +// Folder for temporary files +at3.tempFolder = null; + +// Fix for renameSync failing on Windows when cropping cover images +// See https://github.com/lovell/sharp/issues/415 +sharp.cache(false); + +at3.configEyeD3 = (eyeD3Path, eyeD3PathPythonPath, metaHook) => { + process.env.PYTHONPATH = eyeD3PathPythonPath; + eyed3 = new EyeD3({ eyed3_path: eyeD3Path }); + if (!metaHook) { + metaHook = (m) => m; + } + eyed3.metaHook = metaHook; +}; + +at3.FPCALC_PATH = 'fpcalc'; +at3.setFpcalcPath = (fpcalcPath) => { + at3.FPCALC_PATH = fpcalcPath; +}; + +at3.setFfmpegPaths = (ffmpegPath, ffprobePath) => { + if (ffmpegPath) { + at3.FFMPEG_PATH = ffmpegPath; + } + if (ffprobePath) { + at3.FFPROBE_PATH = ffprobePath; + } +}; + +/** + * Find lyrics for a song + * @param title string + * @param artistName string + * @return Promise + */ +at3.findLyrics = (title, artistName) => { + let promises = []; + + const textln = (html) => { + html.find('br').replaceWith('\n'); + html.find('script').replaceWith(''); + html.find('#video-musictory').replaceWith(''); + html.find('strong').replaceWith(''); + html = _.trim(html.text()); + html = html.replace(/\r\n\n/g, '\n'); + html = html.replace(/\t/g, ''); + html = html.replace(/\n\r\n/g, '\n'); + html = html.replace(/ +/g, ' '); + html = html.replace(/\n /g, '\n'); + return html; + }; + + const lyricsUrl = (title) => { + return _.kebabCase(_.trim(_.toLower(_.deburr(title)))); + }; + const lyricsManiaUrl = (title) => { + return _.snakeCase(_.trim(_.toLower(_.deburr(title)))); + }; + const lyricsManiaUrlAlt = (title) => { + title = _.trim(_.toLower(title)); + title = title.replace("'", ''); + title = title.replace(' ', '_'); + title = title.replace(/_+/g, '_'); + return title; + }; + + const reqWikia = request({ + uri: 'http://lyrics.wikia.com/wiki/' + encodeURIComponent(artistName) + ':' + encodeURIComponent(title), + transform: (body) => { + return cheerio.load(body); + }, + }).then(($) => { + return textln($('.lyricbox')); + }); + + const reqParolesNet = request({ + uri: 'http://www.paroles.net/' + lyricsUrl(artistName) + '/paroles-' + lyricsUrl(title), + transform: (body) => { + return cheerio.load(body); + }, + }).then(($) => { + if ($('.song-text').length === 0) { + return Promise.reject(); + } + return textln($('.song-text')); + }); + + const reqLyricsMania1 = request({ + uri: 'http://www.lyricsmania.com/' + lyricsManiaUrl(title) + '_lyrics_' + lyricsManiaUrl(artistName) + '.html', + transform: (body) => { + return cheerio.load(body); + }, + }).then(($) => { + if ($('.lyrics-body').length === 0) { + return Promise.reject(); + } + return textln($('.lyrics-body')); + }); + + const reqLyricsMania2 = request({ + uri: 'http://www.lyricsmania.com/' + lyricsManiaUrl(title) + '_' + lyricsManiaUrl(artistName) + '.html', + transform: (body) => { + return cheerio.load(body); + }, + }).then(($) => { + if ($('.lyrics-body').length === 0) { + return Promise.reject(); + } + return textln($('.lyrics-body')); + }); + + const reqLyricsMania3 = request({ + uri: + 'http://www.lyricsmania.com/' + + lyricsManiaUrlAlt(title) + + '_lyrics_' + + encodeURIComponent(lyricsManiaUrlAlt(artistName)) + + '.html', + transform: (body) => { + return cheerio.load(body); + }, + }).then(($) => { + if ($('.lyrics-body').length === 0) { + return Promise.reject(); + } + return textln($('.lyrics-body')); + }); + + const reqSweetLyrics = request({ + method: 'POST', + uri: 'http://www.sweetslyrics.com/search.php', + form: { + search: 'title', + searchtext: title, + }, + transform: (body) => { + return cheerio.load(body); + }, + }) + .then(($) => { + let closestLink, + closestScore = -1; + _.forEach($('.search_results_row_color'), (e) => { + let artist = $(e) + .text() + .replace(/ - .+$/, ''); + let currentScore = levenshtein.get(artistName, artist); + if (closestScore === -1 || currentScore < closestScore) { + closestScore = currentScore; + closestLink = $(e).find('a').last().attr('href'); + } + }); + if (!closestLink) { + return Promise.reject(); + } + return request({ + uri: 'http://www.sweetslyrics.com/' + closestLink, + transform: (body) => { + return cheerio.load(body); + }, + }); + }) + .then(($) => { + return textln($('.lyric_full_text')); + }); + + if (/\(.*\)/.test(title) || /\[.*\]/.test(title)) { + promises.push(at3.findLyrics(title.replace(/\(.*\)/g, '').replace(/\[.*\]/g, ''), artistName)); + } + + promises.push(reqWikia); + promises.push(reqParolesNet); + promises.push(reqLyricsMania1); + promises.push(reqLyricsMania2); + promises.push(reqLyricsMania3); + promises.push(reqSweetLyrics); + + return Promise.any(promises).then((lyrics) => { + return lyrics; + }); +}; + +/** + * Returns true if the query corresponds + * to an URL, else false + * @param query string + * @return boolean + */ +at3.isURL = (query) => { + return /^http(s?):\/\//.test(query); +}; + +/** + * Get a fresh access token from Spotify API + * @return {Promise} + */ +at3.spotifyToken = () => { + return request + .post({ + uri: 'https://accounts.spotify.com/api/token', + headers: { + Authorization: 'Basic ' + API_SPOTIFY, + }, + form: { + grant_type: 'client_credentials', + }, + json: true, + }) + .then((r) => { + return r.access_token; + }); +}; + +/** + * Perform a GET request to the Spotify API `url` endpoint + * @param {String} url URL of Spotify API endpoint to get + * @return {Promise} The request + */ +at3.requestSpotify = (url) => { + return at3.spotifyToken().then((token) => { + return request({ + uri: url, + json: true, + headers: { + Authorization: 'Bearer ' + token, + }, + }); + }); +}; + +/** + * Download a single video + * @param url + * @param outputFile + * @return Event + */ +at3.downloadWithYoutubeDl = (url, outputFile) => { + const download = ytdl(url, { quality: 'highestaudio' }); + download.pipe(fs.createWriteStream(outputFile)); + const downloadEmitter = new EventEmitter(); + let aborted = false; + + const onProgress = (_chunk, nbDownloaded, nbTotal) => { + const percent = ((nbDownloaded / nbTotal) * 100).toFixed(2); + downloadEmitter.emit('download-progress', { + progress: percent, + }); + }; + + download.on('progress', onProgress); + + download.once('end', () => { + if (aborted) { + return; + } + download.removeListener('progress', onProgress); + downloadEmitter.emit('download-end'); + }); + + download.once('error', (error) => { + download.removeListener('progress', onProgress); + downloadEmitter.emit('error', new Error(error)); + }); + + const abort = () => { + aborted = true; + download.abort(); + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + }; + + downloadEmitter.once('abort', abort); + + return downloadEmitter; +}; + +/** + * Convert a outputFile in MP3 + * @param inputFile + * @param outputFile + * @param bitrate string + * @return Event + */ +at3.convertInMP3 = (inputFile, outputFile, bitrate) => { + const convertEmitter = new EventEmitter(); + let aborted = false; + let started = false; + + let convert = ffmpeg(inputFile); + if (at3.FFMPEG_PATH) { + convert.setFfmpegPath(at3.FFMPEG_PATH); + } + if (at3.FFPROBE_PATH) { + convert.setFfprobePath(at3.FFPROBE_PATH); + } + + const onProgress = (progress) => { + convertEmitter.emit('convert-progress', { + progress: progress.percent, + }); + }; + + convert + .audioBitrate(bitrate) + .audioCodec('libmp3lame') + .once('codecData', (_data) => { + convertEmitter.emit('convert-start'); + }) + .on('progress', onProgress) + .once('end', () => { + convert.removeListener('progress', onProgress); + fs.unlinkSync(inputFile); + convertEmitter.emit('convert-end'); + }) + .once('error', (e) => { + convert.removeListener('progress', onProgress); + if (!aborted) { + convertEmitter.emit('error', e); + } else { + if (fs.existsSync(inputFile)) { + fs.unlink(inputFile, () => {}); + } + if (fs.existsSync(outputFile)) { + fs.unlink(outputFile, () => {}); + } + } + }) + .once('start', () => { + started = true; + if (aborted) { + abort(); + } + }) + .save(outputFile); + + const abort = () => { + aborted = true; + if (started) { + convert.kill(); + } + }; + + convertEmitter.once('abort', abort); + + return convertEmitter; +}; + +/** + * Get infos about an online video + * @param url + * @return Promise + */ +at3.getInfosWithYoutubeDl = (url) => { + return ytdl.getBasicInfo(url).then((infos) => { + const thumbnails = infos.player_response.videoDetails.thumbnail.thumbnails; + + return { + title: infos.videoDetails.title, + author: infos.videoDetails.author.name, + picture: thumbnails[thumbnails.length - 1].url, + }; + }); + + // youtubedl.getInfo(url, ['--no-check-certificate'], (err, infos) => { + // if (err || infos === undefined) { + // reject(); + // } else { + // resolve({ + // title: infos.title, + // author: infos.uploader, + // picture: infos.thumbnail, + // }); + // } + // }); + // }); +}; + +/** + * Download a single URL in MP3 + * @param url + * @param outputFile + * @param bitrate + * @return Event + */ +at3.downloadSingleURL = (url, outputFile, bitrate) => { + const progressEmitter = new EventEmitter(); + let tempFile = outputFile + '.video'; + let downloadEnded = false; + let convert; + + const dl = at3.downloadWithYoutubeDl(url, tempFile); + const onDlProgress = (infos) => { + progressEmitter.emit('download', { + progress: infos.progress, + }); + }; + + dl.once('download-start', () => { + progressEmitter.emit('start'); + }); + dl.on('download-progress', onDlProgress); + + dl.once('download-end', () => { + downloadEnded = true; + dl.removeListener('download-progress', onDlProgress); + progressEmitter.emit('download-end'); + + convert = at3.convertInMP3(tempFile, outputFile, bitrate); + const onConvertProgress = (infos) => { + progressEmitter.emit('convert', { + progress: infos.progress, + }); + }; + convert.on('convert-progress', onConvertProgress); + convert.once('convert-end', () => { + convert.removeListener('convert-progress', onConvertProgress); + progressEmitter.emit('end'); + }); + convert.once('error', (error) => { + progressEmitter.emit('error', error); + }); + }); + + dl.once('error', (error) => { + dl.removeListener('download-progress', onDlProgress); + progressEmitter.emit('error', new Error(error)); + }); + + progressEmitter.once('abort', () => { + if (!downloadEnded) { + dl.emit('abort'); + } else { + convert.emit('abort'); + } + }); + + return progressEmitter; +}; + +/** + * Try to find to title and artist from a string + * (example: a YouTube video title) + * @param query string + * @param exact boolean Can the query be modified or not + * @param last boolean Last call + * @param v boolean Verbose + * @return Promise + */ +at3.guessTrackFromString = (query, exact, last, v) => { + // [TODO] Replace exact by a level of strictness + // 0: no change at all + // 4: remove every thing useless + if (exact === undefined) { + exact = false; + } + if (last === undefined) { + last = false; + } + if (v === undefined) { + v = false; + } + + if (v) { + console.log('Query: ', query); + } + + let searchq = query; + if (!exact) { + searchq = searchq.replace(/\(.*\)/g, ''); + searchq = searchq.replace(/\[.*\]/g, ''); + searchq = searchq.replace(/lyric(s?)|parole(s?)/gi, ''); + searchq = searchq.replace(/^'/, ''); + searchq = searchq.replace(/ '/g, ' '); + searchq = searchq.replace(/' /g, ' '); + searchq = searchq.replace(/Original Motion Picture Soundtrack/i, ''); + searchq = searchq.replace(/bande originale/i, ''); + } + + const requests = []; + const infos = { + title: null, + artistName: null, + }; + + // We search on Deezer and iTunes + // [TODO] Adding Spotify + + // Deezer + const requestDeezer = request({ + url: 'https://api.deezer.com/2.0/search?q=' + encodeURIComponent(searchq), + json: true, + }).then((body) => { + let title, artistName, tempTitle; + _.forEach(body.data, (s) => { + if (!title) { + if (vsimpleName(searchq, exact).replace(new RegExp(vsimpleName(s.artist.name), 'ig'))) { + if ( + delArtist(s.artist.name, searchq, exact).match(new RegExp(vsimpleName(s.title_short), 'ig')) || + vsimpleName(s.title_short).match(new RegExp(delArtist(s.artist.name, searchq, exact), 'ig')) + ) { + artistName = s.artist.name; + title = s.title; + } else if (!artistName) { + artistName = s.artist.name; + tempTitle = s.title; + } + } + } + }); + if (title && artistName) { + infos.title = title; + infos.artistName = artistName; + } + if (v) { + console.log('Deezer answer: ', title, '-', artistName); + } + }); + + // iTunes + const requestiTunes = request({ + url: 'https://itunes.apple.com/search?media=music&term=' + encodeURIComponent(searchq), + json: true, + }).then((body) => { + let title, artistName, tempTitle; + _.forEach(body.results, (s) => { + if (!title) { + if (vsimpleName(searchq, exact).match(new RegExp(vsimpleName(s.artistName), 'gi'))) { + if (delArtist(s.artistName, searchq, exact).match(new RegExp(vsimpleName(s.trackCensoredName), 'gi'))) { + artistName = s.artistName; + title = s.trackCensoredName; + } else if (delArtist(s.artistName, searchq, exact).match(new RegExp(vsimpleName(s.trackName), 'gi'))) { + artistName = s.artistName; + title = s.trackName; + } else if (!artistName) { + artistName = s.artistName; + temp_title = s.trackName; + } + } + } + }); + if (title && artistName) { + infos.title = title; + infos.artistName = artistName; + } + if (v) { + console.log('iTunes answer: ', title, '-', artistName); + } + }); + + requests.push(requestDeezer); + requests.push(requestiTunes); + + return Promise.all(requests).then(() => { + if (!last && (!infos.title || !infos.artistName)) { + searchq = searchq.replace(/f(ea)?t(\.)? [^-]+/gi, ' '); + return at3.guessTrackFromString(searchq, false, true, v); + } + return infos; + }); +}; + +/** + * Try to guess title and artist from mp3 file + * @param file + * @return Promise + */ +at3.guessTrackFromFile = (file) => { + return new Promise((resolve, _reject) => { + acoustid(file, { key: API_ACOUSTID, fpcalc: { command: at3.FPCALC_PATH } }, (err, results) => { + if ( + err || + results.length === 0 || + !results[0].recordings || + results[0].recordings.length === 0 || + !results[0].recordings[0].artists || + results[0].recordings[0].artists.length === 0 + ) { + resolve({}); + return; + } + resolve({ + title: results[0].recordings[0].title, + artistName: results[0].recordings[0].artists[0].name, + }); + }); + }); +}; + +/** + * Retrieve informations about a track from artist and title + * @param title + * @param artistName + * @param exact boolean Exact search or not + * @param v boolean Verbose + * @return Promise + */ +at3.retrieveTrackInformations = (title, artistName, exact, v) => { + if (exact === undefined) { + exact = false; + } + if (v === undefined) { + v = false; + } + + if (!exact) { + title = title.replace(/((\[)|(\())?radio edit((\])|(\)))?/gi, ''); + } + + const infos = { + title: title, + artistName: artistName, + }; + + const requests = []; + + const requestDeezer = request({ + url: 'https://api.deezer.com/2.0/search?q=' + encodeURIComponent(artistName + ' ' + title), + json: true, + }).then((body) => { + let deezerInfos; + _.forEach(body.data, (s) => { + if ( + !infos.deezerId && + imatch(vsimpleName(title), vsimpleName(s.title)) && + imatch(vsimpleName(artistName), vsimpleName(s.artist.name)) + ) { + infos.deezerId = s.id; + deezerInfos = _.clone(s); + } + }); + if (infos.deezerId) { + infos.artistName = deezerInfos.artist.name; + infos.title = deezerInfos.title; + + return at3 + .getDeezerTrackInfos(infos.deezerId, v) + .then((deezerInfos) => { + infos = deezerInfos; + }) + .catch(() => {}); + } + }); + + const requestiTunes = request({ + url: 'https://itunes.apple.com/search?media=music&term=' + encodeURIComponent(artistName + ' ' + title), + json: true, + }).then((body) => { + let itunesInfos; + _.forEach(body.results, (s) => { + if ( + !infos.itunesId && + (imatch(vsimpleName(title), vsimpleName(s.trackName)) || + imatch(vsimpleName(title), vsimpleName(s.trackCensoredName))) && + imatch(vsimpleName(artistName), vsimpleName(s.artistName)) + ) { + infos.itunesId = s.trackId; + itunesInfos = _.clone(s); + } + }); + if (!infos.deezerId && itunesInfos) { + infos.artistName = itunesInfos.artistName; + if (imatch(vsimpleName(infos.title), vsimpleName(itunesInfos.trackName))) { + infos.title = itunesInfos.trackName; + } else { + infos.title = itunesInfos.trackCensoredName; + } + infos.itunesAlbum = itunesInfos.collectionId; + infos.position = itunesInfos.trackNumber; + infos.nbTracks = itunesInfos.trackCount; + infos.album = itunesInfos.collectionName; + infos.releaseDate = itunesInfos.releaseDate.replace(/T.+/, ''); + infos.cover = itunesInfos.artworkUrl100.replace('100x100', '200x200'); + infos.genre = itunesInfos.primaryGenreName; + infos.discNumber = itunesInfos.discNumber; + infos.duration = itunesInfos.trackTimeMillis / 1000; + } + + if (v) { + console.log('iTunes infos: ', itunesInfos); + } + }); + + requests.push(requestDeezer); + requests.push(requestiTunes); + + return Promise.all(requests).then(() => infos); +}; + +/** + * Retrieve detailed infos about a Deezer Track + * @param trackId + * @param v boolean Verbosity + * @return Promise(trackInfos) + */ +at3.getDeezerTrackInfos = (trackId, v) => { + const infos = { + deezerId: trackId, + }; + + return request({ + url: 'https://api.deezer.com/2.0/track/' + infos.deezerId, + json: true, + }) + .then((trackInfos) => { + if (trackInfos.error) { + return Promise.reject(); + } + + infos.title = trackInfos.title; + infos.artistName = trackInfos.artist.name; + infos.position = trackInfos.track_position; + infos.duration = trackInfos.duration; + infos.deezerAlbum = trackInfos.album.id; + infos.discNumber = trackInfos.disk_number; + + return request({ + url: 'https://api.deezer.com/2.0/album/' + infos.deezerAlbum, + json: true, + }); + }) + .then((albumInfos) => { + infos.album = albumInfos.title; + infos.releaseDate = albumInfos.release_date; + infos.nbTracks = albumInfos.tracks.data.length; + infos.genreId = albumInfos.genre_id; + infos.cover = albumInfos.cover_big; + + return request({ + url: 'https://api.deezer.com/2.0/genre/' + infos.genreId, + json: true, + }); + }) + .then((genreInfos) => { + infos.genre = genreInfos.name; + + if (v) { + console.log('Deezer infos: ', infos); + } + + return infos; + }); +}; + +/** + * Get complete information (title, artist, release date, genre, album name...) + * for a Spotify track + * @param {trackId} string The Spotify track id + * @param {v} boolean The verbosity + * @return Promise + */ +at3.getSpotifyTrackInfos = (trackId, v) => { + const infos = { + spotifyId: trackId, + }; + + return at3 + .requestSpotify('https://api.spotify.com/v1/tracks/' + trackId) + .then((trackInfos) => { + infos.title = trackInfos.name; + infos.artistName = trackInfos.artists[0].name; + infos.duration = Math.ceil(trackInfos.duration_ms / 1000); + infos.position = trackInfos.track_number; + infos.discNumber = trackInfos.disc_number; + infos.spotifyAlbum = trackInfos.album.id; + + return at3.requestSpotify('https://api.spotify.com/v1/albums/' + trackInfos.album.id); + }) + .then((albumInfos) => { + infos.album = albumInfos.name; + infos.cover = albumInfos.images[0].url; + infos.genre = albumInfos.genres[0] || ''; + infos.nbTracks = albumInfos.tracks.total; + infos.releaseDate = albumInfos.release_date; + + return infos; + }); +}; + +/** + * Add tags to MP3 file + * @param file + * @param infos + * @return Promise + */ +at3.tagFile = (file, infos) => { + const meta = { + title: infos.title, + artist: infos.artistName, + }; + + if (infos.album) { + meta.album = infos.album; + } + if (infos.position) { + meta.track = infos.position; + } + if (infos.nbTracks) { + meta.trackTotal = infos.nbTracks; + } + if (infos.discNumber) { + meta.disc = infos.discNumber; + } + if (infos.lyrics) { + meta.lyrics = infos.lyrics; + } + if (infos.releaseDate) { + meta.year = /[0-9]{4}/.exec(infos.releaseDate)[0]; + } + if (infos.genre) { + meta.genre = infos.genre.replace(/\/.+/g, ''); + } + + return new Promise((resolve, reject) => { + eyed3.updateMeta(file, eyed3.metaHook(meta), (err) => { + if (err) { + return reject(err); + } + if (infos.cover) { + let coverPath = file + '.cover.jpg'; + + requestNoPromise(infos.cover, () => { + // Check that the cover is a square + const coverFile = sharp(coverPath); + coverFile + .metadata() + .then((metadata) => { + if (metadata.width != metadata.height) { + // In that case we will crop the cover to get a square + const tempCoverPath = file + '.cover.resized.jpg'; + return smartcrop + .crop(coverPath, { width: 100, height: 100 }) + .then((result) => { + let crop = result.topCrop; + return coverFile + .extract({ + width: crop.width, + height: crop.height, + left: crop.x, + top: crop.y, + }) + .toFile(tempCoverPath); + }) + .then(() => { + fs.renameSync(tempCoverPath, coverPath); + }); + } + }) + .then(() => { + eyed3.updateMeta(file, eyed3.metaHook({ image: coverPath }), (err) => { + fs.unlinkSync(coverPath); + + if (err) { + return reject(err); + } + + resolve(); + }); + }); + }).pipe(fs.createWriteStream(coverPath)); + } else { + resolve(); + } + }); + }); +}; + +/** + * Search and return complete information about a single video url + * @param url + * @param v boolean Verbosity + * @return Promise(object) + */ +at3.getCompleteInfosFromURL = (url, v) => { + let infosFromString; + // Try to find information based on video title + return at3 + .getInfosWithYoutubeDl(url) + .then((videoInfos) => { + infosFromString = { + title: videoInfos.title, + artistName: videoInfos.author, + cover: videoInfos.picture.replace('hqdefault', 'mqdefault'), // [TODO]: getting a better resolution and removing the black borders + originalTitle: videoInfos.title, + }; + + if (v) { + console.log('Video infos: ', infosFromString); + } + + // progressEmitter.emit('infos', _.clone(infosFromString)); + + return at3.guessTrackFromString(videoInfos.title, false, false, v); + }) + .then((guessStringInfos) => { + if (guessStringInfos.title && guessStringInfos.artistName) { + return at3.retrieveTrackInformations(guessStringInfos.title, guessStringInfos.artistName, false, v); + } else { + return Promise.resolve(); + } + }) + .then((guessStringInfos) => { + if (guessStringInfos) { + guessStringInfos.originalTitle = infosFromString.originalTitle; + infosFromString = guessStringInfos; + // progressEmitter.emit('infos', _.clone(infosFromString)); + if (v) { + console.log('guessStringInfos: ', guessStringInfos); + } + } else { + if (v) { + console.log('Cannot retrieve detailed information from video title'); + } + } + + return infosFromString; + }) + .then((guessStringInfos) => { + if (guessStringInfos.deezerId) { + return at3.getDeezerTrackInfos(guessStringInfos.deezerId, v); + } else if (guessStringInfos.spotifyId) { + return at3.getSpotifyTrackInfos(guessStringInfos.spotifyId, v); + } else { + return guessStringInfos; + } + }) + .catch((_error) => { + // The download must have failed to, and emit an error + }); +}; + +/** + * Identify the song from a file and then search complete information about it + * @param file string + * @param v boolean Verbosity + * @return Promise(object) + */ +at3.getCompleteInfosFromFile = (file, v) => { + return at3 + .guessTrackFromFile(file) + .then((guessFileInfos) => { + if (guessFileInfos.title && guessFileInfos.artistName) { + return at3.retrieveTrackInformations(guessFileInfos.title, guessFileInfos.artistName, false, v); + } else { + return Promise.resolve(); + } + }) + .then((guessFileInfos) => { + if (guessFileInfos) { + if (v) { + console.log('guessFileInfos: ', guessFileInfos); + } + return guessFileInfos; + } else { + if (v) { + console.log('Cannot retrieve detailed information from MP3 file'); + } + } + }); +}; + +/** + * Simplify a string so it works well as a filename + * @param {String} string + * @return {String} + */ +at3.escapeForFilename = (string) => { + return _.startCase(_.toLower(_.deburr(string))) + .replace(/^\.+/, '') + .replace(/\.+$/, ''); +}; + +/** + * Return a correctly formatted filename for a song. + * Example: "02 - On Top Of The World" + * @param title string Title of the song + * @param artist string Artist + * @param position int Position on the disk + * @return string + */ +at3.formatSongFilename = (title, artist, position) => { + let filename = at3.escapeForFilename(artist) + ' - '; + if (position) { + if (position < 10) { + filename += '0'; + } + filename += position + ' - '; + } + + filename += at3.escapeForFilename(title); + + return filename; +}; + +/** + * Create necessary folders for a subpath + * @param baseFolder {string} The path of the outputfolder + * @param subPathFormat {string} The subPath format: {artist}/{title}/ + * @param title {string} Title + * @param artist {string} Artist + * @return {String} The complete path + */ +at3.createSubPath = (baseFolder, subPathFormat, title, artist) => { + subPathFormat = subPathFormat.replace(/\{artist\}/g, at3.escapeForFilename(artist)); + subPathFormat = subPathFormat.replace(/\{title\}/g, at3.escapeForFilename(title)); + + let p = path.join(baseFolder, subPathFormat); + if (p.charAt(p.length - 1) != path.sep) { + p += path.sep; + } + + const folders = subPathFormat.split(path.sep); + let currentFolder = baseFolder; + folders.forEach((f) => { + currentFolder = path.join(currentFolder, f); + if (!fs.existsSync(currentFolder)) { + fs.mkdirSync(currentFolder); + } + }); + + return p; +}; + +/** + * Download and convert a single URL, + * retrieve and add tags to the MP3 file + * @param url + * @param outputFolder + * @param callback Callback function + * @param title string Optional requested title + * @param infos object Basic infos to tag the file + * @param v boolean Verbosity + * @param options object { bitrate: '256k' } output audio bitrate + * @return Event + */ +at3.downloadAndTagSingleURL = (url, outputFolder, callback, title, v, infos, options = {}) => { + if (v === undefined) { + v = false; + } + if (callback === undefined) { + callback = () => {}; + } + if (outputFolder.charAt(outputFolder.length - 1) !== path.sep) { + outputFolder += path.sep; + } + title = title || ''; + const bitrate = options.bitrate || '256k'; + + const progressEmitter = new EventEmitter(); + + const tempFile = (at3.tempFolder || outputFolder) + randomstring.generate(10) + '.mp3'; + + // Download and convert file + const dl = at3.downloadSingleURL(url, tempFile, bitrate); + const onDownload = (infos) => { + progressEmitter.emit('download', infos); + }; + const onConvert = (infos) => { + progressEmitter.emit('convert', infos, infos); + }; + dl.on('download', onDownload); + dl.once('download-end', () => { + dl.removeListener('download', onDownload); + progressEmitter.emit('download-end'); + }); + dl.on('convert', onConvert); + dl.once('error', (error) => { + dl.removeListener('download', onDownload); + dl.removeListener('convert', onConvert); + callback(null, 'error'); + progressEmitter.emit('error', new Error(error)); + }); + progressEmitter.once('abort', () => { + dl.emit('abort'); + }); + + let infosFromString, + infosFromFile, + infosRequests = []; + + if (infos && infos.deezerId) { + // If deezer track id is provided, with fetch more information + let getMoreInfos = at3 + .getDeezerTrackInfos(infos.deezerId, v) + .then((inf) => { + infosFromString = inf; + }) + .catch(() => { + infosFromString = { + title: infos.title, + artistName: infos.artistName, + }; + }); + + infosRequests.push(getMoreInfos); + } else if (infos && infos.spotifyId) { + // If spotify track id is provided, with fetch more information + let getMoreInfos = at3 + .getSpotifyTrackInfos(infos.spotifyId, v) + .then((inf) => { + infosFromString = inf; + }) + .catch(() => { + infosFromString = { + title: infos.title, + artistName: infos.artistName, + }; + }); + infosRequests.push(getMoreInfos); + } else { + // Try to find information based on video title + let getStringInfos = at3 + .getCompleteInfosFromURL(url, v) + .then((inf) => { + if (title === undefined) { + title = inf.originalTitle; + } + infosFromString = inf; + progressEmitter.emit('infos', _.clone(infosFromString)); + }) + .catch(() => { + // The download must have failed to, and emit an error + }); + + infosRequests.push(getStringInfos); + } + + // Try to find information based on MP3 file when dl is finished + dl.once('end', () => { + dl.removeListener('convert', onConvert); + progressEmitter.emit('convert-end'); + + if (!infos || (!infos.deezerId && !infos.spotifyId)) { + let getFileInfos = at3.getCompleteInfosFromFile(tempFile, v).then((inf) => { + infosFromFile = inf; + if (infosFromFile && infosFromFile.title && infosFromFile.artistName) { + progressEmitter.emit('infos', _.clone(infosFromFile)); + } + }); + + infosRequests.push(getFileInfos); + } + + // [TODO] Improve network issue resistance + Promise.all(infosRequests).then(() => { + // ça on peut garder + let infos = infosFromString; + if (infosFromFile) { + let scoreFromFile = Math.min( + levenshtein.get(simpleName(infosFromFile.title + ' ' + infosFromFile.artistName), simpleName(title)), + levenshtein.get(simpleName(infosFromFile.artistName + ' ' + infosFromFile.title), simpleName(title)), + ); + let scoreFromString = Math.min( + levenshtein.get(simpleName(infosFromString.title + ' ' + infosFromString.artistName), simpleName(title)), + levenshtein.get(simpleName(infosFromString.artistName + ' ' + infosFromString.title), simpleName(title)), + ); + + if (v) { + console.log('Infos from file score: ', scoreFromFile); + console.log('Infos from string score: ', scoreFromString); + } + + if (infosFromFile.cover && scoreFromFile < scoreFromString + Math.ceil(simpleName(title).length / 10.0)) { + infos = infosFromFile; + } + } + + progressEmitter.emit('infos', _.clone(infos)); + + if (v) { + console.log('Final infos: ', infos); + } + + at3 + .findLyrics(infos.title, infos.artistName) + .then((lyrics) => { + return new Promise((resolve, reject) => { + fs.writeFile(tempFile + '.lyrics', lyrics, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + }) + .then(() => { + infos.lyrics = tempFile + '.lyrics'; + }) + .catch(() => { + // no lyrics + }) + .finally(() => { + return at3.tagFile(tempFile, infos); + }) + .then(() => { + let finalFile = outputFolder; + finalFile += at3.formatSongFilename(infos.title, infos.artistName, infos.position) + '.mp3'; + fs.moveSync(tempFile, finalFile, { overwrite: true }); + if (infos.lyrics) { + fs.unlinkSync(tempFile + '.lyrics'); + } + const finalInfos = { + infos: infos, + file: finalFile, + }; + progressEmitter.emit('end', finalInfos); + callback(finalInfos); + }) + .catch((err) => { + progressEmitter.emit('error', err); + }); + }); + }); + + return progressEmitter; +}; + +/** + * Search a query on YouTube and return the detailed results + * @param query string + * @param regionCode string ISO 3166-1 alpha-2 country code (ex: FR, US) + * @param relevanceLanguage string ISO 639-1 two-letter language code (ex: en: fr) + * @param v boolean Verbosity + * @return Promise + */ +at3.searchOnYoutube = (query, regionCode, relevanceLanguage, v) => { + if (v === undefined) { + v = false; + } + + /** + * Remove useless information in the title + * like (audio only), (lyrics)... + * @param title string + * @return string + */ + const improveTitle = (title) => { + let useless = [ + 'audio only', + 'audio', + 'paroles/lyrics', + 'lyrics/paroles', + 'with lyrics', + 'w/lyrics', + 'w / lyrics', + 'avec paroles', + 'avec les paroles', + 'avec parole', + 'lyrics', + 'paroles', + 'parole', + 'radio edit.', + 'radio edit', + 'radio-edit', + 'shazam version', + 'shazam v...', + 'music video', + 'clip officiel', + 'officiel', + 'new song', + 'official video', + 'official', + ]; + + _.forEach(useless, (u) => { + title = title.replace(new RegExp('((\\(|\\[)?)( ?)' + u + '( ?)((\\)|\\])?)', 'gi'), ''); + }); + + title = title.replace(new RegExp('(\\(|\\[)( ?)hd( ?)(\\)|\\])', 'gi'), ''); + title = title.replace(new RegExp('hd', 'gi'), ''); + title = _.trim(title); + + return title; + }; + + // We simply search on YouTube + return ytsr(query, { limit: 20 }).then(({ items }) => { + const videos = items.filter((item) => item.type === 'video'); + + if (videos.length === 0) { + return Promise.reject(); + } + + return Promise.all( + videos.map(async (video) => { + const infos = await ytdl.getInfo(video.link); + + let ratio = 1.0; + if (infos.dislikes > 0) { + ratio = infos.likes / infos.dislikes; + } + if (ratio === 0) { + ratio = 1; + } + const realLike = (infos.likes - infos.dislikes) * ratio; + + return { + id: infos.video_id, + url: video.link, + title: improveTitle(infos.title), + hd: infos.formats.some( + ({ qualityLabel }) => qualityLabel && (qualityLabel.startsWith('720p') || qualityLabel.startsWith('1080p')), + ), + duration: parseInt(infos.length_seconds, 10), + views: video.views, + realLike, + }; + }), + ); + }); +}; + +/** + * @param song Object Searched song + * @param videos Array List of videos + * @param v boolean Verbosity + */ +at3.findBestVideo = (song, videos, v) => { + if (v === undefined) { + v = false; + } + + /** + * Returns the score of a video, comparing to the request + * @param song Object Searched song + * @param video object + * @param largestRealLike + * @param largestViews + * @return Object + */ + const score = (song, video, largestRealLike, largestViews) => { + // weight of each argument + let weights = { + title: 30, + hd: 0.3, + duration: 20, + views: 10, + realLike: 15, + }; + + let duration = song.duration || video.duration; + + // Score for title + let videoTitle = ' ' + _.lowerCase(video.title) + ' '; + let songTitle = ' ' + _.lowerCase(song.title) + ' '; // we add spaces to help longest-common-substring + let songArtist = ' ' + _.lowerCase(song.artistName) + ' '; // (example: the artist "M") + + // for longest-common-substring, which works with arrays + let videoTitlea = videoTitle.split(''); + let songTitlea = songTitle.split(''); + let songArtista = songArtist.split(''); + + const videoSongTitle = lcs(videoTitlea, songTitlea); + if ( + videoSongTitle.length > 0 && + videoSongTitle.startString2 === 0 && + videoTitle[videoSongTitle.startString1 + videoSongTitle.length - 1] === ' ' + ) { + // The substring must start at the beginning of the song title, and the next char in the video title must be a space + videoTitle = + videoTitle.substring(0, videoSongTitle.startString1) + + ' ' + + videoTitle.substring(videoSongTitle.startString1 + videoSongTitle.length); + videoTitlea = videoTitle.split(''); + } + const videoSongArtist = lcs(videoTitlea, songArtista); + if ( + videoSongArtist.length > 0 && + videoSongArtist.startString2 === 0 && + videoTitle[videoSongArtist.startString1 + videoSongArtist.length - 1] === ' ' + ) { + // The substring must start at the beginning of the song title, and the next char in the video title must be a space + videoTitle = + videoTitle.substring(0, videoSongArtist.startString1) + + videoTitle.substring(videoSongArtist.startString1 + videoSongArtist.length); + } + + videoTitle = _.lowerCase(videoTitle); + const sTitle = + videoTitle.length + (songTitle.length - videoSongTitle.length) + (songArtist.length - videoSongArtist.length); + + const videoScore = { + title: sTitle * weights.title, + hd: video.hd * weights.hd, + duration: Math.sqrt(Math.abs(video.duration - duration)) * weights.duration, + views: (video.views / largestViews) * weights.views, + realLike: (video.realLike / largestRealLike) * weights.realLike || -50, // video.realLike is NaN when the likes has been deactivated, which is a very bad sign + }; + video.videoScore = videoScore; + + let preVideoScore = videoScore.views + videoScore.realLike - videoScore.title - videoScore.duration; + preVideoScore = preVideoScore + Math.abs(preVideoScore) * videoScore.hd; + + return preVideoScore; + }; + + const largestRealLike = _.reduce( + videos, + (v, r) => { + if (r.realLike > v) { + return r.realLike; + } + return v; + }, + 0, + ); + const largestViews = _.reduce( + videos, + (v, r) => { + if (r.views > v) { + return r.views; + } + return v; + }, + 0, + ); + + _.forEach(videos, (r) => { + r.score = score(song, r, largestRealLike, largestViews); + }); + + return _.reverse(_.sortBy(videos, 'score')); +}; + +/** + * Try to find the best video matching a song + * @param song Object Searched song + * @param v boolean Verbosity + * @return Promise + */ +at3.findVideoForSong = (song, v) => { + if (v === undefined) { + v = false; + } + + let query = song.title + ' - ' + song.artistName; + return at3.searchOnYoutube(query, at3.regionCode, at3.relevanceLanguage, v).then((youtubeResults) => { + return at3.findBestVideo(song, youtubeResults, v); + }); +}; + +// [TODO] we could also add a method that just take the first youtube video and download it +/** + * Try to find the best video matching a song request + * @param query string + * @param v boolean Verbosity + * @return Promise + */ +at3.findVideo = (query, v) => { + if (v === undefined) { + v = false; + } + + // We try to find the song + return at3 + .guessTrackFromString(query, true, false, v) + .then((guessStringInfos) => { + if (guessStringInfos.title && guessStringInfos.artistName) { + return at3.retrieveTrackInformations(guessStringInfos.title, guessStringInfos.artistName, true, v); + } else { + return Promise.reject({ error: 'No song corresponds to your query' }); + } + }) + .then((song) => { + return at3.findVideoForSong(song, v); + }); +}; + +/** + * Find a song from a query, then download the corresponding video, + * convert and tag it + * @param query string + * @param outputFolder + * @param callback Callback function + * @param v boolean Verbosity + * @return Event + */ +at3.findAndDownload = (query, outputFolder, callback, v) => { + if (v === undefined) { + v = false; + } + const progressEmitter = new EventEmitter(); + + at3 + .findVideo(query, v) + .then((results) => { + if (results.length === 0) { + progressEmitter.emit('error', new Error('Cannot find any video matching')); + return callback(null, 'Cannot find any video matching'); + } + let i = 0; + progressEmitter.emit('search-end'); + let dl = at3.downloadAndTagSingleURL(results[i].url, outputFolder, callback, query); + + const onDownload = (infos) => { + progressEmitter.emit('download', infos); + }; + const onConvert = (infos) => { + progressEmitter.emit('convert', infos); + }; + const onInfos = (infos) => { + progressEmitter.emit('infos', infos); + }; + + dl.on('download', onDownload); + dl.once('download-end', () => { + dl.removeListener('download', onDownload); + progressEmitter.emit('download-end'); + }); + dl.on('convert', onConvert); + dl.once('convert-end', () => { + dl.removeListener('convert', onConvert); + progressEmitter.emit('convert-end'); + }); + dl.on('infos', onInfos); + dl.once('error', (error) => { + dl.removeListener('download', onDownload); + dl.removeListener('convert', onConvert); + dl.removeListener('infos', onInfos); + // [TODO]: try to download the next video, in case of ytdl error only + // if (i < results.length) { + // dl = at3.downloadAndTagSingleURL(results[i++].url, outputFolder, callback, query); + // } else { + progressEmitter.emit('error', new Error(error)); + // } + }); + dl.once('end', () => { + dl.removeListener('infos', onInfos); + }); + }) + .catch(() => { + progressEmitter.emit('error', new Error('Cannot find any video matching')); + return callback(null, 'Cannot find any video matching'); + }); + + return progressEmitter; +}; + +/** + * Find videos for a track, and download it + * @param track trackInfos + * @param outputFolder + * @param callback Callback function + * @param v boolean Verbosity + * @return Event + */ +at3.downloadTrack = (track, outputFolder, callback, v) => { + if (v === undefined) { + v = false; + } + const progressEmitter = new EventEmitter(); + let aborted = false; + + at3 + .findVideoForSong(track, v) + .then((results) => { + if (aborted) { + return; + } + if (results.length === 0) { + progressEmitter.emit('error', new Error('Cannot find any video matching')); + return callback(null, 'Cannot find any video matching'); + } + let i = 0; + progressEmitter.emit('search-end'); + const dlNext = () => { + if (i >= results.length) { + progressEmitter.emit('error', new Error('Cannot find any video matching')); + return; + } + if (v) { + console.log('Will be downloaded:', results[i].url); + } + let aborted = false; + let dl = at3.downloadAndTagSingleURL(results[i].url, outputFolder, callback, '', v, track); + const onDownload = (infos) => { + progressEmitter.emit('download', infos); + }; + const onConvert = (infos) => { + progressEmitter.emit('convert', infos); + }; + const onInfos = (infos) => { + progressEmitter.emit('infos', infos); + }; + dl.on('download', onDownload); + dl.once('download-end', () => { + dl.removeListener('download', onDownload); + progressEmitter.emit('download-end'); + }); + dl.on('convert', onConvert); + dl.once('convert-end', () => { + dl.removeListener('convert', onConvert); + progressEmitter.emit('convert-end'); + }); + dl.on('infos', onInfos); + dl.once('end', (finalInfos) => { + dl.removeListener('infos', onInfos); + progressEmitter.emit('end', finalInfos); + }); + dl.once('error', (_error) => { + dl.removeListener('download', onDownload); + dl.removeListener('convert', onConvert); + dl.removeListener('infos', onInfos); + i += 1; + aborted = true; + dlNext(); + }); + progressEmitter.once('abort', () => { + if (!aborted) { + dl.emit('abort'); + } + }); + }; + dlNext(); + }) + .catch(() => { + progressEmitter.emit('error', new Error('Cannot find any video matching')); + return callback(null, 'Cannot find any video matching'); + }); + + progressEmitter.on('abort', () => { + aborted = true; + }); + + return progressEmitter; +}; + +/** + * Return URLs contained in a playlist (YouTube or SoundCloud) + * @param url + * @return Promise(object) + */ +at3.getPlaylistURLsInfos = (url) => { + let type = at3.guessURLType(url); + + if (type === 'youtube') { + let playlistId = url.match(/list=([0-9a-zA-Z_-]+)/); + playlistId = playlistId[1]; + return ytpl(playlistId).then((playlist) => { + return { + title: playlist.title, + cover: playlist.author.avatar, + artistName: playlist.author.name, + items: playlist.items.map((item) => { + return { + url: item.url_simple, + title: item.title, + cover: item.thumbnail, + }; + }), + }; + }); + } else if (type === 'soundcloud') { + return request({ + url: 'http://api.soundcloud.com/resolve?client_id=' + API_SOUNDCLOUD + '&url=' + url, + json: true, + }).then((playlistDetails) => { + let playlistInfos = { + title: playlistDetails.title, + artistName: playlistDetails.user.username, + cover: playlistDetails.artwork_url, + }; + let items = []; + + _.forEach(playlistDetails.tracks, (track) => { + items.push({ + url: track.permalink_url, + title: track.title, + cover: track.artwork_url, + artistName: track.user.username, + }); + }); + + playlistInfos.items = items; + + return playlistInfos; + }); + } +}; + +/** + * Returns info (title, cover, songs) about a playlist (Deezer or Spotify) + * @param url + * @return Promise(object) + */ +at3.getPlaylistTitlesInfos = (url) => { + // Deezer Playlist + // Deezer Album + // Deezer Loved Tracks [TODO] + // Spotify playlist + // Spotify Album + const type = at3.guessURLType(url); + + const regDeezerPlaylist = /playlist\/([0-9]+)/; + const regDeezerAlbum = /album\/([0-9]+)/; + + const regSpotifyPlaylist = /playlist\/([0-9a-zA-Z]+)/; + const regSpotifyAlbum = /album\/([0-9a-zA-Z]+)/; + + if (type === 'deezer') { + // Deezer Playlist + if (regDeezerPlaylist.test(url)) { + const playlistId = url.match(regDeezerPlaylist)[1]; + + return request({ + url: 'https://api.deezer.com/playlist/' + playlistId, + json: true, + }).then((playlistDetails) => { + const playlist = {}; + const items = []; + + playlist.title = playlistDetails.title; + playlist.artistName = playlistDetails.creator.name; + playlist.cover = playlistDetails.picture_big; + + _.forEach(playlistDetails.tracks.data, (track) => { + items.push({ + title: track.title, + artistName: track.artist.name, + deezerId: track.id, + album: track.album.title, + cover: track.album.cover, + }); + }); + + playlist.items = items; + + return playlist; + }); + } else if (regDeezerAlbum.test(url)) { + // Deezer Album + let albumId = url.match(regDeezerAlbum)[1]; + let albumInfos = {}; + + return request({ + url: 'https://api.deezer.com/album/' + albumId, + json: true, + }) + .then((ralbumInfos) => { + albumInfos.cover = ralbumInfos.cover_big; + albumInfos.title = ralbumInfos.title; + albumInfos.artistName = ralbumInfos.artist.name; + + return request({ + url: 'https://api.deezer.com/album/' + albumId + '/tracks', + json: true, + }); + }) + .then((albumTracks) => { + let items = []; + + _.forEach(albumTracks.data, (track) => { + items.push({ + title: track.title, + artistName: track.artist.name, + deezerId: track.id, + album: albumInfos.title, + cover: albumInfos.cover, + duration: track.duration, + }); + }); + + albumInfos.items = items; + + return albumInfos; + }); + } + } else if (type === 'spotify') { + // Spotify Playlist + if (regSpotifyPlaylist.test(url)) { + const playlistId = url.match(regSpotifyPlaylist)[1]; + + return at3.requestSpotify('https://api.spotify.com/v1/playlists/' + playlistId).then((playlistDetails) => { + const playlist = {}; + const items = []; + + playlist.title = playlistDetails.name; + playlist.artistName = playlistDetails.owner.id; + playlist.cover = playlistDetails.images[0].url; + + playlist.items = items; + + const processSpotifyPage = (page) => { + page.items.forEach((t) => { + let track = t.track; + items.push({ + title: track.name, + artistName: track.artists[0].name, + spotifyId: track.id, + album: track.album.name, + cover: track.album.images[0] ? track.album.images[0].url : undefined, + duration: Math.ceil(track.duration_ms / 1000), + }); + }); + + if (page.next) { + return at3.requestSpotify(page.next).then(processSpotifyPage); + } else { + return playlist; + } + }; + + return processSpotifyPage(playlistDetails.tracks); + }); + } else if (regSpotifyAlbum.test(url)) { + // Spotify Album + let albumId = url.match(regSpotifyAlbum)[1]; + let albumInfos = {}; + + return at3.requestSpotify('https://api.spotify.com/v1/albums/' + albumId).then((ralbumInfos) => { + albumInfos.title = ralbumInfos.name; + albumInfos.artistName = ralbumInfos.artists[0].name; + albumInfos.cover = ralbumInfos.images[0].url; + + let items = []; + + ralbumInfos.tracks.items.forEach((track) => { + items.push({ + title: track.name, + artistName: track.artists[0].name, + spotifyId: track.id, + album: albumInfos.title, + cover: albumInfos.cover, + duration: Math.ceil(track.duration_ms / 1000), + }); + }); + + albumInfos.items = items; + + return albumInfos; + }); + } + } +}; + +/** + * Download a playlist containing URLs + * @param url {string} + * @param outputFolder {string} + * @param callback {Function} + * @param maxSimultaneous {number} Maximum number of simultaneous track processing + * @param subPathFormat {string} The format of the subfolder: {artist}/{title}/ + * @return {Event} + */ +at3.downloadPlaylistWithURLs = (url, outputFolder, callback, maxSimultaneous, subPathFormat) => { + if (maxSimultaneous === undefined) { + maxSimultaneous = 1; + } + if (subPathFormat === undefined) { + subPathFormat = ''; + } + + const emitter = new EventEmitter(); + let running = 0; + let lastIndex = 0; + let aborted = false; + + at3.getPlaylistURLsInfos(url).then((playlistInfos) => { + if (aborted) { + return; + } + + outputFolder = at3.createSubPath(outputFolder, subPathFormat, playlistInfos.title, playlistInfos.artistName); + + emitter.emit('playlist-infos', playlistInfos); + + for (let i = 0; i < maxSimultaneous; i += 1) { + downloadNext(playlistInfos.items, i); + } + }); + + const downloadNext = (urls, currentIndex) => { + if (aborted) { + return; + } + if (urls.length === currentIndex) { + if (running === 0) { + emitter.emit('end'); + callback(urls); + } + return; + } + running += 1; + if (currentIndex > lastIndex) { + lastIndex = currentIndex; + } + + const currentUrl = urls[currentIndex]; + + currentUrl.progress = {}; + + emitter.emit('begin-url', currentIndex); + + const dl = at3.downloadAndTagSingleURL(currentUrl.url, outputFolder, (infos, _error) => { + if (infos) { + currentUrl.file = infos.file; + currentUrl.infos = infos.infos; + } + running -= 1; + + emitter.emit('end-url', currentIndex); + + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + }); + + emitter.once('abort', () => { + aborted = true; + dl.emit('abort'); + }); + + const onDownload = (infos) => { + currentUrl.progress.download = infos; + emitter.emit('download', currentIndex); + }; + const onConvert = (infos) => { + currentUrl.progress.convert = infos; + emitter.emit('convert', currentIndex); + }; + const onInfos = (infos) => { + currentUrl.infos = infos; + emitter.emit('infos', currentIndex); + }; + + dl.on('download', onDownload); + dl.once('download-end', () => { + dl.removeListener('download', onDownload); + emitter.emit('download-end', currentIndex); + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + }); + dl.on('convert', onConvert); + dl.once('convert-end', () => { + dl.removeListener('convert', onConvert); + emitter.emit('convert-end', currentIndex); + }); + dl.on('infos', onInfos); + dl.once('error', () => { + dl.removeListener('download', onDownload); + dl.removeListener('convert', onConvert); + dl.removeListener('infos', onInfos); + emitter.emit('error', new Error(currentIndex)); + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + }); + dl.once('end', () => { + dl.removeListener('infos', onInfos); + }); + }; + + emitter.once('abort', () => { + aborted = true; + }); + + return emitter; +}; + +/** + * Download a playlist containing titles + * @param url {string} + * @param outputFolder {string} + * @param callback {Function} + * @param maxSimultaneous {number} Maximum number of simultaneous track processing + * @param subPathFormat {string} The format of the subfolder: {artist}/{title}/ + * @return {Event} + */ +at3.downloadPlaylistWithTitles = (url, outputFolder, callback, maxSimultaneous, subPathFormat) => { + if (maxSimultaneous === undefined) { + maxSimultaneous = 1; + } + if (subPathFormat === undefined) { + subPathFormat = ''; + } + + const emitter = new EventEmitter(); + let running = 0; + let lastIndex = 0; + let aborted = false; + + at3.getPlaylistTitlesInfos(url).then((playlistInfos) => { + if (aborted) { + return; + } + + outputFolder = at3.createSubPath(outputFolder, subPathFormat, playlistInfos.title, playlistInfos.artistName); + + emitter.emit('playlist-infos', playlistInfos); + + for (let i = 0; i < maxSimultaneous; i += 1) { + downloadNext(playlistInfos.items, i); + } + }); + + const downloadNext = (urls, currentIndex) => { + if (aborted) { + return; + } + if (urls.length === currentIndex) { + if (running === 0) { + emitter.emit('end'); + callback(urls); + } + return; + } + running += 1; + if (currentIndex > lastIndex) { + lastIndex = currentIndex; + } + + let currentTrack = urls[currentIndex]; + + currentTrack.progress = {}; + + emitter.emit('begin-url', currentIndex); + + at3 + .findVideoForSong(currentTrack) + .then((videos) => { + if (aborted) { + return; + } + emitter.emit('search-end', currentIndex); + + const downloadFinished = (infos, error) => { + if (!infos || error) { + return; + } + currentTrack.file = infos.file; + currentTrack.infos = infos.infos; + running -= 1; + + emitter.emit('end-url', currentIndex); + + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + }; + + let i = 0; + + const handleDl = (dl) => { + const onDownload = (infos) => { + currentTrack.progress.download = infos; + emitter.emit('download', currentIndex); + }; + const onConvert = (infos) => { + currentTrack.progress.convert = infos; + emitter.emit('convert', currentIndex); + }; + const onInfos = (infos) => { + currentTrack.infos = infos; + emitter.emit('infos', currentIndex); + }; + + dl.on('download', onDownload); + dl.once('download-end', () => { + dl.removeListener('download', onDownload); + emitter.emit('download-end', currentIndex); + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + }); + dl.on('convert', onConvert); + dl.once('convert-end', () => { + dl.removeListener('convert', onConvert); + emitter.emit('convert-end', currentIndex); + }); + dl.on('infos', onInfos); + dl.once('end', () => { + dl.removeListener('infos', onInfos); + }); + dl.once('error', () => { + dl.removeListener('download', onDownload); + dl.removeListener('convert', onConvert); + if (i < videos.length - 1) { + i += 1; + handleDl( + at3.downloadAndTagSingleURL( + videos[i].url, + outputFolder, + downloadFinished, + undefined, + false, + currentTrack, + ), + ); + } else { + emitter.emit('error', new Error(currentIndex)); + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + } + }); + emitter.once('abort', () => { + aborted = true; + dl.emit('abort'); + }); + }; + + handleDl( + at3.downloadAndTagSingleURL(videos[i].url, outputFolder, downloadFinished, undefined, false, currentTrack), + ); + }) + .catch(() => { + emitter.emit('error', new Error(currentIndex)); + if (running < maxSimultaneous) { + downloadNext(urls, lastIndex + 1); + } + }); + }; + + emitter.once('abort', () => { + aborted = true; + }); + + return emitter; +}; + +/** + * Download a playlist containing urls or titles + * @param url {string} + * @param outputFolder {string} + * @param callback {Function} + * @param maxSimultaneous {number} Maximum number of simultaneous track processing + * @return {Event} + */ +at3.downloadPlaylist = (url, outputFolder, callback, maxSimultaneous, subPathFormat) => { + const type = at3.guessURLType(url); + const sitesTitles = ['deezer', 'spotify']; + const sitesURLs = ['youtube', 'soundcloud']; + + if (sitesTitles.indexOf(type) >= 0) { + return at3.downloadPlaylistWithTitles(url, outputFolder, callback, maxSimultaneous, subPathFormat); + } else if (sitesURLs.indexOf(type) >= 0) { + return at3.downloadPlaylistWithURLs(url, outputFolder, callback, maxSimultaneous, subPathFormat); + } else { + callback(null, 'Website not supported yet'); + return new EventEmitter().emit('error', new Error('Website not supported yet')); + } +}; + +/** + * Download a track from an URL + * @param url + * @param outputFolder + * @param callback + * @param v boolean Verbose + * @return Event + */ +at3.downloadTrackURL = (url, outputFolder, callback, v) => { + if (v === undefined) { + v = false; + } + const type = at3.guessURLType(url); + const emitter = new EventEmitter(); + + if (type === 'spotify') { + const trackId = url.match(/\/track\/([0-9a-zA-Z]+)/)[1]; + at3.requestSpotify('https://api.spotify.com/v1/tracks/' + trackId).then((trackInfos) => { + const track = { + title: trackInfos.name, + artistName: trackInfos.artists[0].name, + duration: Math.ceil(trackInfos.duration_ms / 1000), + spotifyId: trackId, + cover: trackInfos.album.images[0].url, + }; + const e = at3.downloadTrack(track, outputFolder, callback, v); + + at3.forwardEvents(e, emitter); + }); + } else if (type === 'deezer') { + const trackId = url.match(/\/track\/([0-9]+)/)[1]; + at3.getDeezerTrackInfos(trackId, v).then((trackInfos) => { + const e = at3.downloadTrack(trackInfos, outputFolder, callback, v); + + at3.forwardEvents(e, emitter); + }); + } + + return emitter; +}; + +/** + * Forward any classical event from e1 to e2, and abort from e2 to e1 + * @param e1 Event The source + * @param e2 Event the destination + * @return e2 + */ +at3.forwardEvents = (e1, e2) => { + const events = [ + 'download', + 'download-end', + 'convert', + 'convert-end', + 'infos', + 'error', + 'playlist-infos', + 'begin-url', + 'end-url', + 'end', + 'search-end', + ]; + events.forEach((e) => { + e1.on(e, (data) => { + e2.emit(e, data); + }); + }); + e2.once('abort', () => { + e1.emit('abort'); + }); + return e2; +}; + +/** + * Return the suggested songs for the query + * @param query string + * @param limit number + * @return Promise> Array of potential songs + */ +at3.suggestedSongs = (query, limit) => { + if (!limit) { + limit = 5; + } + + return request({ + uri: 'https://api.deezer.com/search?limit=' + limit + '&q=' + encodeURIComponent(query), + json: true, + }).then((results) => { + return _.map(results.data, (r) => { + return { + title: r.title, + artistName: r.artist.name, + duration: r.duration, + cover: r.album.cover_medium, + deezerId: r.id, + }; + }); + }); +}; + +/** + * Return the suggested albums for the query + * @param query string + * @param limit number + * @return Promise> Array of potential albums + */ +at3.suggestedAlbums = (query, limit) => { + if (!limit) { + limit = 5; + } + + return request({ + uri: 'https://api.deezer.com/search/album?limit=' + limit + '&q=' + encodeURIComponent(query), + json: true, + }).then((results) => { + return _.map(results.data, (r) => { + return { + title: r.title, + artistName: r.artist.name, + cover: r.cover_medium, + deezerId: r.id, + link: r.link, + nbTracks: r.nb_tracks, + }; + }); + }); +}; + +/** + * Return the type of the query + * @param query string + * @return string: text, single-url, playlist-url, track-url, not-supported + */ +at3.typeOfQuery = (query) => { + if (!at3.isURL(query)) { + return 'text'; + } + const type = at3.guessURLType(query); + if (!type) { + return 'not-supported'; + } + + if (type === 'youtube' && /list=([0-9a-zA-Z_-]+)/.test(query)) { + return 'playlist-url'; + } else if (type === 'deezer') { + if (/\/(playlist|album)\//.test(query)) { + return 'playlist-url'; + } else if (/\/track\//.test(query)) { + return 'track-url'; + } + return 'not-supported'; + } else if (type === 'soundcloud' && /\/sets\//.test(query)) { + return 'playlist-url'; + } else if (type === 'spotify') { + if (/\/(playlist|album)\//.test(query)) { + return 'playlist-url'; + } else if (/\/track\//.test(query)) { + return 'track-url'; + } + return 'not-supported'; + } + + return 'single-url'; +}; + +/** + * Return URL type + * @param url + * @return string + */ +at3.guessURLType = (url) => { + if (/^(https?:\/\/)?((www|m)\.)?((youtube\.([a-z]{2,4}))|(youtu\.be))/.test(url)) { + return 'youtube'; + } else if (/^(https?:\/\/)?(((www)|(m))\.)?(soundcloud\.([a-z]{2,4}))/.test(url)) { + return 'soundcloud'; + } else if (/^(https?:\/\/)?(www\.)?(deezer\.([a-z]{2,4}))\//.test(url)) { + return 'deezer'; + } else if (/^(https?:\/\/)?((open|play)\.)?spotify\.([a-z]{2,4})/.test(url)) { + return 'spotify'; + } +}; + +const imatch = (textSearched, text) => { + // [TODO] Improve this function (use .test and espace special caracters + use it everywhere else) + return text.match(new RegExp(textSearched, 'gi')); +}; +const vsimpleName = (text, exact) => { + if (exact === undefined) { + exact = false; + } + text = text.toLowerCase(); + if (!exact) { + // text = text.replace('feat', ''); + } + text = text.replace(/((\[)|(\())?radio edit((\])|(\)))?/gi, ''); + text = text.replace(/[^a-zA-Z0-9]/gi, ''); + return text; +}; +const delArtist = (artist, text, exact) => { + if (exact === undefined) { + exact = false; + } + if (vsimpleName(artist).length <= 2) { + // Artist with a very short name (Mathieu Chedid - M) + return vsimpleName(text, exact); + } else { + // [TODO] Improve, escape regex special caracters in vsimpleName(artist) + return vsimpleName(text, exact).replace(new RegExp(vsimpleName(artist), 'ig'), ''); + } +}; +const simpleName = (text) => { + return text.replace(/\(.+\)/g, ''); +}; + +module.exports = at3; diff --git a/paxt002/test.js b/paxt002/test.js new file mode 100644 index 0000000..d12dae4 --- /dev/null +++ b/paxt002/test.js @@ -0,0 +1,211 @@ +const alltomp3 = require('.'); +const util = require('util'); + +// alltomp3.spotifyToken().then(o => console.log(o)).catch(e => console.log(e)); + +// alltomp3.getPlaylistTitlesInfos('https://open.spotify.com/user/spotify_france/playlist/1h4ZB3lW7lD5RmfE6DIRRI').then(d => console.log(util.inspect(d, {depth: 2}))); + +// alltomp3.getSpotifyTrackInfos('1NOPjzkLIEUM6mwGxCm2mM').then(i => console.log(i)); + +// alltomp3.configEyeD3('/Users/ntag/Projets/alltomp3/alltomp3-app/bin/eyeD3/bin/eyeD3', '/Users/ntag/Projets/alltomp3/alltomp3-app/bin/eyeD3/build/lib'); +// alltomp3.getInfosWithYoutubeDl('https://soundcloud.com/taylorythm/coda', function(infos) { +// console.log(infos); +// }); +// alltomp3.getInfosWithYoutubeDl('https://www.youtube.com/watch?v=e74VMNgARvY', function(infos) { +// console.log(infos); +// }); +// not working URL https://www.youtube.com/watch?v=yzi-7G2u89g +// var dl = alltomp3.findAndDownload("mika elle me dit", "./", () => {}, true); +// dl.on("download", function (infos) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(infos.progress + "%"); +// }); +// dl.on("download-end", function () { +// console.log("Download end"); +// }); +// dl.on("convert", function (infos) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(infos.progress + "%"); +// }); +// dl.on("error", function (e) { +// console.log("Error la la", util.inspect(e)); +// }); + +// alltomp3.getInfosWithYoutubeDl('https://www.youtube.com/watch?v=sX9s-bSOYxk').then(console.log); + +// const dl = alltomp3.downloadWithYoutubeDl('https://www.youtube.com/watch?v=sX9s-bSOYxk', 'test.mp3'); +// dl.on('download-progress', console.log); +// dl.once('download-end', console.log); +// dl.on('error', console.error); + +// alltomp3.guessTrackFromString('Imagine Dragons - On Top of the World - Lyrics', false, false, true); +// alltomp3.guessTrackFromString('C2C - Happy Ft. D.Martin', false, false, true); +// alltomp3.guessTrackFromString('David Guetta - Bang My Head (Official Video) feat Sia & Fetty Wap', false, false, true); +// alltomp3.guessTrackFromString('David Guetta - Hey Mama (Official Video) ft Nicki Minaj, Bebe Rexha & Afrojack', false, false, true); +// alltomp3.retrieveTrackInformations('On Top of the World', 'Imagine Dragons').then(function (infos) { +// console.log("Infos: ", infos); +// }); + +// alltomp3.guessTrackFromFile('./test.mp3').then(function (infos) { +// return alltomp3.retrieveTrackInformations(infos.title, infos.artistName); +// }).then(function (infos) { +// console.log(infos); +// alltomp3.tagFile('./test.mp3', infos); +// }); + +// alltomp3.downloadAndTagSingleURL( +// 'https://www.youtube.com/watch?v=sX9s-bSOYxk', +// './', +// function (infos) { +// console.log(infos); +// }, +// undefined, +// true, +// ); + +// var title = "hide and seek imogen heap"; +// alltomp3.findVideo(title).then(function(results) { +// var dl = alltomp3.downloadAndTagSingleURL(results[0].url, function(infos) { +// console.log("FINI ", infos); +// }, title); +// dl.on('download', function(infos) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(infos.progress + '%'); +// }); +// dl.on('download-end', function() { +// console.log('Download end'); +// }); +// dl.on('convert', function(infos) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(infos.progress + '%'); +// }); +// dl.on('convert-end', function() { +// console.log('Convert end'); +// }); +// dl.on('infos', function(infos) { +// console.log('Got infos: ', infos); +// }); +// }); + +// var dl = alltomp3.downloadAndTagSingleURL("https://soundcloud.com/user-523607375/sets/john-legend-start-a-fire-la-la", 'mp3/', function(infos) { +// console.log("FINI ", infos); +// }); +// dl.on('download', function(infos) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(infos.progress + '%'); +// }); +// dl.on('download-end', function() { +// console.log('Download end'); +// }); +// dl.on('convert', function(infos) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(infos.progress + '%'); +// }); +// dl.on('convert-end', function() { +// console.log('Convert end'); +// }); +// dl.on('infos', function(infos) { +// console.log('Got infos: ', infos); +// }); + +var dl = alltomp3.findAndDownload('imagine dragons on top of the world', './mp3/', function (infos) { + console.log("It's finished: ", infos); +}); +dl.on('search-end', function () { + console.log('Search end'); +}); +dl.on('download', function (infos) { + process.stdout.cursorTo(0); + process.stdout.clearLine(1); + process.stdout.write(infos.progress + '%'); +}); +dl.on('download-end', function () { + console.log('', 'Download end'); +}); +dl.on('convert', function (infos) { + process.stdout.cursorTo(0); + process.stdout.clearLine(1); + process.stdout.write(infos.progress + '%'); +}); +dl.on('convert-end', function () { + console.log('', 'Convert end'); +}); +dl.on('infos', function (infos) { + console.log('New infos received: ', infos); +}); + +// alltomp3.guessTrackFromString('Imagine Dragons - On Top of the World - Lyrics').then(function(infos) { +// console.log(infos); +// }); +// alltomp3.guessTrackFromString('C2C - Happy Ft. D.Martin').then(function(infos) { +// console.log(infos); +// }); +// alltomp3.guessTrackFromString('David Guetta - Bang My Head (Official Video) feat Sia & Fetty Wap').then(function(infos) { +// console.log(infos); +// }); +// alltomp3.guessTrackFromString('David Guetta - Hey Mama (Official Video) ft Nicki Minaj, Bebe Rexha & Afrojack').then(function(infos) { +// console.log(infos); +// }); +// alltomp3.guessTrackFromString('hans zimmer no time for caution').then(function(infos) { +// console.log(infos); +// }); + +// alltomp3.findLyrics('Radioactive', 'Imagine Dragons').then(function (lyrics) { +// console.log(lyrics); +// }).catch(function() { +// console.log('No lyrics'); +// }); + +// alltomp3.getURLsInPlaylist('https://soundcloud.com/20syl/sets/20syl-remixes-2016').then(function(items) { +// console.log(items); +// }); + +// alltomp3.getTracksInPlaylist('http://www.deezer.com/album/11111444').then(function(items) { +// console.log(items); +// }); + +// var urls; +// var dl = alltomp3.downloadPlaylist("https://open.spotify.com/album/2tVnLYqhc0iGdSCLxoaLjD", "./mp3/", function (urls) { +// console.log("It's finished: ", urls); +// }, 8); +// dl.on('search-end', function() { +// console.log('Search end'); +// }); +// dl.on('download', function(index) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(urls.items[index].progress.download.progress + '%'); +// }); +// dl.on('download-end', function() { +// console.log('', 'Download end'); +// }); +// dl.on('convert', function(index) { +// process.stdout.cursorTo(0); +// process.stdout.clearLine(1); +// process.stdout.write(urls.items[index].progress.convert.progress + '%'); +// }); +// dl.on('convert-end', function(index) { +// console.log('', 'Convert end'); +// }); +// dl.on('error', function(index) { +// console.log('', 'Error with ' + index); +// }); +// dl.on('infos', function(index) { +// // console.log('New infos received: ', infos); +// }); +// dl.on('playlist-infos', function(urlss) { +// urls = urlss; +// console.log('URLs received: ', urlss); +// }); +// dl.on('begin-url', function(index) { +// console.log('Begin: ', index); +// }); +// dl.on('end-url', function(index) { +// console.log('End: ', index); +// });