Skip to content

Commit 61f1a4a

Browse files
committed
fix: wire user cookies through search and tighten cookie UX
Apply client cookies to YouTube search and suggest, clear them on reset, surface validation errors in settings, and validate admin cookie saves.
1 parent 3ee3344 commit 61f1a4a

7 files changed

Lines changed: 62 additions & 8 deletions

File tree

src/app/admin/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Title,
1616
} from "@mantine/core";
1717
import { IconAlertCircle, IconCheck, IconCookie } from "@tabler/icons-react";
18+
import { validateCookies } from "@/lib/cookies";
1819

1920
export default function AdminPage() {
2021
const [password, setPassword] = useState("");
@@ -68,6 +69,15 @@ export default function AdminPage() {
6869
setCookiesError(null);
6970
setCookiesSuccess(false);
7071

72+
if (cookies.trim()) {
73+
const validation = validateCookies(cookies);
74+
if (!validation.valid) {
75+
setCookiesError(validation.error || "Invalid cookie format.");
76+
setCookiesLoading(false);
77+
return;
78+
}
79+
}
80+
7181
try {
7282
const res = await fetch("/api/admin/cookies", {
7383
method: "POST",
@@ -142,6 +152,7 @@ export default function AdminPage() {
142152
placeholder={`# Netscape HTTP Cookie File
143153
.youtube.com\tTRUE\t/\tTRUE\t0\tCOOKIE_NAME\tVALUE`}
144154
minRows={8}
155+
maxRows={10}
145156
autosize
146157
styles={{
147158
input: {

src/app/api/youtube/search/route.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { apiError, searchErrorCode } from "@/lib/apiError";
1+
import { searchErrorCode } from "@/lib/apiError";
2+
import { readRequestCookies } from "@/lib/cookies";
23
import {
34
type MusicSearchResult,
45
type YouTubeSearchResult,
@@ -31,13 +32,16 @@ export async function GET(request: Request) {
3132
}
3233

3334
try {
34-
const musicResults = await searchMusic(query, { limit: Math.min(limit, 50) });
35+
const cookies = readRequestCookies(request);
36+
const searchOptions = { limit: Math.min(limit, 50), cookies };
37+
38+
const musicResults = await searchMusic(query, searchOptions);
3539

3640
if (musicResults.length > 0) {
3741
return Response.json({ results: flattenToSearchResults(musicResults) });
3842
}
3943

40-
const videoResults = await searchYouTubeVideos(query, { limit: Math.min(limit, 50) });
44+
const videoResults = await searchYouTubeVideos(query, searchOptions);
4145
return Response.json({ results: videoResults });
4246
} catch (error) {
4347
const message = error instanceof Error ? error.message : "Failed to search YouTube.";

src/app/api/youtube/suggest/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { searchErrorCode } from "@/lib/apiError";
2+
import { readRequestCookies } from "@/lib/cookies";
23
import { getSearchSuggestions } from "@/lib/youtubei";
34

45
export async function GET(request: Request) {
@@ -13,6 +14,7 @@ export async function GET(request: Request) {
1314
try {
1415
const suggestions = await getSearchSuggestions(query, {
1516
limit: Math.min(Math.max(limit || 10, 1), 20),
17+
cookies: readRequestCookies(request),
1618
});
1719
return Response.json({ suggestions });
1820
} catch (error) {

src/app/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import MediaResultRow, { type MediaResultItem } from "@/components/MediaResultRo
4848
import ResetModal from "@/components/ResetModal";
4949
import { useAppContext } from "@/context/AppContext";
5050
import type { Media } from "@/interfaces";
51+
import { cookieRequestHeaders } from "@/lib/cookies";
5152
import { SEARCH_ACCENT_VAR } from "@/lib/theme";
5253
import { getYouTubeId, isDirectMediaURL, isYoutubeURL } from "@/utils";
5354
import { setMediaCache } from "@/utils/cache";
@@ -318,7 +319,7 @@ function SearchPanel({
318319
try {
319320
const response = await fetch(
320321
`/api/youtube/suggest?q=${encodeURIComponent(clean)}&limit=10`,
321-
{ signal: controller.signal },
322+
{ signal: controller.signal, headers: cookieRequestHeaders() },
322323
);
323324
const data = (await response.json()) as {
324325
suggestions?: string[];
@@ -355,7 +356,7 @@ function SearchPanel({
355356
try {
356357
const response = await fetch(
357358
`/api/youtube/search?q=${encodeURIComponent(value)}&limit=${SEARCH_LIMIT}`,
358-
{ signal },
359+
{ signal, headers: cookieRequestHeaders() },
359360
);
360361
const data = (await response.json()) as {
361362
results?: YouTubeResult[];

src/components/CookiesModal.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,35 @@ export default function CookiesModal({ opened, onClose }: CookiesModalProps) {
1919
const [cookies, setCookiesState] = useState("");
2020
const [customEnabled, setCustomEnabled] = useState(false);
2121
const [saving, setSaving] = useState(false);
22+
const [validationError, setValidationError] = useState<string | null>(null);
2223

2324
useEffect(() => {
2425
if (!opened) return;
2526
const id = requestAnimationFrame(() => {
2627
setCookiesState(getUserCookies());
2728
setCustomEnabled(isCustomCookiesEnabled());
29+
setValidationError(null);
2830
});
2931
return () => cancelAnimationFrame(id);
3032
}, [opened]);
3133

3234
const handleSave = async () => {
3335
setSaving(true);
36+
setValidationError(null);
3437
try {
3538
if (customEnabled && cookies.trim()) {
3639
const validation = validateCookies(cookies);
3740
if (!validation.valid) {
38-
setSaving(false);
41+
setValidationError(validation.error || "Invalid cookie format.");
3942
return;
4043
}
4144
}
4245
setUserCookies(cookies);
4346
setCustomCookiesEnabled(customEnabled);
4447
onClose();
4548
} catch {
49+
setValidationError("Failed to save cookies. Please try again.");
50+
} finally {
4651
setSaving(false);
4752
}
4853
};
@@ -70,13 +75,20 @@ export default function CookiesModal({ opened, onClose }: CookiesModalProps) {
7075
</Text>
7176
}
7277
checked={customEnabled}
73-
onChange={(e) => setCustomEnabled(e.currentTarget.checked)}
78+
onChange={(e) => {
79+
setCustomEnabled(e.currentTarget.checked);
80+
setValidationError(null);
81+
}}
7482
/>
7583

7684
{customEnabled && (
7785
<Textarea
7886
value={cookies}
79-
onChange={(e) => setCookiesState(e.currentTarget.value)}
87+
onChange={(e) => {
88+
setCookiesState(e.currentTarget.value);
89+
setValidationError(null);
90+
}}
91+
error={validationError}
8092
placeholder="Paste your exported cookies.txt here..."
8193
minRows={6}
8294
maxRows={10}

src/lib/cookies.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const COOKIES_KEY = "moonlit-yt-cookies";
22
const CUSTOM_COOKIES_ENABLED_KEY = "moonlit-custom-cookies-enabled";
33

4+
export const MOONLIT_COOKIES_HEADER = "X-Moonlit-Cookies";
5+
46
/** Get user cookies from localStorage */
57
export function getUserCookies(): string {
68
if (typeof window === "undefined") return "";
@@ -36,6 +38,26 @@ export function getCookiesToUse(): { cookies: string } {
3638
return { cookies: "" };
3739
}
3840

41+
/** Headers to attach when calling server APIs that accept user cookies. */
42+
export function cookieRequestHeaders(): HeadersInit {
43+
const { cookies } = getCookiesToUse();
44+
if (!cookies.trim()) return {};
45+
return { [MOONLIT_COOKIES_HEADER]: cookies };
46+
}
47+
48+
/** Read user cookies sent from the client on a server request. */
49+
export function readRequestCookies(request: Request): string | undefined {
50+
const cookies = request.headers.get(MOONLIT_COOKIES_HEADER)?.trim();
51+
return cookies || undefined;
52+
}
53+
54+
/** Remove saved user cookies and the enable preference. */
55+
export function clearUserCookies(): void {
56+
if (typeof window === "undefined") return;
57+
localStorage.removeItem(COOKIES_KEY);
58+
localStorage.removeItem(CUSTOM_COOKIES_ENABLED_KEY);
59+
}
60+
3961
/** Validate Netscape cookie format — each non-empty, non-comment line must have 7 tab-separated fields */
4062
export function validateCookies(content: string): { valid: boolean; error?: string } {
4163
if (!content.trim()) return { valid: true };

src/utils/reset.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import localforage from "localforage";
2+
import { clearUserCookies } from "@/lib/cookies";
23
import { clearPlaybackPrefs, clearShowVideoPref } from "@/lib/playerPrefs";
34

45
export interface ResetOptions {
@@ -22,6 +23,7 @@ export async function resetAllData(options: ResetOptions = { settings: true }) {
2223
}
2324
}
2425
keysToRemove.forEach((key) => localStorage.removeItem(key));
26+
clearUserCookies();
2527
clearShowVideoPref();
2628
clearPlaybackPrefs();
2729
}

0 commit comments

Comments
 (0)