Skip to content

Commit

Permalink
Search for YouTube videos and convert them to Spotify URIs
Browse files Browse the repository at this point in the history
  • Loading branch information
niksudan committed Oct 29, 2019
1 parent cf030ca commit 33a5fad
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 25 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ninbot retrieves the last week's worth of songs in `#non-nin-music` and then upd

Your song will be added to the playlist if it is in one of the following formats:

- Spotify track (https://open.spotify.com/track/...)
- Spotify track (ninbot ignores album links for now)
- YouTube video (ninbot will try it's best to match)

The playlist updates every Monday at 00:00 UTC.
The playlist updates every Monday at 00:00 UTC.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"discord.js": "^11.5.1",
"dotenv": "^8.0.0",
"express": "^4.17.1",
"fuse.js": "^3.4.5",
"get-youtube-id": "^1.0.1",
"moment": "^2.24.0",
"node-fetch": "^2.6.0",
"query-string": "^6.8.3",
"spotify-web-api-node": "^4.0.0",
"typescript": "^3.5.2"
},
Expand Down
105 changes: 88 additions & 17 deletions src/lib/ninbot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
Collection,
} from 'discord.js';
import * as moment from 'moment';
import * as Fuse from 'fuse.js';

import Spotify from './spotify';
import YouTube from './youtube';

require('dotenv').config();

Expand Down Expand Up @@ -106,36 +109,104 @@ export default class Ninbot {
const messages = await this.fetchMessages(channel, fromDate, toDate);
console.log(
`${messages.size} message(s) were fetched in total`,
messages.map(message => message.content),
// messages.map(message => message.content),
);

// Filter the messages to those that contain Spotify links
let spotifyUrls: string[] = [];
let items: {
url: string;
service: 'spotify' | 'youtube';
}[] = [];

// Filter for messages to those that contain valid links
messages.forEach(message => {
const match = message.content.match(
const spotifyMatch = message.content.match(
/https:\/\/open.spotify.com\/track\/([^? ]+)/gi,
);
if (match) {
spotifyUrls = spotifyUrls.concat(
match.map(
url =>
`spotify:track:${url.replace(
/https:\/\/open.spotify.com\/track\//gi,
'',
)}`,
),
if (spotifyMatch) {
items = items.concat(
spotifyMatch.map(url => ({
url,
service: 'spotify',
})),
);
}

const youtubeMatch = message.content.match(
/^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/.+/gi,
);
if (youtubeMatch) {
items = items.concat(
youtubeMatch.map(url => ({
url,
service: 'youtube',
})),
);
}
});

// Convert the submissions into Spotify URIs if possible
const tracks: string[] = [];
let spotifyTrackCount = 0;
let youtubeTrackCount = 0;

for (let item of items) {
switch (item.service) {
case 'spotify':
spotifyTrackCount += 1;
tracks.push(
`spotify:track:${item.url.replace(
/https:\/\/open.spotify.com\/track\//gi,
'',
)}`,
);
break;

case 'youtube':
const video = await YouTube.getVideo(item.url);
if (!video) {
break;
}

const formattedTitle = video.title
.replace(/ *\([^)]*\) */g, '')
.replace(/[^A-Za-z0-9 ]/g, '')
.replace(/\s{2,}/g, ' ');

const fuse = new Fuse(await spotify.searchTracks(formattedTitle), {
threshold: 0.8,
keys: [
{
name: 'title',
weight: 0.7,
},
{
name: 'artists.name',
weight: 0.5,
},
{
name: 'album',
weight: 0.1,
},
],
});

const fuzzyResults = fuse.search(video.title);
if (fuzzyResults.length) {
youtubeTrackCount += 1;
tracks.push(fuzzyResults[0].uri);
}
break;
}
}

console.log(
`${spotifyUrls.length} Spotify track(s) were detected`,
spotifyUrls,
`${tracks.length} track(s) found (Spotify: ${spotifyTrackCount}. YouTube: ${youtubeTrackCount}`,
tracks,
);

// Update the playlist with the tracks
if (spotifyUrls.length) {
await spotify.addTracksToPlaylist(spotifyUrls.reverse());
if (tracks.length) {
await spotify.addTracksToPlaylist(tracks.reverse());
}

console.log(
Expand Down
11 changes: 5 additions & 6 deletions src/lib/spotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,17 @@ export default class Spotify {
);
}

public async searchTracks(
query: string,
limit = 10,
): Promise<SpotifyTrack[]> {
console.log(`Searching for tracks about "${query}"...`);
public async searchTracks(query: string, limit = 5): Promise<SpotifyTrack[]> {
console.log(`Searching tracks for "${query}"...`);
const response = await this.client.searchTracks(query, { limit });
return response.body.tracks.items.map(item => ({
uri: item.uri,
name: item.name,
popularity: item.popularity,
album: item.album.name,
artists: item.artists.map(artist => artist.name),
artists: item.artists.map(artist => ({
name: artist.name,
})),
}));
}
}
29 changes: 29 additions & 0 deletions src/lib/youtube.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import fetch from 'node-fetch';
import * as queryString from 'query-string';
const getYouTubeID = require('get-youtube-id');

interface YouTubeVideo {
id: string;
title: string;
}

export default class YouTube {
public static async getVideo(url: string): Promise<YouTubeVideo> {
const id = getYouTubeID(url);
if (!id) {
return;
}

const response = await fetch(
`http://youtube.com/get_video_info?html5=1&video_id=${id}`,
).then(res => res.text());

const query = queryString.parse(response);
const { videoDetails } = JSON.parse(query.player_response.toString());

return {
id: videoDetails.videoId,
title: videoDetails.title,
};
}
}
28 changes: 28 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,10 @@ fsevents@^1.2.7:
nan "^2.12.1"
node-pre-gyp "^0.12.0"

fuse.js@^3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6"

gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
Expand All @@ -741,6 +745,10 @@ get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"

get-youtube-id@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-youtube-id/-/get-youtube-id-1.0.1.tgz#adb6f475e292d98f98ed5bfb530887656193e157"

glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
Expand Down Expand Up @@ -1232,6 +1240,10 @@ [email protected]:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"

node-fetch@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"

node-pre-gyp@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
Expand Down Expand Up @@ -1446,6 +1458,14 @@ [email protected], qs@^6.5.1:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"

query-string@^6.8.3:
version "6.8.3"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.8.3.tgz#fd9fb7ffb068b79062b43383685611ee47777d4b"
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"

range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
Expand Down Expand Up @@ -1680,6 +1700,10 @@ source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"

split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"

split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
Expand All @@ -1703,6 +1727,10 @@ static-extend@^0.1.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"

strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"

string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
Expand Down

0 comments on commit 33a5fad

Please sign in to comment.