Skip to content

Commit

Permalink
♻️ Further refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
niksudan committed Jul 28, 2020
1 parent d86f5ed commit d6bd36b
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 89 deletions.
4 changes: 2 additions & 2 deletions src/generate-playlist.ts
Expand Up @@ -8,7 +8,7 @@ import './config';
(async () => {
// Abort the playlist generation if we take longer than 2 minutes
setTimeout(() => {
console.log('Took too long, exiting');
console.log('⚠️ Took too long, exiting');
process.exit(1);
}, 1000 * 120);

Expand All @@ -17,7 +17,7 @@ import './config';
const spotify = new Spotify();
new Server(spotify);
if (!spotify.isAuthenticated) {
console.log("WARNING: Spotify features won't work until you log in");
console.log("⚠️ WARNING: Spotify features won't work until you log in");
return;
}

Expand Down
149 changes: 73 additions & 76 deletions src/lib/bot.ts
Expand Up @@ -11,7 +11,7 @@ import moment from 'moment';
import Fuse from 'fuse.js';
import { uniq } from 'lodash';

import Spotify from './spotify';
import Spotify, { SpotifyTrack } from './spotify';
import YouTube from './youtube';
import AppleMusic from './apple';
import SoundCloud from './soundcloud';
Expand Down Expand Up @@ -68,14 +68,11 @@ export default class Bot {
* Login to Discord and connect to guild
*/
public async login() {
console.log('Logging in...');
await this.client.login(process.env.DISCORD_TOKEN);
this.guild = this.client.guilds.cache.find(
(guild) => guild.id == process.env.DISCORD_GUILD_ID!,
);
console.log(
`Logged in to Discord and connected to ${this.guild.name} (#${this.guild.id})`,
);
console.log(`✅ Connected to Discord`);
}

/**
Expand All @@ -93,7 +90,7 @@ export default class Bot {

// Fetch 50 messages before the specified end date
console.log(
`Fetching messages in ${
`🌐 Fetching messages in ${
channel.name
} from ${fromDate.toString()} to ${toDate.toString()}...`,
);
Expand Down Expand Up @@ -157,8 +154,9 @@ export default class Bot {
* Convert track data into Spotify URIs
*/
public async convertTrackData(spotify: Spotify, trackData: TrackData[]) {
const tracks: string[] = [];
const tracks: SpotifyTrack[] = [];
const contributions: { author: User; count: number }[] = [];
// const artists =
const stats: Record<ServiceType, number> = {
spotify: 0,
youtube: 0,
Expand All @@ -178,59 +176,56 @@ export default class Bot {
};

for (let { author, service, url } of trackData) {
// Push the Spotify track directly
// Add Spotify track directly
if (service.type === 'spotify') {
tracks.push(
`spotify:track:${url.replace(
/https:\/\/open.spotify.com\/track\//gi,
'',
)}`,
const track = await spotify.getTrack(
url.replace(/https:\/\/open.spotify.com\/track\//gi, ''),
);
stats.spotify += 1;
addContribution(author);
continue;
}

// Attempt to parse title from service
const title = await service.get(url);

// Prepare a search query
let searchQuery = title;
if (service.type === 'youtube') {
searchQuery = title
.replace(/ *\([^)]*\) */g, '')
.replace(/[^A-Za-z0-9 ]/g, '')
.replace(/\s{2,}/g, ' ');
}
if (searchQuery.length < 3) {
break;
}

// Create a search query
const fuse = new Fuse(await spotify.searchTracks(searchQuery), {
threshold: 0.8,
keys: [
{
name: 'title',
weight: 0.7,
},
{
name: 'artists.name',
weight: 0.5,
},
{
name: 'album',
weight: 0.1,
},
],
});
if (track) {
tracks.push(track);
stats.spotify += 1;
addContribution(author);
}
} else {
// Parse title from service
const title = await service.get(url);

// Search for track on Spotify
let searchQuery = title;
if (service.type === 'youtube') {
searchQuery = title
.replace(/ *\([^)]*\) */g, '')
.replace(/[^A-Za-z0-9 ]/g, '')
.replace(/\s{2,}/g, ' ');
}

// Find the best song match
const fuzzyResults = fuse.search(title);
if (fuzzyResults.length) {
tracks.push(fuzzyResults[0].item.uri);
addContribution(author);
stats[service.type] += 1;
if (searchQuery.length >= 3) {
const fuse = new Fuse(await spotify.searchTracks(searchQuery), {
threshold: 0.8,
keys: [
{
name: 'title',
weight: 0.7,
},
{
name: 'artists',
weight: 0.5,
},
{
name: 'album',
weight: 0.1,
},
],
});

// Find the best match
const fuzzyResults = fuse.search(title);
if (fuzzyResults.length) {
tracks.push(fuzzyResults[0].item);
addContribution(author);
stats[service.type] += 1;
}
}
}
}

Expand All @@ -245,7 +240,7 @@ export default class Bot {
weeksAgo = 1,
savePlaylist = true,
) {
console.log(`Generating playlist from ${weeksAgo} week(s) ago...`);
console.log(`Generating playlist from ${weeksAgo} week(s) ago...`);
const channel = await this.findChannel(process.env.MUSIC_SOURCE_CHANNEL_ID);
if (!channel) {
return;
Expand All @@ -263,38 +258,41 @@ export default class Bot {
'Do MMMM',
)} - ${toDate.format('Do MMMM')})`;

// Reset playlist
if (savePlaylist) {
await spotify.clearPlaylist();
await spotify.renamePlaylist(playlistName);
}

// Fetch all messages from the channel within the past week
const messages = await (
await this.fetchMessages(channel, fromDate, toDate)
).filter((message) => !message.author.bot);
console.log(`${messages.size} message(s) fetched in total`);
console.log(`💬 ${messages.size} messages were sent`);

// Parse track URLs
const trackData = this.parseTrackData(messages);
console.log(`❓ ${trackData.length} contained track links`);

// Convert URLs into Spotify URIs if possible
const { tracks, stats, contributions } = await this.convertTrackData(
spotify,
trackData,
);
console.log(`${tracks.length} track(s) found`, stats);

// Update the playlist with the tracks
if (savePlaylist && tracks.length) {
const uris = uniq(tracks.map((track) => track.uri).reverse());
await spotify.addTracksToPlaylist(uris);
// Exit if we didn't find any tracks
if (!tracks.length) {
console.log('⚠️ No tracks were found...');
return;
}

console.log(
'Playlist was updated successfully',
`https://open.spotify.com/playlist/${process.env.PLAYLIST_ID}`,
);
const uris = uniq(tracks.map((track) => track.uri).reverse());
console.log(`🎵 ${uris.length} tracks found`, stats);

// Reset and update playlist
if (savePlaylist) {
await spotify.clearPlaylist();
await spotify.renamePlaylist(playlistName);
await spotify.addTracksToPlaylist(uniq(uris));
console.log(
'✨ Playlist updated successfully',
`https://open.spotify.com/playlist/${process.env.PLAYLIST_ID}`,
);
}

// Send the news update
const newsChannel = await this.findChannel(
Expand All @@ -318,8 +316,7 @@ export default class Bot {

message += `\nListen now!\nhttps://open.spotify.com/playlist/${process.env.PLAYLIST_ID}`;

console.log(message);
await newsChannel.send(message);
console.log(`News update sent to ${newsChannel.name}!`);
console.log(`News update sent to ${newsChannel.name}!`);
}
}
4 changes: 3 additions & 1 deletion src/lib/server.ts
Expand Up @@ -32,7 +32,9 @@ export default class Server {
app.set('port', process.env.SERVER_PORT || 9000);
app.listen(app.get('port'), () => {
console.log(
`Spotify auth server is live at http://localhost:${app.get('port')}`,
`✅ Spotify auth server is live at http://localhost:${app.get(
'port',
)}, go log in!`,
);
});
}
Expand Down
38 changes: 28 additions & 10 deletions src/lib/spotify.ts
Expand Up @@ -5,14 +5,21 @@ import { chunk } from 'lodash';

require('dotenv').config();

interface SpotifyTrack {
export interface SpotifyTrack {
uri: string;
name: string;
popularity: number;
album: string;
artists: string[];
}

export interface SpotifyAlbum {
id: string;
name: string;
genres: string[];
artists: string[];
}

export default class Spotify {
client: SpotifyWebApi;
id?: string;
Expand Down Expand Up @@ -83,7 +90,7 @@ export default class Spotify {
const response = await this.client.refreshAccessToken();
this.setTokens(response.body.access_token, response.body.refresh_token);
await this.getAccountDetails();
console.log('Logged in to Spotify as', this.accountName);
console.log('Logged in to Spotify as', this.accountName);
}

private async getAccountDetails() {
Expand All @@ -93,7 +100,7 @@ export default class Spotify {
}

public async renamePlaylist(name: string) {
console.log(`Renaming playlist to \"${name}\"...`);
console.log(`✏️ Renaming playlist to \"${name}\"...`);
await this.client.changePlaylistDetails(process.env.PLAYLIST_ID, {
name,
});
Expand All @@ -104,7 +111,7 @@ export default class Spotify {
const payloads = chunk(tracks, TRACKS_PER_PAYLOAD);
for (let i = 0; i < payloads.length; i += 1) {
console.log(
`Adding tracks ${i * TRACKS_PER_PAYLOAD}-${
`Adding tracks ${i * TRACKS_PER_PAYLOAD}-${
i * TRACKS_PER_PAYLOAD + TRACKS_PER_PAYLOAD
} to playlist...`,
);
Expand All @@ -114,7 +121,7 @@ export default class Spotify {
}

public async clearPlaylist() {
console.log('Clearing playlist...');
console.log('🗑 Clearing playlist...');
const response = await this.client.getPlaylistTracks(
process.env.PLAYLIST_ID,
);
Expand All @@ -139,17 +146,28 @@ export default class Spotify {
}
}

public async getTrack(id: string): Promise<SpotifyTrack> {
const response = await this.client.getTrack(id);
return {
uri: response.body.uri,
name: response.body.name,
popularity: response.body.popularity,
album: response.body.album.name,
artists: response.body.artists.map((artist) => ({
name: artist.name,
})),
};
}

public async searchTracks(query: string, limit = 5): Promise<SpotifyTrack[]> {
console.log(`Searching tracks for "${query}"...`);
console.log(`🔍 Searching Spotify 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) => ({
name: artist.name,
})),
album: item.album.id,
artists: item.artists.map((artist) => artist.name),
}));
}
}

0 comments on commit d6bd36b

Please sign in to comment.