diff --git a/lib/models/download_models.dart b/lib/models/download_models.dart index 008d6324..e128e544 100644 --- a/lib/models/download_models.dart +++ b/lib/models/download_models.dart @@ -41,13 +41,6 @@ class DownloadProgress { String get downloadedFormatted => ByteFormatter.formatBytes(downloadedBytes); String get totalFormatted => ByteFormatter.formatBytes(totalBytes); - Duration? get estimatedTimeRemaining { - if (speed <= 0 || totalBytes <= 0) return null; - final remainingBytes = totalBytes - downloadedBytes; - if (remainingBytes <= 0) return Duration.zero; - return Duration(seconds: (remainingBytes / speed).round()); - } - /// Check if this progress update includes artwork paths bool get hasArtworkPaths => thumbPath != null; diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index e00dc861..1e5a7e1b 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -230,34 +230,6 @@ class DownloadProvider extends ChangeNotifier { /// All metadata for downloads Map get metadata => Map.unmodifiable(_metadata); - /// Get all queued/downloading items (for Queue tab) - List get queuedDownloads { - return _downloads.values - .where( - (p) => - p.status == DownloadStatus.queued || - p.status == DownloadStatus.downloading || - p.status == DownloadStatus.paused, - ) - .toList(); - } - - /// Get all completed downloads - List get completedDownloads { - return _downloads.values.where((p) => p.status == DownloadStatus.completed).toList(); - } - - /// Get completed TV episode downloads (individual episodes) - List get downloadedEpisodes { - return _metadata.entries - .where((entry) { - final progress = _downloads[entry.key]; - return progress?.status == DownloadStatus.completed && entry.value.type == 'episode'; - }) - .map((entry) => entry.value) - .toList(); - } - /// Get unique TV shows that have downloaded episodes /// Returns stored show metadata, or synthesizes from episode metadata as fallback List get downloadedShows { diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index c7307b88..7457c1ae 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -1817,11 +1817,6 @@ class DownloadManagerService { } } - /// Get all downloads with a specific status - Stream> watchDownloadsByStatus(DownloadStatus status) { - return (_database.select(_database.downloadedMedia)..where((t) => t.status.equals(status.index))).watch(); - } - /// Get all downloaded media items (for loading persisted data) Future> getAllDownloads() { return _database.select(_database.downloadedMedia).get(); @@ -1879,18 +1874,6 @@ class DownloadManagerService { await _apiCache.pinForOffline(serverId, ratingKey); } - /// Cache children (seasons or episodes) in the API response format - Future cacheChildrenForOffline(String serverId, String parentRatingKey, List children) async { - final endpoint = '/library/metadata/$parentRatingKey/children'; - - // Build a response structure that matches the Plex API format - final cachedResponse = { - 'MediaContainer': {'Metadata': children.map((c) => c.toJson()).toList()}, - }; - - await _apiCache.put(serverId, endpoint, cachedResponse); - } - void dispose() { _disposed = true; for (final timer in _progressDebounceTimers.values) { diff --git a/lib/services/download_storage_service.dart b/lib/services/download_storage_service.dart index 6054a141..c6d855a1 100644 --- a/lib/services/download_storage_service.dart +++ b/lib/services/download_storage_service.dart @@ -84,12 +84,6 @@ class DownloadStorageService { return dir.path; } - /// Get default download path (for "Reset to Default" functionality) - Future getDefaultDownloadPath() async { - final baseDir = await _getBaseAppDir(); - return path.join(baseDir.path, 'downloads'); - } - /// Check if a directory is writable Future isDirectoryWritable(Directory dir) async { try { @@ -194,12 +188,6 @@ class DownloadStorageService { return path.join(mediaDir.path, 'video.$extension'); } - /// Get artwork file path (poster, art, thumb) - Future getArtworkPath(String serverId, String ratingKey, String artworkType) async { - final mediaDir = await getMediaDirectory(serverId, ratingKey); - return path.join(mediaDir.path, '$artworkType.jpg'); - } - /// Get subtitles directory Future getSubtitlesDirectory(String serverId, String ratingKey) async { final mediaDir = await getMediaDirectory(serverId, ratingKey); @@ -272,12 +260,6 @@ class DownloadStorageService { return path.join(movieDir.path, '$fileName.$extension'); } - /// Get movie artwork path: .../Movie Name (YYYY)/{artworkType}.jpg - Future getMovieArtworkPath(PlexMetadata movie, String artworkType) async { - final movieDir = await getMovieDirectory(movie); - return path.join(movieDir.path, '$artworkType.jpg'); - } - /// Get show directory: downloads/TV Shows/{Show Name} ({Year})/ /// [showYear]: Pass the show's premiere year explicitly (for episodes, the episode's /// year may differ from the show's year). If not provided, uses metadata.year. @@ -287,12 +269,6 @@ class DownloadStorageService { return _ensureDirectoryExists(Directory(path.join(baseDir.path, 'TV Shows', showFolder))); } - /// Get show artwork path: downloads/TV Shows/{Show}/poster.jpg - Future getShowArtworkPath(PlexMetadata metadata, String artworkType, {int? showYear}) async { - final showDir = await getShowDirectory(metadata, showYear: showYear); - return path.join(showDir.path, '$artworkType.jpg'); - } - /// Get season directory: .../TV Shows/{Show}/Season {XX}/ /// [showYear]: Pass the show's premiere year (not episode or season year) Future getSeasonDirectory(PlexMetadata metadata, {int? showYear}) async { @@ -301,12 +277,6 @@ class DownloadStorageService { return _ensureDirectoryExists(Directory(path.join(showDir.path, 'Season $seasonNum'))); } - /// Get season artwork path: .../Season XX/poster.jpg - Future getSeasonArtworkPath(PlexMetadata metadata, String artworkType, {int? showYear}) async { - final seasonDir = await getSeasonDirectory(metadata, showYear: showYear); - return path.join(seasonDir.path, '$artworkType.jpg'); - } - /// Get base path info for episode files (season directory path and formatted filename). /// [showYear]: Pass the show's premiere year (not episode year) Future<({String seasonDirPath, String fileName})> _getEpisodeBasePath(PlexMetadata episode, {int? showYear}) async { @@ -356,14 +326,6 @@ class DownloadStorageService { return path.join(subsDir.path, '$trackId.$extension'); } - /// Delete all files for a media item - Future deleteMediaFiles(String serverId, String ratingKey) async { - final mediaDir = await getMediaDirectory(serverId, ratingKey); - if (await mediaDir.exists()) { - await mediaDir.delete(recursive: true); - } - } - /// Convert an absolute file path to a relative path (for database storage) /// This ensures paths remain valid across app reinstalls on iOS where /// the container UUID can change. @@ -469,80 +431,6 @@ class DownloadStorageService { return fallback; } - /// Calculate total storage used by downloads - Future getTotalStorageUsed() async { - final baseDir = await getDownloadsDirectory(); - return _calculateDirectorySize(baseDir); - } - - Future _calculateDirectorySize(Directory dir) async { - int size = 0; - if (!await dir.exists()) return size; - - await for (var entity in dir.list(recursive: true, followLinks: false)) { - if (entity is File) { - try { - size += await entity.length(); - } catch (_) { - // Ignore errors reading file size - } - } - } - return size; - } - - /// Format bytes to human readable string - static String formatBytes(int bytes) => ByteFormatter.formatBytes(bytes); - - // ============================================================ - // SAF (Storage Access Framework) SUPPORT FOR ANDROID - // ============================================================ - - /// Get temporary cache directory for initial downloads - /// Files are downloaded here first, then copied to SAF if using SAF mode - Future getCacheDownloadDirectory() async { - final cacheDir = await getApplicationDocumentsDirectory(); - return _ensureDirectoryExists(Directory(path.join(cacheDir.path, '.download_cache'))); - } - - /// Get temporary file path for downloading (before copying to SAF) - Future getTempDownloadPath(String fileName) async { - final cacheDir = await getCacheDownloadDirectory(); - return path.join(cacheDir.path, fileName); - } - - /// Get the MIME type for a file extension - String getMimeType(String extension) { - switch (extension.toLowerCase()) { - case 'mp4': - return 'video/mp4'; - case 'mkv': - return 'video/x-matroska'; - case 'm4v': - return 'video/x-m4v'; - case 'avi': - return 'video/x-msvideo'; - case 'ogv': - return 'video/ogg'; - case 'webm': - return 'video/webm'; - case 'srt': - return 'application/x-subrip'; - case 'vtt': - return 'text/vtt'; - case 'ass': - case 'ssa': - return 'text/x-ssa'; - case 'jpg': - case 'jpeg': - return 'image/jpeg'; - case 'png': - return 'image/png'; - default: - return 'application/octet-stream'; - } - } - /// Get path components for SAF based on media type /// Returns list of directory names to create under the SAF base List getMovieSafPathComponents(PlexMetadata movie) { diff --git a/lib/services/saf_storage_service.dart b/lib/services/saf_storage_service.dart index d8c206c6..608b885d 100644 --- a/lib/services/saf_storage_service.dart +++ b/lib/services/saf_storage_service.dart @@ -1,11 +1,9 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:saf_util/saf_util.dart'; import '../utils/platform_detector.dart'; import 'package:saf_util/saf_util_platform_interface.dart'; -import 'package:saf_stream/saf_stream.dart'; /// Handles Storage Access Framework (SAF) operations for Android class SafStorageService { @@ -14,7 +12,6 @@ class SafStorageService { SafStorageService._(); final SafUtil _safUtil = SafUtil(); - final SafStream _safStream = SafStream(); /// Check if SAF is available (Android only) bool get isAvailable => Platform.isAndroid; @@ -35,28 +32,6 @@ class SafStorageService { } } - /// Check if we have persisted access to a URI - Future hasPersistedPermission(String contentUri) async { - if (!isAvailable) return false; - try { - return await _safUtil.hasPersistedPermission(contentUri, checkRead: true, checkWrite: true); - } catch (e) { - debugPrint('SAF hasPersistedPermission error: $e'); - return false; - } - } - - /// Get document file info for a URI - Future getDocumentFile(String contentUri, {bool isDir = true}) async { - if (!isAvailable) return null; - try { - return await _safUtil.documentFileFromUri(contentUri, isDir); - } catch (e) { - debugPrint('SAF getDocumentFile error: $e'); - return null; - } - } - /// Create a subdirectory in a SAF directory /// Returns the URI of the created directory Future createDirectory(String parentUri, String name) async { @@ -70,17 +45,6 @@ class SafStorageService { } } - /// List files in a SAF directory - Future> listDirectory(String contentUri) async { - if (!isAvailable) return []; - try { - return await _safUtil.list(contentUri); - } catch (e) { - debugPrint('SAF listDirectory error: $e'); - return []; - } - } - /// Get a child file/directory in a SAF directory Future getChild(String parentUri, String name) async { if (!isAvailable) return null; @@ -92,30 +56,6 @@ class SafStorageService { } } - /// Delete a file or directory in SAF - Future delete(String contentUri, {bool isDir = false}) async { - if (!isAvailable) return false; - try { - await _safUtil.delete(contentUri, isDir); - return true; - } catch (e) { - debugPrint('SAF delete error: $e'); - return false; - } - } - - /// Get a display name for a SAF URI (for UI purposes) - Future getDisplayName(String contentUri) async { - if (!isAvailable) return null; - try { - final doc = await _safUtil.documentFileFromUri(contentUri, true); - return doc?.name; - } catch (e) { - debugPrint('SAF getDisplayName error: $e'); - return null; - } - } - /// Create nested directories in a SAF directory /// Returns the URI of the deepest directory Future createNestedDirectories(String parentUri, List pathComponents) async { @@ -129,43 +69,4 @@ class SafStorageService { } } - /// Write bytes directly to a SAF file - /// Returns the SAF URI of the created file, or null on failure - Future writeFileBytes(String directoryUri, String fileName, String mimeType, Uint8List bytes) async { - if (!isAvailable) return null; - try { - final result = await _safStream.writeFileBytes(directoryUri, fileName, mimeType, bytes); - return result.uri.toString(); - } catch (e) { - debugPrint('SAF writeFileBytes error: $e'); - return null; - } - } - - /// Read bytes from a SAF file - Future readFileBytes(String fileUri) async { - if (!isAvailable) return null; - try { - return await _safStream.readFileBytes(fileUri); - } catch (e) { - debugPrint('SAF readFileBytes error: $e'); - return null; - } - } - - /// Check if a file exists in a SAF directory - Future fileExists(String parentUri, String fileName) async { - if (!isAvailable) return false; - try { - final child = await _safUtil.child(parentUri, [fileName]); - return child != null; - } catch (e) { - debugPrint('SAF fileExists error: $e'); - return false; - } - } - - /// Get the content URI for a file that should be readable by MediaStore/media players - /// For SAF files, this returns the same URI as input (content:// URIs are already readable) - String getReadableUri(String safUri) => safUri; }