mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-26 19:26:26 +00:00
fix: remove anything regarding downloads
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import {JobStatus} from "@/utils/optimize-server";
|
||||
import {processesAtom} from "@/providers/DownloadProvider";
|
||||
import {useSettings} from "@/utils/atoms/settings";
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
item: BaseItemDto;
|
||||
execute: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export const runningAtom = atom<boolean>(false);
|
||||
|
||||
export const queueAtom = atom<Job[]>([]);
|
||||
|
||||
export const queueActions = {
|
||||
enqueue: (queue: Job[], setQueue: (update: Job[]) => void, ...job: Job[]) => {
|
||||
const updatedQueue = [...queue, ...job];
|
||||
console.info("Enqueueing job", job, updatedQueue);
|
||||
setQueue(updatedQueue);
|
||||
},
|
||||
processJob: async (
|
||||
queue: Job[],
|
||||
setQueue: (update: Job[]) => void,
|
||||
setProcessing: (processing: boolean) => void
|
||||
) => {
|
||||
const [job, ...rest] = queue;
|
||||
|
||||
console.info("Processing job", job);
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
// Allow job to execute so that it gets added as a processes first BEFORE updating new queue
|
||||
try {
|
||||
await job.execute();
|
||||
} finally {
|
||||
setQueue(rest);
|
||||
}
|
||||
|
||||
console.info("Job done", job);
|
||||
|
||||
setProcessing(false);
|
||||
},
|
||||
clear: (
|
||||
setQueue: (update: Job[]) => void,
|
||||
setProcessing: (processing: boolean) => void
|
||||
) => {
|
||||
setQueue([]);
|
||||
setProcessing(false);
|
||||
},
|
||||
};
|
||||
|
||||
export const useJobProcessor = () => {
|
||||
const [queue, setQueue] = useAtom(queueAtom);
|
||||
const [running, setRunning] = useAtom(runningAtom);
|
||||
const [processes] = useAtom<JobStatus[]>(processesAtom);
|
||||
const [settings] = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (!running && queue.length > 0 && settings && processes.length < settings?.remuxConcurrentLimit) {
|
||||
console.info("Processing queue", queue);
|
||||
queueActions.processJob(queue, setQueue, setRunning);
|
||||
}
|
||||
}, [processes, queue, running, setQueue, setRunning]);
|
||||
};
|
||||
@@ -8,13 +8,6 @@ import {
|
||||
SubtitlePlaybackMode,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
|
||||
export type DownloadQuality = "original" | "high" | "low";
|
||||
|
||||
export type DownloadOption = {
|
||||
label: string;
|
||||
value: DownloadQuality;
|
||||
};
|
||||
|
||||
export const ScreenOrientationEnum: Record<
|
||||
ScreenOrientation.OrientationLock,
|
||||
string
|
||||
@@ -31,21 +24,6 @@ export const ScreenOrientationEnum: Record<
|
||||
[ScreenOrientation.OrientationLock.UNKNOWN]: "Unknown",
|
||||
};
|
||||
|
||||
export const DownloadOptions: DownloadOption[] = [
|
||||
{
|
||||
label: "Original quality",
|
||||
value: "original",
|
||||
},
|
||||
{
|
||||
label: "High quality",
|
||||
value: "high",
|
||||
},
|
||||
{
|
||||
label: "Small file size",
|
||||
value: "low",
|
||||
},
|
||||
];
|
||||
|
||||
export type LibraryOptions = {
|
||||
display: "row" | "list";
|
||||
cardStyle: "compact" | "detailed";
|
||||
@@ -68,7 +46,6 @@ export type Settings = {
|
||||
searchEngine: "Marlin" | "Jellyfin";
|
||||
marlinServerUrl?: string;
|
||||
openInVLC?: boolean;
|
||||
downloadQuality?: DownloadOption;
|
||||
libraryOptions: LibraryOptions;
|
||||
defaultAudioLanguage: CultureDto | null;
|
||||
playDefaultAudioTrack: boolean;
|
||||
@@ -81,8 +58,6 @@ export type Settings = {
|
||||
forwardSkipTime: number;
|
||||
rewindSkipTime: number;
|
||||
optimizedVersionsServerUrl?: string | null;
|
||||
downloadMethod: "optimized" | "remux";
|
||||
autoDownload: boolean;
|
||||
showCustomMenuLinks: boolean;
|
||||
subtitleSize: number;
|
||||
remuxConcurrentLimit: 1 | 2 | 3 | 4;
|
||||
@@ -100,7 +75,6 @@ const loadSettings = (): Settings => {
|
||||
searchEngine: "Jellyfin",
|
||||
marlinServerUrl: "",
|
||||
openInVLC: false,
|
||||
downloadQuality: DownloadOptions[0],
|
||||
libraryOptions: {
|
||||
display: "list",
|
||||
cardStyle: "detailed",
|
||||
@@ -119,8 +93,6 @@ const loadSettings = (): Settings => {
|
||||
forwardSkipTime: 30,
|
||||
rewindSkipTime: 10,
|
||||
optimizedVersionsServerUrl: null,
|
||||
downloadMethod: "remux",
|
||||
autoDownload: false,
|
||||
showCustomMenuLinks: false,
|
||||
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
||||
remuxConcurrentLimit: 1,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import useImageStorage from "@/hooks/useImageStorage";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
const useDownloadHelper = () => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const { saveImage } = useImageStorage();
|
||||
|
||||
const saveSeriesPrimaryImage = async (item: BaseItemDto) => {
|
||||
console.log(`Attempting to save primary image for item: ${item.Id}`);
|
||||
if (
|
||||
item.Type === "Episode" &&
|
||||
item.SeriesId &&
|
||||
!storage.getString(item.SeriesId)
|
||||
) {
|
||||
console.log(`Saving primary image for series: ${item.SeriesId}`);
|
||||
await saveImage(
|
||||
item.SeriesId,
|
||||
getPrimaryImageUrlById({ api, id: item.SeriesId })
|
||||
);
|
||||
console.log(`Primary image saved for series: ${item.SeriesId}`);
|
||||
} else {
|
||||
console.log(`Skipping primary image save for item: ${item.Id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { saveSeriesPrimaryImage };
|
||||
};
|
||||
|
||||
export default useDownloadHelper;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { atomWithStorage, createJSONStorage } from "jotai/utils";
|
||||
import { storage } from "./mmkv";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import React, {createContext, useContext} from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
type LogLevel = "INFO" | "WARN" | "ERROR";
|
||||
|
||||
@@ -19,10 +19,9 @@ const mmkvStorage = createJSONStorage(() => ({
|
||||
}));
|
||||
const logsAtom = atomWithStorage("logs", [], mmkvStorage);
|
||||
|
||||
const LogContext = createContext<ReturnType<typeof useLogProvider> | null>(null);
|
||||
const DownloadContext = createContext<ReturnType<
|
||||
typeof useLogProvider
|
||||
> | null>(null);
|
||||
const LogContext = createContext<ReturnType<typeof useLogProvider> | null>(
|
||||
null
|
||||
);
|
||||
|
||||
function useLogProvider() {
|
||||
const { data: logs } = useQuery({
|
||||
@@ -32,11 +31,10 @@ function useLogProvider() {
|
||||
});
|
||||
|
||||
return {
|
||||
logs
|
||||
}
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const writeToLog = (level: LogLevel, message: string, data?: any) => {
|
||||
const newEntry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -55,8 +53,10 @@ export const writeToLog = (level: LogLevel, message: string, data?: any) => {
|
||||
storage.set("logs", JSON.stringify(recentLogs));
|
||||
};
|
||||
|
||||
export const writeInfoLog = (message: string, data?: any) => writeToLog("INFO", message, data);
|
||||
export const writeErrorLog = (message: string, data?: any) => writeToLog("ERROR", message, data);
|
||||
export const writeInfoLog = (message: string, data?: any) =>
|
||||
writeToLog("INFO", message, data);
|
||||
export const writeErrorLog = (message: string, data?: any) =>
|
||||
writeToLog("ERROR", message, data);
|
||||
|
||||
export const readFromLog = (): LogEntry[] => {
|
||||
const logs = storage.getString("logs");
|
||||
@@ -75,14 +75,10 @@ export function useLog() {
|
||||
return context;
|
||||
}
|
||||
|
||||
export function LogProvider({children}: { children: React.ReactNode }) {
|
||||
export function LogProvider({ children }: { children: React.ReactNode }) {
|
||||
const provider = useLogProvider();
|
||||
|
||||
return (
|
||||
<LogContext.Provider value={provider}>
|
||||
{children}
|
||||
</LogContext.Provider>
|
||||
)
|
||||
return <LogContext.Provider value={provider}>{children}</LogContext.Provider>;
|
||||
}
|
||||
|
||||
export default logsAtom;
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
import { itemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import axios from "axios";
|
||||
import { writeToLog } from "./log";
|
||||
import { DownloadedItem } from "@/providers/DownloadProvider";
|
||||
import { MMKV } from "react-native-mmkv";
|
||||
|
||||
interface IJobInput {
|
||||
deviceId?: string | null;
|
||||
authHeader?: string | null;
|
||||
url?: string | null;
|
||||
}
|
||||
|
||||
export interface JobStatus {
|
||||
id: string;
|
||||
status:
|
||||
| "queued"
|
||||
| "optimizing"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "downloading";
|
||||
progress: number;
|
||||
outputPath: string;
|
||||
inputUrl: string;
|
||||
deviceId: string;
|
||||
itemId: string;
|
||||
item: BaseItemDto;
|
||||
speed?: number;
|
||||
timestamp: Date;
|
||||
base64Image?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all jobs for a specific device.
|
||||
*
|
||||
* @param {IGetAllDeviceJobs} params - The parameters for the API request.
|
||||
* @param {string} params.deviceId - The ID of the device to fetch jobs for.
|
||||
* @param {string} params.authHeader - The authorization header for the API request.
|
||||
* @param {string} params.url - The base URL for the API endpoint.
|
||||
*
|
||||
* @returns {Promise<JobStatus[]>} A promise that resolves to an array of job statuses.
|
||||
*
|
||||
* @throws {Error} Throws an error if the API request fails or returns a non-200 status code.
|
||||
*/
|
||||
export async function getAllJobsByDeviceId({
|
||||
deviceId,
|
||||
authHeader,
|
||||
url,
|
||||
}: IJobInput): Promise<JobStatus[]> {
|
||||
const statusResponse = await axios.get(`${url}all-jobs`, {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
params: {
|
||||
deviceId,
|
||||
},
|
||||
});
|
||||
if (statusResponse.status !== 200) {
|
||||
console.error(
|
||||
statusResponse.status,
|
||||
statusResponse.data,
|
||||
statusResponse.statusText
|
||||
);
|
||||
throw new Error("Failed to fetch job status");
|
||||
}
|
||||
|
||||
return statusResponse.data;
|
||||
}
|
||||
|
||||
interface ICancelJob {
|
||||
authHeader: string;
|
||||
url: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function cancelJobById({
|
||||
authHeader,
|
||||
url,
|
||||
id,
|
||||
}: ICancelJob): Promise<boolean> {
|
||||
const statusResponse = await axios.delete(`${url}cancel-job/${id}`, {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
});
|
||||
if (statusResponse.status !== 200) {
|
||||
throw new Error("Failed to cancel process");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) {
|
||||
if (!deviceId) return false;
|
||||
if (!authHeader) return false;
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
await getAllJobsByDeviceId({
|
||||
deviceId,
|
||||
authHeader,
|
||||
url,
|
||||
}).then((jobs) => {
|
||||
jobs.forEach((job) => {
|
||||
cancelJobById({
|
||||
authHeader,
|
||||
url,
|
||||
id: job.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Failed to cancel all jobs", error);
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches statistics for a specific device.
|
||||
*
|
||||
* @param {IJobInput} params - The parameters for the API request.
|
||||
* @param {string} params.deviceId - The ID of the device to fetch statistics for.
|
||||
* @param {string} params.authHeader - The authorization header for the API request.
|
||||
* @param {string} params.url - The base URL for the API endpoint.
|
||||
*
|
||||
* @returns {Promise<any | null>} A promise that resolves to the statistics data or null if the request fails.
|
||||
*
|
||||
* @throws {Error} Throws an error if any required parameter is missing.
|
||||
*/
|
||||
export async function getStatistics({
|
||||
authHeader,
|
||||
url,
|
||||
deviceId,
|
||||
}: IJobInput): Promise<any | null> {
|
||||
if (!deviceId || !authHeader || !url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusResponse = await axios.get(`${url}statistics`, {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
params: {
|
||||
deviceId,
|
||||
},
|
||||
});
|
||||
|
||||
return statusResponse.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch statistics:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the download item info to disk - this data is used temporarily to fetch additional download information
|
||||
* in combination with the optimize server. This is used to not have to send all item info to the optimize server.
|
||||
*
|
||||
* @param {BaseItemDto} item - The item to save.
|
||||
* @param {MediaSourceInfo} mediaSource - The media source of the item.
|
||||
* @param {string} url - The URL of the item.
|
||||
* @return {boolean} A promise that resolves when the item info is saved.
|
||||
*/
|
||||
export function saveDownloadItemInfoToDiskTmp(
|
||||
item: BaseItemDto,
|
||||
mediaSource: MediaSourceInfo,
|
||||
url: string
|
||||
): boolean {
|
||||
try {
|
||||
const storage = new MMKV();
|
||||
|
||||
const downloadInfo = JSON.stringify({
|
||||
item,
|
||||
mediaSource,
|
||||
url,
|
||||
});
|
||||
|
||||
storage.set(`tmp_download_info_${item.Id}`, downloadInfo);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to save download item info to disk:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the download item info from disk.
|
||||
*
|
||||
* @param {string} itemId - The ID of the item to retrieve.
|
||||
* @return {{
|
||||
* item: BaseItemDto;
|
||||
* mediaSource: MediaSourceInfo;
|
||||
* url: string;
|
||||
* } | null} The retrieved download item info or null if not found.
|
||||
*/
|
||||
export function getDownloadItemInfoFromDiskTmp(itemId: string): {
|
||||
item: BaseItemDto;
|
||||
mediaSource: MediaSourceInfo;
|
||||
url: string;
|
||||
} | null {
|
||||
try {
|
||||
const storage = new MMKV();
|
||||
const rawInfo = storage.getString(`tmp_download_info_${itemId}`);
|
||||
|
||||
if (rawInfo) {
|
||||
return JSON.parse(rawInfo);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Failed to retrieve download item info from disk:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the download item info from disk.
|
||||
*
|
||||
* @param {string} itemId - The ID of the item to delete.
|
||||
* @return {boolean} True if the item info was successfully deleted, false otherwise.
|
||||
*/
|
||||
export function deleteDownloadItemInfoFromDiskTmp(itemId: string): boolean {
|
||||
try {
|
||||
const storage = new MMKV();
|
||||
storage.delete(`tmp_download_info_${itemId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete download item info from disk:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user