Skip to content

Commit

Permalink
feat(spotify): add ban and unban commands (#4009)
Browse files Browse the repository at this point in the history
* feat(spotify): add ban and unban commands

* add missing migrations

* add ban unban docs
  • Loading branch information
sogehige committed Jul 29, 2020
1 parent 42366bb commit 03bcf10
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 17 deletions.
34 changes: 34 additions & 0 deletions docs/integrations/spotify.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,37 @@ Current integration is enabling `$spotifySong` and song requests(PREMIUM) from S
<strong>bot:</strong> @testuser, you requested song
Circle of Life - 『ライオン・キング』より from Carmen Twillie
</blockquote>

### Ban current song through !spotify ban command

`!spotify ban`

!> Default permission is **DISABLED**

#### Examples

<blockquote>
<strong>testuser:</strong> !spotify ban<br>
<strong>bot:</strong> @testuser, song
Circle of Life - 『ライオン・キング』より from Carmen Twillie was banned.
</blockquote>

### Unban song through !spotify unban command

`!spotify unban <spotifyURI>` or `!spotify unban <song link>`

!> Default permission is **DISABLED**

#### Parameters

- `<spotifyURI>` - spotify URI of a song you want to unban, e.g. `spotify:track:14Vp3NpYyRP3cTu8XkubfS`
- `<song link>` - song link, e.g.
`https://open.spotify.com/track/14Vp3NpYyRP3cTu8XkubfS?si=7vJWxZJdRu2VsBdvcVdAuA`

#### Examples

<blockquote>
<strong>testuser:</strong> !spotify unban spotify:track:0GrhBz0am9KFJ20MN9o6Lp<br>
<strong>bot:</strong> @testuser, song
Circle of Life - 『ライオン・キング』より from Carmen Twillie was unbanned.
</blockquote>
7 changes: 6 additions & 1 deletion locales/en/integrations/spotify.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"song-not-found": "Sorry, $sender, track was not found on spotify",
"song-requested": "$sender, you requested song $name from $artist"
"song-requested": "$sender, you requested song $name from $artist",
"not-banned-song-not-playing": "$sender, no song is currently playing to ban.",
"song-banned": "$sender, song $name from $artist is banned.",
"song-unbanned": "$sender, song $name from $artist is unbanned.",
"song-not-found-in-banlist": "$sender, song by spotifyURI $uri was not found in ban list.",
"cannot-request-song-is-banned": "$sender, cannot request banned song $name from $artist."
}
1 change: 1 addition & 0 deletions locales/en/ui.menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"queue": "Queue",
"playlist": "Playlist",
"bannedsongs": "Banned songs",
"spotifybannedsongs": "Spotify banned songs",
"duel": "Duel",
"fightme": "FightMe",
"seppuku": "Seppuku",
Expand Down
16 changes: 16 additions & 0 deletions src/bot/database/entity/spotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EntitySchema } from 'typeorm';

export interface SpotifySongBanInterface {
spotifyUri: string;
title: string;
artists: string[];
}

export const SpotifySongBan = new EntitySchema<Readonly<Required<SpotifySongBanInterface>>>({
name: 'spotify_song_ban',
columns: {
spotifyUri: { type: String, primary: true },
title: { type: String },
artists: { type: 'simple-array' },
},
});
14 changes: 14 additions & 0 deletions src/bot/database/migration/mysql/1596014060721-spotifyBanList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from 'typeorm';

export class spotifyBanList1596014060721 implements MigrationInterface {
name = 'spotifyBanList1596014060721';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE TABLE `spotify_song_ban` (`spotifyUri` varchar(255) NOT NULL, `title` varchar(255) NOT NULL, `artists` text NOT NULL, PRIMARY KEY (`spotifyUri`)) ENGINE=InnoDB');
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE `spotify_song_ban`');
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from 'typeorm';

export class spotifyBanList1596014156038 implements MigrationInterface {
name = 'spotifyBanList1596014156038';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "spotify_song_ban" ("spotifyUri" character varying NOT NULL, "title" character varying NOT NULL, "artists" text NOT NULL, CONSTRAINT "PK_f9ba62ed678a1e426db17acc387" PRIMARY KEY ("spotifyUri"))`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "spotify_song_ban"`);
}

}
14 changes: 14 additions & 0 deletions src/bot/database/migration/sqlite/1595974777207-spotifyBanList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from 'typeorm';

