diff --git a/src/features/library/spotify/index.ts b/src/features/library/spotify/index.ts new file mode 100644 index 00000000..73ad8d43 --- /dev/null +++ b/src/features/library/spotify/index.ts @@ -0,0 +1,9 @@ +export { SpotifyPlayerWrapper as SpotifyPlayer } from './internal/SpotifyPlayerWrapper.class.js'; +export { + PLAYER_TRACK_TYPE, + PLAYER_MEDIA_TYPE, +} from './internal/SpotifyPlayerWrapper.constants.js'; +export type { + SpotifyPlayerState, + SpotifyPlayerTrack, +} from './internal/SpotifyPlayerWrapper.types.js'; diff --git a/src/features/library/spotify/internal/LICENSE.md b/src/features/library/spotify/internal/LICENSE.md new file mode 100644 index 00000000..35049e60 --- /dev/null +++ b/src/features/library/spotify/internal/LICENSE.md @@ -0,0 +1,16 @@ +The MIT License (MIT) + +Copyright (c) 2022 Filip Linqvist + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/features/library/spotify/internal/README.md b/src/features/library/spotify/internal/README.md new file mode 100644 index 00000000..d7dfdc4f --- /dev/null +++ b/src/features/library/spotify/internal/README.md @@ -0,0 +1,25 @@ +# Forked from spotify-web-playback-ts + +This repository is a hard fork of the [spotify-web-playback-ts](https://github.com/tkhduracell/spotify-web-playback-ts) +project, specifically tailored for enhanced internal implementation and robust type and constant definitions. This fork +is intended to strengthen the core functionalities and provide a more structured and type-safe interface. + +## Overview + +This forked version of spotify-web-playback-ts focuses on enhancing the internal mechanics of the Spotify Web Playback +API integration. The key modifications include the reinforcement of type definitions and constant values, aimed at +increasing the reliability and maintainability of the library in a TypeScript environment. + +## Enhancements + +Type Definitions: Strengthened type safety for Spotify Web Playback integration. +Constant Values: Extended and robust constant definitions for improved code readability and maintenance. + +## License + +This project is released under the MIT License. For more details, see the LICENSE.md file. + +## Acknowledgements + +This fork is based on the foundational work of the spotify-web-playback-ts project. We extend our sincere thanks to the +original author and contributors. diff --git a/src/features/library/spotify/internal/SpotifyPlayerWrapper.class.ts b/src/features/library/spotify/internal/SpotifyPlayerWrapper.class.ts new file mode 100644 index 00000000..12e50292 --- /dev/null +++ b/src/features/library/spotify/internal/SpotifyPlayerWrapper.class.ts @@ -0,0 +1,144 @@ +import { EVENT_TYPE, SCRIPT_SRC } from './SpotifyPlayerWrapper.constants.js'; +import type { + SpotifyDeviceId, + SpotifyPlayerError, + SpotifyPlayerInternal, + SpotifyPlayerState, + SpotifyTokenRefreshFunction, +} from './SpotifyPlayerWrapper.types.js'; + +export class SpotifyPlayerWrapper { + player: SpotifyPlayerInternal; + + private constructor(player: SpotifyPlayerInternal) { + this.player = player; + } + + static init({ + document, + name, + initialVolume, + getOAuthToken, + }: { + document: Document; + name: string; + initialVolume: number; + getOAuthToken: SpotifyTokenRefreshFunction; + }): Promise { + const script = document.createElement('script'); + script.src = SCRIPT_SRC; + script.async = true; + + document.body.appendChild(script); + const w = window as unknown as { + onSpotifyWebPlaybackSDKReady: () => void; + Spotify: { + Player: { + new (args: { + name: string; + getOAuthToken: SpotifyTokenRefreshFunction; + volume: number; + }): SpotifyPlayerInternal; + }; + }; + }; + return new Promise(resolve => { + w.onSpotifyWebPlaybackSDKReady = () => { + const player = new w.Spotify.Player({ + name, + getOAuthToken, + volume: initialVolume, + }); + resolve(new SpotifyPlayerWrapper(player)); + }; + }); + } + + on(event: SpotifyPlayerError, callback: (error: unknown) => void) { + this.player.on(event, callback); + } + + connect(): Promise { + return this.player.connect(); + } + + disconnect(): void { + return this.player.disconnect(); + } + + onReady(callback: (device: SpotifyDeviceId) => void) { + return this.player.addListener(EVENT_TYPE.READY, callback); + } + + onNotReady(callback: (device: SpotifyDeviceId) => void) { + return this.player.addListener(EVENT_TYPE.NOT_READY, callback); + } + + onPlayerStateChanged(callback: (state: SpotifyPlayerState) => void) { + return this.player.addListener(EVENT_TYPE.PLAYER_STATE_CHANGED, callback); + } + + onAutoPlayFailed(callback: () => void) { + return this.player.addListener(EVENT_TYPE.AUTOPLAY_FAILED, callback); + } + + removeReady() { + this.player.removeListener(EVENT_TYPE.READY); + } + + removeNotReady() { + this.player.removeListener(EVENT_TYPE.NOT_READY); + } + + removePlayerStateChanged() { + this.player.removeListener(EVENT_TYPE.PLAYER_STATE_CHANGED); + } + + removeAutoPlayFailed() { + this.player.removeListener(EVENT_TYPE.AUTOPLAY_FAILED); + } + + getCurrentState(): Promise { + return this.player.getCurrentState(); + } + + setName(name: string): void { + this.player.setName(name); + } + + getVolume(): Promise { + return this.player.getVolume(); + } + + setVolume(volume: number): Promise { + return this.player.setVolume(volume); + } + + pause(): Promise { + return this.player.pause(); + } + + resume(): Promise { + return this.player.resume(); + } + + togglePlay(): Promise { + return this.player.togglePlay(); + } + + seek(): Promise { + return this.player.seek(); + } + + previousTrack(): Promise { + return this.player.previousTrack(); + } + + nextTrack(): Promise { + return this.player.nextTrack(); + } + + activateElement(): Promise { + return this.player.activateElement(); + } +} diff --git a/src/features/library/spotify/internal/SpotifyPlayerWrapper.constants.ts b/src/features/library/spotify/internal/SpotifyPlayerWrapper.constants.ts new file mode 100644 index 00000000..c481fe09 --- /dev/null +++ b/src/features/library/spotify/internal/SpotifyPlayerWrapper.constants.ts @@ -0,0 +1,26 @@ +export const SCRIPT_SRC = 'https://sdk.scdn.co/spotify-player.js'; + +export const EVENT_TYPE = { + READY: 'ready', + NOT_READY: 'not_ready', + PLAYER_STATE_CHANGED: 'player_state_changed', + AUTOPLAY_FAILED: 'autoplay_failed', +} as const; + +export const ERROR_TYPE = { + INITIALIZATION_ERROR: 'initialization_error', + AUTHENTICATION_ERROR: 'authentication_error', + ACCOUNT_ERROR: 'account_error', + PLAYBACK_ERROR: 'playback_error', +} as const; + +export const PLAYER_TRACK_TYPE = { + TRACK: 'track', + EPISODE: 'episode', + AD: 'ad', +} as const; + +export const PLAYER_MEDIA_TYPE = { + AUDIO: 'audio', + VIDEO: 'video', +} as const; diff --git a/src/features/library/spotify/internal/SpotifyPlayerWrapper.types.ts b/src/features/library/spotify/internal/SpotifyPlayerWrapper.types.ts new file mode 100644 index 00000000..d51f741f --- /dev/null +++ b/src/features/library/spotify/internal/SpotifyPlayerWrapper.types.ts @@ -0,0 +1,90 @@ +import type { + ERROR_TYPE, + EVENT_TYPE, + PLAYER_MEDIA_TYPE, + PLAYER_TRACK_TYPE, +} from './SpotifyPlayerWrapper.constants.js'; + +export type SpotifyPlayerInternal = { + on: (event: SpotifyPlayerError, callback: (error: Error) => void) => boolean; + + connect: () => Promise; + disconnect: () => void; + + addListener: ( + event: SpotifyPlayerEvent, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (data: any) => void + ) => boolean; + removeListener: (event: SpotifyPlayerEvent) => boolean; + getCurrentState: () => Promise; + + setName: (name: string) => void; + getVolume: () => Promise; + setVolume: (volume: number) => Promise; + pause: () => Promise; + resume: () => Promise; + togglePlay: () => Promise; + seek: () => Promise; + previousTrack: () => Promise; + nextTrack: () => Promise; + activateElement: () => Promise; +}; + +export type SpotifyDeviceId = { device_id: string }; + +export type SpotifyTokenRefreshFunction = ( + callback: (token: string) => void +) => void; + +export type SpotifyTrackType = + (typeof PLAYER_TRACK_TYPE)[keyof typeof PLAYER_TRACK_TYPE]; + +export type SpotifyPlayerMediaType = + (typeof PLAYER_MEDIA_TYPE)[keyof typeof PLAYER_MEDIA_TYPE]; + +export type SpotifyPlayerEvent = (typeof EVENT_TYPE)[keyof typeof EVENT_TYPE]; + +export type SpotifyPlayerError = (typeof ERROR_TYPE)[keyof typeof ERROR_TYPE]; + +export type SpotifyPlayerTrack = { + uri: string; // Spotify URI + id?: string; // Spotify ID from URI (can be null) + type: SpotifyTrackType; // Content type: can be "track", "episode" or "ad" + media_type: SpotifyPlayerMediaType; // Type of file: can be "audio" or "video" + name: string; // Name of content + is_playable: boolean; // Flag indicating whether it can be played + album: { + uri: string; // Spotify Album URI + name: string; + images: ReadonlyArray<{ url: string }>; + }; + artists: ReadonlyArray<{ uri: string; name: string }>; +}; + +export type SpotifyPlayerState = { + context: { + uri?: string; // The URI of the context (can be null) + metadata?: Record; // Additional metadata for the context (can be null) + }; + disallows: { + // A simplified set of restriction controls for + pausing: boolean; // The current track. By default, these fields + peeking_next: boolean; // will either be set to false or undefined, which + peeking_prev: boolean; // indicates that the particular operation is + resuming: boolean; // allowed. When the field is set to `true`, this + seeking: boolean; // means that the operation is not permitted. For + skipping_next: boolean; // example, `skipping_next`, `skipping_prev` and + skipping_prev: boolean; // `seeking` will be set to `true` when playing an ad track. + }; + paused: boolean; // Whether the current track is paused. + position: number; // The position_ms of the current track. + repeat_mode: 0 | 1 | 2; // The repeat mode. No repeat mode is 0, + // repeat context is 1 and repeat track is 2. + shuffle: boolean; // True if shuffled, false otherwise. + track_window: { + current_track: SpotifyPlayerTrack; // The track currently on local playback + previous_tracks: ReadonlyArray; // Previously played tracks. Number can vary. + next_tracks: ReadonlyArray; // Tracks queued next. Number can vary. + }; +}; diff --git a/tsconfig.json b/tsconfig.json index 6a85888b..25aedc7f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@tsconfig/strictest", "compilerOptions": { "lib": [ + "DOM", "ESNext" ], "incremental": false,