diff --git a/packages/api/src/service/FissaService.ts b/packages/api/src/service/FissaService.ts index f075465..f97bdf1 100644 --- a/packages/api/src/service/FissaService.ts +++ b/packages/api/src/service/FissaService.ts @@ -4,12 +4,14 @@ import { biasSort, differenceInMilliseconds, FissaIsPaused, + ForceStopFissa, NoNextTrack, NotAbleToAccessSpotify, NotTheHost, randomize, sleep, sortFissaTracksOrder, + UnableToCreateFissa, type SpotifyService, } from "@fissa/utils"; @@ -31,7 +33,7 @@ export class FissaService extends ServiceWithContext { }; create = async (tracks: { trackId: string; durationMs: number }[], userId: string) => { - if (!tracks[0]) throw new Error("No tracks"); + if (!tracks[0]) throw new UnableToCreateFissa("No tracks"); const { access_token } = await this.db.account.findFirstOrThrow({ where: { userId }, @@ -66,7 +68,7 @@ export class FissaService extends ServiceWithContext { } } while (!fissa && tries < 50); - if (!fissa) throw new Error("Failed to create fissa"); + if (!fissa) throw new UnableToCreateFissa("No unique pin found"); if (tracks.length <= TRACKS_BEFORE_ADDING_RECOMMENDATIONS) { await this.addRecommendedTracks(fissa.pin, tracks, access_token); @@ -144,7 +146,7 @@ export class FissaService extends ServiceWithContext { try { const isPlaying = await this.spotifyService.isStillPlaying(access_token); - if (!instantPlay && (!currentlyPlayingId || !isPlaying)) throw new Error("Stop fissa"); + if (!instantPlay && (!currentlyPlayingId || !isPlaying)) throw new ForceStopFissa(); const nextTracks = this.getNextTracks(tracks, currentlyPlayingId); if (!nextTracks[0]) throw new NoNextTrack(); @@ -217,15 +219,19 @@ export class FissaService extends ServiceWithContext { { trackId, durationMs }: Pick, accessToken: string, ) => { - await this.spotifyService.playTrack(accessToken, trackId); + const playing = await this.spotifyService.playTrack(accessToken, trackId); - return this.db.fissa.update({ + await this.db.fissa.update({ where: { pin }, data: { currentlyPlaying: { connect: { pin_trackId: { pin, trackId } } }, expectedEndTime: addMilliseconds(new Date(), durationMs), }, }); + + // TODO: We should ban this track from being played again + // as apparently it's not playable + if (!playing) return this.playNextTrack(pin); }; private getNextTracks = (tracks: Track[], currentlyPlayingId?: string | null) => { diff --git a/packages/utils/classes/Error.ts b/packages/utils/classes/Error.ts index 8eb579f..cca2005 100644 --- a/packages/utils/classes/Error.ts +++ b/packages/utils/classes/Error.ts @@ -25,3 +25,31 @@ export class NoNextTrack extends Error { this.name = NoNextTrack.name; } } + +export class UnableToPlayTrack extends Error { + public constructor(message = "Unable to play track") { + super(message); + this.name = UnableToPlayTrack.name; + } +} + +export class NotImplemented extends Error { + public constructor(message = "Not implemented") { + super(message); + this.name = NotImplemented.name; + } +} + +export class ForceStopFissa extends Error { + public constructor(message = "Forcefully stop fissa") { + super(message); + this.name = ForceStopFissa.name; + } +} + +export class UnableToCreateFissa extends Error { + public constructor(message = "Unable to create fissa") { + super(message); + this.name = UnableToCreateFissa.name; + } +} diff --git a/packages/utils/classes/Toaster.ts b/packages/utils/classes/Toaster.ts index 8773cef..51e99b2 100644 --- a/packages/utils/classes/Toaster.ts +++ b/packages/utils/classes/Toaster.ts @@ -1,3 +1,5 @@ +import { NotImplemented } from "./Error"; + export class Toaster { protected defaultIcon(type: ToastType) { switch (type) { @@ -31,11 +33,11 @@ export class Toaster { } protected show(_: ToasterProps) { - throw new Error("Not implemented"); + throw new NotImplemented(); } public hide() { - throw new Error("Not implemented"); + throw new NotImplemented(); } } diff --git a/packages/utils/services/SpotifyService.ts b/packages/utils/services/SpotifyService.ts index 3099351..004cebd 100644 --- a/packages/utils/services/SpotifyService.ts +++ b/packages/utils/services/SpotifyService.ts @@ -1,7 +1,10 @@ import SpotifyWebApi from "spotify-web-api-node"; +import { UnableToPlayTrack } from "../classes"; import { sleep } from "../sleep"; +const TRIES_TO_PLAY = 3; + export class SpotifyService { private spotify = new SpotifyWebApi({ clientId: process.env.SPOTIFY_CLIENT_ID, @@ -29,31 +32,33 @@ export class SpotifyService { return body.is_playing; }; - playTrack = async (accessToken: string, trackId: string, triesLeft = 3): Promise => { + playTrack = async (accessToken: string, trackId: string, trial = 0): Promise => { this.spotify.setAccessToken(accessToken); try { await this.spotify.play({ uris: [`spotify:track:${trackId}`] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - if ((e as { body: { error: { reason: string } } }).body.error.reason === "NO_ACTIVE_DEVICE") { + await sleep(500); // Arbitrary wait time, sue me + const { body } = await this.spotify.getMyCurrentPlaybackState(); + return Promise.resolve(body.is_playing); + } catch (e: unknown) { + if (trial > TRIES_TO_PLAY) throw new UnableToPlayTrack("Could not play track"); + + const error = e as { body: { error: { reason: string } } }; + const reason = error?.body?.error?.reason ?? ""; + if (reason === "NO_ACTIVE_DEVICE") { console.log("No active device found, trying to transfer playback"); const { body } = await this.spotify.getMyDevices(); const firstDevice = body.devices[0]; - if (!firstDevice?.id) throw new Error("No playback device(s) found"); - - if (triesLeft === 0) throw new Error("Could not transfer playback"); + if (!firstDevice?.id) throw new UnableToPlayTrack("No playback device(s) found"); await this.spotify.transferMyPlayback([firstDevice.id]); - await sleep(250 + (3 % (triesLeft + 1))); - return this.playTrack(accessToken, trackId, triesLeft - 1); + await sleep(500); // Arbitrary wait time, sue me + return this.playTrack(accessToken, trackId, trial + 1); } - console.error(JSON.stringify(e)); - - return Promise.resolve(); + return Promise.resolve(false); } };