aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx')
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx191
1 files changed, 191 insertions, 0 deletions
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx
new file mode 100755
index 0000000..bf586b1
--- /dev/null
+++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx
@@ -0,0 +1,191 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import useWidgetRefresh from "../../hooks/use-widget-refresh";
5import useServerSocket from "../../hooks/use-server-socket";
6import { useSimpleBarContext } from "../simple-bar-context.jsx";
7import * as Icons from "../icons/icons.jsx";
8import * as Utils from "../../utils";
9
10export { youtubeMusicStyles as styles } from "../../styles/components/data/youtube-music";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 10000;
15
16/**
17 * YouTube Music Widget component.
18 * @returns {JSX.Element} The YouTube Music widget.
19 */
20export const Widget = React.memo(() => {
21 const { displayIndex, settings } = useSimpleBarContext();
22 const {
23 widgets: { youtubeMusicWidget },
24 youtubeMusicWidgetOptions: {
25 refreshFrequency,
26 showSpecter,
27 showOnDisplay,
28 youtubeMusicPort,
29 showIcon,
30 },
31 } = settings;
32
33 // Determine the refresh frequency for the widget
34 const refresh = React.useMemo(
35 () =>
36 Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY),
37 [refreshFrequency],
38 );
39
40 // Determine if the widget should be visible on the current display
41 const visible =
42 Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && youtubeMusicWidget;
43
44 const [state, setState] = React.useState();
45 const [cachedAccessToken, setCachedAccessToken] = React.useState();
46 const [loading, setLoading] = React.useState(visible);
47
48 const apiUrl = `http://localhost:${youtubeMusicPort}`;
49
50 /**
51 * Resets the widget state.
52 */
53 const resetWidget = () => {
54 setState(undefined);
55 setCachedAccessToken(undefined);
56 setLoading(false);
57 };
58
59 /**
60 * Fetches the access token for the YouTube Music API.
61 * @returns {Promise<string>} The access token.
62 */
63 const getAccessToken = React.useCallback(async () => {
64 // As of 2024/12/01, the generated access tokens don't expire
65 if (cachedAccessToken) return cachedAccessToken;
66
67 const response = await fetch(`${apiUrl}/auth/simple-bar`, {
68 method: "POST",
69 });
70 const json = await response.json();
71 setCachedAccessToken(json.accessToken);
72 return json.accessToken;
73 }, [apiUrl, cachedAccessToken]);
74
75 /**
76 * Fetches data from a specified route.
77 * @param {string} route - The API route to fetch data from.
78 * @param {string} [method="POST"] - The HTTP method to use.
79 * @returns {Promise<Response>} The fetch response.
80 */
81 const fetchRoute = React.useCallback(
82 async (route, method = "POST") => {
83 const headers = {};
84 const url = new URL(route, apiUrl);
85
86 // All routes under /api are protected
87 const needsAuthentication = url.pathname.startsWith("/api");
88 if (needsAuthentication) {
89 const accessToken = await getAccessToken();
90 if (!accessToken) {
91 throw new Error("Failed to get access token");
92 }
93 headers.Authorization = `Bearer ${accessToken}`;
94 }
95
96 return await fetch(url, { method, headers });
97 },
98 [apiUrl, getAccessToken],
99 );
100
101 /**
102 * Refreshes the widget state by fetching the current song info.
103 */
104 const refreshState = React.useCallback(async () => {
105 if (!visible) return;
106
107 try {
108 const response = await fetchRoute("/api/v1/song-info", "GET");
109 const json = await response.json();
110 setState(json);
111 } catch {
112 // most likely due to offline server, reset state
113 resetWidget();
114 }
115 }, [visible, fetchRoute]);
116
117 // Use server socket to listen for updates
118 useServerSocket(
119 "youtube-music",
120 visible,
121 refreshState,
122 resetWidget,
123 setLoading,
124 );
125 // Use widget refresh hook to periodically refresh the state
126 useWidgetRefresh(visible, refreshState, refresh);
127
128 if (loading) return <DataWidgetLoader.Widget className="youtube-music" />;
129
130 /**
131 * Handles click event to toggle play/pause.
132 * @param {React.MouseEvent} e - The click event.
133 */
134 const onClick = async (e) => {
135 Utils.clickEffect(e);
136 await fetchRoute("/api/v1/toggle-play");
137 await refreshState();
138 };
139
140 /**
141 * Handles right-click event to skip to the next song.
142 * @param {React.MouseEvent} e - The right-click event.
143 */
144 const onRightClick = async (e) => {
145 Utils.clickEffect(e);
146 await fetchRoute("/api/v1/next");
147 await refreshState();
148 };
149
150 /**
151 * Handles middle-click event to open YouTube Music app.
152 * @param {React.MouseEvent} e - The middle-click event.
153 */
154 const onMiddleClick = async (e) => {
155 Utils.clickEffect(e);
156 Uebersicht.run(`open -a 'YouTube Music'`);
157 await refreshState();
158 };
159
160 const classes = Utils.classNames("youtube-music", {});
161 if (!state) return null;
162 const { title, artist, isPaused } = state;
163 if (!title) return null;
164
165 const label = artist.length ? `${title} - ${artist}` : title;
166 const Icon = getIcon(isPaused);
167
168 return (
169 <DataWidget.Widget
170 classes={classes}
171 Icon={showIcon ? Icon : null}
172 onClick={onClick}
173 onRightClick={onRightClick}
174 onMiddleClick={onMiddleClick}
175 showSpecter={showSpecter && !isPaused}
176 >
177 {label}
178 </DataWidget.Widget>
179 );
180});
181
182Widget.displayName = "YouTube Music";
183
184/**
185 * Returns the appropriate icon based on the play/pause state.
186 * @param {boolean} isPaused - Whether the music is paused.
187 * @returns {JSX.Element} The icon component.
188 */
189function getIcon(isPaused) {
190 return isPaused ? Icons.Paused : Icons.Playing;
191}