diff --git a/README.md b/README.md index a65ba1b..b39679a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # ninbot [![Add to Discord](https://img.shields.io/badge/Add%20to-Discord-7289da.svg)](https://discordapp.com/api/oauth2/authorize?client_id=594276600892358666&permissions=0&scope=bot) -## Features +**ninbot** is a Discord bot created specifically for the Nine Inch Nails Discord server. -- Retrieves the last week's worth of songs in `#non-nin-music` and then updates a Spotify playlist with the results. +## 🎵 NINcord Weekly + +ninbot retrieves the last week's worth of songs in `#non-nin-music` and then updates a [Spotify playlist](https://open.spotify.com/playlist/1pMms99VVgmLZhkr2MN010?si=0YvWAK2aR-yik6-OcV_g7g) with the results. + +Your song will be added to the playlist if it is in one of the following formats: + +- Spotify track (https://open.spotify.com/track/...) + +The playlist updates every Monday at 00:00 UTC. \ No newline at end of file diff --git a/package.json b/package.json index d6821fc..ec193b9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "ts-node": "^8.3.0" }, "scripts": { - "start": "node_modules/.bin/ts-node --files ./src/index.ts", + "generate-playlist": "node_modules/.bin/ts-node --files ./src/generate-playlist.ts", "dev": "nodemon" } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..d924b1e --- /dev/null +++ b/src/config.ts @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/node'; + +require('dotenv').config(); + +if (process.env.SENTRY_DSN) { + Sentry.init({ dsn: process.env.SENTRY_DSN }); +} diff --git a/src/index.ts b/src/generate-playlist.ts similarity index 76% rename from src/index.ts rename to src/generate-playlist.ts index ca0bb72..07185c6 100644 --- a/src/index.ts +++ b/src/generate-playlist.ts @@ -3,31 +3,34 @@ import Server from './lib/server'; import Spotify from './lib/spotify'; import * as Sentry from '@sentry/node'; -require('dotenv').config(); - -if (process.env.SENTRY_DSN) { - Sentry.init({ dsn: process.env.SENTRY_DSN }); -} +import './config'; (async () => { + // Abort the playlist generation if we take longer than 30 seconds setTimeout(() => { console.log('Took too long, exiting'); process.exit(1); }, 1000 * 30); try { + // Start a new Spotify authentication server const spotify = new Spotify(); new Server(spotify); - if (!spotify.isAuthenticated) { console.log("WARNING: Spotify features won't work until you log in"); return; } + + // Validate Spotify credentials await spotify.refreshTokens(); + // Boot up ninbot const ninbot = new Ninbot(); await ninbot.login(); + + // Generate a playlist await ninbot.generatePlaylist(spotify, 1); + process.exit(0); } catch (e) { console.log(e); diff --git a/src/lib/ninbot.ts b/src/lib/ninbot.ts index 1a5ca1c..7644c3a 100644 --- a/src/lib/ninbot.ts +++ b/src/lib/ninbot.ts @@ -4,12 +4,12 @@ import { TextChannel, SnowflakeUtil, Message, - Collection -} from "discord.js"; -import * as moment from "moment"; -import Spotify from "./spotify"; + Collection, +} from 'discord.js'; +import * as moment from 'moment'; +import Spotify from './spotify'; -require("dotenv").config(); +require('dotenv').config(); type Messages = Collection; @@ -25,15 +25,13 @@ export default class Ninbot { * Initiate year zero */ public async login() { - console.log("Logging in..."); + console.log('Logging in...'); await this.client.login(process.env.DISCORD_TOKEN); this.guild = this.client.guilds.find( - guild => guild.id === process.env.DISCORD_GUILD_ID + guild => guild.id === process.env.DISCORD_GUILD_ID, ); console.log( - `Logged in to Discord and connected to ${this.guild.name} (#${ - this.guild.id - })` + `Logged in to Discord and connected to ${this.guild.name} (#${this.guild.id})`, ); } @@ -44,7 +42,7 @@ export default class Ninbot { channel: TextChannel, fromDate: moment.Moment, toDate: moment.Moment, - messages: Messages = new Collection() + messages: Messages = new Collection(), ): Promise { if (fromDate.isAfter(toDate)) { return messages; @@ -52,10 +50,10 @@ export default class Ninbot { // Fetch 50 messages before the specified end date console.log( - `Fetching messages from ${fromDate.toString()} to ${toDate.toString()}...` + `Fetching messages from ${fromDate.toString()} to ${toDate.toString()}...`, ); const newMessages = await channel.fetchMessages({ - before: SnowflakeUtil.generate(toDate.toDate()) + before: SnowflakeUtil.generate(toDate.toDate()), }); // If the payload is empty, there are no more messages left in the channel @@ -65,15 +63,15 @@ export default class Ninbot { // We're only interested in getting the messages before the target date const messagesToAdd = newMessages.filter(message => - moment(message.createdAt).isBetween(fromDate, toDate) + moment(message.createdAt).isBetween(fromDate, toDate), ); // If all messages were after the target date, fetch for more return this.fetchMessages( channel, fromDate, - moment(newMessages.last().createdAt).subtract(1, "ms"), - messages.concat(messagesToAdd) + moment(newMessages.last().createdAt).subtract(1, 'ms'), + messages.concat(messagesToAdd), ); } @@ -83,7 +81,7 @@ export default class Ninbot { public async generatePlaylist(spotify: Spotify, weeksAgo = 1) { console.log(`Generating playlist from ${weeksAgo} week(s) ago...`); const channel = this.guild.channels.find( - channel => channel.name === "non-nin-music" && channel.type === "text" + channel => channel.name === 'non-nin-music' && channel.type === 'text', ) as TextChannel; if (!channel) { return; @@ -91,31 +89,31 @@ export default class Ninbot { // Calculate the date range const fromDate = moment() - .startOf("isoWeek") - .startOf("day") - .subtract(weeksAgo, "week"); - const toDate = fromDate.clone().endOf("isoWeek"); + .startOf('isoWeek') + .startOf('day') + .subtract(weeksAgo, 'week'); + const toDate = fromDate.clone().endOf('isoWeek'); // Reset playlist await spotify.clearPlaylist(); await spotify.renamePlaylist( `${process.env.PLAYLIST_NAME} (${fromDate.format( - "Do MMMM" - )} - ${toDate.format("Do MMMM")})` + 'Do MMMM', + )} - ${toDate.format('Do MMMM')})`, ); // Fetch all messages from the channel within the past week 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[] = []; messages.forEach(message => { const match = message.content.match( - /https:\/\/open.spotify.com\/track\/([^? ]+)/gi + /https:\/\/open.spotify.com\/track\/([^? ]+)/gi, ); if (match) { spotifyUrls = spotifyUrls.concat( @@ -123,16 +121,16 @@ export default class Ninbot { url => `spotify:track:${url.replace( /https:\/\/open.spotify.com\/track\//gi, - "" - )}` - ) + '', + )}`, + ), ); } }); console.log( `${spotifyUrls.length} Spotify track(s) were detected`, - spotifyUrls + spotifyUrls, ); // Update the playlist with the tracks @@ -141,8 +139,8 @@ export default class Ninbot { } console.log( - "Playlist was updated successfully", - `https://open.spotify.com/playlist/${process.env.PLAYLIST_ID}` + 'Playlist was updated successfully', + `https://open.spotify.com/playlist/${process.env.PLAYLIST_ID}`, ); } } diff --git a/src/lib/server.ts b/src/lib/server.ts index cda070b..e8368f4 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -1,6 +1,6 @@ -import * as express from "express"; -import * as bodyParser from "body-parser"; -import Spotify from "./spotify"; +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import Spotify from './spotify'; export default class Server { constructor(spotify: Spotify) { @@ -8,7 +8,7 @@ export default class Server { app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); - app.get("/", async (req, res) => { + app.get('/', async (req, res) => { if (spotify.isAuthenticated) { await spotify.refreshTokens(); res.sendStatus(200); @@ -16,7 +16,7 @@ export default class Server { res.redirect(spotify.authorizationUrl); }); - app.get("/callback", async (req, res) => { + app.get('/callback', async (req, res) => { if (spotify.isAuthenticated) { await spotify.refreshTokens(); res.sendStatus(200); @@ -29,10 +29,10 @@ export default class Server { } }); - app.set("port", process.env.SERVER_PORT || 9000); - app.listen(app.get("port"), () => { + 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')}`, ); }); } diff --git a/src/lib/spotify.ts b/src/lib/spotify.ts index ed503e2..6378786 100644 --- a/src/lib/spotify.ts +++ b/src/lib/spotify.ts @@ -1,8 +1,8 @@ -import * as SpotifyWebApi from "spotify-web-api-node"; -import * as fs from "fs"; -import * as path from "path"; +import * as SpotifyWebApi from 'spotify-web-api-node'; +import * as fs from 'fs'; +import * as path from 'path'; -require("dotenv").config(); +require('dotenv').config(); export default class Spotify { client: SpotifyWebApi; @@ -13,12 +13,12 @@ export default class Spotify { this.client = new SpotifyWebApi({ clientId: process.env.SPOTIFY_CLIENT_ID, clientSecret: process.env.SPOTIFY_CLIENT_SECRET, - redirectUri: process.env.SPOTIFY_REDIRECT_URI + redirectUri: process.env.SPOTIFY_REDIRECT_URI, }); } private get authTokenFilepath() { - return path.join(__dirname, "/../../spotify-auth-tokens.txt"); + return path.join(__dirname, '/../../spotify-auth-tokens.txt'); } public get isAuthenticated() { @@ -28,9 +28,9 @@ export default class Spotify { try { const tokens = fs .readFileSync(this.authTokenFilepath, { - encoding: "utf8" + encoding: 'utf8', }) - .split("\n"); + .split('\n'); this.setTokens(tokens[0], tokens[1]); return !!this.client.getAccessToken(); } catch (e) { @@ -40,8 +40,8 @@ export default class Spotify { public get authorizationUrl() { return this.client.createAuthorizeURL( - ["playlist-modify-public", "playlist-modify-private"], - new Date().getTime().toString() + ['playlist-modify-public', 'playlist-modify-private'], + new Date().getTime().toString(), ); } @@ -56,7 +56,7 @@ export default class Spotify { } fs.writeFileSync( this.authTokenFilepath, - `${accessToken}\n${refreshToken || this.client.getRefreshToken()}` + `${accessToken}\n${refreshToken || this.client.getRefreshToken()}`, ); } @@ -70,7 +70,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() { @@ -82,26 +82,26 @@ export default class Spotify { public async renamePlaylist(name: string) { console.log(`Renaming playlist to \"${name}\"...`); await this.client.changePlaylistDetails(process.env.PLAYLIST_ID, { - name + name, }); } public async addTracksToPlaylist(tracks: string[]) { - console.log("Adding tracks to playlist..."); + console.log('Adding tracks to playlist...'); return this.client.addTracksToPlaylist(process.env.PLAYLIST_ID, tracks); } public async clearPlaylist() { - console.log("Clearing playlist..."); + console.log('Clearing playlist...'); const response = await this.client.getPlaylistTracks( - process.env.PLAYLIST_ID + process.env.PLAYLIST_ID, ); const tracks = response.body.items.map(item => ({ - uri: item.track.uri + uri: item.track.uri, })); return this.client.removeTracksFromPlaylist( process.env.PLAYLIST_ID, - tracks + tracks, ); } }