export class spotifyBanList1595974777207 implements MigrationInterface {
name = 'spotifyBanList1595974777207';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "spotify_song_ban" ("spotifyUri" varchar PRIMARY KEY NOT NULL, "title" varchar NOT NULL, "artists" text NOT NULL)`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "spotify_song_ban"`);
}

}
150 changes: 134 additions & 16 deletions src/bot/integrations/spotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import api from '../api';
import { addUIError } from '../panel';
import { HOUR } from '../constants';
import { ioServer } from '../helpers/panel';
import { getRepository } from 'typeorm';
import { SpotifySongBan } from '../database/entity/spotify';

type SpotifyTrack = {
uri: string; name: string; artists: { name: string }[]
Expand Down Expand Up @@ -97,6 +99,7 @@ class Spotify extends Integration {
super();

this.addWidget('spotify', 'widget-title-spotify', 'fab fa-spotify');
this.addMenu({ category: 'manage', name: 'spotifybannedsongs', id: 'manage/spotify/bannedsongs', this: this });

if (isMainThread) {
this.timeouts.IRefreshToken = global.setTimeout(() => this.IRefreshToken(), 60000);
Expand Down Expand Up @@ -228,6 +231,57 @@ class Spotify extends Integration {
this.cSkipSong();
callback(null);
});
adminEndpoint(this.nsp, 'spotify::addBan', async (spotifyUri, cb) => {
try {
if (!this.client) {
addUIError({ name: 'Spotify Ban Import', message: 'You are not connected to spotify API, authorize your user' });
throw Error('client');
}
let id = '';
if (spotifyUri.startsWith('spotify:')) {
id = spotifyUri.replace('spotify:track:', '');
} else {
const regex = new RegExp('\\S+open\\.spotify\\.com\\/track\\/(\\w+)(.*)?', 'gi');
const exec = regex.exec(spotifyUri as unknown as string);
if (exec) {
id = exec[1];
} else {
throw Error('ID was not found in ' + spotifyUri);
}
}

const response = await axios({
method: 'get',
url: 'https://api.spotify.com/v1/tracks/' + id,
headers: {
'Authorization': 'Bearer ' + this.client.getAccessToken(),
},
});
const track = response.data as SpotifyTrack;
await getRepository(SpotifySongBan).save({
artists: track.artists.map(o => o.name), spotifyUri: track.uri, title: track.name,
});
} catch (e) {
if (e.message !== 'client') {
addUIError({ name: 'Spotify Ban Import', message: 'Something went wrong with banning song. Check your spotifyURI.' });
}
}
if (cb) {
cb(null, null);
}
});
adminEndpoint(this.nsp, 'spotify::deleteBan', async (where, cb) => {
where = where || {};
if (cb) {
cb(null, await getRepository(SpotifySongBan).delete(where));
}
});
adminEndpoint(this.nsp, 'spotify::getAllBanned', async (where, cb) => {
where = where || {};
if (cb) {
cb(null, await getRepository(SpotifySongBan).find(where));
}
});
adminEndpoint(this.nsp, 'spotify::code', async (token, cb) => {
const waitForUsername = () => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -377,6 +431,45 @@ class Spotify extends Integration {
return this.client.createAuthorizeURL(this.scopes, state) + '&show_dialog=true';
}

@command('!spotify unban')
@default_permission(null)
async unban (opts: CommandOptions): Promise<CommandResponse[]> {
try {
const songToUnban = await getRepository(SpotifySongBan).findOneOrFail({ where: { spotifyUri: opts.parameters }});
await getRepository(SpotifySongBan).delete({ spotifyUri: opts.parameters });
return [{ response: prepare('integrations.spotify.song-unbanned', {
artist: songToUnban.artists[0], uri: songToUnban.spotifyUri, name: songToUnban.title,
}), ...opts }];
} catch (e) {
return [{ response: prepare('integrations.spotify.song-not-found-in-banlist', {
uri: opts.parameters,
}), ...opts }];
}
}

@command('!spotify ban')
@default_permission(null)
async ban (opts: CommandOptions): Promise<CommandResponse[]> {
if (!this.client) {
error(`${chalk.bgRed('SPOTIFY')}: you are not connected to spotify API, authorize your user.`);
return [];
}

// ban current playing song only
const currentSong: any = JSON.parse(this.currentSong);
if (Object.keys(currentSong).length === 0) {
return [{ response: prepare('integrations.spotify.not-banned-song-not-playing'), ...opts }];
} else {
await getRepository(SpotifySongBan).save({
artists: currentSong.artists.split(', '), spotifyUri: currentSong.uri, title: currentSong.song,
});
this.cSkipSong();
return [{ response: prepare('integrations.spotify.song-banned', {
artists: currentSong.artists, artist: currentSong.artist, uri: currentSong.uri, name: currentSong.song,
}), ...opts }];
}
}

@command('!spotify')
@default_permission(null)
async main (opts: CommandOptions): Promise<CommandResponse[]> {
Expand Down Expand Up @@ -419,10 +512,15 @@ class Spotify extends Integration {
},
});
const track = response.data as SpotifyTrack;
await this.requestSongByAPI(track.uri);
return [{ response: prepare('integrations.spotify.song-requested', {
name: track.name, artist: track.artists[0].name, artists: track.artists.map(o => o.name).join(', '),
}), ...opts }];
if(await this.requestSongByAPI(track.uri)) {
return [{ response: prepare('integrations.spotify.song-requested', {
name: track.name, artist: track.artists[0].name, artists: track.artists.map(o => o.name).join(', '),
}), ...opts }];
} else {
return [{ response: prepare('integrations.spotify.cannot-request-song-is-banned', {
name: track.name, artist: track.artists[0].name, artists: track.artists.map(o => o.name).join(', '),
}), ...opts }];
}
} else {
const response = await axios({
method: 'get',
Expand All @@ -433,10 +531,15 @@ class Spotify extends Integration {
},
});
const track = (response.data.tracks.items[0] as SpotifyTrack);
await this.requestSongByAPI(track.uri);
return [{ response: prepare('integrations.spotify.song-requested', {
name: track.name, artist: track.artists[0].name,
}), ...opts }];
if(await this.requestSongByAPI(track.uri)) {
return [{ response: prepare('integrations.spotify.song-requested', {
name: track.name, artist: track.artists[0].name,
}), ...opts }];
} else {
return [{ response: prepare('integrations.spotify.song-is-banned', {
name: track.name, artist: track.artists[0].name,
}), ...opts }];
}
}
} catch (e) {
if (e.response.status === 401) {
Expand All @@ -450,14 +553,29 @@ class Spotify extends Integration {

async requestSongByAPI(uri: string) {
if (this.client) {
const queueResponse = await axios({
method: 'post',
url: 'https://api.spotify.com/v1/me/player/queue?uri=' + uri,
headers: {
'Authorization': 'Bearer ' + this.client.getAccessToken(),
},
});
ioServer?.emit('api.stats', { method: 'POST', data: queueResponse.data, timestamp: Date.now(), call: 'spotify::queue', api: 'other', endpoint: 'https://api.spotify.com/v1/me/player/queue?uri=' + uri, code: queueResponse.status });
try {
const isSongBanned = (await getRepository(SpotifySongBan).count({ where: { spotifyUri: uri }})) > 0;
if (isSongBanned) {
throw new Error('Song is banned');
}

const queueResponse = await axios({
method: 'post',
url: 'https://api.spotify.com/v1/me/player/queue?uri=' + uri,
headers: {
'Authorization': 'Bearer ' + this.client.getAccessToken(),
},
});
ioServer?.emit('api.stats', { method: 'POST', data: queueResponse.data, timestamp: Date.now(), call: 'spotify::queue', api: 'other', endpoint: 'https://api.spotify.com/v1/me/player/queue?uri=' + uri, code: queueResponse.status });
return true;
} catch (e) {
if (!e.isAxiosError) {
return false;
} else {
// rethrow error
throw(e);
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/panel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const main = async () => {
{ path: '/manage/ranks/edit/:id?', name: 'ranksManagerEdit', component: () => import('./views/managers/ranks/ranks-edit.vue') },
{ path: '/manage/songs/playlist', name: 'songsManagerPlaylist', component: () => import('./views/managers/songs/songs-playlist.vue') },
{ path: '/manage/songs/bannedsongs', name: 'songsManagerBannedsongs', component: () => import('./views/managers/songs/songs-bannedsongs.vue') },
{ path: '/manage/spotify/bannedsongs', name: 'spotifyManagerBannedsongs', component: () => import('./views/managers/spotify/spotify-bannedsongs.vue') },
{ path: '/manage/timers/list', name: 'TimersManagerList', component: () => import('./views/managers/timers/timers-list.vue') },
{ path: '/manage/timers/edit/:id?', name: 'TimersManagerEdit', component: () => import('./views/managers/timers/timers-edit.vue') },
{ path: '/manage/viewers/list', name: 'viewersManagerList', component: () => import('./views/managers/viewers/viewers-list.vue') },
Expand Down

0 comments on commit 03bcf10

Please sign in to comment.