Keep a favorite's formats when saving it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
Simon
2026-11-02 22:50:40 +00:00
parent 93463bce3a
commit ba6045a2b1
2 changed files with 29 additions and 25 deletions

View File

@@ -16,13 +16,9 @@ App.favorites = App.favorites || {};
const raw = localStorage.getItem(FAVORITES_KEY);
const parsed = raw ? JSON.parse(raw) : [];
if (!Array.isArray(parsed)) return [];
// Two things are repaired on the way in, and written back once if
// One thing is repaired on the way in, and written back once if
// anything changed, so the fix happens exactly one time:
//
// `meta`, from older versions, is a blob of resolved formats whose
// URLs are signed and long expired -- dropped so no code path can
// reach for one; everything re-resolves from `url` at play time.
//
// `favoriteDate` didn't exist before sorting needed it. There's no
// way to recover when an old favorite was actually saved, so it
// gets now: they sort together, as one batch, at the point the
@@ -31,10 +27,9 @@ App.favorites = App.favorites || {};
const now = new Date().toISOString();
const items = parsed.map((item) => {
if (!item || typeof item !== 'object') return item;
if (!item.meta && item.favoriteDate) return item;
if (item.favoriteDate) return item;
const clean = Object.assign({}, item);
delete clean.meta;
if (!clean.favoriteDate) clean.favoriteDate = now;
clean.favoriteDate = now;
repaired = true;
return clean;
});
@@ -103,6 +98,18 @@ App.favorites = App.favorites || {};
return video.key || meta.key || video.id || meta.id || video.url || meta.url || null;
};
const formatsOf = function(video, meta) {
const src = Array.isArray(video.formats) && video.formats.length ? video
: (meta && Array.isArray(meta.formats) && meta.formats.length ? meta : null);
if (!src) return undefined;
return {
url: src.url || video.url || '',
formats: src.formats,
http_headers: src.http_headers || {},
isLive: !!(src.isLive || video.isLive)
};
};
App.favorites.normalize = function(video) {
const key = App.favorites.getKey(video);
if (!key) return null;
@@ -110,9 +117,7 @@ App.favorites = App.favorites || {};
return {
key,
id: video.id || null,
// The page/source URL (e.g. the YouTube watch URL), not a resolved
// CDN media URL -- those expire, so favorites must always re-resolve
// via the server at play time instead of caching a stream link.
// The page/source URL (e.g. the YouTube watch URL).
url: video.url || (meta && meta.url) || '',
title: video.title || '',
thumb: video.thumb || '',
@@ -122,13 +127,10 @@ App.favorites = App.favorites || {};
isLive: !!(video.isLive || (meta && meta.isLive)),
// When it was saved. An import carries the date the other client
// recorded; anything saved here is saved now.
favoriteDate: video.favoriteDate || new Date().toISOString()
// No `meta` field: persisting resolved formats would freeze their
// (expiring) CDN URLs into localStorage. Leaving it unset makes a
// favorite look like a fresh, unresolved listing item again, so
// playback/download/info all re-resolve through the backend from
// `url` -- see resolveStreamSources' no-formats fallback, which the
// backend resolves live via yt-dlp (main.py stream_video).
favoriteDate: video.favoriteDate || new Date().toISOString(),
// The formats the video was saved with, so it plays without being
// resolved again. Only when there are some.
meta: formatsOf(video, meta)
};
};
@@ -251,7 +253,7 @@ App.favorites = App.favorites || {};
'uploader', 'duration', 'isLive', 'favoriteDate'];
const sameEntry = function(a, b) {
return ENTRY_FIELDS.every((field) =>
return JSON.stringify(a.meta || null) === JSON.stringify(b.meta || null) && ENTRY_FIELDS.every((field) =>
String(a[field] === undefined || a[field] === null ? '' : a[field]) ===
String(b[field] === undefined || b[field] === null ? '' : b[field]));
};
@@ -499,9 +501,6 @@ App.favorites = App.favorites || {};
card.onclick = () => {
if (card.classList.contains('is-loading')) return;
card.classList.add('is-loading');
// Ignore any stale `meta` a favorite saved before this fix may
// still carry in localStorage -- always re-resolve from `item`
// (id/url) so playback never reuses an expired stream URL.
App.player.open(item, { originEl: card });
};
const favoriteBtn = card.querySelector('.favorite-btn');
@@ -525,9 +524,6 @@ App.favorites = App.favorites || {};
showInfoBtn.onclick = (event) => {
event.stopPropagation();
App.videos.closeAllMenus();
// Favorites deliberately store no resolved metadata; the
// panel opens on what the entry holds and resolves the rest
// itself.
App.ui.openInfo(item);
};
}

View File

@@ -61,6 +61,14 @@ def main():
ok = bool(asked) and asked[0]["channel"] == "rule34video" and asked[0]["server"] == SERVER
print(f" [{'PASS' if ok else 'FAIL'}] asked the right channel and server {asked[:1]}")
failed += 0 if ok else 1
saved = page.evaluate("""(item) => {
localStorage.setItem('favorites', '[]');
App.favorites.toggle(Object.assign({}, item, { formats: [{ url: 'https://x/y.mp4', height: 720 }] }));
return App.favorites.getAll()[0];
}""", FAV)
ok = bool(saved) and bool(saved.get("meta")) and saved["meta"]["formats"][0]["url"] == "https://x/y.mp4"
print(f" [{'PASS' if ok else 'FAIL'}] a saved favorite keeps its formats")
failed += 0 if ok else 1
b.close()
return 1 if failed else 0