diff --git a/src/generate-playlist.ts b/src/generate-playlist.ts index 18f3054..5880783 100644 --- a/src/generate-playlist.ts +++ b/src/generate-playlist.ts @@ -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); @@ -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; } diff --git a/src/lib/bot.ts b/src/lib/bot.ts index 7749cff..d2a18bd 100644 --- a/src/lib/bot.ts +++ b/src/lib/bot.ts @@ -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'; @@ -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`); } /** @@ -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()}...`, ); @@ -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 = { spotify: 0, youtube: 0, @@ -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; + } + } } } @@ -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; @@ -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( @@ -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}!`); } } diff --git a/src/lib/server.ts b/src/lib/server.ts index e312eee..23d0f5f 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -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!`, ); }); } diff --git a/src/lib/spotify.ts b/src/lib/spotify.ts index c1ff854..3bccfa6 100644 --- a/src/lib/spotify.ts +++ b/src/lib/spotify.ts @@ -5,7 +5,7 @@ import { chunk } from 'lodash'; require('dotenv').config(); -interface SpotifyTrack { +export interface SpotifyTrack { uri: string; name: string; popularity: number; @@ -13,6 +13,13 @@ interface SpotifyTrack { artists: string[]; } +export interface SpotifyAlbum { + id: string; + name: string; + genres: string[]; + artists: string[]; +} + export default class Spotify { client: SpotifyWebApi; id?: string; @@ -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() { @@ -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, }); @@ -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...`, ); @@ -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, ); @@ -139,17 +146,28 @@ export default class Spotify { } } + public async getTrack(id: string): Promise { + 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 { - 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), })); } }