From b5bf5fd5a750fdef9f544cb9b5d0b63f7fa16f78 Mon Sep 17 00:00:00 2001 From: codey Date: Sat, 21 Feb 2026 08:00:04 -0500 Subject: [PATCH] update references to codey.lol -> codey.horse [new domain] --- README.md | 4 +- src/components/DiscordLogs.tsx | 2 +- src/components/Login.tsx | 2 +- src/components/Memes.tsx | 2 +- src/components/Radio.tsx | 2 +- src/components/TRip/MediaRequestForm.tsx | 271 +++++++++++++++------- src/components/TRip/RequestManagement.tsx | 6 +- src/config.ts | 8 +- src/layouts/Nav.astro | 10 +- src/middleware.js | 6 +- src/middleware.ts | 6 +- src/pages/login.astro | 2 +- src/utils/authFetch.ts | 2 +- 13 files changed, 215 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index d49d750..b553c8a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# codey.lol -codey.lol is a web app built with Astro, TypeScript, Tailwind CSS, React, and Vite. +# codey.horse +codey.horse is a web app built with Astro, TypeScript, Tailwind CSS, React, and Vite. ## Pages Overview diff --git a/src/components/DiscordLogs.tsx b/src/components/DiscordLogs.tsx index 922c612..f052118 100644 --- a/src/components/DiscordLogs.tsx +++ b/src/components/DiscordLogs.tsx @@ -665,7 +665,7 @@ const ARCHIVE_USERS = { id: '456226577798135808', username: 'slip', displayName: 'poopboy', - avatar: 'https://codey.lol/images/456226577798135808.png', + avatar: 'https://codey.horse/images/456226577798135808.png', color: null, }, }; diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 7418106..e57bb7f 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -15,7 +15,7 @@ function clearCookie(name: string): void { } // Trusted domains for redirect validation -const TRUSTED_DOMAINS = ['codey.lol', 'boatson.boats']; +const TRUSTED_DOMAINS = ['codey.horse', 'boatson.boats']; /** * Parse and decode the redirect URL from query params diff --git a/src/components/Memes.tsx b/src/components/Memes.tsx index f7ca60b..4bb5fc0 100644 --- a/src/components/Memes.tsx +++ b/src/components/Memes.tsx @@ -8,7 +8,7 @@ import { toast } from 'react-toastify'; import { API_URL } from '../config'; const MEME_API_URL = `${API_URL}/memes/list_memes`; -const BASE_IMAGE_URL = "https://codey.lol/meme"; +const BASE_IMAGE_URL = "https://codey.horse/meme"; interface MemeImage { id: string; diff --git a/src/components/Radio.tsx b/src/components/Radio.tsx index 0c6c5d5..3a5792d 100644 --- a/src/components/Radio.tsx +++ b/src/components/Radio.tsx @@ -203,7 +203,7 @@ export default function Player({ user }: PlayerProps) { setIsStreamReady(false); }; - const streamUrl = `https://stream.codey.lol/hls/${station}/${station}.m3u8`; + const streamUrl = `https://stream.codey.horse/hls/${station}/${station}.m3u8`; // Check for native HLS support (Safari) if (audio.canPlayType("application/vnd.apple.mpegurl")) { diff --git a/src/components/TRip/MediaRequestForm.tsx b/src/components/TRip/MediaRequestForm.tsx index 3483ecf..26b5921 100644 --- a/src/components/TRip/MediaRequestForm.tsx +++ b/src/components/TRip/MediaRequestForm.tsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect, useRef, Suspense, lazy, useMemo } from "react"; +import React, { useState, useEffect, useRef, Suspense, lazy, useMemo, useCallback } from "react"; import type { RefObject } from "react"; import type { Id } from "react-toastify"; import { toast } from "react-toastify"; import { Button } from "@mui/joy"; import { Accordion, AccordionTab } from "primereact/accordion"; import { AutoComplete } from "primereact/autocomplete"; -import { authFetch } from "@/utils/authFetch"; +import { authFetch, doRefresh } from "@/utils/authFetch"; import { useAutoCompleteScrollFix } from '@/hooks/useAutoCompleteScrollFix'; import BreadcrumbNav from "./BreadcrumbNav"; import { API_URL, ENVIRONMENT } from "@/config"; @@ -114,7 +114,21 @@ export default function MediaRequestForm() { const [videoPreviewId, setVideoPreviewId] = useState(null); const [videoStreamUrl, setVideoStreamUrl] = useState(null); const [isVideoLoading, setIsVideoLoading] = useState(false); + const [videoDownloadProgress, setVideoDownloadProgress] = useState(0); // 0-100 const [showVideoSection, setShowVideoSection] = useState(false); + const videoAbortRef = useRef(null); + + const resetVideoState = () => { + videoAbortRef.current?.abort(); + if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl); + setVideoResults([]); + setSelectedVideos(new Set()); + setVideoPreviewId(null); + setVideoStreamUrl(null); + setVideoSearchQuery(""); + setVideoDownloadProgress(0); + setIsVideoLoading(false); + }; const { attachScrollFix, cleanupScrollFix } = useAutoCompleteScrollFix(); @@ -506,6 +520,8 @@ export default function MediaRequestForm() { toast.dismiss(); setIsSearching(true); resetQueueState(); + const videosWereOpen = showVideoSection; + resetVideoState(); setShuffleAlbums({}); if (audioRef.current) { audioRef.current.pause(); @@ -558,6 +574,11 @@ export default function MediaRequestForm() { return acc; }, {}) ); + + // If the video section was open before, auto-fetch videos for the new artist + if (videosWereOpen && selectedArtist) { + fetchArtistVideos(selectedArtist.id); + } } catch (err) { toast.error("Failed to fetch albums for artist."); setAlbums([]); @@ -793,27 +814,80 @@ export default function MediaRequestForm() { }; const handleVideoPreview = async (video: Video) => { - if (videoPreviewId === video.id && videoStreamUrl) { - // Already previewing this video, close it + if (videoPreviewId === video.id) { + // Already previewing this video — toggle off + videoAbortRef.current?.abort(); + if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl); setVideoPreviewId(null); setVideoStreamUrl(null); + setVideoDownloadProgress(0); return; } + // Abort any in-flight preview download + videoAbortRef.current?.abort(); + if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl); + + const abort = new AbortController(); + videoAbortRef.current = abort; + setVideoPreviewId(video.id); setVideoStreamUrl(null); setIsVideoLoading(true); + setVideoDownloadProgress(0); + + // Use XMLHttpRequest instead of fetch to avoid tying up a fetch + // connection slot — the browser limits concurrent fetch connections + // per origin (~6), and a long video download via ReadableStream + // would starve the sequential track metadata fetches. + const doXhr = (retry: boolean): Promise => new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", `${API_URL}/trip/video/${video.id}/download`); + xhr.responseType = "blob"; + xhr.withCredentials = true; // send cookies + + abort.signal.addEventListener("abort", () => { + xhr.abort(); + reject(new DOMException("Aborted", "AbortError")); + }); + + xhr.onprogress = (e) => { + if (e.lengthComputable) { + setVideoDownloadProgress(Math.round((e.loaded / e.total) * 100)); + } else { + setVideoDownloadProgress(Math.min(95, Math.round(e.loaded / 1024 / 1024))); + } + }; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + const blobUrl = URL.createObjectURL(xhr.response); + setVideoStreamUrl(blobUrl); + setVideoDownloadProgress(100); + resolve(); + } else if (xhr.status === 401 && retry) { + // Token expired — refresh and retry once + doRefresh().then((ok) => { + if (!ok || abort.signal.aborted) { + reject(new Error("Auth refresh failed")); + return; + } + setVideoDownloadProgress(0); + doXhr(false).then(resolve, reject); + }).catch(reject); + } else { + reject(new Error(`Failed to fetch video: ${xhr.status}`)); + } + }; + + xhr.onerror = () => reject(new Error("Network error downloading video")); + xhr.send(); + }); try { - const res = await authFetch(`${API_URL}/trip/video/${video.id}/stream`); - if (!res.ok) throw new Error("Failed to fetch video stream URL"); - const data = await res.json(); - if (data.stream_url || data.url) { - setVideoStreamUrl(data.stream_url || data.url); - } else { - throw new Error("No stream URL returned"); - } - } catch (err) { + await doXhr(true); + } catch (err: any) { + if (err?.name === "AbortError") return; console.error(err); toast.error("Failed to load video preview."); setVideoPreviewId(null); @@ -993,6 +1067,8 @@ export default function MediaRequestForm() { audio.removeEventListener("seeked", updateProgress); Object.values(audioSourcesRef.current).forEach((url) => URL.revokeObjectURL(url)); audioSourcesRef.current = {}; + // Abort any in-flight video download and revoke blob URL + videoAbortRef.current?.abort(); }; }, []); @@ -1072,6 +1148,7 @@ export default function MediaRequestForm() { const fetchTracksSequentially: FetchTracksSequentiallyFn = async () => { const minDelay = 650; // ms between API requests + const maxRetries = 3; // retry up to 3 times on failure setIsFetching(true); const totalAlbums = albumsToFetch.length; @@ -1083,28 +1160,38 @@ export default function MediaRequestForm() { setLoadingAlbumId(album.id); - try { - const now = Date.now(); - if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0; - const elapsed = now - fetchTracksSequentially.lastCall; - if (elapsed < minDelay) await delay(minDelay - elapsed); - fetchTracksSequentially.lastCall = Date.now(); + let succeeded = false; + for (let attempt = 0; attempt < maxRetries && !isCancelled; attempt++) { + try { + const now = Date.now(); + if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0; + const elapsed = now - fetchTracksSequentially.lastCall; + const currentDelay = attempt === 0 ? minDelay : minDelay + attempt * 1000; + if (elapsed < currentDelay) await delay(currentDelay - elapsed); + fetchTracksSequentially.lastCall = Date.now(); - const res = await authFetch(`${API_URL}/trip/get_tracks_by_album_id/${album.id}`); - if (!res.ok) throw new Error("API error"); - const data = await res.json(); + const res = await authFetch(`${API_URL}/trip/get_tracks_by_album_id/${album.id}`); + if (!res.ok) throw new Error("API error"); + const data = await res.json(); - if (isCancelled) break; + if (isCancelled) break; - setTracksByAlbum((prev) => ({ ...prev, [album.id]: data })); - setSelectedTracks((prev) => ({ - ...prev, - [album.id]: data.map((t) => String(t.id)), - })); - } catch (err) { - toast.error(`Failed to fetch tracks for album ${album.album}.`); - setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] })); - setSelectedTracks((prev) => ({ ...prev, [album.id]: [] })); + setTracksByAlbum((prev) => ({ ...prev, [album.id]: data })); + setSelectedTracks((prev) => ({ + ...prev, + [album.id]: data.map((t) => String(t.id)), + })); + succeeded = true; + break; // success — move to next album + } catch (err) { + if (attempt < maxRetries - 1) { + console.warn(`Retry ${attempt + 1}/${maxRetries} for album "${album.album}" (id ${album.id})`); + } else { + toast.error(`Failed to fetch tracks for album ${album.album}.`); + setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] })); + setSelectedTracks((prev) => ({ ...prev, [album.id]: [] })); + } + } } // Update progress toast @@ -1596,8 +1683,8 @@ export default function MediaRequestForm() { - {/* Videos Section - Show when artist is selected */} - {selectedArtist && ( + {/* Videos Section - Show when artist is selected or videos are loaded */} + {(selectedArtist || (showVideoSection && videoResults.length > 0)) && (

