mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-17 09:53:02 +01:00
feat: choose download location (sd card)
This commit is contained in:
@@ -6,16 +6,20 @@ import type {
|
||||
import { Directory, File, Paths } from "expo-file-system";
|
||||
import { getItemImage } from "@/utils/getItemImage";
|
||||
import { fetchAndParseSegments } from "@/utils/segments";
|
||||
import { filePathToUri } from "@/utils/storage";
|
||||
import { generateTrickplayUrl, getTrickplayInfo } from "@/utils/trickplay";
|
||||
import type { MediaTimeSegment, TrickPlayData } from "./types";
|
||||
import { generateFilename } from "./utils";
|
||||
|
||||
/**
|
||||
* Downloads trickplay images for an item
|
||||
* @param item - The item to download trickplay images for
|
||||
* @param storagePath - Optional custom storage path (for Android SD card support)
|
||||
* @returns TrickPlayData with path and size, or undefined if not available
|
||||
*/
|
||||
export async function downloadTrickplayImages(
|
||||
item: BaseItemDto,
|
||||
storagePath?: string,
|
||||
): Promise<TrickPlayData | undefined> {
|
||||
const trickplayInfo = getTrickplayInfo(item);
|
||||
if (!trickplayInfo || !item.Id) {
|
||||
@@ -23,7 +27,11 @@ export async function downloadTrickplayImages(
|
||||
}
|
||||
|
||||
const filename = generateFilename(item);
|
||||
const trickplayDir = new Directory(Paths.document, `${filename}_trickplay`);
|
||||
|
||||
// Use custom storage path if provided (Android SD card), otherwise use Paths.document
|
||||
const trickplayDir = storagePath
|
||||
? new Directory(filePathToUri(storagePath), `${filename}_trickplay`)
|
||||
: new Directory(Paths.document, `${filename}_trickplay`);
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!trickplayDir.exists) {
|
||||
@@ -69,12 +77,17 @@ export async function downloadTrickplayImages(
|
||||
|
||||
/**
|
||||
* Downloads external subtitle files and updates their delivery URLs to local paths
|
||||
* @param mediaSource - The media source containing subtitle information
|
||||
* @param item - The item to download subtitles for
|
||||
* @param apiBasePath - The base path for the API
|
||||
* @param storagePath - Optional custom storage path (for Android SD card support)
|
||||
* @returns Updated media source with local subtitle paths
|
||||
*/
|
||||
export async function downloadSubtitles(
|
||||
mediaSource: MediaSourceInfo,
|
||||
item: BaseItemDto,
|
||||
apiBasePath: string,
|
||||
storagePath?: string,
|
||||
): Promise<MediaSourceInfo> {
|
||||
const externalSubtitles = mediaSource.MediaStreams?.filter(
|
||||
(stream) =>
|
||||
@@ -91,10 +104,17 @@ export async function downloadSubtitles(
|
||||
|
||||
const url = apiBasePath + subtitle.DeliveryUrl;
|
||||
const extension = subtitle.Codec || "srt";
|
||||
const destination = new File(
|
||||
Paths.document,
|
||||
`${filename}_subtitle_${subtitle.Index}.${extension}`,
|
||||
);
|
||||
|
||||
// Use custom storage path if provided (Android SD card), otherwise use Paths.document
|
||||
const destination = storagePath
|
||||
? new File(
|
||||
filePathToUri(storagePath),
|
||||
`${filename}_subtitle_${subtitle.Index}.${extension}`,
|
||||
)
|
||||
: new File(
|
||||
Paths.document,
|
||||
`${filename}_subtitle_${subtitle.Index}.${extension}`,
|
||||
);
|
||||
|
||||
// Skip if already exists
|
||||
if (destination.exists) {
|
||||
@@ -208,13 +228,21 @@ export async function downloadAdditionalAssets(params: {
|
||||
api: Api;
|
||||
saveImageFn: (itemId: string, url?: string) => Promise<void>;
|
||||
saveSeriesImageFn: (item: BaseItemDto) => Promise<void>;
|
||||
storagePath?: string;
|
||||
}): Promise<{
|
||||
trickPlayData?: TrickPlayData;
|
||||
updatedMediaSource: MediaSourceInfo;
|
||||
introSegments?: MediaTimeSegment[];
|
||||
creditSegments?: MediaTimeSegment[];
|
||||
}> {
|
||||
const { item, mediaSource, api, saveImageFn, saveSeriesImageFn } = params;
|
||||
const {
|
||||
item,
|
||||
mediaSource,
|
||||
api,
|
||||
saveImageFn,
|
||||
saveSeriesImageFn,
|
||||
storagePath,
|
||||
} = params;
|
||||
|
||||
// Run all downloads in parallel for speed
|
||||
const [
|
||||
@@ -223,11 +251,11 @@ export async function downloadAdditionalAssets(params: {
|
||||
segments,
|
||||
// Cover images (fire and forget, errors are logged)
|
||||
] = await Promise.all([
|
||||
downloadTrickplayImages(item),
|
||||
downloadTrickplayImages(item, storagePath),
|
||||
// Only download subtitles for non-transcoded streams
|
||||
mediaSource.TranscodingUrl
|
||||
? Promise.resolve(mediaSource)
|
||||
: downloadSubtitles(mediaSource, item, api.basePath || ""),
|
||||
: downloadSubtitles(mediaSource, item, api.basePath || "", storagePath),
|
||||
item.Id
|
||||
? fetchSegments(item.Id, api)
|
||||
: Promise.resolve({
|
||||
|
||||
@@ -6,13 +6,16 @@ import { File, Paths } from "expo-file-system";
|
||||
import type { MutableRefObject } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import { toast } from "sonner-native";
|
||||
import type { Bitrate } from "@/components/BitrateSelector";
|
||||
import useImageStorage from "@/hooks/useImageStorage";
|
||||
import { BackgroundDownloader } from "@/modules";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getOrSetDeviceId } from "@/utils/device";
|
||||
import useDownloadHelper from "@/utils/download";
|
||||
import { getStoragePath } from "@/utils/storage";
|
||||
import { downloadAdditionalAssets } from "../additionalDownloads";
|
||||
import {
|
||||
clearAllDownloadedItems,
|
||||
@@ -49,6 +52,7 @@ export function useDownloadOperations({
|
||||
onDataChange,
|
||||
}: UseDownloadOperationsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { settings } = useSettings();
|
||||
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
||||
const { saveImage } = useImageStorage();
|
||||
|
||||
@@ -79,6 +83,12 @@ export function useDownloadOperations({
|
||||
return;
|
||||
}
|
||||
|
||||
// Get storage path if custom location is set
|
||||
let storagePath: string | undefined;
|
||||
if (Platform.OS === "android" && settings.downloadStorageLocation) {
|
||||
storagePath = await getStoragePath(settings.downloadStorageLocation);
|
||||
}
|
||||
|
||||
// Download all additional assets BEFORE starting native video download
|
||||
const additionalAssets = await downloadAdditionalAssets({
|
||||
item,
|
||||
@@ -86,6 +96,7 @@ export function useDownloadOperations({
|
||||
api,
|
||||
saveImageFn: saveImage,
|
||||
saveSeriesImageFn: saveSeriesPrimaryImage,
|
||||
storagePath,
|
||||
});
|
||||
|
||||
// Ensure URL is absolute (not relative) before storing
|
||||
@@ -119,10 +130,19 @@ export function useDownloadOperations({
|
||||
// Add to processes
|
||||
setProcesses((prev) => [...prev, jobStatus]);
|
||||
|
||||
// Generate destination path
|
||||
// Generate destination path using custom storage location if set
|
||||
const filename = generateFilename(item);
|
||||
const videoFile = new File(Paths.document, `${filename}.mp4`);
|
||||
const destinationPath = uriToFilePath(videoFile.uri);
|
||||
let destinationPath: string;
|
||||
|
||||
if (storagePath) {
|
||||
// Use custom storage location
|
||||
destinationPath = `${storagePath}/${filename}.mp4`;
|
||||
console.log(`[DOWNLOAD] Using custom storage: ${destinationPath}`);
|
||||
} else {
|
||||
// Use default Paths.document
|
||||
const videoFile = new File(Paths.document, `${filename}.mp4`);
|
||||
destinationPath = uriToFilePath(videoFile.uri);
|
||||
}
|
||||
|
||||
console.log(`[DOWNLOAD] Starting video: ${item.Name}`);
|
||||
console.log(`[DOWNLOAD] Download URL: ${downloadUrl}`);
|
||||
|
||||
Reference in New Issue
Block a user