Skip to content

Commit

Permalink
feat(providers): Shared generic typedef for player state, send transf…
Browse files Browse the repository at this point in the history
…ormed WNP state
  • Loading branch information
busybox11 committed Dec 3, 2024
1 parent 275f4ae commit 71c782f
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 15 deletions.
3 changes: 2 additions & 1 deletion app/providers/spotify/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/types/providers/client";
import spotifyProviderMeta from "@/providers/spotify";
import { PlayerState } from "@/types/player";
import makePlayerStateObj from "@/providers/spotify/utils/makePlayerStateObj";

const { VITE_SPOTIFY_CLIENT_ID, VITE_SPOTIFY_REDIRECT_URI } = providerConfig;

Expand Down Expand Up @@ -81,7 +82,7 @@ export default class SpotifyProvider implements IProviderClient {
this._playerLoopInstance = window.setInterval(async () => {
const playerState = await this._getPlayerState();

this.sendPlayerState(playerState);
this.sendPlayerState(makePlayerStateObj(playerState));
}, 1000);
}

Expand Down
67 changes: 67 additions & 0 deletions app/providers/spotify/utils/makePlayerStateObj.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { PlayerObj, PlayerState } from "@/types/player";
import type { PlaybackState } from "@spotify/web-api-ts-sdk";

function makeItemObj(item: PlaybackState["item"]): PlayerObj["item"] {
const baseObj = {
title: item.name,
duration_ms: item.duration_ms,
url: item.external_urls.spotify,
};

if (item.type === "episode" && "show" in item) {
const show = item.show;

return {
...baseObj,
show: {
name: show.name,
url: show.external_urls.spotify,
},
type: "episode",
};
}

const artists = "artists" in item ? item.artists : [];
const album = "album" in item ? item.album : null;

return {
...baseObj,
artists: artists.map((artist) => ({
name: artist.name,
url: artist.external_urls.spotify,
})),
album:
album ?
{
name: album.name,
url: album.external_urls.spotify,
}
: undefined,
type: "track",
};
}

export default function makePlayerStateObj(
state: PlaybackState | null
): PlayerState {
if (!state) return null;

const item = state.item;
if (!item) return null;

const playerObj: PlayerState = {
item: makeItemObj(item),
meta: {
is_playing: state.is_playing,
shuffle_state: state.shuffle_state,
repeat_state: state.repeat_state as "off" | "track" | "context",
position_ms: state.progress_ms,
duration_ms: state.item.duration_ms,
main_img_url: ("album" in state.item ? state.item.album : state.item)
.images[0]?.url,
provider: "spotify",
},
};

return playerObj;
}
8 changes: 6 additions & 2 deletions app/providers/webnowplaying/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
} from "@/types/providers/client";
import { PlayerState } from "@/types/player";
import webnowplayingProviderMeta from "@/providers/webnowplaying";
import { WebNowPlayingPlayerState } from "@/providers/webnowplaying/types";
import makePlayerStateObj from "@/providers/webnowplaying/utils/makePlayerStateObj";

