Skip to content

Commit

Permalink
feat: add basic error handling and message notification for users
Browse files Browse the repository at this point in the history
  • Loading branch information
gloaysa committed Mar 16, 2024
1 parent 80fdbea commit 9b59689
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 214 deletions.
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 120
}
23 changes: 3 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,26 @@
import React, { useEffect } from "react";
import { Route, Switch, useLocation } from "wouter";
import {
useLibraryStore,
useMediaPlayerStore,
useUserStore,
} from "./store/store";
import { useLibraryStore, useMediaPlayerStore, useUserStore } from "./store/store";
import { Box } from "@mui/material";
import { MediaPlayers } from "./views/MediaPlayers";
import MusicLibrary from "./views/MusicLibrary";
import { Configuration } from "./views/Configuration";
import { Notification } from "./components/Notification";

function App() {
const {
configuration: { plexToken },
} = useUserStore((state) => state);
const { mediaPlayers, selectedMediaPlayer, getMediaPlayers } =
useMediaPlayerStore((state) => state);
const { library, getLibrary } = useLibraryStore((state) => state);

const [, setLocation] = useLocation();

useEffect(() => {
if (!mediaPlayers?.length && plexToken) {
getMediaPlayers();
}
}, [mediaPlayers, getMediaPlayers]);

useEffect(() => {
if (!library?.length && selectedMediaPlayer && plexToken) {
getLibrary(selectedMediaPlayer);
}
}, [library, getLibrary, selectedMediaPlayer]);

if (!plexToken) {
setLocation("/config");
}

return (
<Box>
<Notification />
<Switch>
<Route path="/config">
<Configuration />
Expand Down
30 changes: 30 additions & 0 deletions src/components/Notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FunctionComponent } from "react";
import { Alert, Fade, Snackbar } from "@mui/material";
import { useMediaPlayerStore } from "../store/store";

export const Notification: FunctionComponent = () => {
const { error: mediaPlayerError, removeError: removeMediaPlayerError } = useMediaPlayerStore((state) => state);

const handleClose = (index: number) => {
removeMediaPlayerError(index);
};

return (
<>
{mediaPlayerError.map((error, index) => (
<Snackbar
key={index}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
open={true}
onClose={() => handleClose(index)}
TransitionComponent={Fade}
autoHideDuration={5000}
>
<Alert onClose={() => handleClose(index)} severity="error" variant="filled" sx={{ width: "100%" }}>
{error}
</Alert>
</Snackbar>
))}
</>
);
};
2 changes: 2 additions & 0 deletions src/store/media-player.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type MediaPlayerState = {
devicesOrder: string[];
plexToken: string;
};
error: string[];
removeError: (index: number) => void;
mediaPlayers: MediaPlayer[];
selectedMediaPlayer: MediaPlayer | undefined;
setSelectMediaPlayer: (player: MediaPlayer) => void;
Expand Down
67 changes: 19 additions & 48 deletions src/store/media_player.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,31 @@
// @ts-ignore
import XMLParser from "react-xml-parser";
import {
Lyrics,
MediaPlayer,
MetadataFlat,
MetadataWithChildren,
Queue,
} from "./media-player.type";
import { Lyrics, MediaPlayer, MetadataFlat, MetadataWithChildren, Queue } from "./media-player.type";
import { mediaPlayerHeaders } from "./utils/getHeaders";

export async function sendPlayBackCommand(
mediaPlayer: MediaPlayer,
action: string,
): Promise<void> {
export async function sendPlayBackCommand(mediaPlayer: MediaPlayer, action: string): Promise<void> {
const response = await fetch(`${mediaPlayer.uri}/player/playback/${action}`, {
headers: mediaPlayerHeaders(mediaPlayer),
method: "GET",
});
if (!response.ok) {
console.error("Error sending playback command", response);
throw new Error(`Error sending playback action: ${action}`);
}
}

export async function setParameterCommand(
mediaPlayer: MediaPlayer,
params: string,
): Promise<void> {
const response = await fetch(
`${mediaPlayer.uri}/player/playback/setParameters?${params}`,
{
headers: mediaPlayerHeaders(mediaPlayer),
method: "GET",
},
);
export async function setParameterCommand(mediaPlayer: MediaPlayer, params: string): Promise<void> {
const response = await fetch(`${mediaPlayer.uri}/player/playback/setParameters?${params}`, {
headers: mediaPlayerHeaders(mediaPlayer),
method: "GET",
});
if (!response.ok) {
console.error("Error sending parameter command", response);
throw new Error(`Error sending parameter command: ${params}`);
}
}

export async function updateMediaPlayer(
mediaPlayer: MediaPlayer,
commandId: number,
): Promise<MediaPlayer> {
export async function updateMediaPlayer(mediaPlayer: MediaPlayer, commandId: number): Promise<MediaPlayer> {
try {
const baseUrl = `${mediaPlayer.uri}/player/timeline/poll?commandID=${commandId}`;
const response = await fetch(`${baseUrl}&includeMetadata=1&type=music`, {
Expand All @@ -50,9 +34,7 @@ export async function updateMediaPlayer(
});
const data = await response.text();
const json = new XMLParser().parseFromString(data, "application/xml");
const timeline = json.children.find(
(child: any) => child.attributes.type === "music",
);
const timeline = json.children.find((child: any) => child.attributes.type === "music");
mediaPlayer = {
...mediaPlayer,
...timeline.attributes,
Expand All @@ -67,9 +49,6 @@ export async function updateMediaPlayer(
(metadata) => metadata.playQueueItemID === mediaPlayer.playQueueItemID,
);
if (!currentlyPlaying) {
console.log(`${mediaPlayer.name} is not playing anything`);
console.log(`${mediaPlayer.playQueueItemID}`);
console.log(queueData.Metadata);
return mediaPlayer;
}
const thumbUrl = currentlyPlaying?.thumb;
Expand All @@ -82,9 +61,8 @@ export async function updateMediaPlayer(
mediaPlayer.queue = queueData;
}
return mediaPlayer;
} catch (e) {
console.error("Error updating state", e);
return mediaPlayer;
} catch (e: any) {
throw new Error(`Error updating media player ${mediaPlayer.name}: ${e.message}`);
}
}

Expand All @@ -107,28 +85,21 @@ const flattenMetadata = (metadata: MetadataWithChildren): MetadataFlat => {
};
};

const getPlayQueues = async (
mediaPlayer: MediaPlayer,
): Promise<Queue | undefined> => {
const getPlayQueues = async (mediaPlayer: MediaPlayer): Promise<Queue | undefined> => {
if (!mediaPlayer.containerKey) {
return undefined;
}
const response = await fetch(
`${mediaPlayer.server.uri}${mediaPlayer.containerKey}`,
{
headers: mediaPlayerHeaders(mediaPlayer),
method: "GET",
},
);
const response = await fetch(`${mediaPlayer.server.uri}${mediaPlayer.containerKey}`, {
headers: mediaPlayerHeaders(mediaPlayer),
method: "GET",
});
if (response.ok) {
const data = await response.json();
return data.MediaContainer;
}
};

export const getLyrics = async (
mediaPlayer: MediaPlayer,
): Promise<Lyrics | undefined> => {
export const getLyrics = async (mediaPlayer: MediaPlayer): Promise<Lyrics | undefined> => {
if (!mediaPlayer.metadata?.Media.Part.Stream?.key) {
return undefined;
}
Expand Down
80 changes: 34 additions & 46 deletions src/store/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
// @ts-ignore
import XMLParser from "react-xml-parser";
import {
BaseMediaPlayerServer,
PlexResource,
PlexSonosResource,
PlexUser,
} from "./server.interface";
import { BaseMediaPlayerServer, PlexResource, PlexSonosResource, PlexUser } from "./server.interface";
import { MediaPlayer } from "./media-player.type";

const getHeaders = (token: string) => ({
Expand Down Expand Up @@ -45,9 +40,14 @@ export async function getMediaPlayers(token: string): Promise<MediaPlayer[]> {
method: "GET",
});
const resources = await response.json();
if (response.status === 401) {
throw new Error("The Plex token is invalid! Please update it in the settings. You are going to be redirected", {
cause: "token",
});
}
const server = getServerInfo(resources);
if (!server) {
return [];
throw new Error("We couldn't find a server in your local network :(");
}
const clients = await getClients(resources, server, token);
const sonos = await getSonosResource(server, token);
Expand All @@ -57,10 +57,11 @@ export async function getMediaPlayers(token: string): Promise<MediaPlayer[]> {
// sorts by name
.sort((a, b) => a.name.localeCompare(b.name))
);
} catch (error) {
throw new Error(
"Error fetching media players, are you providing the plex token?",
);
} catch (error: any) {
if (error instanceof Error) {
throw error;
}
throw new Error("An error occurred while fetching the media players", error);
}
}

Expand All @@ -69,47 +70,40 @@ export async function getMediaPlayers(token: string): Promise<MediaPlayer[]> {
* @returns An array of sonos media players.
* @param server
*/
async function getSonosResource(
server: BaseMediaPlayerServer,
token: string,
): Promise<MediaPlayer[]> {
async function getSonosResource(server: BaseMediaPlayerServer, token: string): Promise<MediaPlayer[]> {
const response = await fetch(`https://sonos.plex.tv/resources`, {
headers: getHeaders(token),
method: "GET",
});
const data = await response.text();

const json = new XMLParser().parseFromString(data, "application/xml");
return json.children.map(
({ attributes }: { attributes: PlexSonosResource }) => ({
name: attributes.title,
product: attributes.product,
productVersion: attributes.platformVersion,
clientIdentifier: attributes.machineIdentifier,
protocol: attributes.protocol,
address: attributes.lanIP,
uri: "https://sonos.plex.tv",
token: token,
server: server,
state: "unknown",
media_duration: 0,
media_position: 0,
shuffle: "0",
repeat: "0",
volume_level: 0,
is_volume_muted: false,
}),
);
return json.children.map(({ attributes }: { attributes: PlexSonosResource }) => ({
name: attributes.title,
product: attributes.product,
productVersion: attributes.platformVersion,
clientIdentifier: attributes.machineIdentifier,
protocol: attributes.protocol,
address: attributes.lanIP,
uri: "https://sonos.plex.tv",
token: token,
server: server,
state: "unknown",
media_duration: 0,
media_position: 0,
shuffle: "0",
repeat: "0",
volume_level: 0,
is_volume_muted: false,
}));
}

/**
* Function that gets the server info from the resources.
* @returns The server info to be used in the media players.
* @param resources
*/
function getServerInfo(
resources: PlexResource[],
): BaseMediaPlayerServer | undefined {
function getServerInfo(resources: PlexResource[]): BaseMediaPlayerServer | undefined {
const server = resources.find((resource) => resource.provides === "server");
if (!server) {
return undefined;
Expand All @@ -132,9 +126,7 @@ async function getClients(
token: string,
): Promise<MediaPlayer[]> {
// should find all the resources that are not the server, returning an array
const clients = resources.filter((resource) =>
resource.provides.match("client"),
);
const clients = resources.filter((resource) => resource.provides.match("client"));

// iterate over the clients and do a fetch to get the client info, if fetch fails, remove the client from the array
for (const client of clients) {
Expand Down Expand Up @@ -170,11 +162,7 @@ async function getClients(
}));
}

function fetchWithTimeout(
url: string,
options: RequestInit,
timeout = 5000,
): Promise<Response> {
function fetchWithTimeout(url: string, options: RequestInit, timeout = 5000): Promise<Response> {
return new Promise((resolve, reject) => {
// Set up the timeout
const timer = setTimeout(() => {
Expand Down
Loading

0 comments on commit 9b59689

Please sign in to comment.