@@ -1694,41 +1781,78 @@ export default function MediaRequestForm() { : "border-neutral-200 dark:border-neutral-700" }`} > - {/* Video Thumbnail */} -
handleVideoPreview(video)} - > - {video.image || video.imageUrl ? ( - {video.title} +
- - {/* Video Preview Player */} - {videoPreviewId === video.id && videoStreamUrl && ( -
-
- )}

))}
diff --git a/src/components/TRip/RequestManagement.tsx b/src/components/TRip/RequestManagement.tsx index 0881b5e..d6a93a4 100644 --- a/src/components/TRip/RequestManagement.tsx +++ b/src/components/TRip/RequestManagement.tsx @@ -36,7 +36,7 @@ interface RequestJob { } const STATUS_OPTIONS = ["Queued", "Started", "Compressing", "Finished", "Failed"]; -const TAR_BASE_URL = "https://kr.codey.lol"; // configurable prefix +const TAR_BASE_URL = "https://kr.codey.horse"; // configurable prefix export default function RequestManagement() { const [requests, setRequests] = useState([]); @@ -65,8 +65,8 @@ export default function RequestManagement() { // Check if path is /storage/music/TRIP if (absPath.includes("/storage/music/TRIP/")) { return isVideo - ? `https://_music.codey.lol/TRIP/videos/${filename}` - : `https://_music.codey.lol/TRIP/${filename}`; + ? `https://_music.codey.horse/TRIP/videos/${filename}` + : `https://_music.codey.horse/TRIP/${filename}`; } // Otherwise, assume /storage/music2/completed/{quality} format diff --git a/src/config.ts b/src/config.ts index f263bc7..7ee7a16 100644 --- a/src/config.ts +++ b/src/config.ts @@ -39,9 +39,9 @@ export interface ProtectedRoute { } export const metaData: MetaData = { - baseUrl: "https://codey.lol/", + baseUrl: "https://codey.horse/", title: "CODEY STUFF", - name: "codey.lol", + name: "codey.horse", owner: "codey", ogImage: "/images/favicon.png", description: "CODEY STUFF!", @@ -57,8 +57,8 @@ export const metaData: MetaData = { ], }; -export const API_URL: string = "https://api.codey.lol"; -export const RADIO_API_URL: string = "https://radio-api.codey.lol"; +export const API_URL: string = "https://api.codey.horse"; +export const RADIO_API_URL: string = "https://radio-api.codey.horse"; export const socialLinks: Record = { }; diff --git a/src/layouts/Nav.astro b/src/layouts/Nav.astro index 4598968..d56a9d4 100644 --- a/src/layouts/Nav.astro +++ b/src/layouts/Nav.astro @@ -24,15 +24,15 @@ const navItems = [ { label: "Discord Logs", href: "/discord-logs", auth: true }, { label: "Lighting", href: "/lighting", auth: true, adminOnly: true }, { label: "Git", href: "https://kode.boatson.boats", icon: "external" }, - { label: "Glances", href: "https://_gl.codey.lol", auth: true, icon: "external", + { label: "Glances", href: "https://_gl.codey.horse", auth: true, icon: "external", adminOnly: true }, - { label: "PSQL", href: "https://_pg.codey.lol", auth: true, icon: "external", + { label: "PSQL", href: "https://_pg.codey.horse", auth: true, icon: "external", adminOnly: true }, - { label: "qBitTorrent", href: "https://_qb.codey.lol", auth: true, icon: "external", + { label: "qBitTorrent", href: "https://_qb.codey.horse", auth: true, icon: "external", adminOnly: true }, - { label: "RQ", href: "https://_rq.codey.lol", auth: true, icon: "external", + { label: "RQ", href: "https://_rq.codey.horse", auth: true, icon: "external", adminOnly: true }, - { label: "RI", href: "https://_r0.codey.lol", auth: true, icon: "external", + { label: "RI", href: "https://_r0.codey.horse", auth: true, icon: "external", adminOnly: true }, // { label: "Status", href: "https://status.boatson.boats", icon: "external" }, { label: "Login", href: "/login", guestOnly: true }, diff --git a/src/middleware.js b/src/middleware.js index 889a750..025b2bb 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -26,7 +26,7 @@ if (typeof globalThis.Headers !== 'undefined' && typeof globalThis.Headers.proto } } -const API_URL = "https://api.codey.lol"; +const API_URL = "https://api.codey.horse"; const AUTH_TIMEOUT_MS = 3000; // 3 second timeout for auth requests // Deduplication for concurrent refresh requests (prevents race condition where @@ -392,7 +392,7 @@ export const onRequest = defineMiddleware(async (context, next) => { } } - // Block /subsites/req/* on main domain (codey.lol, local.codey.lol, etc) + // Block /subsites/req/* on main domain (codey.horse, local.codey.horse, etc) const isMainDomain = !wantsSubsite; if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) { // Immediately return a 404 for /req on the main domain @@ -460,7 +460,7 @@ export const onRequest = defineMiddleware(async (context, next) => { "font-src 'self' https://fonts.gstatic.com data:", "img-src 'self' data: blob: https: http:", "media-src 'self' blob: https:", - "connect-src 'self' https://api.codey.lol https://*.codey.lol https://*.audio.tidal.com wss:", + "connect-src 'self' https://api.codey.horse https://*.codey.horse https://*.audio.tidal.com wss:", // Allow YouTube for video embeds and Cloudflare for challenges/Turnstile "frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com", "object-src 'none'", diff --git a/src/middleware.ts b/src/middleware.ts index 6ec8145..514dc6b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -64,7 +64,7 @@ if (typeof globalThis.Headers !== 'undefined' && typeof (globalThis.Headers.prot } } -const API_URL: string = "https://api.codey.lol"; +const API_URL: string = "https://api.codey.horse"; const AUTH_TIMEOUT_MS: number = 3000; // 3 second timeout for auth requests // Deduplication for concurrent refresh requests (prevents race condition where @@ -430,7 +430,7 @@ export const onRequest = defineMiddleware(async (context, next) => { } } - // Block /subsites/req/* on main domain (codey.lol, local.codey.lol, etc) + // Block /subsites/req/* on main domain (codey.horse, local.codey.horse, etc) const isMainDomain = !wantsSubsite; if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) { // Immediately return a 404 for /req on the main domain @@ -501,7 +501,7 @@ export const onRequest = defineMiddleware(async (context, next) => { "font-src 'self' https://fonts.gstatic.com data:", "img-src 'self' data: blob: https: http:", "media-src 'self' blob: https:", - "connect-src 'self' https://api.codey.lol https://*.codey.lol https://*.audio.tidal.com wss:", + "connect-src 'self' https://api.codey.horse https://*.codey.horse https://*.audio.tidal.com wss:", // Allow YouTube for video embeds and Cloudflare for challenges/Turnstile "frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com", "object-src 'none'", diff --git a/src/pages/login.astro b/src/pages/login.astro index 197d0a4..26aa2c1 100644 --- a/src/pages/login.astro +++ b/src/pages/login.astro @@ -23,7 +23,7 @@ const getCookie = (name: string): string | null => { // Detect if returnUrl is external (nginx-protected vhost redirect) // If logged-in user arrives with external returnUrl, nginx denied them - show access denied const returnUrl = Astro.url.searchParams.get('returnUrl') || Astro.url.searchParams.get('redirect'); -const trustedDomains = ['codey.lol', 'boatson.boats']; +const trustedDomains = ['codey.horse', 'boatson.boats']; let isExternalReturn = false; let externalHostname = ''; diff --git a/src/utils/authFetch.ts b/src/utils/authFetch.ts index 82ed4f2..2ab1f94 100644 --- a/src/utils/authFetch.ts +++ b/src/utils/authFetch.ts @@ -39,7 +39,7 @@ export const authFetch = async ( }; // Centralized refresh function that handles deduplication properly -async function doRefresh(): Promise { +export async function doRefresh(): Promise { const now = Date.now(); // If a refresh just succeeded recently, assume we're good