export default class WebNowPlayingProvider implements IProviderClient {
readonly meta = webnowplayingProviderMeta;
Expand All @@ -25,12 +27,14 @@ export default class WebNowPlayingProvider implements IProviderClient {
}

// Private properties and methods
private _lastPlaybackState: PlayerState | null = null;
private _lastPlaybackState: WebNowPlayingPlayerState | null = null;

private _handleMessage = (msg: MessageEvent) => {
if (msg.data.type != "wnp-info" || !msg.data.player) return;

this._lastPlaybackState = msg.data.player;
this._lastPlaybackState = msg.data.player as WebNowPlayingPlayerState;

this.sendPlayerState(makePlayerStateObj(this._lastPlaybackState));
};
private _beginListening() {
window.addEventListener("message", this._handleMessage);
Expand Down
34 changes: 34 additions & 0 deletions app/providers/webnowplaying/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type WebNowPlayingPlayerState = {
id: number;
name: string /* Website name */;

title: string;
artist: string;
album: string;
cover: string;

state: number;

duration: number;
position: number;
volume: number;
rating: number;
repeat: number;
shuffle: boolean;

ratingSystem: number;
availableRepeat: number;

canSetState: boolean;
canSkipPrevious: boolean;
canSkipNext: boolean;
canSetPosition: boolean;
canSetVolume: boolean;
canSetRating: boolean;
canSetRepeat: boolean;
canSetShuffle: boolean;

createdAt: number;
updatedAt: number;
activeAt: number;
};
39 changes: 39 additions & 0 deletions app/providers/webnowplaying/utils/makePlayerStateObj.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { WebNowPlayingPlayerState } from "@/providers/webnowplaying/types";
import type { PlayerState } from "@/types/player";

export default function makePlayerStateObj(
state: WebNowPlayingPlayerState | null
): PlayerState {
if (!state) return null;

const playerObj: PlayerState = {
item: {
title: state.title,
artists: [
{
name: state.artist,
},
],
album: {
name: state.album,
images: [state.cover],
},
duration_ms: state.duration * 1000,
type: "track",
},
meta: {
is_playing: state.state === 0,
shuffle_state: state.shuffle,
repeat_state:
state.repeat === 1 ? "track"
: state.repeat === 2 ? "context"
: "off",
position_ms: state.position * 1000,
duration_ms: state.duration * 1000,
main_img_url: state.cover,
provider: state.name,
},
};

return playerObj;
}
23 changes: 13 additions & 10 deletions app/routes/playing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,9 @@ function PlayingRouteComponent() {
navigate({ to: "/" });
}

const image =
playerState && "album" in playerState?.item ?
playerState?.item?.album?.images[0]?.url
: "assets/images/no_song.png";
const image = playerState?.meta.main_img_url ?? "assets/images/no_song.png";

const title = playerState?.item?.name ?? "NowPlaying";
const title = playerState?.item?.title ?? "NowPlaying";
const artist =
playerState && "artists" in playerState?.item ?
playerState?.item?.artists?.map((artist) => artist.name).join(", ")
Expand All @@ -49,17 +46,20 @@ function PlayingRouteComponent() {
"type" in playerState?.item &&
playerState?.item?.type === "episode";

const isPaused = !playerState || playerState.is_playing === false;
const isPaused = !playerState || playerState.meta.is_playing === false;

const positionNow = playerState?.progress_ms ?? 0;
const positionTotal = playerState?.item?.duration_ms ?? 0;
const positionNow = playerState?.meta.position_ms ?? 0;
const positionTotal = playerState?.item.duration_ms ?? 0;
const positionPercent = positionNow / positionTotal;

const shouldAnimateProgress =
Math.abs(
(playerState?.progress_ms ?? 0) - (previousPlayerState?.progress_ms ?? 0)
(playerState?.meta.position_ms ?? 0) -
(previousPlayerState?.meta.position_ms ?? 0)
) < 5000;

const stateProvider = playerState?.meta.provider ?? activeProvider?.meta.name;

return (
<main className="flex h-full w-full">
<div
Expand Down Expand Up @@ -182,7 +182,10 @@ function PlayingRouteComponent() {
</div>

<span className="text-xl font-bold">
<span>{activeProvider?.meta.name ?? activePlayer}</span>
<span>
{activeProvider?.meta.name ?? activePlayer}{" "}
{stateProvider}
</span>
<span className="text-white/80 font-semibold"></span>
</span>

Expand Down
57 changes: 55 additions & 2 deletions app/types/player/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
import { PlaybackState } from "@spotify/web-api-ts-sdk";
type ArtistItem = {
name: string;
images?: string[];
url?: string;
};

export type PlayerState = PlaybackState | null;
type AlbumItem = {
name?: string;
images?: string[];
url?: string;
};

type PlayerTrackItem = {
title: string;
artists: ArtistItem[];
album?: AlbumItem;
duration_ms?: number;

type?: "track" | string;
url?: string;
};

type PlayerShowItem = {
name: string;
episodes?: PlayerEpisodeItem[];
images?: string[];
url?: string;
};

type PlayerEpisodeItem = {
title: string;
show?: PlayerShowItem;
season?: number;
episode?: number;
duration_ms?: number;

type: "episode";
url?: string;
};

export type PlayerMeta = {
is_playing?: boolean;
shuffle_state?: boolean;
repeat_state?: "off" | "track" | "context";
position_ms?: number;
duration_ms?: number;
main_img_url?: string;
provider?: string;
};

export type PlayerObj = {
item: PlayerTrackItem | PlayerEpisodeItem;
meta: PlayerMeta;
};

export type PlayerState = PlayerObj | null;

0 comments on commit 71c782f

Please sign in to comment.