Skip to content

Commit

Permalink
Tidy ups
Browse files Browse the repository at this point in the history
  • Loading branch information
niksudan committed Oct 29, 2019
1 parent 753dee3 commit 90a02da
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 66 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
15 changes: 9 additions & 6 deletions src/index.ts → src/generate-playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
60 changes: 29 additions & 31 deletions src/lib/ninbot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Message>;

Expand All @@ -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})`,
);
}

Expand All @@ -44,18 +42,18 @@ export default class Ninbot {
channel: TextChannel,
fromDate: moment.Moment,
toDate: moment.Moment,
messages: Messages = new Collection<string, Message>()
messages: Messages = new Collection<string, Message>(),
): Promise<Messages> {
if (fromDate.isAfter(toDate)) {
return messages;
}

// 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
Expand All @@ -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),
);
}

Expand All @@ -83,56 +81,56 @@ 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;
}

// 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(
match.map(
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
Expand All @@ -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}`,
);
}
}
16 changes: 8 additions & 8 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
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) {
const app = express();
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);
}
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);
Expand All @@ -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')}`,
);
});
}
Expand Down
36 changes: 18 additions & 18 deletions src/lib/spotify.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -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) {
Expand All @@ -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(),
);
}

Expand All @@ -56,7 +56,7 @@ export default class Spotify {
}
fs.writeFileSync(
this.authTokenFilepath,
`${accessToken}\n${refreshToken || this.client.getRefreshToken()}`
`${accessToken}\n${refreshToken || this.client.getRefreshToken()}`,
);
}

Expand All @@ -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() {
Expand All @@ -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,
);
}
}

0 comments on commit 90a02da

Please sign in to comment.