mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-03 12:38:26 +01:00
fix: Improve perf download database and correct crash when changing episode (#1547)
Co-authored-by: BuildTools <unconfigured@null.spigotmc.org> Co-authored-by: Gauvain <contact@uruk.dev>
This commit is contained in:
committed by
GitHub
parent
ceb9b5a1ae
commit
7d16e7d5c7
@@ -274,6 +274,11 @@ export default function DirectPlayerPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
|
setItem(null);
|
||||||
|
setDownloadedItem(null);
|
||||||
|
// Clear the previous episode's stream so the loader gate stays closed
|
||||||
|
// until the new item's stream resolves (avoids a stale MPV source frame).
|
||||||
|
setStream(null);
|
||||||
fetchItemData();
|
fetchItemData();
|
||||||
}
|
}
|
||||||
}, [itemId, offline, api, user?.Id]);
|
}, [itemId, offline, api, user?.Id]);
|
||||||
@@ -316,6 +321,12 @@ export default function DirectPlayerPage() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure item matches the current itemId to avoid race conditions
|
||||||
|
if (item.Id !== itemId) {
|
||||||
|
setStreamStatus({ isLoading: false, isError: false });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let result: Stream | null = null;
|
let result: Stream | null = null;
|
||||||
if (offline && downloadedItem?.mediaSource) {
|
if (offline && downloadedItem?.mediaSource) {
|
||||||
const url = downloadedItem.videoFilePath;
|
const url = downloadedItem.videoFilePath;
|
||||||
@@ -388,6 +399,7 @@ export default function DirectPlayerPage() {
|
|||||||
item,
|
item,
|
||||||
user?.Id,
|
user?.Id,
|
||||||
downloadedItem,
|
downloadedItem,
|
||||||
|
offline,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const isOffline = useOfflineMode();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
// Read the live (cached) downloads DB inside the query rather than the
|
||||||
|
// provider's downloadedItems snapshot, so refetches after
|
||||||
|
// updateDownloadedItem() reflect the latest state instead of a stale
|
||||||
|
// refreshKey-gated snapshot. getAllDownloadedItems() is cached, so this stays cheap.
|
||||||
const { getDownloadedItems } = useDownload();
|
const { getDownloadedItems } = useDownload();
|
||||||
|
|
||||||
const scrollRef = useRef<HorizontalScrollRef>(null);
|
const scrollRef = useRef<HorizontalScrollRef>(null);
|
||||||
|
|||||||
@@ -365,7 +365,9 @@ export const Controls: FC<Props> = ({
|
|||||||
{ applyLanguagePreferences: true },
|
{ applyLanguagePreferences: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
// Use setParams instead of replace to avoid unmounting/remounting the player,
|
||||||
|
// which would create a new MPV native view and crash with "mp_initialize already initialized".
|
||||||
|
router.setParams({
|
||||||
...(offline && { offline: "true" }),
|
...(offline && { offline: "true" }),
|
||||||
itemId: item.Id ?? "",
|
itemId: item.Id ?? "",
|
||||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||||
@@ -374,11 +376,17 @@ export const Controls: FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue?.toString(),
|
bitrateValue: bitrateValue?.toString(),
|
||||||
playbackPosition:
|
playbackPosition:
|
||||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
item.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
||||||
}).toString();
|
});
|
||||||
|
|
||||||
router.replace(`player/direct-player?${queryParams}` as any);
|
|
||||||
},
|
},
|
||||||
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
|
[
|
||||||
|
settings,
|
||||||
|
subtitleIndex,
|
||||||
|
audioIndex,
|
||||||
|
mediaSource,
|
||||||
|
bitrateValue,
|
||||||
|
router,
|
||||||
|
offline,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Read the live (cached) downloads DB inside the query rather than the
|
||||||
|
// provider's downloadedItems snapshot. The snapshot only refreshes on the
|
||||||
|
// provider refreshKey, so after updateDownloadedItem() invalidates
|
||||||
|
// ["episodes"]/["seasons"] (e.g. progress/played writes) the refetch would
|
||||||
|
// return stale data. getAllDownloadedItems() is cached, so this stays cheap.
|
||||||
const { getDownloadedItems } = useDownload();
|
const { getDownloadedItems } = useDownload();
|
||||||
|
|
||||||
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
|
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
|
||||||
|
|||||||
@@ -4,28 +4,68 @@ import type { DownloadedItem, DownloadsDatabase } from "./types";
|
|||||||
|
|
||||||
const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";
|
const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";
|
||||||
|
|
||||||
|
// Performance optimization: Cache the parsed database to avoid repeated JSON.parse calls
|
||||||
|
let cachedDb: DownloadsDatabase | null = null;
|
||||||
|
let cacheVersion = 0;
|
||||||
|
|
||||||
|
// Performance optimization: Cache the flattened items array
|
||||||
|
let cachedItems: DownloadedItem[] | null = null;
|
||||||
|
let itemsCacheVersion = -1;
|
||||||
|
|
||||||
|
// Performance optimization: Index for O(1) item lookups by ID
|
||||||
|
let itemIndex: Map<string, DownloadedItem> | null = null;
|
||||||
|
let indexCacheVersion = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the downloads database from storage
|
* Get the downloads database from storage
|
||||||
|
* PERFORMANCE: Caches the parsed database to avoid repeated JSON.parse calls.
|
||||||
|
* NOTE: Returns the shared cached instance — do NOT mutate it directly. Go
|
||||||
|
* through addDownloadedItem/updateDownloadedItem/removeDownloadedItem so
|
||||||
|
* saveDownloadsDatabase() runs and the derived caches stay consistent.
|
||||||
*/
|
*/
|
||||||
export function getDownloadsDatabase(): DownloadsDatabase {
|
export function getDownloadsDatabase(): DownloadsDatabase {
|
||||||
|
// Return cached database if available
|
||||||
|
if (cachedDb !== null) {
|
||||||
|
return cachedDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse from storage and cache the result
|
||||||
const file = storage.getString(DOWNLOADS_DATABASE_KEY);
|
const file = storage.getString(DOWNLOADS_DATABASE_KEY);
|
||||||
if (file) {
|
if (file) {
|
||||||
return JSON.parse(file) as DownloadsDatabase;
|
cachedDb = JSON.parse(file) as DownloadsDatabase;
|
||||||
|
return cachedDb;
|
||||||
}
|
}
|
||||||
return { movies: {}, series: {}, other: {} };
|
|
||||||
|
const emptyDb = { movies: {}, series: {}, other: {} };
|
||||||
|
cachedDb = emptyDb;
|
||||||
|
return emptyDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the downloads database to storage
|
* Save the downloads database to storage
|
||||||
|
* PERFORMANCE: Updates cache and invalidates derived caches
|
||||||
*/
|
*/
|
||||||
export function saveDownloadsDatabase(db: DownloadsDatabase): void {
|
export function saveDownloadsDatabase(db: DownloadsDatabase): void {
|
||||||
storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db));
|
storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db));
|
||||||
|
// Update the cache with the new database
|
||||||
|
cachedDb = db;
|
||||||
|
// Invalidate derived caches (items array and index)
|
||||||
|
cachedItems = null;
|
||||||
|
itemIndex = null;
|
||||||
|
cacheVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all downloaded items as a flat array
|
* Get all downloaded items as a flat array
|
||||||
|
* PERFORMANCE: Caches the flattened array to avoid rebuilding on every call
|
||||||
*/
|
*/
|
||||||
export function getAllDownloadedItems(): DownloadedItem[] {
|
export function getAllDownloadedItems(): DownloadedItem[] {
|
||||||
|
// Return cached items if available and up-to-date
|
||||||
|
if (cachedItems !== null && itemsCacheVersion === cacheVersion) {
|
||||||
|
return cachedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the items array from the database
|
||||||
const db = getDownloadsDatabase();
|
const db = getDownloadsDatabase();
|
||||||
const items: DownloadedItem[] = [];
|
const items: DownloadedItem[] = [];
|
||||||
|
|
||||||
@@ -47,34 +87,41 @@ export function getAllDownloadedItems(): DownloadedItem[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cachedItems = items;
|
||||||
|
itemsCacheVersion = cacheVersion;
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a downloaded item by its ID
|
* Build or refresh the item index for O(1) lookups
|
||||||
*/
|
*/
|
||||||
export function getDownloadedItemById(id: string): DownloadedItem | undefined {
|
function ensureItemIndex(): void {
|
||||||
const db = getDownloadsDatabase();
|
if (itemIndex !== null && indexCacheVersion === cacheVersion) {
|
||||||
|
return; // Index is up-to-date
|
||||||
if (db.movies[id]) {
|
|
||||||
return db.movies[id];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const series of Object.values(db.series)) {
|
// Build new index from all items
|
||||||
for (const season of Object.values(series.seasons)) {
|
itemIndex = new Map<string, DownloadedItem>();
|
||||||
for (const episode of Object.values(season.episodes)) {
|
const items = getAllDownloadedItems();
|
||||||
if (episode.item.Id === id) {
|
|
||||||
return episode;
|
for (const item of items) {
|
||||||
}
|
if (item.item.Id) {
|
||||||
}
|
itemIndex.set(item.item.Id, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db.other?.[id]) {
|
indexCacheVersion = cacheVersion;
|
||||||
return db.other[id];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
/**
|
||||||
|
* Get a downloaded item by its ID
|
||||||
|
* PERFORMANCE: Uses O(1) index lookup instead of O(n²) iteration
|
||||||
|
*/
|
||||||
|
export function getDownloadedItemById(id: string): DownloadedItem | undefined {
|
||||||
|
ensureItemIndex();
|
||||||
|
return itemIndex!.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,4 +268,5 @@ export function updateDownloadedItem(
|
|||||||
*/
|
*/
|
||||||
export function clearAllDownloadedItems(): void {
|
export function clearAllDownloadedItems(): void {
|
||||||
saveDownloadsDatabase({ movies: {}, series: {}, other: {} });
|
saveDownloadsDatabase({ movies: {}, series: {}, other: {} });
|
||||||
|
// saveDownloadsDatabase already invalidates caches
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user