update references to codey.lol -> codey.horse [new domain]
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# codey.lol
|
# codey.horse
|
||||||
codey.lol is a web app built with Astro, TypeScript, Tailwind CSS, React, and Vite.
|
codey.horse is a web app built with Astro, TypeScript, Tailwind CSS, React, and Vite.
|
||||||
|
|
||||||
## Pages Overview
|
## Pages Overview
|
||||||
|
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ const ARCHIVE_USERS = {
|
|||||||
id: '456226577798135808',
|
id: '456226577798135808',
|
||||||
username: 'slip',
|
username: 'slip',
|
||||||
displayName: 'poopboy',
|
displayName: 'poopboy',
|
||||||
avatar: 'https://codey.lol/images/456226577798135808.png',
|
avatar: 'https://codey.horse/images/456226577798135808.png',
|
||||||
color: null,
|
color: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function clearCookie(name: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trusted domains for redirect validation
|
// 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
|
* Parse and decode the redirect URL from query params
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { API_URL } from '../config';
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
const MEME_API_URL = `${API_URL}/memes/list_memes`;
|
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 {
|
interface MemeImage {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export default function Player({ user }: PlayerProps) {
|
|||||||
setIsStreamReady(false);
|
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)
|
// Check for native HLS support (Safari)
|
||||||
if (audio.canPlayType("application/vnd.apple.mpegurl")) {
|
if (audio.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
|
|||||||
@@ -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 { RefObject } from "react";
|
||||||
import type { Id } from "react-toastify";
|
import type { Id } from "react-toastify";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { Button } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import { Accordion, AccordionTab } from "primereact/accordion";
|
import { Accordion, AccordionTab } from "primereact/accordion";
|
||||||
import { AutoComplete } from "primereact/autocomplete";
|
import { AutoComplete } from "primereact/autocomplete";
|
||||||
import { authFetch } from "@/utils/authFetch";
|
import { authFetch, doRefresh } from "@/utils/authFetch";
|
||||||
import { useAutoCompleteScrollFix } from '@/hooks/useAutoCompleteScrollFix';
|
import { useAutoCompleteScrollFix } from '@/hooks/useAutoCompleteScrollFix';
|
||||||
import BreadcrumbNav from "./BreadcrumbNav";
|
import BreadcrumbNav from "./BreadcrumbNav";
|
||||||
import { API_URL, ENVIRONMENT } from "@/config";
|
import { API_URL, ENVIRONMENT } from "@/config";
|
||||||
@@ -114,7 +114,21 @@ export default function MediaRequestForm() {
|
|||||||
const [videoPreviewId, setVideoPreviewId] = useState<string | number | null>(null);
|
const [videoPreviewId, setVideoPreviewId] = useState<string | number | null>(null);
|
||||||
const [videoStreamUrl, setVideoStreamUrl] = useState<string | null>(null);
|
const [videoStreamUrl, setVideoStreamUrl] = useState<string | null>(null);
|
||||||
const [isVideoLoading, setIsVideoLoading] = useState(false);
|
const [isVideoLoading, setIsVideoLoading] = useState(false);
|
||||||
|
const [videoDownloadProgress, setVideoDownloadProgress] = useState<number>(0); // 0-100
|
||||||
const [showVideoSection, setShowVideoSection] = useState(false);
|
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();
|
const { attachScrollFix, cleanupScrollFix } = useAutoCompleteScrollFix();
|
||||||
|
|
||||||
@@ -506,6 +520,8 @@ export default function MediaRequestForm() {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
resetQueueState();
|
resetQueueState();
|
||||||
|
const videosWereOpen = showVideoSection;
|
||||||
|
resetVideoState();
|
||||||
setShuffleAlbums({});
|
setShuffleAlbums({});
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
audioRef.current.pause();
|
audioRef.current.pause();
|
||||||
@@ -558,6 +574,11 @@ export default function MediaRequestForm() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If the video section was open before, auto-fetch videos for the new artist
|
||||||
|
if (videosWereOpen && selectedArtist) {
|
||||||
|
fetchArtistVideos(selectedArtist.id);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error("Failed to fetch albums for artist.");
|
toast.error("Failed to fetch albums for artist.");
|
||||||
setAlbums([]);
|
setAlbums([]);
|
||||||
@@ -793,27 +814,80 @@ export default function MediaRequestForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleVideoPreview = async (video: Video) => {
|
const handleVideoPreview = async (video: Video) => {
|
||||||
if (videoPreviewId === video.id && videoStreamUrl) {
|
if (videoPreviewId === video.id) {
|
||||||
// Already previewing this video, close it
|
// Already previewing this video — toggle off
|
||||||
|
videoAbortRef.current?.abort();
|
||||||
|
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
|
||||||
setVideoPreviewId(null);
|
setVideoPreviewId(null);
|
||||||
setVideoStreamUrl(null);
|
setVideoStreamUrl(null);
|
||||||
|
setVideoDownloadProgress(0);
|
||||||
return;
|
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);
|
setVideoPreviewId(video.id);
|
||||||
setVideoStreamUrl(null);
|
setVideoStreamUrl(null);
|
||||||
setIsVideoLoading(true);
|
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 {
|
try {
|
||||||
const res = await authFetch(`${API_URL}/trip/video/${video.id}/stream`);
|
await doXhr(true);
|
||||||
if (!res.ok) throw new Error("Failed to fetch video stream URL");
|
} catch (err: any) {
|
||||||
const data = await res.json();
|
if (err?.name === "AbortError") return;
|
||||||
if (data.stream_url || data.url) {
|
|
||||||
setVideoStreamUrl(data.stream_url || data.url);
|
|
||||||
} else {
|
|
||||||
throw new Error("No stream URL returned");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error("Failed to load video preview.");
|
toast.error("Failed to load video preview.");
|
||||||
setVideoPreviewId(null);
|
setVideoPreviewId(null);
|
||||||
@@ -993,6 +1067,8 @@ export default function MediaRequestForm() {
|
|||||||
audio.removeEventListener("seeked", updateProgress);
|
audio.removeEventListener("seeked", updateProgress);
|
||||||
Object.values(audioSourcesRef.current).forEach((url) => URL.revokeObjectURL(url));
|
Object.values(audioSourcesRef.current).forEach((url) => URL.revokeObjectURL(url));
|
||||||
audioSourcesRef.current = {};
|
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 fetchTracksSequentially: FetchTracksSequentiallyFn = async () => {
|
||||||
const minDelay = 650; // ms between API requests
|
const minDelay = 650; // ms between API requests
|
||||||
|
const maxRetries = 3; // retry up to 3 times on failure
|
||||||
setIsFetching(true);
|
setIsFetching(true);
|
||||||
|
|
||||||
const totalAlbums = albumsToFetch.length;
|
const totalAlbums = albumsToFetch.length;
|
||||||
@@ -1083,28 +1160,38 @@ export default function MediaRequestForm() {
|
|||||||
|
|
||||||
setLoadingAlbumId(album.id);
|
setLoadingAlbumId(album.id);
|
||||||
|
|
||||||
try {
|
let succeeded = false;
|
||||||
const now = Date.now();
|
for (let attempt = 0; attempt < maxRetries && !isCancelled; attempt++) {
|
||||||
if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0;
|
try {
|
||||||
const elapsed = now - fetchTracksSequentially.lastCall;
|
const now = Date.now();
|
||||||
if (elapsed < minDelay) await delay(minDelay - elapsed);
|
if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0;
|
||||||
fetchTracksSequentially.lastCall = Date.now();
|
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}`);
|
const res = await authFetch(`${API_URL}/trip/get_tracks_by_album_id/${album.id}`);
|
||||||
if (!res.ok) throw new Error("API error");
|
if (!res.ok) throw new Error("API error");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (isCancelled) break;
|
if (isCancelled) break;
|
||||||
|
|
||||||
setTracksByAlbum((prev) => ({ ...prev, [album.id]: data }));
|
setTracksByAlbum((prev) => ({ ...prev, [album.id]: data }));
|
||||||
setSelectedTracks((prev) => ({
|
setSelectedTracks((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[album.id]: data.map((t) => String(t.id)),
|
[album.id]: data.map((t) => String(t.id)),
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
succeeded = true;
|
||||||
toast.error(`Failed to fetch tracks for album ${album.album}.`);
|
break; // success — move to next album
|
||||||
setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] }));
|
} catch (err) {
|
||||||
setSelectedTracks((prev) => ({ ...prev, [album.id]: [] }));
|
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
|
// Update progress toast
|
||||||
@@ -1596,8 +1683,8 @@ export default function MediaRequestForm() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Videos Section - Show when artist is selected */}
|
{/* Videos Section - Show when artist is selected or videos are loaded */}
|
||||||
{selectedArtist && (
|
{(selectedArtist || (showVideoSection && videoResults.length > 0)) && (
|
||||||
<div className="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-700">
|
<div className="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-700">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
<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"
|
: "border-neutral-200 dark:border-neutral-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Video Thumbnail */}
|
{/* Video Thumbnail / Player area */}
|
||||||
<div
|
{videoPreviewId === video.id && videoStreamUrl ? (
|
||||||
className="relative aspect-video bg-neutral-200 dark:bg-neutral-800 cursor-pointer group"
|
/* Active video player — blob URL has correct duration & seeking */
|
||||||
onClick={() => handleVideoPreview(video)}
|
<div className="relative aspect-video bg-black">
|
||||||
>
|
<video
|
||||||
{video.image || video.imageUrl ? (
|
src={videoStreamUrl}
|
||||||
<img
|
controls
|
||||||
src={video.image || video.imageUrl}
|
autoPlay
|
||||||
alt={video.title}
|
className="w-full h-full"
|
||||||
className="w-full h-full object-cover"
|
onError={() => {
|
||||||
|
toast.error("Failed to play video");
|
||||||
|
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
|
||||||
|
setVideoPreviewId(null);
|
||||||
|
setVideoStreamUrl(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<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">
|
/* Thumbnail with play/progress overlay */
|
||||||
<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" />
|
<div
|
||||||
</svg>
|
className="relative aspect-video bg-neutral-200 dark:bg-neutral-800 cursor-pointer group"
|
||||||
</div>
|
onClick={() => handleVideoPreview(video)}
|
||||||
)}
|
>
|
||||||
{/* Play overlay */}
|
{video.image || video.imageUrl ? (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
|
<img
|
||||||
{isVideoLoading && videoPreviewId === video.id ? (
|
src={video.image || video.imageUrl}
|
||||||
<InlineSpinner sizeClass="h-8 w-8" />
|
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">
|
<div className="w-full h-full flex items-center justify-center text-neutral-400">
|
||||||
<path d="M8 5v14l11-7z" />
|
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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>
|
</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 */}
|
{/* Video Info */}
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
@@ -1759,23 +1883,6 @@ export default function MediaRequestForm() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ interface RequestJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_OPTIONS = ["Queued", "Started", "Compressing", "Finished", "Failed"];
|
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() {
|
export default function RequestManagement() {
|
||||||
const [requests, setRequests] = useState<RequestJob[]>([]);
|
const [requests, setRequests] = useState<RequestJob[]>([]);
|
||||||
@@ -65,8 +65,8 @@ export default function RequestManagement() {
|
|||||||
// Check if path is /storage/music/TRIP
|
// Check if path is /storage/music/TRIP
|
||||||
if (absPath.includes("/storage/music/TRIP/")) {
|
if (absPath.includes("/storage/music/TRIP/")) {
|
||||||
return isVideo
|
return isVideo
|
||||||
? `https://_music.codey.lol/TRIP/videos/${filename}`
|
? `https://_music.codey.horse/TRIP/videos/${filename}`
|
||||||
: `https://_music.codey.lol/TRIP/${filename}`;
|
: `https://_music.codey.horse/TRIP/${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, assume /storage/music2/completed/{quality} format
|
// Otherwise, assume /storage/music2/completed/{quality} format
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ export interface ProtectedRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const metaData: MetaData = {
|
export const metaData: MetaData = {
|
||||||
baseUrl: "https://codey.lol/",
|
baseUrl: "https://codey.horse/",
|
||||||
title: "CODEY STUFF",
|
title: "CODEY STUFF",
|
||||||
name: "codey.lol",
|
name: "codey.horse",
|
||||||
owner: "codey",
|
owner: "codey",
|
||||||
ogImage: "/images/favicon.png",
|
ogImage: "/images/favicon.png",
|
||||||
description: "CODEY STUFF!",
|
description: "CODEY STUFF!",
|
||||||
@@ -57,8 +57,8 @@ export const metaData: MetaData = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_URL: string = "https://api.codey.lol";
|
export const API_URL: string = "https://api.codey.horse";
|
||||||
export const RADIO_API_URL: string = "https://radio-api.codey.lol";
|
export const RADIO_API_URL: string = "https://radio-api.codey.horse";
|
||||||
|
|
||||||
export const socialLinks: Record<string, string> = {
|
export const socialLinks: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ const navItems = [
|
|||||||
{ label: "Discord Logs", href: "/discord-logs", auth: true },
|
{ label: "Discord Logs", href: "/discord-logs", auth: true },
|
||||||
{ label: "Lighting", href: "/lighting", auth: true, adminOnly: true },
|
{ label: "Lighting", href: "/lighting", auth: true, adminOnly: true },
|
||||||
{ label: "Git", href: "https://kode.boatson.boats", icon: "external" },
|
{ 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 },
|
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 },
|
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 },
|
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 },
|
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 },
|
adminOnly: true },
|
||||||
// { label: "Status", href: "https://status.boatson.boats", icon: "external" },
|
// { label: "Status", href: "https://status.boatson.boats", icon: "external" },
|
||||||
{ label: "Login", href: "/login", guestOnly: true },
|
{ label: "Login", href: "/login", guestOnly: true },
|
||||||
|
|||||||
@@ -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
|
const AUTH_TIMEOUT_MS = 3000; // 3 second timeout for auth requests
|
||||||
|
|
||||||
// Deduplication for concurrent refresh requests (prevents race condition where
|
// 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;
|
const isMainDomain = !wantsSubsite;
|
||||||
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
||||||
// Immediately return a 404 for /req on the main domain
|
// 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:",
|
"font-src 'self' https://fonts.gstatic.com data:",
|
||||||
"img-src 'self' data: blob: https: http:",
|
"img-src 'self' data: blob: https: http:",
|
||||||
"media-src 'self' blob: https:",
|
"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
|
// 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",
|
"frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
|
|||||||
@@ -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
|
const AUTH_TIMEOUT_MS: number = 3000; // 3 second timeout for auth requests
|
||||||
|
|
||||||
// Deduplication for concurrent refresh requests (prevents race condition where
|
// 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;
|
const isMainDomain = !wantsSubsite;
|
||||||
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
||||||
// Immediately return a 404 for /req on the main domain
|
// 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:",
|
"font-src 'self' https://fonts.gstatic.com data:",
|
||||||
"img-src 'self' data: blob: https: http:",
|
"img-src 'self' data: blob: https: http:",
|
||||||
"media-src 'self' blob: https:",
|
"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
|
// 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",
|
"frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const getCookie = (name: string): string | null => {
|
|||||||
// Detect if returnUrl is external (nginx-protected vhost redirect)
|
// Detect if returnUrl is external (nginx-protected vhost redirect)
|
||||||
// If logged-in user arrives with external returnUrl, nginx denied them - show access denied
|
// 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 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 isExternalReturn = false;
|
||||||
let externalHostname = '';
|
let externalHostname = '';
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const authFetch = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Centralized refresh function that handles deduplication properly
|
// Centralized refresh function that handles deduplication properly
|
||||||
async function doRefresh(): Promise<boolean> {
|
export async function doRefresh(): Promise<boolean> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// If a refresh just succeeded recently, assume we're good
|
// If a refresh just succeeded recently, assume we're good
|
||||||
|
|||||||
Reference in New Issue
Block a user