feat: choose download location (sd card)

This commit is contained in:
Fredrik Burmester
2025-11-14 21:34:13 +01:00
parent 631a5ef94e
commit 259306df52
10 changed files with 545 additions and 18 deletions

View File

@@ -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({

View File

@@ -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}`);