From 46d9a58913ca4adabe2fc2bc6d32745476627195 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Mon, 3 Jun 2024 13:44:07 -0400 Subject: [PATCH 1/7] Added more refresh token fixes/overload protection --- src/firebot/events/spotifyEventSource.ts | 10 ++++ src/firebot/variables/index.ts | 4 ++ .../variables/spotifyPlayerRelativeVolume.ts | 13 +++++ src/firebot/variables/spotifyPlayerVolume.ts | 12 ++++ src/spotifyIntegration.ts | 23 ++++++-- src/utils/spotify/api.ts | 57 +++++++++++++------ src/utils/spotify/auth.ts | 7 +-- src/utils/spotify/player/index.ts | 26 ++++++++- 8 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 src/firebot/variables/spotifyPlayerRelativeVolume.ts create mode 100644 src/firebot/variables/spotifyPlayerVolume.ts diff --git a/src/firebot/events/spotifyEventSource.ts b/src/firebot/events/spotifyEventSource.ts index 857039e..8f0959e 100644 --- a/src/firebot/events/spotifyEventSource.ts +++ b/src/firebot/events/spotifyEventSource.ts @@ -24,6 +24,16 @@ export const SpotifyEventSource = { getMessage: () => "Spotify playback state changed", }, }, + { + id: "volume-changed", + name: "Volume Changed", + description: "Spotify volume changed", + cached: false, + activityFeed: { + icon: "fab fa-spotify", + getMessage: () => "Spotify volume changed", + }, + }, { id: "track-changed", name: "Track Changed", diff --git a/src/firebot/variables/index.ts b/src/firebot/variables/index.ts index f2afefc..268139c 100644 --- a/src/firebot/variables/index.ts +++ b/src/firebot/variables/index.ts @@ -1,4 +1,6 @@ import { SpotifyIsPlayingVariable } from "./spotifyIsPlaying"; +import { SpotifyPlayerVolumeVariable } from "./spotifyPlayerVolume"; +import { SpotifyPlayerRelativeVolumeVariable } from "./spotifyPlayerRelativeVolume"; import { SpotifyTrackArtistVariable } from "./spotifyTrackArtist"; import { SpotifyTrackArtistsVariable } from "./spotifyTrackArtists"; import { SpotifyTrackAlbumVariable } from "./spotifyTrackAlbum"; @@ -13,6 +15,8 @@ import { SpotifyTrackRelativePositionVariable } from "./spotifyTrackRelativePosi export const AllSpotifyReplaceVariables = [ SpotifyIsPlayingVariable, + SpotifyPlayerVolumeVariable, + SpotifyPlayerRelativeVolumeVariable, SpotifyTrackArtistVariable, SpotifyTrackArtistsVariable, SpotifyTrackAlbumVariable, diff --git a/src/firebot/variables/spotifyPlayerRelativeVolume.ts b/src/firebot/variables/spotifyPlayerRelativeVolume.ts new file mode 100644 index 0000000..a278c9d --- /dev/null +++ b/src/firebot/variables/spotifyPlayerRelativeVolume.ts @@ -0,0 +1,13 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlayerRelativeVolumeVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlayerRelativeVolume", + description: + "Gets the relative volume of the active Spotify Device as a value from 0.0 to 1.0", + usage: "spotifyPlayerRelativeVolume", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.volume / 100, +}; diff --git a/src/firebot/variables/spotifyPlayerVolume.ts b/src/firebot/variables/spotifyPlayerVolume.ts new file mode 100644 index 0000000..da10657 --- /dev/null +++ b/src/firebot/variables/spotifyPlayerVolume.ts @@ -0,0 +1,12 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlayerVolumeVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlayerVolume", + description: "Gets the volume of the active Spotify device", + usage: "spotifyPlayerVolume", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.volume, +}; diff --git a/src/spotifyIntegration.ts b/src/spotifyIntegration.ts index 205fecc..6ce8eef 100644 --- a/src/spotifyIntegration.ts +++ b/src/spotifyIntegration.ts @@ -29,7 +29,7 @@ let spotifyDefinition: IntegrationDefinition | null = null; export class SpotifyIntegration extends EventEmitter { connected: boolean = false; - currentTrack: SpotifyTrackDetails | null = null; + expiresAt: number | null = null; constructor(client: ClientCredentials) { super(); @@ -67,9 +67,19 @@ export class SpotifyIntegration extends EventEmitter { logger.info("Unlinking from Spotify Integration..."); } - async refreshToken(): Promise { + async refreshToken(): Promise { try { - logger.info("Refreshing Spotify Token..."); + const currentAuth = getSpotifyAuthFromIntegration(); + + if ( + currentAuth.access_token && + this.expiresAt && + this.expiresAt - Date.now() > 5000 + ) { + return currentAuth; + } + + logger.info("Token expired, refreshing..."); // @ts-ignore const { authProviderDetails: authProvider } = spotifyDefinition; @@ -107,9 +117,14 @@ export class SpotifyIntegration extends EventEmitter { const data = (await response.json()) as SpotifyRefreshTokenResponse; data.refresh_token = auth.refresh_token; + this.expiresAt = Date.now() + data.expires_in * 1000; + logger.info( + `New token expires at ${new Date(this.expiresAt).toUTCString()}` + ); + updateIntegrationAuth(data); - return data; + return getSpotifyAuthFromIntegration(); } } catch (error) { logger.error("Error refreshing Spotify token", error); diff --git a/src/utils/spotify/api.ts b/src/utils/spotify/api.ts index 572df24..43206d4 100644 --- a/src/utils/spotify/api.ts +++ b/src/utils/spotify/api.ts @@ -1,19 +1,21 @@ -import { logger } from "@utils/firebot"; +import { chatFeedAlert, logger } from "@utils/firebot"; import { SpotifyService } from "."; import ResponseError from "@/models/responseError"; -const baseUrl = "https://api.spotify.com/v1"; +type SpotifyRateLimits = { + [endpoint: string]: number; +}; export default class SpotifyApiService { private readonly spotify: SpotifyService; - private retryAfter: number | null = null; + private rateLimits: SpotifyRateLimits = {}; constructor(spotifyService: SpotifyService) { this.spotify = spotifyService; } public readonly baseUrl = "https://api.spotify.com/v1"; - public getUrlFromPath = (path: string): string => `${baseUrl}${path}`; + public getUrlFromPath = (path: string): string => `${this.baseUrl}${path}`; public async fetch( endpoint: string, @@ -21,10 +23,15 @@ export default class SpotifyApiService { options?: any ) { try { - if (this.retryAfter && Date.now() < this.retryAfter) { + const sanitizedEndpoint = endpoint.split("?")[0]; + + if ( + this.rateLimits.hasOwnProperty(sanitizedEndpoint) && + Date.now() < this.rateLimits[sanitizedEndpoint] + ) { throw new Error( - `API Rate Limit Exceeded, will be able to use again after ${new Date( - this.retryAfter + `API endpoint ${endpoint} Rate Limit Exceeded, will be able to use again after ${new Date( + this.rateLimits[sanitizedEndpoint] ).toUTCString()}` ); } @@ -40,16 +47,28 @@ export default class SpotifyApiService { }); if (!response.ok) { - if (response.status === 429) { - const retryAfter = response.headers.get("retry-after"); - this.retryAfter = - Date.now() + (retryAfter ? parseInt(retryAfter) : 3600) * 1000; + switch (response.status) { + case 401: + throw new ResponseError( + `Spotify API endpoint ${endpoint} responded Unauthorized, try unlinking and relinking Spotify to generate a new Access/Refresh token pair`, + response + ); + case 429: + const retryAfter = response.headers.get("retry-after"); + this.rateLimits[sanitizedEndpoint] = + Date.now() + (retryAfter ? parseInt(retryAfter) : 3600) * 1000; + throw new ResponseError( + `Spotify API endpoint ${endpoint} responded Rate Limit Exceeded, will be able to use again after ${new Date( + this.rateLimits[sanitizedEndpoint] + ).toUTCString()}`, + response + ); + default: + throw new ResponseError( + `Spotify API ${endpoint} returned status ${response.status}`, + response + ); } - - throw new ResponseError( - `Spotify API /v1/${endpoint} returned status ${response.status}`, - response - ); } return { @@ -58,6 +77,12 @@ export default class SpotifyApiService { data: response.status === 204 ? null : ((await response.json()) as T), }; } catch (error) { + let message = + error instanceof Error + ? error.message + : "Unspecified Error with Spotify API request " + endpoint; + chatFeedAlert(message); + logger.error("Error making Spotify API request", error); throw error; } diff --git a/src/utils/spotify/auth.ts b/src/utils/spotify/auth.ts index 2161312..282aaac 100644 --- a/src/utils/spotify/auth.ts +++ b/src/utils/spotify/auth.ts @@ -7,14 +7,10 @@ export default class SpotifyAuthService { private spotify: SpotifyService; private expiresAt: number = 0; - //#region Constructor - constructor(spotifyService: SpotifyService) { this.spotify = spotifyService; } - //#endregion - //#region Getters /** @@ -71,7 +67,8 @@ export default class SpotifyAuthService { private async tokenExpiredAsync(accessToken: string | undefined) { if (!accessToken) return true; - if (!this.expiresAt || this.expiresAt - Date.now() < 5000) return true; + if (this.expiresAt && this.expiresAt - Date.now() > 5000) return false; + // Check against API just in case of config issue return !(await this.spotifyIsConnectedAsync(accessToken)); } diff --git a/src/utils/spotify/player/index.ts b/src/utils/spotify/player/index.ts index 53a8597..9489f04 100644 --- a/src/utils/spotify/player/index.ts +++ b/src/utils/spotify/player/index.ts @@ -1,4 +1,4 @@ -import { effectManager, eventManager, logger } from "@utils/firebot"; +import { eventManager, logger } from "@utils/firebot"; import { SpotifyService } from "@utils/spotify"; import SpotifyQueueService from "./queue"; import { delay } from "@/utils/timing"; @@ -30,6 +30,7 @@ export default class SpotifyPlayerService { private _progressMs: number = 0; private _isPlaying: boolean = false; private _track: SpotifyTrackDetails | null = null; + private _volume: number = 0; constructor(spotifyService: SpotifyService) { this.spotify = spotifyService; @@ -52,6 +53,15 @@ export default class SpotifyPlayerService { return this._isPlaying; } + /** + * Gets the volume of the user's Spotify player. + * + * @return {number} The volume of the player, between 0 and 100. + */ + public get volume(): number { + return this._volume; + } + /** * Gets the currently playing track summary, or null if no track is playing. * @@ -265,10 +275,17 @@ export default class SpotifyPlayerService { if (volume < 0 || volume > 100 || volume % 1 !== 0) throw new Error("Spotify volume must be an integer between 0 and 100"); - await this.spotify.api.fetch( + const response = await this.spotify.api.fetch( `/me/player/volume?volume_percent=${volume}`, "PUT" ); + + if (response.ok) { + this._volume = volume; + eventManager.triggerEvent("oceanity-spotify", "volume-changed", { + volume: this._volume, + }); + } } catch (error) { logger.error("Error setting Spotify volume", error); } @@ -327,6 +344,11 @@ export default class SpotifyPlayerService { this._isPlaying = state.is_playing; } + if (this._volume != state.device.volume_percent) { + eventManager.triggerEvent("oceanity-spotify", "volume-changed", {}); + this._volume = state.device.volume_percent; + } + this._progressMs = state.progress_ms; const nextTrack = state.item; From f56c3165ff28a7648cb72aa267c1f056db65d41b Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Mon, 3 Jun 2024 14:01:26 -0400 Subject: [PATCH 2/7] Log on refresh, bumped version --- package.json | 2 +- src/main.ts | 2 +- src/utils/spotify/auth.ts | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fcefc6b..a690185 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "firebot-song-requests", "scriptOutputName": "oceanitySpotifyIntegration", - "version": "0.6.3", + "version": "0.6.4", "description": "Adds Spotify Song Requests to Firebot", "main": "", "scripts": { diff --git a/src/main.ts b/src/main.ts index 81527d8..568c5b9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ const script: Firebot.CustomScript = { name: "Firebot Spotify Integrations", description: "Let your viewers determine your taste in music", author: "Oceanity", - version: "0.6.3", + version: "0.6.4", firebotVersion: "5", }; }, diff --git a/src/utils/spotify/auth.ts b/src/utils/spotify/auth.ts index 282aaac..0f8c74a 100644 --- a/src/utils/spotify/auth.ts +++ b/src/utils/spotify/auth.ts @@ -1,4 +1,4 @@ -import { integrationManager, logger } from "@utils/firebot"; +import { chatFeedAlert, integrationManager, logger } from "@utils/firebot"; import { integrationId } from "@/main"; import { integration } from "@/spotifyIntegration"; import { SpotifyService } from "@utils/spotify"; @@ -54,8 +54,11 @@ export default class SpotifyAuthService { } this.expiresAt = Date.now() + refreshResponse.expires_in * 1000; - logger.info( - `New token expires at ${new Date(this.expiresAt).toUTCString()}` + + chatFeedAlert( + `Refreshed Spotify Token. New Token will expire at ${new Date( + this.expiresAt + ).toUTCString()}` ); return refreshResponse.access_token; From caa88a0dc336dafd2dc1c131738302c5415e902e Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Thu, 6 Jun 2024 18:44:31 -0400 Subject: [PATCH 3/7] Added spotifyTrackId and spotifyTrackUri variables for matching tracks by unique identifiers, fixed volume reverting on Spotify tick after SetVolume --- src/firebot/events/spotifyEventSource.ts | 3 ++- src/firebot/variables/index.ts | 4 ++++ src/firebot/variables/spotifyTrackId.ts | 13 +++++++++++++ src/firebot/variables/spotifyTrackUri.ts | 8 ++++---- src/firebot/variables/spotifyTrackUrl.ts | 13 +++++++++++++ src/utils/spotify/player/index.ts | 14 +++++++++++++- tsconfig.json | 3 --- webpack.config.js | 3 --- 8 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 src/firebot/variables/spotifyTrackId.ts create mode 100644 src/firebot/variables/spotifyTrackUrl.ts diff --git a/src/firebot/events/spotifyEventSource.ts b/src/firebot/events/spotifyEventSource.ts index 8f0959e..f143ecd 100644 --- a/src/firebot/events/spotifyEventSource.ts +++ b/src/firebot/events/spotifyEventSource.ts @@ -27,7 +27,8 @@ export const SpotifyEventSource = { { id: "volume-changed", name: "Volume Changed", - description: "Spotify volume changed", + description: + "Spotify volume changed, fires faster if volume is changed via Firebot", cached: false, activityFeed: { icon: "fab fa-spotify", diff --git a/src/firebot/variables/index.ts b/src/firebot/variables/index.ts index 268139c..7d2c640 100644 --- a/src/firebot/variables/index.ts +++ b/src/firebot/variables/index.ts @@ -5,8 +5,10 @@ import { SpotifyTrackArtistVariable } from "./spotifyTrackArtist"; import { SpotifyTrackArtistsVariable } from "./spotifyTrackArtists"; import { SpotifyTrackAlbumVariable } from "./spotifyTrackAlbum"; import { SpotifyTrackAlbumArtUrlVariable } from "./spotifyTrackAlbumArtUrl"; +import { SpotifyTrackIdVariable } from "./spotifyTrackId"; import { SpotifyTrackTitleVariable } from "./spotifyTrackTitle"; import { SpotifyTrackUriVariable } from "./spotifyTrackUri"; +import { SpotifyTrackUrlVariable } from "./spotifyTrackUrl"; import { SpotifyTrackDurationVariable } from "./spotifyTrackDuration"; import { SpotifyTrackDurationMsVariable } from "./spotifyTrackDurationMs"; import { SpotifyTrackPositionVariable } from "./spotifyTrackPosition"; @@ -21,8 +23,10 @@ export const AllSpotifyReplaceVariables = [ SpotifyTrackArtistsVariable, SpotifyTrackAlbumVariable, SpotifyTrackAlbumArtUrlVariable, + SpotifyTrackIdVariable, SpotifyTrackTitleVariable, SpotifyTrackUriVariable, + SpotifyTrackUrlVariable, SpotifyTrackDurationVariable, SpotifyTrackDurationMsVariable, SpotifyTrackPositionVariable, diff --git a/src/firebot/variables/spotifyTrackId.ts b/src/firebot/variables/spotifyTrackId.ts new file mode 100644 index 0000000..ed5190c --- /dev/null +++ b/src/firebot/variables/spotifyTrackId.ts @@ -0,0 +1,13 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyTrackIdVariable: ReplaceVariable = { + definition: { + handle: "spotifyTrackId", + description: + "Gets the Id of the currently playing track on Spotify or empty string if not playing", + usage: "spotifyTrackId", + possibleDataOutput: ["text"], + }, + evaluator: async () => spotify.player.track?.id ?? "", +}; diff --git a/src/firebot/variables/spotifyTrackUri.ts b/src/firebot/variables/spotifyTrackUri.ts index 7390431..853f432 100644 --- a/src/firebot/variables/spotifyTrackUri.ts +++ b/src/firebot/variables/spotifyTrackUri.ts @@ -3,11 +3,11 @@ import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/type export const SpotifyTrackUriVariable: ReplaceVariable = { definition: { - handle: "spotifyTrackUrl", + handle: "spotifyTrackUri", description: - "Gets the url of the currently playing track on Spotify or empty string if not playing", - usage: "spotifyTrackUrl", + "Gets the unique Uri of the currently playing track on Spotify or empty string if not playing", + usage: "spotifyTrackUri", possibleDataOutput: ["text"], }, - evaluator: async () => spotify.player.track?.url ?? "", + evaluator: async () => spotify.player.track?.uri ?? "", }; diff --git a/src/firebot/variables/spotifyTrackUrl.ts b/src/firebot/variables/spotifyTrackUrl.ts new file mode 100644 index 0000000..cfb6042 --- /dev/null +++ b/src/firebot/variables/spotifyTrackUrl.ts @@ -0,0 +1,13 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyTrackUrlVariable: ReplaceVariable = { + definition: { + handle: "spotifyTrackUrl", + description: + "Gets the shareable Url of the currently playing track on Spotify or empty string if not playing", + usage: "spotifyTrackUrl", + possibleDataOutput: ["text"], + }, + evaluator: async () => spotify.player.track?.url ?? "", +}; diff --git a/src/utils/spotify/player/index.ts b/src/utils/spotify/player/index.ts index 9489f04..a0c0a39 100644 --- a/src/utils/spotify/player/index.ts +++ b/src/utils/spotify/player/index.ts @@ -5,6 +5,8 @@ import { delay } from "@/utils/timing"; import { msToFormattedString } from "@/utils/strings"; type SpotifyTrackSummary = { + id: string; + uri: string; title: string; artists: string[]; album: string; @@ -31,6 +33,7 @@ export default class SpotifyPlayerService { private _isPlaying: boolean = false; private _track: SpotifyTrackDetails | null = null; private _volume: number = 0; + private _targetVolume: number = -1; constructor(spotifyService: SpotifyService) { this.spotify = spotifyService; @@ -78,6 +81,8 @@ export default class SpotifyPlayerService { const albumArtUrl = this.getBiggestImage(this._track.album.images).url; return { + id: this._track.id, + uri: this._track.uri, title: this._track.name, artists, album: this._track.album.name, @@ -282,6 +287,7 @@ export default class SpotifyPlayerService { if (response.ok) { this._volume = volume; + this._targetVolume = volume; eventManager.triggerEvent("oceanity-spotify", "volume-changed", { volume: this._volume, }); @@ -344,9 +350,15 @@ export default class SpotifyPlayerService { this._isPlaying = state.is_playing; } - if (this._volume != state.device.volume_percent) { + // If target volume, user has manually changed volume and we don't want it falling back + if ( + this._volume != state.device.volume_percent && + this._targetVolume === -1 + ) { eventManager.triggerEvent("oceanity-spotify", "volume-changed", {}); this._volume = state.device.volume_percent; + } else if (state.device.volume_percent === this._targetVolume) { + this._targetVolume = -1; } this._progressMs = state.progress_ms; diff --git a/tsconfig.json b/tsconfig.json index 884098d..1a48d6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,12 +17,9 @@ "downlevelIteration": true, "paths": { "@/*": ["./src/*"], - "@effects*": ["./src/firebot/effects/*"], - "@events*": ["./src/firebot/events/*"], "@models/*": ["./src/models/*"], "@utils/*": ["./src/utils/*"], "@shared/*": ["./src/shared/*"], - "@variables/*": ["./src/firebot/variables/*"], "ts-jest/*": ["./node_modules/ts-jest/dist/*"] }, "lib": ["es5", "es6", "dom", "dom.iterable"], diff --git a/webpack.config.js b/webpack.config.js index a2c9f22..0633755 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,9 +22,6 @@ module.exports = { "@models": path.resolve(__dirname, "./src/models"), "@utils": path.resolve(__dirname, "./src/utils"), "@shared": path.resolve(__dirname, "./src/shared"), - "@effects": path.resolve(__dirname, "./src/firebot/effects"), - "@events": path.resolve(__dirname, "./src/firebot/events"), - "@variables": path.resolve(__dirname, "./src/firebot/variables"), }, }, module: { From 894b58c69a46a2c9183d298c2a220be4ad095b37 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Fri, 7 Jun 2024 17:29:25 -0400 Subject: [PATCH 4/7] Added tons of Playlist goodies, restructured Replace Variables to organize thigns a bit --- package-lock.json | 35 +++++++- package.json | 2 + src/firebot/events/spotifyEventSource.ts | 16 +++- src/firebot/variables/index.ts | 38 ++------- src/firebot/variables/player/index.ts | 9 ++ .../{ => player}/spotifyIsPlaying.ts | 0 .../spotifyPlayerRelativeVolume.ts | 0 .../{ => player}/spotifyPlayerVolume.ts | 0 src/firebot/variables/playlist/index.ts | 15 ++++ .../playlist/spotifyIsPlaylistActive.ts | 12 +++ .../playlist/spotifyPlaylistCoverImageUrl.ts | 13 +++ .../playlist/spotifyPlaylistDescription.ts | 13 +++ .../playlist/spotifyPlaylistLength.ts | 12 +++ .../variables/playlist/spotifyPlaylistName.ts | 12 +++ .../variables/playlist/spotifyPlaylistUrl.ts | 12 +++ src/firebot/variables/track/index.ts | 29 +++++++ .../{ => track}/spotifyTrackAlbum.ts | 0 .../{ => track}/spotifyTrackAlbumArtUrl.ts | 0 .../{ => track}/spotifyTrackArtist.ts | 0 .../{ => track}/spotifyTrackArtists.ts | 0 .../{ => track}/spotifyTrackDuration.ts | 0 .../{ => track}/spotifyTrackDurationMs.ts | 0 .../variables/{ => track}/spotifyTrackId.ts | 0 .../{ => track}/spotifyTrackPosition.ts | 0 .../{ => track}/spotifyTrackPositionMs.ts | 0 .../spotifyTrackRelativePosition.ts | 0 .../{ => track}/spotifyTrackTitle.ts | 0 .../variables/{ => track}/spotifyTrackUri.ts | 0 .../variables/{ => track}/spotifyTrackUrl.ts | 0 src/types/spotify.d.ts | 76 +++++++++++------ src/utils/array.ts | 2 + src/utils/spotify/index.ts | 2 +- src/utils/spotify/player/index.ts | 42 +++++++++- src/utils/spotify/player/playlist.ts | 84 +++++++++++++++++++ 34 files changed, 358 insertions(+), 66 deletions(-) create mode 100644 src/firebot/variables/player/index.ts rename src/firebot/variables/{ => player}/spotifyIsPlaying.ts (100%) rename src/firebot/variables/{ => player}/spotifyPlayerRelativeVolume.ts (100%) rename src/firebot/variables/{ => player}/spotifyPlayerVolume.ts (100%) create mode 100644 src/firebot/variables/playlist/index.ts create mode 100644 src/firebot/variables/playlist/spotifyIsPlaylistActive.ts create mode 100644 src/firebot/variables/playlist/spotifyPlaylistCoverImageUrl.ts create mode 100644 src/firebot/variables/playlist/spotifyPlaylistDescription.ts create mode 100644 src/firebot/variables/playlist/spotifyPlaylistLength.ts create mode 100644 src/firebot/variables/playlist/spotifyPlaylistName.ts create mode 100644 src/firebot/variables/playlist/spotifyPlaylistUrl.ts create mode 100644 src/firebot/variables/track/index.ts rename src/firebot/variables/{ => track}/spotifyTrackAlbum.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackAlbumArtUrl.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackArtist.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackArtists.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackDuration.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackDurationMs.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackId.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackPosition.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackPositionMs.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackRelativePosition.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackTitle.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackUri.ts (100%) rename src/firebot/variables/{ => track}/spotifyTrackUrl.ts (100%) create mode 100644 src/utils/array.ts create mode 100644 src/utils/spotify/player/playlist.ts diff --git a/package-lock.json b/package-lock.json index 982b998..d72772e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "firebot-song-requests", - "version": "0.5.0", + "version": "0.6.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebot-song-requests", - "version": "0.5.0", + "version": "0.6.4", "license": "GNU3", "devDependencies": { "@crowbartools/firebot-custom-scripts-types": "^5.60.1", "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4", + "@types/he": "^1.2.3", "@types/jest": "^29.5.12", "@types/node": "^18.18.2", "@types/webpack": "^5.28.5", @@ -22,6 +23,7 @@ "eslint-config-prettier": "^9.1.0", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", + "he": "^1.2.0", "jest": "^29.7.0", "node-json-db": "^2.3.0", "terser-webpack-plugin": "^5.3.10", @@ -1674,6 +1676,13 @@ "@types/node": "*" } }, + "node_modules/@types/he": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4142,6 +4151,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8855,6 +8874,12 @@ "@types/node": "*" } }, + "@types/he": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -10672,6 +10697,12 @@ "function-bind": "^1.1.2" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/package.json b/package.json index a690185..404a09e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@crowbartools/firebot-custom-scripts-types": "^5.60.1", "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4", + "@types/he": "^1.2.3", "@types/jest": "^29.5.12", "@types/node": "^18.18.2", "@types/webpack": "^5.28.5", @@ -26,6 +27,7 @@ "eslint-config-prettier": "^9.1.0", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", + "he": "^1.2.0", "jest": "^29.7.0", "node-json-db": "^2.3.0", "terser-webpack-plugin": "^5.3.10", diff --git a/src/firebot/events/spotifyEventSource.ts b/src/firebot/events/spotifyEventSource.ts index f143ecd..f94c8ec 100644 --- a/src/firebot/events/spotifyEventSource.ts +++ b/src/firebot/events/spotifyEventSource.ts @@ -16,7 +16,7 @@ export const SpotifyEventSource = { }, { id: "playback-state-changed", - name: "Playback State Changed", + name: "Spotify Playback State Changed", description: "Spotify playback state changed", cached: false, activityFeed: { @@ -26,7 +26,7 @@ export const SpotifyEventSource = { }, { id: "volume-changed", - name: "Volume Changed", + name: "Spotify Volume Changed", description: "Spotify volume changed, fires faster if volume is changed via Firebot", cached: false, @@ -37,7 +37,7 @@ export const SpotifyEventSource = { }, { id: "track-changed", - name: "Track Changed", + name: "Spotify Track Changed", description: "Currently playing Spotify track changed", cached: false, activityFeed: { @@ -45,5 +45,15 @@ export const SpotifyEventSource = { getMessage: () => "Spotify track changed", }, }, + { + id: "playlist-changed", + name: "Spotify Playlist Changed", + description: "Currently active Spotify Playlist has changed", + cached: false, + activityFeed: { + icon: "fab fa-spotify", + getMessage: () => "Spotify playlist changed", + }, + }, ], }; diff --git a/src/firebot/variables/index.ts b/src/firebot/variables/index.ts index 7d2c640..cbf4cc3 100644 --- a/src/firebot/variables/index.ts +++ b/src/firebot/variables/index.ts @@ -1,35 +1,9 @@ -import { SpotifyIsPlayingVariable } from "./spotifyIsPlaying"; -import { SpotifyPlayerVolumeVariable } from "./spotifyPlayerVolume"; -import { SpotifyPlayerRelativeVolumeVariable } from "./spotifyPlayerRelativeVolume"; -import { SpotifyTrackArtistVariable } from "./spotifyTrackArtist"; -import { SpotifyTrackArtistsVariable } from "./spotifyTrackArtists"; -import { SpotifyTrackAlbumVariable } from "./spotifyTrackAlbum"; -import { SpotifyTrackAlbumArtUrlVariable } from "./spotifyTrackAlbumArtUrl"; -import { SpotifyTrackIdVariable } from "./spotifyTrackId"; -import { SpotifyTrackTitleVariable } from "./spotifyTrackTitle"; -import { SpotifyTrackUriVariable } from "./spotifyTrackUri"; -import { SpotifyTrackUrlVariable } from "./spotifyTrackUrl"; -import { SpotifyTrackDurationVariable } from "./spotifyTrackDuration"; -import { SpotifyTrackDurationMsVariable } from "./spotifyTrackDurationMs"; -import { SpotifyTrackPositionVariable } from "./spotifyTrackPosition"; -import { SpotifyTrackPositionMsVariable } from "./spotifyTrackPositionMs"; -import { SpotifyTrackRelativePositionVariable } from "./spotifyTrackRelativePosition"; +import { AllSpotifyPlayerVariables } from "./player"; +import { AllSpotifyPlaylistVariables } from "./playlist"; +import { AllSpotifyTrackVariables } from "./track"; export const AllSpotifyReplaceVariables = [ - SpotifyIsPlayingVariable, - SpotifyPlayerVolumeVariable, - SpotifyPlayerRelativeVolumeVariable, - SpotifyTrackArtistVariable, - SpotifyTrackArtistsVariable, - SpotifyTrackAlbumVariable, - SpotifyTrackAlbumArtUrlVariable, - SpotifyTrackIdVariable, - SpotifyTrackTitleVariable, - SpotifyTrackUriVariable, - SpotifyTrackUrlVariable, - SpotifyTrackDurationVariable, - SpotifyTrackDurationMsVariable, - SpotifyTrackPositionVariable, - SpotifyTrackPositionMsVariable, - SpotifyTrackRelativePositionVariable, + ...AllSpotifyPlayerVariables, + ...AllSpotifyPlaylistVariables, + ...AllSpotifyTrackVariables, ]; diff --git a/src/firebot/variables/player/index.ts b/src/firebot/variables/player/index.ts new file mode 100644 index 0000000..3dc5409 --- /dev/null +++ b/src/firebot/variables/player/index.ts @@ -0,0 +1,9 @@ +import { SpotifyIsPlayingVariable } from "./spotifyIsPlaying"; +import { SpotifyPlayerVolumeVariable } from "./spotifyPlayerVolume"; +import { SpotifyPlayerRelativeVolumeVariable } from "./spotifyPlayerRelativeVolume"; + +export const AllSpotifyPlayerVariables = [ + SpotifyIsPlayingVariable, + SpotifyPlayerVolumeVariable, + SpotifyPlayerRelativeVolumeVariable, +]; diff --git a/src/firebot/variables/spotifyIsPlaying.ts b/src/firebot/variables/player/spotifyIsPlaying.ts similarity index 100% rename from src/firebot/variables/spotifyIsPlaying.ts rename to src/firebot/variables/player/spotifyIsPlaying.ts diff --git a/src/firebot/variables/spotifyPlayerRelativeVolume.ts b/src/firebot/variables/player/spotifyPlayerRelativeVolume.ts similarity index 100% rename from src/firebot/variables/spotifyPlayerRelativeVolume.ts rename to src/firebot/variables/player/spotifyPlayerRelativeVolume.ts diff --git a/src/firebot/variables/spotifyPlayerVolume.ts b/src/firebot/variables/player/spotifyPlayerVolume.ts similarity index 100% rename from src/firebot/variables/spotifyPlayerVolume.ts rename to src/firebot/variables/player/spotifyPlayerVolume.ts diff --git a/src/firebot/variables/playlist/index.ts b/src/firebot/variables/playlist/index.ts new file mode 100644 index 0000000..73f2478 --- /dev/null +++ b/src/firebot/variables/playlist/index.ts @@ -0,0 +1,15 @@ +import { SpotifyIsPlaylistActiveVariable } from "./spotifyIsPlaylistActive"; +import { SpotifyPlaylistDescriptionVariable } from "./spotifyPlaylistDescription"; +import { SpotifyPlaylistUrlVariable } from "./spotifyPlaylistUrl"; +import { SpotifyPlaylistNameVariable } from "./spotifyPlaylistName"; +import { SpotifyPlaylistCoverImageUrlVariable } from "./spotifyPlaylistCoverImageUrl"; +import { SpotifyPlaylistLengthVariable } from "./spotifyPlaylistLength"; + +export const AllSpotifyPlaylistVariables = [ + SpotifyIsPlaylistActiveVariable, + SpotifyPlaylistDescriptionVariable, + SpotifyPlaylistUrlVariable, + SpotifyPlaylistNameVariable, + SpotifyPlaylistCoverImageUrlVariable, + SpotifyPlaylistLengthVariable, +]; diff --git a/src/firebot/variables/playlist/spotifyIsPlaylistActive.ts b/src/firebot/variables/playlist/spotifyIsPlaylistActive.ts new file mode 100644 index 0000000..b50873e --- /dev/null +++ b/src/firebot/variables/playlist/spotifyIsPlaylistActive.ts @@ -0,0 +1,12 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyIsPlaylistActiveVariable: ReplaceVariable = { + definition: { + handle: "spotifyIsPlaylistActive", + description: "Will be `true` if Spotify has playlist open, `false` if not", + usage: "spotifyIsPlaylistActive", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.playlist.isPlaylistActive, +}; diff --git a/src/firebot/variables/playlist/spotifyPlaylistCoverImageUrl.ts b/src/firebot/variables/playlist/spotifyPlaylistCoverImageUrl.ts new file mode 100644 index 0000000..5036b78 --- /dev/null +++ b/src/firebot/variables/playlist/spotifyPlaylistCoverImageUrl.ts @@ -0,0 +1,13 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlaylistCoverImageUrlVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlaylistCoverImageUrl", + description: + "Gets the Cover Image Url of the currently playing Spotify Playlist", + usage: "spotifyPlaylistCoverImageUrl", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.playlist.coverImageUrl, +}; diff --git a/src/firebot/variables/playlist/spotifyPlaylistDescription.ts b/src/firebot/variables/playlist/spotifyPlaylistDescription.ts new file mode 100644 index 0000000..0d9a777 --- /dev/null +++ b/src/firebot/variables/playlist/spotifyPlaylistDescription.ts @@ -0,0 +1,13 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlaylistDescriptionVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlaylistDescription", + description: + "Gets the Description of the currently playing Spotify Playlist", + usage: "spotifyPlaylistDescription", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.playlist.description, +}; diff --git a/src/firebot/variables/playlist/spotifyPlaylistLength.ts b/src/firebot/variables/playlist/spotifyPlaylistLength.ts new file mode 100644 index 0000000..6f07c46 --- /dev/null +++ b/src/firebot/variables/playlist/spotifyPlaylistLength.ts @@ -0,0 +1,12 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlaylistLengthVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlaylistLength", + description: "Gets the Length of the currently playing Spotify Playlist", + usage: "spotifyPlaylistLength", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.playlist.length, +}; diff --git a/src/firebot/variables/playlist/spotifyPlaylistName.ts b/src/firebot/variables/playlist/spotifyPlaylistName.ts new file mode 100644 index 0000000..7e168a4 --- /dev/null +++ b/src/firebot/variables/playlist/spotifyPlaylistName.ts @@ -0,0 +1,12 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlaylistNameVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlaylistName", + description: "Gets the Name of the currently playing Spotify Playlist", + usage: "spotifyPlaylistName", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.playlist.name, +}; diff --git a/src/firebot/variables/playlist/spotifyPlaylistUrl.ts b/src/firebot/variables/playlist/spotifyPlaylistUrl.ts new file mode 100644 index 0000000..aa5e7d6 --- /dev/null +++ b/src/firebot/variables/playlist/spotifyPlaylistUrl.ts @@ -0,0 +1,12 @@ +import { spotify } from "@/main"; +import { ReplaceVariable } from "@crowbartools/firebot-custom-scripts-types/types/modules/replace-variable-manager"; + +export const SpotifyPlaylistUrlVariable: ReplaceVariable = { + definition: { + handle: "spotifyPlaylistUrl", + description: "Gets the Url of the currently playing Spotify Playlist", + usage: "spotifyPlaylistUrl", + possibleDataOutput: ["number"], + }, + evaluator: async () => spotify.player.playlist.url, +}; diff --git a/src/firebot/variables/track/index.ts b/src/firebot/variables/track/index.ts new file mode 100644 index 0000000..2b26296 --- /dev/null +++ b/src/firebot/variables/track/index.ts @@ -0,0 +1,29 @@ +import { SpotifyTrackArtistVariable } from "./spotifyTrackArtist"; +import { SpotifyTrackArtistsVariable } from "./spotifyTrackArtists"; +import { SpotifyTrackAlbumVariable } from "./spotifyTrackAlbum"; +import { SpotifyTrackAlbumArtUrlVariable } from "./spotifyTrackAlbumArtUrl"; +import { SpotifyTrackIdVariable } from "./spotifyTrackId"; +import { SpotifyTrackTitleVariable } from "./spotifyTrackTitle"; +import { SpotifyTrackUriVariable } from "./spotifyTrackUri"; +import { SpotifyTrackUrlVariable } from "./spotifyTrackUrl"; +import { SpotifyTrackDurationVariable } from "./spotifyTrackDuration"; +import { SpotifyTrackDurationMsVariable } from "./spotifyTrackDurationMs"; +import { SpotifyTrackPositionVariable } from "./spotifyTrackPosition"; +import { SpotifyTrackPositionMsVariable } from "./spotifyTrackPositionMs"; +import { SpotifyTrackRelativePositionVariable } from "./spotifyTrackRelativePosition"; + +export const AllSpotifyTrackVariables = [ + SpotifyTrackArtistVariable, + SpotifyTrackArtistsVariable, + SpotifyTrackAlbumVariable, + SpotifyTrackAlbumArtUrlVariable, + SpotifyTrackIdVariable, + SpotifyTrackTitleVariable, + SpotifyTrackUriVariable, + SpotifyTrackUrlVariable, + SpotifyTrackDurationVariable, + SpotifyTrackDurationMsVariable, + SpotifyTrackPositionVariable, + SpotifyTrackPositionMsVariable, + SpotifyTrackRelativePositionVariable, +]; diff --git a/src/firebot/variables/spotifyTrackAlbum.ts b/src/firebot/variables/track/spotifyTrackAlbum.ts similarity index 100% rename from src/firebot/variables/spotifyTrackAlbum.ts rename to src/firebot/variables/track/spotifyTrackAlbum.ts diff --git a/src/firebot/variables/spotifyTrackAlbumArtUrl.ts b/src/firebot/variables/track/spotifyTrackAlbumArtUrl.ts similarity index 100% rename from src/firebot/variables/spotifyTrackAlbumArtUrl.ts rename to src/firebot/variables/track/spotifyTrackAlbumArtUrl.ts diff --git a/src/firebot/variables/spotifyTrackArtist.ts b/src/firebot/variables/track/spotifyTrackArtist.ts similarity index 100% rename from src/firebot/variables/spotifyTrackArtist.ts rename to src/firebot/variables/track/spotifyTrackArtist.ts diff --git a/src/firebot/variables/spotifyTrackArtists.ts b/src/firebot/variables/track/spotifyTrackArtists.ts similarity index 100% rename from src/firebot/variables/spotifyTrackArtists.ts rename to src/firebot/variables/track/spotifyTrackArtists.ts diff --git a/src/firebot/variables/spotifyTrackDuration.ts b/src/firebot/variables/track/spotifyTrackDuration.ts similarity index 100% rename from src/firebot/variables/spotifyTrackDuration.ts rename to src/firebot/variables/track/spotifyTrackDuration.ts diff --git a/src/firebot/variables/spotifyTrackDurationMs.ts b/src/firebot/variables/track/spotifyTrackDurationMs.ts similarity index 100% rename from src/firebot/variables/spotifyTrackDurationMs.ts rename to src/firebot/variables/track/spotifyTrackDurationMs.ts diff --git a/src/firebot/variables/spotifyTrackId.ts b/src/firebot/variables/track/spotifyTrackId.ts similarity index 100% rename from src/firebot/variables/spotifyTrackId.ts rename to src/firebot/variables/track/spotifyTrackId.ts diff --git a/src/firebot/variables/spotifyTrackPosition.ts b/src/firebot/variables/track/spotifyTrackPosition.ts similarity index 100% rename from src/firebot/variables/spotifyTrackPosition.ts rename to src/firebot/variables/track/spotifyTrackPosition.ts diff --git a/src/firebot/variables/spotifyTrackPositionMs.ts b/src/firebot/variables/track/spotifyTrackPositionMs.ts similarity index 100% rename from src/firebot/variables/spotifyTrackPositionMs.ts rename to src/firebot/variables/track/spotifyTrackPositionMs.ts diff --git a/src/firebot/variables/spotifyTrackRelativePosition.ts b/src/firebot/variables/track/spotifyTrackRelativePosition.ts similarity index 100% rename from src/firebot/variables/spotifyTrackRelativePosition.ts rename to src/firebot/variables/track/spotifyTrackRelativePosition.ts diff --git a/src/firebot/variables/spotifyTrackTitle.ts b/src/firebot/variables/track/spotifyTrackTitle.ts similarity index 100% rename from src/firebot/variables/spotifyTrackTitle.ts rename to src/firebot/variables/track/spotifyTrackTitle.ts diff --git a/src/firebot/variables/spotifyTrackUri.ts b/src/firebot/variables/track/spotifyTrackUri.ts similarity index 100% rename from src/firebot/variables/spotifyTrackUri.ts rename to src/firebot/variables/track/spotifyTrackUri.ts diff --git a/src/firebot/variables/spotifyTrackUrl.ts b/src/firebot/variables/track/spotifyTrackUrl.ts similarity index 100% rename from src/firebot/variables/spotifyTrackUrl.ts rename to src/firebot/variables/track/spotifyTrackUrl.ts diff --git a/src/types/spotify.d.ts b/src/types/spotify.d.ts index 0514443..9563209 100644 --- a/src/types/spotify.d.ts +++ b/src/types/spotify.d.ts @@ -1,7 +1,7 @@ //#region Unique Vars type SpotifyRepeatState = "track" | "context" | "off"; -type SpotifySearchType = +type SpotifyContextType = | "album" | "artist" | "playlist" @@ -9,11 +9,20 @@ type SpotifySearchType = | "show" | "episode" | "audiobook"; + +type SpotifyExternalUrls = { [platform: string]: string }; + +type SpotifyContext = { + external_urls: { [platform: string]: string }; + href: string; + type: SpotifyContextType; + uri: string; +} | null; //#endregion type SpotifyRefreshTokenResponse = { access_token: string; - token_type: string; + token_type: SpotifyContextType; scope: string; expires_in: number; refresh_token: string; @@ -27,7 +36,7 @@ type SpotifyUserProfile = { filter_enabled: boolean; filter_locked: boolean; }; - external_urls: { [platform: string]: string }; + external_urls: SpotifyExternalUrls; followers: { href: string; total: number; @@ -36,7 +45,7 @@ type SpotifyUserProfile = { id: string; images: SpotifyImage[]; product: string; - type: string; + type: SpotifyContextType; uri: string; }; @@ -50,16 +59,11 @@ type SpotifyPlayer = { smart_shuffle: boolean; repeat_state: SpotifyRepeatState; timestamp: number; - context: { - type: string; - href: string; - external_urls: { [platform: string]: string }; - uri: string; - }; + context: SpotifyContext; progress_ms: number; item: SpotifyTrackDetails; - currently_playing_type: string; - currently_playing_type: string; + currently_playing_type: SpotifyContextType; + currently_playing_type: SpotifyContextType; actions: { disallows: { resuming?: boolean; @@ -79,7 +83,7 @@ type SpotifyDevice = { is_private_session: boolean; is_restricted: boolean; name: string; - type: string; + type: SpotifyContextType; supports_volume: boolean; volume_percent: number; }; @@ -104,17 +108,12 @@ type SpotifyCurrentlyPlaying = { device: SpotifyDevice; repeat_state: SpotifyRepeatState; shuffle_state: boolean; - context: { - type: string; - href: string; - external_urls: { [platform: string]: string }; - uri: string; - }; + context: SpotifyContext; timestamp: number; progress_ms: number; is_playing: boolean; item: SpotifyTrackDetails; - currently_playing_type: string; + currently_playing_type: SpotifyContextType; actions: { interrupting_playback?: boolean; pausing?: boolean; @@ -129,6 +128,31 @@ type SpotifyCurrentlyPlaying = { }; }; +type SpotifyPlaylistDetails = { + collaborative: boolean; + description: string; + external_urls: SpotifyExternalUrls; + href: string; + id: string; + images: SpotifyImage[]; + name: string; + owner: SpotifyUserProfile; + primary_color: string | null; + public: boolean; + snapshot_id: string; + tracks: { + href: string; + items: SpotifyTrackDetails[]; + limit: number; + next: string; + offset: number; + previous: string; + total: number; + }; + type: SpotifyContextType; + uri: string; +}; + //#region Spotify API /search types type SpotifyTrackDetails = { album: SpotifyAlbumDetails; @@ -138,7 +162,7 @@ type SpotifyTrackDetails = { duration_ms: number; explicit: boolean; external_ids: { [source: string]: string }; - external_urls: { [platform: string]: string }; + external_urls: SpotifyExternalUrls; href: string; id: string; is_playable: boolean; @@ -148,7 +172,7 @@ type SpotifyTrackDetails = { popularity: number; preview_url: string; track_number: number; - type: track; + type: SpotifyContextType; uri: string; is_local: boolean; queue_position?: number; @@ -159,7 +183,7 @@ type SpotifyAlbumArtistDetails = { href: string; id: string; name: string; - type: string; + type: SpotifyContextType; uri: string; }; @@ -174,10 +198,10 @@ type SpotifyArtistDetails = SpotifyAlbumArtistDetails & { }; type SpotifyAlbumDetails = { - album_type: string; + album_type: SpotifyContextType; total_tracks: number; available_markets: string[]; - external_urls: { [platform: string]: string }; + external_urls: SpotifyExternalUrls; href: string; id: string; images: SpotifyImage[]; @@ -185,7 +209,7 @@ type SpotifyAlbumDetails = { release_date: string; release_date_precision: string; restrictions: { reason?: string }; - type: string; + type: SpotifyContextType; uri: string; artists: SpotifyAlbumArtistDetails[]; }; diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 0000000..142cdb1 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,2 @@ +export const getBiggestImageUrl = (images: SpotifyImage[]) => + images.length ? images.reduce((a, b) => (a.width > b.width ? a : b)).url : ""; diff --git a/src/utils/spotify/index.ts b/src/utils/spotify/index.ts index 6184a4f..f7d7bb6 100644 --- a/src/utils/spotify/index.ts +++ b/src/utils/spotify/index.ts @@ -19,7 +19,7 @@ export class SpotifyService { public async searchAsync( query: string, - types: SpotifySearchType[] | SpotifySearchType, + types: SpotifyContextType[] | SpotifyContextType, limit: number = 20, offset: number = 0 ) { diff --git a/src/utils/spotify/player/index.ts b/src/utils/spotify/player/index.ts index a0c0a39..08d5a65 100644 --- a/src/utils/spotify/player/index.ts +++ b/src/utils/spotify/player/index.ts @@ -1,8 +1,10 @@ import { eventManager, logger } from "@utils/firebot"; import { SpotifyService } from "@utils/spotify"; -import SpotifyQueueService from "./queue"; import { delay } from "@/utils/timing"; import { msToFormattedString } from "@/utils/strings"; +import SpotifyQueueService from "./queue"; +import SpotifyPlaylistService from "./playlist"; +import { getBiggestImageUrl } from "@/utils/array"; type SpotifyTrackSummary = { id: string; @@ -17,11 +19,23 @@ type SpotifyTrackSummary = { positionMs: number; position: string; relativePosition: number; + context: SpotifyContext | null; +}; + +type SpotifyPlaylistSummary = { + id: string; + uri: string; + name: string; + description: string; + url: string; + imageUrl: string; + length: number; }; export default class SpotifyPlayerService { private readonly spotify: SpotifyService; public readonly queue: SpotifyQueueService; + public readonly playlist: SpotifyPlaylistService; private readonly minutesToCacheDeviceId: number = 15; private activeDeviceId: string | null = null; @@ -34,11 +48,15 @@ export default class SpotifyPlayerService { private _track: SpotifyTrackDetails | null = null; private _volume: number = 0; private _targetVolume: number = -1; + private _context: SpotifyContext | null = null; + private _playlistId: string | null = null; + private _playlist: SpotifyPlaylistSummary | null = null; constructor(spotifyService: SpotifyService) { this.spotify = spotifyService; this.queue = new SpotifyQueueService(this.spotify); + this.playlist = new SpotifyPlaylistService(this.spotify); } public init() { @@ -78,7 +96,7 @@ export default class SpotifyPlayerService { const artists = this._track.artists.map((a) => a.name); // Get the URL of the biggest image for the album - const albumArtUrl = this.getBiggestImage(this._track.album.images).url; + const albumArtUrl = getBiggestImageUrl(this._track.album.images); return { id: this._track.id, @@ -93,6 +111,7 @@ export default class SpotifyPlayerService { positionMs: this._progressMs, position: msToFormattedString(this._progressMs, false), relativePosition: this._progressMs / this._track.duration_ms, + context: this._context ?? null, }; } @@ -321,6 +340,21 @@ export default class SpotifyPlayerService { } } + public async getCurrentPlaylistName(): Promise { + try { + if (!this._context || this._context.type != "playlist") return ""; + + const playlist = await this.spotify.api.fetch( + `/playlists/${this._context.uri.split(":")[2]}` + ); + + return playlist.data?.name ?? ""; + } catch (error) { + logger.error("Error getting current playlist name", error); + return ""; + } + } + //#region Continuous methods private async updatePlaybackState(): Promise { const start = Date.now(); @@ -350,6 +384,10 @@ export default class SpotifyPlayerService { this._isPlaying = state.is_playing; } + if (state.context && this.playlist.id !== state.context.uri) { + await this.playlist.updateCurrentPlaylistAsync(state.context.uri); + } + // If target volume, user has manually changed volume and we don't want it falling back if ( this._volume != state.device.volume_percent && diff --git a/src/utils/spotify/player/playlist.ts b/src/utils/spotify/player/playlist.ts new file mode 100644 index 0000000..fdb2db6 --- /dev/null +++ b/src/utils/spotify/player/playlist.ts @@ -0,0 +1,84 @@ +import { getBiggestImageUrl } from "@utils/array"; +import { decode } from "he"; +import { eventManager, logger } from "@utils/firebot"; +import { SpotifyService } from "@utils/spotify"; + +export default class SpotifyPlaylistService { + private readonly spotify: SpotifyService; + + private _playlist: SpotifyPlaylistDetails | null = null; + + public constructor(spotifyService: SpotifyService) { + this.spotify = spotifyService; + } + + /* Getters */ + public get id(): string | null { + return this._playlist?.id ?? null; + } + + public get isPlaylistActive(): boolean { + return !!this._playlist; + } + + public get name(): string { + return this._playlist ? decode(this._playlist.name) : ""; + } + + public get description(): string { + return this._playlist ? decode(this._playlist.description) : ""; + } + + public get url(): string { + return this._playlist?.external_urls.spotify ?? ""; + } + + public get coverImageUrl(): string { + return getBiggestImageUrl(this._playlist?.images ?? []); + } + + public get owner(): string { + return this._playlist ? decode(this._playlist.owner.display_name) : ""; + } + + public get ownerUrl(): string { + return this._playlist?.owner.external_urls.spotify ?? ""; + } + + public get length(): number { + return this._playlist?.tracks.total ?? -1; + } + + public async updateCurrentPlaylistAsync(playlistUri: string | null) { + try { + if (!playlistUri) { + this._playlist = null; + return; + } + + const id = this.getIdFromUri(playlistUri); + + if (this._playlist && this._playlist.id === id) { + return; + } + + const response = await this.spotify.api.fetch( + `/playlists/${id}` + ); + + if (!response.data) { + this._playlist === null; + throw new Error("No Spotify playlist found with provided Id"); + } + + this._playlist = response.data; + + eventManager.triggerEvent("oceanity-spotify", "playlist-changed", {}); + } catch (error) { + logger.error("Error getting Spotify Queue", error); + throw error; + } + } + + private getIdFromUri = (uri: string): string => uri.split(":")[2]; +} From 6dc04667e840a688999a6d3f9d5560d3a7e7f31b Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Fri, 7 Jun 2024 17:30:06 -0400 Subject: [PATCH 5/7] Bumped Version --- package.json | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 404a09e..df5430d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "firebot-song-requests", "scriptOutputName": "oceanitySpotifyIntegration", - "version": "0.6.4", + "version": "0.6.5", "description": "Adds Spotify Song Requests to Firebot", "main": "", "scripts": { diff --git a/src/main.ts b/src/main.ts index 568c5b9..eba7714 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ const script: Firebot.CustomScript = { name: "Firebot Spotify Integrations", description: "Let your viewers determine your taste in music", author: "Oceanity", - version: "0.6.4", + version: "0.6.5", firebotVersion: "5", }; }, From 95581009d18bb186bf48e2a41aabff73d738e671 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Fri, 7 Jun 2024 17:34:51 -0400 Subject: [PATCH 6/7] Updated Readme and rearranged Events --- README.md | 40 ++++++++++++++++-------- src/firebot/events/spotifyEventSource.ts | 34 ++++++++++---------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ce1daa1..d31249b 100644 --- a/README.md +++ b/README.md @@ -48,19 +48,33 @@ This script adds the following features to Firebot **Any Spotify Account** - Replace Variables - - spotifyIsPlaying: `bool` - - spotifyTrackAlbum: `string` - - spotifyTrackAlbumArtUrl: `string` - - spotifyTrackArtist: `string` - - spotifyTrackArtists: `string[]` - - spotifyTrackDuration: `string` - - spotifyTrackDurationMs: `number` - - spotifyTrackPosition: `string` - - spotifyTrackPositionMs: `number` - - spotifyTrackRelativePosition: `float` - - spotifyTrackTitle: `string` - - spotifyTrackUrl: `string` + - Player + - spotifyIsPlaying: `bool` + - spotifyPlayerRelativeVolume: `float` + - spotifyPlayerVolume: `integer` + - Playlist + - spotifyIsPlaylistActive: `bool` + - spotifyCoverImageUrl: `string` + - spotifyPlaylistDescription: `string` + - spotifyPlaylistLength: `integer` + - spotifyPlaylistUrl: `string` + - Track + - spotifyTrackAlbum: `string` + - spotifyTrackAlbumArtUrl: `string` + - spotifyTrackArtist: `string` + - spotifyTrackArtists: `string[]` + - spotifyTrackDuration: `string` + - spotifyTrackDurationMs: `integer` + - spotifyTrackId: `string` + - spotifyTrackPosition: `string` + - spotifyTrackPositionMs: `integer` + - spotifyTrackRelativePosition: `float` + - spotifyTrackTitle: `string` + - spotifyTrackUri: `string` + - spotifyTrackUrl: `string` - Events - - Spotify Tick - Playback State Changed + - Playlist Changed + - Tick - Track Changed + - Volume Changed diff --git a/src/firebot/events/spotifyEventSource.ts b/src/firebot/events/spotifyEventSource.ts index f94c8ec..ce32f30 100644 --- a/src/firebot/events/spotifyEventSource.ts +++ b/src/firebot/events/spotifyEventSource.ts @@ -4,35 +4,34 @@ export const SpotifyEventSource = { description: "Events related to Oceanity's Spotify integration", events: [ { - id: "tick", - name: "Spotify Tick", - description: - "Fired around once per second after current playback state has been updated", + id: "playback-state-changed", + name: "Spotify Playback State Changed", + description: "Spotify playback state changed", cached: false, activityFeed: { icon: "fab fa-spotify", - getMessage: () => "Spotify tick", + getMessage: () => "Spotify playback state changed", }, }, { - id: "playback-state-changed", - name: "Spotify Playback State Changed", - description: "Spotify playback state changed", + id: "playlist-changed", + name: "Spotify Playlist Changed", + description: "Currently active Spotify Playlist has changed", cached: false, activityFeed: { icon: "fab fa-spotify", - getMessage: () => "Spotify playback state changed", + getMessage: () => "Spotify playlist changed", }, }, { - id: "volume-changed", - name: "Spotify Volume Changed", + id: "tick", + name: "Spotify Tick", description: - "Spotify volume changed, fires faster if volume is changed via Firebot", + "Fired around once per second after current playback state has been updated", cached: false, activityFeed: { icon: "fab fa-spotify", - getMessage: () => "Spotify volume changed", + getMessage: () => "Spotify tick", }, }, { @@ -46,13 +45,14 @@ export const SpotifyEventSource = { }, }, { - id: "playlist-changed", - name: "Spotify Playlist Changed", - description: "Currently active Spotify Playlist has changed", + id: "volume-changed", + name: "Spotify Volume Changed", + description: + "Spotify volume changed, fires faster if volume is changed via Firebot", cached: false, activityFeed: { icon: "fab fa-spotify", - getMessage: () => "Spotify playlist changed", + getMessage: () => "Spotify volume changed", }, }, ], From 5da0c58e630d4154dca6a0b7db854682617069b6 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Fri, 7 Jun 2024 18:18:35 -0400 Subject: [PATCH 7/7] Removed duplicate entry --- src/types/spotify.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/spotify.d.ts b/src/types/spotify.d.ts index 9563209..b9232ad 100644 --- a/src/types/spotify.d.ts +++ b/src/types/spotify.d.ts @@ -63,7 +63,6 @@ type SpotifyPlayer = { progress_ms: number; item: SpotifyTrackDetails; currently_playing_type: SpotifyContextType; - currently_playing_type: SpotifyContextType; actions: { disallows: { resuming?: boolean;