mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-23 17:24:42 +01:00
feat(tv): persist downloaded opensubtitles across app restarts
This commit is contained in:
162
utils/atoms/downloadedSubtitles.ts
Normal file
162
utils/atoms/downloadedSubtitles.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Downloaded Subtitles Storage
|
||||
*
|
||||
* Persists metadata about client-side downloaded subtitles (from OpenSubtitles).
|
||||
* Subtitle files are stored in Paths.cache/streamyfin-subtitles/ directory.
|
||||
* Filenames are prefixed with itemId for organization: {itemId}_{filename}
|
||||
*
|
||||
* While files are in cache, metadata is persisted in MMKV so subtitles survive
|
||||
* app restarts (unless cache is manually cleared by the user).
|
||||
*
|
||||
* TV platform only.
|
||||
*/
|
||||
|
||||
import { storage } from "../mmkv";
|
||||
|
||||
// MMKV storage key
|
||||
const DOWNLOADED_SUBTITLES_KEY = "downloadedSubtitles.json";
|
||||
|
||||
/**
|
||||
* Metadata for a downloaded subtitle file
|
||||
*/
|
||||
export interface DownloadedSubtitle {
|
||||
/** Unique identifier (uuid) */
|
||||
id: string;
|
||||
/** Jellyfin item ID */
|
||||
itemId: string;
|
||||
/** Local file path in documents directory */
|
||||
filePath: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** 3-letter language code */
|
||||
language: string;
|
||||
/** File format (srt, ass, etc.) */
|
||||
format: string;
|
||||
/** Source provider */
|
||||
source: "opensubtitles";
|
||||
/** Unix timestamp when downloaded */
|
||||
downloadedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage structure for downloaded subtitles
|
||||
*/
|
||||
interface DownloadedSubtitlesStorage {
|
||||
/** Map of itemId to array of downloaded subtitles */
|
||||
byItemId: Record<string, DownloadedSubtitle[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the storage from MMKV
|
||||
*/
|
||||
function loadStorage(): DownloadedSubtitlesStorage {
|
||||
try {
|
||||
const data = storage.getString(DOWNLOADED_SUBTITLES_KEY);
|
||||
if (data) {
|
||||
return JSON.parse(data) as DownloadedSubtitlesStorage;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors, return empty storage
|
||||
}
|
||||
return { byItemId: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the storage to MMKV
|
||||
*/
|
||||
function saveStorage(data: DownloadedSubtitlesStorage): void {
|
||||
try {
|
||||
storage.set(DOWNLOADED_SUBTITLES_KEY, JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error("Failed to save downloaded subtitles:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all downloaded subtitles for a specific Jellyfin item
|
||||
*/
|
||||
export function getSubtitlesForItem(itemId: string): DownloadedSubtitle[] {
|
||||
const data = loadStorage();
|
||||
return data.byItemId[itemId] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a downloaded subtitle to storage
|
||||
*/
|
||||
export function addDownloadedSubtitle(subtitle: DownloadedSubtitle): void {
|
||||
const data = loadStorage();
|
||||
|
||||
// Initialize array for item if it doesn't exist
|
||||
if (!data.byItemId[subtitle.itemId]) {
|
||||
data.byItemId[subtitle.itemId] = [];
|
||||
}
|
||||
|
||||
// Check if subtitle with same id already exists and update it
|
||||
const existingIndex = data.byItemId[subtitle.itemId].findIndex(
|
||||
(s) => s.id === subtitle.id,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update existing entry
|
||||
data.byItemId[subtitle.itemId][existingIndex] = subtitle;
|
||||
} else {
|
||||
// Add new entry
|
||||
data.byItemId[subtitle.itemId].push(subtitle);
|
||||
}
|
||||
|
||||
saveStorage(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a downloaded subtitle from storage
|
||||
*/
|
||||
export function removeDownloadedSubtitle(
|
||||
itemId: string,
|
||||
subtitleId: string,
|
||||
): void {
|
||||
const data = loadStorage();
|
||||
|
||||
if (data.byItemId[itemId]) {
|
||||
data.byItemId[itemId] = data.byItemId[itemId].filter(
|
||||
(s) => s.id !== subtitleId,
|
||||
);
|
||||
|
||||
// Clean up empty arrays
|
||||
if (data.byItemId[itemId].length === 0) {
|
||||
delete data.byItemId[itemId];
|
||||
}
|
||||
|
||||
saveStorage(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all downloaded subtitles for a specific item
|
||||
*/
|
||||
export function removeAllSubtitlesForItem(itemId: string): void {
|
||||
const data = loadStorage();
|
||||
|
||||
if (data.byItemId[itemId]) {
|
||||
delete data.byItemId[itemId];
|
||||
saveStorage(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a subtitle file already exists for an item by language
|
||||
*/
|
||||
export function hasSubtitleForLanguage(
|
||||
itemId: string,
|
||||
language: string,
|
||||
): boolean {
|
||||
const subtitles = getSubtitlesForItem(itemId);
|
||||
return subtitles.some((s) => s.language === language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all downloaded subtitles across all items
|
||||
*/
|
||||
export function getAllDownloadedSubtitles(): DownloadedSubtitle[] {
|
||||
const data = loadStorage();
|
||||
return Object.values(data.byItemId).flat();
|
||||
}
|
||||
Reference in New Issue
Block a user