mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
chore: update dependencies and refactor config plugin imports (#993)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Api } from "@jellyfin/sdk";
|
||||
import {
|
||||
type BaseItemKind,
|
||||
type CultureDto,
|
||||
@@ -6,12 +7,11 @@ import {
|
||||
type SortOrder,
|
||||
SubtitlePlaybackMode,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { atom, useAtom, useAtomValue } from "jotai";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { writeInfoLog } from "@/utils/log";
|
||||
import { storage } from "../mmkv";
|
||||
|
||||
@@ -276,8 +276,7 @@ export const pluginSettingsAtom = atom<PluginLockableSettings | undefined>(
|
||||
loadPluginSettings(),
|
||||
);
|
||||
|
||||
export const useSettings = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
export const useSettings = (api: Api | null) => {
|
||||
const [_settings, setSettings] = useAtom(settingsAtom);
|
||||
const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom);
|
||||
|
||||
@@ -301,11 +300,11 @@ export const useSettings = () => {
|
||||
return;
|
||||
}
|
||||
const settings = await api.getStreamyfinPluginConfig().then(
|
||||
({ data }) => {
|
||||
({ data }: any) => {
|
||||
writeInfoLog("Got plugin settings", data?.settings);
|
||||
return data?.settings;
|
||||
},
|
||||
(_err) => undefined,
|
||||
(_err: any) => undefined,
|
||||
);
|
||||
setPluginSettings(settings);
|
||||
return settings;
|
||||
|
||||
@@ -1,50 +1,110 @@
|
||||
import * as BackgroundTask from "expo-background-task";
|
||||
import * as TaskManager from "expo-task-manager";
|
||||
import { Platform } from "react-native";
|
||||
import { writeErrorLog } from "@/utils/log";
|
||||
|
||||
const BackgroundFetch = !Platform.isTV
|
||||
? require("expo-background-fetch")
|
||||
: null;
|
||||
const BackgroundTaskModule = !Platform.isTV ? BackgroundTask : null;
|
||||
|
||||
export const BACKGROUND_FETCH_TASK = "background-fetch";
|
||||
|
||||
export async function registerBackgroundFetchAsync() {
|
||||
try {
|
||||
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
||||
minimumInterval: 60 * 1, // 1 minutes
|
||||
stopOnTerminate: false, // android only,
|
||||
startOnBoot: false, // android only
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error registering background fetch task", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterBackgroundFetchAsync() {
|
||||
try {
|
||||
BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
|
||||
} catch (error) {
|
||||
console.log("Error unregistering background fetch task", error);
|
||||
}
|
||||
}
|
||||
|
||||
export const BACKGROUND_FETCH_TASK_SESSIONS = "background-fetch-sessions";
|
||||
|
||||
export async function registerBackgroundFetchAsyncSessions() {
|
||||
export async function registerBackgroundFetchAsync(): Promise<boolean> {
|
||||
if (!BackgroundTaskModule) {
|
||||
console.log(
|
||||
"BackgroundTask module not available (TV platform or not supported)",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Registering background fetch sessions");
|
||||
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, {
|
||||
minimumInterval: 1 * 60, // 1 minutes
|
||||
stopOnTerminate: false, // android only,
|
||||
startOnBoot: true, // android only
|
||||
// Check if task is already registered
|
||||
const isRegistered = await TaskManager.isTaskRegisteredAsync(
|
||||
BACKGROUND_FETCH_TASK,
|
||||
);
|
||||
if (isRegistered) {
|
||||
console.log("Background fetch task already registered");
|
||||
return true;
|
||||
}
|
||||
|
||||
await BackgroundTaskModule!.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
|
||||
const minimumInterval = Platform.OS === "android" ? 600 : 900;
|
||||
await BackgroundTaskModule!.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
||||
minimumInterval,
|
||||
});
|
||||
console.log("Successfully registered background fetch task");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error registering background fetch task", error);
|
||||
// Log error but don't throw - background fetch is not critical
|
||||
console.warn("Failed to register background fetch task:", error);
|
||||
writeErrorLog("Error registering background fetch task", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterBackgroundFetchAsyncSessions() {
|
||||
export async function unregisterBackgroundFetchAsync(): Promise<boolean> {
|
||||
if (!BackgroundTaskModule) return false;
|
||||
try {
|
||||
BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS);
|
||||
await BackgroundTaskModule!.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
|
||||
console.log("Successfully unregistered background fetch task");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error unregistering background fetch task", error);
|
||||
// Log error but don't throw - unregistering is not critical
|
||||
console.warn("Failed to unregister background fetch task:", error);
|
||||
writeErrorLog("Error unregistering background fetch task", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterBackgroundFetchAsyncSessions(): Promise<boolean> {
|
||||
if (!BackgroundTaskModule) return false;
|
||||
try {
|
||||
await BackgroundTaskModule!.unregisterTaskAsync(
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
);
|
||||
console.log("Successfully unregistered background fetch sessions task");
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log error but don't throw - unregistering is not critical
|
||||
console.warn("Failed to unregister background fetch sessions task:", error);
|
||||
writeErrorLog("Error unregistering background fetch sessions task", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerBackgroundFetchAsyncSessions(): Promise<boolean> {
|
||||
if (!BackgroundTaskModule) {
|
||||
console.log(
|
||||
"BackgroundTask module not available (TV platform or not supported)",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if task is already registered
|
||||
const isRegistered = await TaskManager.isTaskRegisteredAsync(
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
);
|
||||
if (isRegistered) {
|
||||
console.log("Background fetch sessions task already registered");
|
||||
return true;
|
||||
}
|
||||
|
||||
await BackgroundTaskModule!.unregisterTaskAsync(
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
);
|
||||
const minimumInterval = Platform.OS === "android" ? 600 : 900;
|
||||
await BackgroundTaskModule!.registerTaskAsync(
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
{
|
||||
minimumInterval,
|
||||
},
|
||||
);
|
||||
console.log("Successfully registered background fetch sessions task");
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log error but don't throw - background fetch is not critical
|
||||
console.warn("Failed to register background fetch sessions task:", error);
|
||||
writeErrorLog("Error registering background fetch sessions task", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import React from "react";
|
||||
import { DownloadedItem, MediaTimeSegment } from "@/providers/Downloads/types";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { getAuthHeaders } from "./jellyfin/jellyfin";
|
||||
|
||||
interface IntroTimestamps {
|
||||
@@ -28,11 +26,16 @@ interface CreditTimestamps {
|
||||
};
|
||||
}
|
||||
|
||||
export const useSegments = (itemId: string, isOffline: boolean) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const { downloadedFiles } = useDownload();
|
||||
const downloadedItem = downloadedFiles?.find(
|
||||
(d: DownloadedItem) => d.item.Id === itemId,
|
||||
export const useSegments = (
|
||||
itemId: string,
|
||||
isOffline: boolean,
|
||||
downloadedFiles: DownloadedItem[] | undefined,
|
||||
api: Api | null,
|
||||
) => {
|
||||
// Memoize the lookup so the array is only traversed when dependencies change
|
||||
const downloadedItem = React.useMemo(
|
||||
() => downloadedFiles?.find((d) => d.item.Id === itemId),
|
||||
[downloadedFiles, itemId],
|
||||
);
|
||||
|
||||
return useQuery({
|
||||
@@ -46,7 +49,7 @@ export const useSegments = (itemId: string, isOffline: boolean) => {
|
||||
}
|
||||
return fetchAndParseSegments(itemId, api);
|
||||
},
|
||||
enabled: !!api,
|
||||
enabled: isOffline ? !!downloadedItem : !!api,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -76,15 +79,11 @@ export const fetchAndParseSegments = async (
|
||||
const [introRes, creditRes] = await Promise.allSettled([
|
||||
api.axiosInstance.get<IntroTimestamps>(
|
||||
`${api.basePath}/Episode/${itemId}/IntroTimestamps`,
|
||||
{
|
||||
headers: getAuthHeaders(api),
|
||||
},
|
||||
{ headers: getAuthHeaders(api) },
|
||||
),
|
||||
api.axiosInstance.get<CreditTimestamps>(
|
||||
`${api.basePath}/Episode/${itemId}/Timestamps`,
|
||||
{
|
||||
headers: getAuthHeaders(api),
|
||||
},
|
||||
{ headers: getAuthHeaders(api) },
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
65
utils/trickplay.ts
Normal file
65
utils/trickplay.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { store } from "@/utils/store";
|
||||
import { ticksToMs } from "@/utils/time";
|
||||
|
||||
export interface TrickplayInfo {
|
||||
resolution: string;
|
||||
aspectRatio: number;
|
||||
data: any;
|
||||
totalImageSheets: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the trickplay metadata from a BaseItemDto.
|
||||
* @param item The Jellyfin media item.
|
||||
* @returns Parsed trickplay information or null if not available.
|
||||
*/
|
||||
export const getTrickplayInfo = (item: BaseItemDto): TrickplayInfo | null => {
|
||||
if (!item.Id || !item.Trickplay) return null;
|
||||
|
||||
const mediaSourceId = item.Id;
|
||||
const trickplayDataForSource = item.Trickplay[mediaSourceId];
|
||||
|
||||
if (!trickplayDataForSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstResolution = Object.keys(trickplayDataForSource)[0];
|
||||
if (!firstResolution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = trickplayDataForSource[firstResolution];
|
||||
const { Interval, TileWidth, TileHeight, Width, Height } = data;
|
||||
|
||||
if (
|
||||
!Interval ||
|
||||
!TileWidth ||
|
||||
!TileHeight ||
|
||||
!Width ||
|
||||
!Height ||
|
||||
!item.RunTimeTicks
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tilesPerSheet = TileWidth * TileHeight;
|
||||
const totalTiles = Math.ceil(ticksToMs(item.RunTimeTicks) / Interval);
|
||||
const totalImageSheets = Math.ceil(totalTiles / tilesPerSheet);
|
||||
|
||||
return {
|
||||
resolution: firstResolution,
|
||||
aspectRatio: Width / Height,
|
||||
data,
|
||||
totalImageSheets,
|
||||
};
|
||||
};
|
||||
|
||||
/** Generates a trickplay URL based on the item, resolution, and sheet index. */
|
||||
export const generateTrickplayUrl = (item: BaseItemDto, sheetIndex: number) => {
|
||||
const api = store.get(apiAtom);
|
||||
const resolution = getTrickplayInfo(item)?.resolution;
|
||||
if (!resolution || !api) return null;
|
||||
return `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${sheetIndex}.jpg?api_key=${api.accessToken}`;
|
||||
};
|
||||
Reference in New Issue
Block a user