update references to codey.lol -> codey.horse [new domain]

This commit is contained in:
2026-02-21 08:00:04 -05:00
parent 94166904f7
commit b5bf5fd5a7
13 changed files with 215 additions and 108 deletions

View File

@@ -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

View File

@@ -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,
},
};

View File

@@ -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

View File

@@ -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;

View File

@@ -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")) {

View File

@@ -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<string | number | null>(null);
const [videoStreamUrl, setVideoStreamUrl] = useState<string | null>(null);
const [isVideoLoading, setIsVideoLoading] = useState(false);
const [videoDownloadProgress, setVideoDownloadProgress] = useState<number>(0); // 0-100
const [showVideoSection, setShowVideoSection] = useState(false);
const videoAbortRef = useRef<AbortController | null>(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<void> => 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() {
</Button>
</div>
{/* Videos Section - Show when artist is selected */}
{selectedArtist && (
{/* Videos Section - Show when artist is selected or videos are loaded */}
{(selectedArtist || (showVideoSection && videoResults.length > 0)) && (
<div className="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
@@ -1694,41 +1781,78 @@ export default function MediaRequestForm() {
: "border-neutral-200 dark:border-neutral-700"
}`}
>
{/* Video Thumbnail */}
<div
className="relative aspect-video bg-neutral-200 dark:bg-neutral-800 cursor-pointer group"
onClick={() => handleVideoPreview(video)}
>
{video.image || video.imageUrl ? (
<img
src={video.image || video.imageUrl}
alt={video.title}
className="w-full h-full object-cover"
{/* Video Thumbnail / Player area */}
{videoPreviewId === video.id && videoStreamUrl ? (
/* Active video player — blob URL has correct duration & seeking */
<div className="relative aspect-video bg-black">
<video
src={videoStreamUrl}
controls
autoPlay
className="w-full h-full"
onError={() => {
toast.error("Failed to play video");
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
setVideoPreviewId(null);
setVideoStreamUrl(null);
}}
/>
) : (
<div className="w-full h-full flex items-center justify-center text-neutral-400">
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
)}
{/* Play overlay */}
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
{isVideoLoading && videoPreviewId === video.id ? (
<InlineSpinner sizeClass="h-8 w-8" />
</div>
) : (
/* Thumbnail with play/progress overlay */
<div
className="relative aspect-video bg-neutral-200 dark:bg-neutral-800 cursor-pointer group"
onClick={() => handleVideoPreview(video)}
>
{video.image || video.imageUrl ? (
<img
src={video.image || video.imageUrl}
alt={video.title}
className="w-full h-full object-cover"
/>
) : (
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
<div className="w-full h-full flex items-center justify-center text-neutral-400">
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
)}
{/* Play / Download progress overlay */}
<div className={`absolute inset-0 flex flex-col items-center justify-center transition-opacity ${
isVideoLoading && videoPreviewId === video.id
? 'bg-black/60 opacity-100'
: 'bg-black/30 opacity-0 group-hover:opacity-100'
}`}>
{isVideoLoading && videoPreviewId === video.id ? (
<>
<InlineSpinner sizeClass="h-6 w-6" />
<span className="text-white text-xs mt-2 font-medium">
{videoDownloadProgress > 0
? `Loading… ${videoDownloadProgress}%`
: 'Connecting…'}
</span>
{/* Progress bar */}
<div className="w-3/4 mt-2 h-1.5 bg-white/20 rounded-full overflow-hidden">
<div
className="h-full bg-blue-400 rounded-full transition-all duration-300"
style={{ width: `${videoDownloadProgress}%` }}
/>
</div>
</>
) : (
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
)}
</div>
{/* Duration badge */}
{video.duration && (
<span className="absolute bottom-2 right-2 px-1.5 py-0.5 bg-black/70 text-white text-xs rounded">
{formatVideoDuration(video.duration)}
</span>
)}
</div>
{/* Duration badge */}
{video.duration && (
<span className="absolute bottom-2 right-2 px-1.5 py-0.5 bg-black/70 text-white text-xs rounded">
{formatVideoDuration(video.duration)}
</span>
)}
</div>
)}
{/* Video Info */}
<div className="p-3">
@@ -1759,23 +1883,6 @@ export default function MediaRequestForm() {
</button>
</div>
</div>
{/* Video Preview Player */}
{videoPreviewId === video.id && videoStreamUrl && (
<div className="border-t border-neutral-200 dark:border-neutral-700">
<video
src={videoStreamUrl}
controls
autoPlay
className="w-full"
onError={() => {
toast.error("Failed to play video");
setVideoPreviewId(null);
setVideoStreamUrl(null);
}}
/>
</div>
)}
</div>
))}
</div>

View File

@@ -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<RequestJob[]>([]);
@@ -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

View File

@@ -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<string, string> = {
};

View File

@@ -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 },

View File

@@ -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'",

View File

@@ -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'",

View File

@@ -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 = '';

View File

@@ -39,7 +39,7 @@ export const authFetch = async (
};
// Centralized refresh function that handles deduplication properly
async function doRefresh(): Promise<boolean> {
export async function doRefresh(): Promise<boolean> {
const now = Date.now();
// If a refresh just succeeded recently, assume we're good