feat: jellyfin
This commit is contained in:
@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/formatters.dart';
|
||||
import 'settings_service.dart';
|
||||
@@ -86,7 +86,7 @@ class DownloadStorageService {
|
||||
}
|
||||
|
||||
/// Format episode filename base: S{XX}E{XX} - {Title}
|
||||
String _formatEpisodeFileName(PlexMetadata episode) {
|
||||
String _formatEpisodeFileName(MediaItem episode) {
|
||||
final season = padNumber(episode.parentIndex ?? 0, 2);
|
||||
final ep = padNumber(episode.index ?? 0, 2);
|
||||
final episodeName = _sanitizeFileName(episode.title!);
|
||||
@@ -168,8 +168,11 @@ class DownloadStorageService {
|
||||
return artworkDir;
|
||||
}
|
||||
|
||||
/// Get artwork file path from Plex thumb path (synchronous, requires initialization)
|
||||
/// Returns path to cached artwork file using hash of the thumb URL, or null if not initialized
|
||||
/// Get artwork file path from a server-side thumb path (synchronous, requires initialization).
|
||||
/// Works for any backend — the thumb path is hashed alongside the serverId,
|
||||
/// so Plex `/library/metadata/.../thumb` and Jellyfin
|
||||
/// `/Items/.../Images/Primary` paths both round-trip cleanly.
|
||||
/// Returns path to cached artwork file using hash of the thumb URL, or null if not initialized.
|
||||
/// Example: artwork/a1b2c3d4e5f6.jpg
|
||||
String? getArtworkPathSync(String serverId, String thumbPath) {
|
||||
if (_artworkDirectoryPath == null) return null;
|
||||
@@ -178,7 +181,8 @@ class DownloadStorageService {
|
||||
return path.join(_artworkDirectoryPath!, '$hash.jpg');
|
||||
}
|
||||
|
||||
/// Get artwork file path from Plex thumb path (async version)
|
||||
/// Get artwork file path from a server-side thumb path (async version).
|
||||
/// Backend-neutral — see [getArtworkPathSync] for details.
|
||||
Future<String> getArtworkPathFromThumb(String serverId, String thumbPath) async {
|
||||
final artworkDir = await getArtworkDirectory();
|
||||
final hash = _hashArtworkPath(serverId, thumbPath);
|
||||
@@ -260,27 +264,27 @@ class DownloadStorageService {
|
||||
}
|
||||
|
||||
/// Get the folder name for a movie: "Movie Name (YYYY)"
|
||||
String _getMovieFolderName(PlexMetadata movie) {
|
||||
String _getMovieFolderName(MediaItem movie) {
|
||||
return _formatTitleWithYear(movie.title!, movie.year);
|
||||
}
|
||||
|
||||
/// Get the folder name for a TV show: "Show Name (YYYY)"
|
||||
/// [showYear]: Pass explicitly for episodes (episode.year may differ from show's year)
|
||||
String _getShowFolderName(PlexMetadata metadata, {int? showYear}) {
|
||||
String _getShowFolderName(MediaItem metadata, {int? showYear}) {
|
||||
final title = metadata.grandparentTitle ?? metadata.title!;
|
||||
final year = showYear ?? metadata.year;
|
||||
return _formatTitleWithYear(title, year);
|
||||
}
|
||||
|
||||
/// Get movie directory: downloads/Movies/{Movie Name} ({Year})/
|
||||
Future<Directory> getMovieDirectory(PlexMetadata movie) async {
|
||||
Future<Directory> getMovieDirectory(MediaItem movie) async {
|
||||
final baseDir = await getDownloadsDirectory();
|
||||
final movieFolder = _getMovieFolderName(movie);
|
||||
return _ensureDirectoryExists(Directory(path.join(baseDir.path, 'Movies', movieFolder)));
|
||||
}
|
||||
|
||||
/// Get movie video file path: .../Movie Name (YYYY)/Movie Name (YYYY).{ext}
|
||||
Future<String> getMovieVideoPath(PlexMetadata movie, String extension) async {
|
||||
Future<String> getMovieVideoPath(MediaItem movie, String extension) async {
|
||||
final movieDir = await getMovieDirectory(movie);
|
||||
final fileName = _getMovieFolderName(movie);
|
||||
return path.join(movieDir.path, '$fileName.$extension');
|
||||
@@ -289,7 +293,7 @@ class DownloadStorageService {
|
||||
/// 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.
|
||||
Future<Directory> getShowDirectory(PlexMetadata metadata, {int? showYear}) async {
|
||||
Future<Directory> getShowDirectory(MediaItem metadata, {int? showYear}) async {
|
||||
final baseDir = await getDownloadsDirectory();
|
||||
final showFolder = _getShowFolderName(metadata, showYear: showYear);
|
||||
return _ensureDirectoryExists(Directory(path.join(baseDir.path, 'TV Shows', showFolder)));
|
||||
@@ -297,7 +301,7 @@ class DownloadStorageService {
|
||||
|
||||
/// Get season directory: .../TV Shows/{Show}/Season {XX}/
|
||||
/// [showYear]: Pass the show's premiere year (not episode or season year)
|
||||
Future<Directory> getSeasonDirectory(PlexMetadata metadata, {int? showYear}) async {
|
||||
Future<Directory> getSeasonDirectory(MediaItem metadata, {int? showYear}) async {
|
||||
final showDir = await getShowDirectory(metadata, showYear: showYear);
|
||||
final seasonNum = padNumber(metadata.parentIndex ?? 0, 2);
|
||||
return _ensureDirectoryExists(Directory(path.join(showDir.path, 'Season $seasonNum')));
|
||||
@@ -305,7 +309,7 @@ class DownloadStorageService {
|
||||
|
||||
/// 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 {
|
||||
Future<({String seasonDirPath, String fileName})> _getEpisodeBasePath(MediaItem episode, {int? showYear}) async {
|
||||
final seasonDir = await getSeasonDirectory(episode, showYear: showYear);
|
||||
final fileName = _formatEpisodeFileName(episode);
|
||||
return (seasonDirPath: seasonDir.path, fileName: fileName);
|
||||
@@ -313,41 +317,41 @@ class DownloadStorageService {
|
||||
|
||||
/// Get episode video file path: .../Season XX/S{XX}E{XX} - {Title}.{ext}
|
||||
/// [showYear]: Pass the show's premiere year (not episode year)
|
||||
Future<String> getEpisodeVideoPath(PlexMetadata episode, String extension, {int? showYear}) async {
|
||||
Future<String> getEpisodeVideoPath(MediaItem episode, String extension, {int? showYear}) async {
|
||||
final base = await _getEpisodeBasePath(episode, showYear: showYear);
|
||||
return path.join(base.seasonDirPath, '${base.fileName}.$extension');
|
||||
}
|
||||
|
||||
/// Get episode thumbnail path: .../Season XX/S{XX}E{XX} - {Title}.jpg
|
||||
/// [showYear]: Pass the show's premiere year (not episode year)
|
||||
Future<String> getEpisodeThumbnailPath(PlexMetadata episode, {int? showYear}) async {
|
||||
Future<String> getEpisodeThumbnailPath(MediaItem episode, {int? showYear}) async {
|
||||
final base = await _getEpisodeBasePath(episode, showYear: showYear);
|
||||
return path.join(base.seasonDirPath, '${base.fileName}.jpg');
|
||||
}
|
||||
|
||||
/// Get subtitles directory for episode: .../Season XX/S{XX}E{XX} - {Title}_subs/
|
||||
/// [showYear]: Pass the show's premiere year (not episode year)
|
||||
Future<Directory> getEpisodeSubtitlesDirectory(PlexMetadata episode, {int? showYear}) async {
|
||||
Future<Directory> getEpisodeSubtitlesDirectory(MediaItem episode, {int? showYear}) async {
|
||||
final base = await _getEpisodeBasePath(episode, showYear: showYear);
|
||||
return _ensureDirectoryExists(Directory(path.join(base.seasonDirPath, '${base.fileName}_subs')));
|
||||
}
|
||||
|
||||
/// Get episode subtitle path
|
||||
/// [showYear]: Pass the show's premiere year (not episode year)
|
||||
Future<String> getEpisodeSubtitlePath(PlexMetadata episode, int trackId, String extension, {int? showYear}) async {
|
||||
Future<String> getEpisodeSubtitlePath(MediaItem episode, int trackId, String extension, {int? showYear}) async {
|
||||
final subsDir = await getEpisodeSubtitlesDirectory(episode, showYear: showYear);
|
||||
return path.join(subsDir.path, '$trackId.$extension');
|
||||
}
|
||||
|
||||
/// Get subtitles directory for movie
|
||||
Future<Directory> getMovieSubtitlesDirectory(PlexMetadata movie) async {
|
||||
Future<Directory> getMovieSubtitlesDirectory(MediaItem movie) async {
|
||||
final movieDir = await getMovieDirectory(movie);
|
||||
final baseName = _getMovieFolderName(movie);
|
||||
return _ensureDirectoryExists(Directory(path.join(movieDir.path, '${baseName}_subs')));
|
||||
}
|
||||
|
||||
/// Get movie subtitle path
|
||||
Future<String> getMovieSubtitlePath(PlexMetadata movie, int trackId, String extension) async {
|
||||
Future<String> getMovieSubtitlePath(MediaItem movie, int trackId, String extension) async {
|
||||
final subsDir = await getMovieSubtitlesDirectory(movie);
|
||||
return path.join(subsDir.path, '$trackId.$extension');
|
||||
}
|
||||
@@ -461,43 +465,43 @@ class DownloadStorageService {
|
||||
|
||||
/// Get path components for SAF based on media type
|
||||
/// Returns list of directory names to create under the SAF base
|
||||
List<String> getMovieSafPathComponents(PlexMetadata movie) {
|
||||
List<String> getMovieSafPathComponents(MediaItem movie) {
|
||||
return ['Movies', _getMovieFolderName(movie)];
|
||||
}
|
||||
|
||||
/// Get path components for episode SAF storage
|
||||
List<String> getEpisodeSafPathComponents(PlexMetadata episode, {int? showYear}) {
|
||||
List<String> getEpisodeSafPathComponents(MediaItem episode, {int? showYear}) {
|
||||
final showFolder = _getShowFolderName(episode, showYear: showYear);
|
||||
final seasonNum = padNumber(episode.parentIndex ?? 0, 2);
|
||||
return ['TV Shows', showFolder, 'Season $seasonNum'];
|
||||
}
|
||||
|
||||
/// Get SAF path components for a show directory: ['TV Shows', {showFolder}]
|
||||
List<String> getShowSafPathComponents(PlexMetadata metadata, {int? showYear}) {
|
||||
List<String> getShowSafPathComponents(MediaItem metadata, {int? showYear}) {
|
||||
return ['TV Shows', _getShowFolderName(metadata, showYear: showYear)];
|
||||
}
|
||||
|
||||
/// Get SAF path components for a season directory when called with season metadata:
|
||||
/// ['TV Shows', {showFolder}, 'Season XX']. Uses season.index for the season number.
|
||||
List<String> getSeasonSafPathComponents(PlexMetadata season, {int? showYear}) {
|
||||
List<String> getSeasonSafPathComponents(MediaItem season, {int? showYear}) {
|
||||
final showFolder = _getShowFolderName(season, showYear: showYear);
|
||||
final seasonNum = padNumber(season.index ?? 0, 2);
|
||||
return ['TV Shows', showFolder, 'Season $seasonNum'];
|
||||
}
|
||||
|
||||
/// Get SAF file name for a movie
|
||||
String getMovieSafFileName(PlexMetadata movie, String extension) {
|
||||
String getMovieSafFileName(MediaItem movie, String extension) {
|
||||
return '${_getMovieFolderName(movie)}.$extension';
|
||||
}
|
||||
|
||||
/// Get SAF file name for an episode
|
||||
String getEpisodeSafFileName(PlexMetadata episode, String extension) {
|
||||
String getEpisodeSafFileName(MediaItem episode, String extension) {
|
||||
final fileName = _formatEpisodeFileName(episode);
|
||||
return '$fileName.$extension';
|
||||
}
|
||||
|
||||
/// Get the extension-less episode filename used for SAF lookups.
|
||||
String getEpisodeSafBaseName(PlexMetadata episode) => _formatEpisodeFileName(episode);
|
||||
String getEpisodeSafBaseName(MediaItem episode) => _formatEpisodeFileName(episode);
|
||||
|
||||
/// Check if a path is a SAF content URI
|
||||
bool isSafUri(String storedPath) {
|
||||
|
||||
Reference in New Issue
Block a user