direct stream content from plex if needed (#1165)
* start to stream directly from plex * update metadata and statistics with one plex api call * stream movies from plex * scanning bug fix; update changelog
This commit is contained in:
@@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
### Fixed
|
||||
- Align default docker image (no acceleration) with new images from [ErsatzTV-ffmpeg](https://github.com/jasongdove/ErsatzTV-ffmpeg)
|
||||
|
||||
### Changed
|
||||
- Plex libraries now retrieve all metadata and statistics from Plex; ffprobe is no longer used
|
||||
- Plex libraries now direct stream content from Plex when files are not found on ErsatzTV's file system
|
||||
- Content will still be normalized according to the Channel and FFmpeg Profile settings
|
||||
- Streaming from disk is preferred, so every playback attempt will first check the local file system
|
||||
|
||||
## [0.7.4-beta] - 2023-02-12
|
||||
### Added
|
||||
- Add button to copy/clone schedule from schedules table
|
||||
|
||||
+32
-4
@@ -1,4 +1,5 @@
|
||||
using CliWrap;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
@@ -126,7 +127,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
.Include(i => i.Watermark)
|
||||
.ForChannelAndTime(channel.Id, now)
|
||||
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
||||
.BindT(ValidatePlayoutItemPath);
|
||||
.BindT(item => ValidatePlayoutItemPath(dbContext, item));
|
||||
|
||||
if (maybePlayoutItem.LeftAsEnumerable().Any(e => e is UnableToLocatePlayoutItem))
|
||||
{
|
||||
@@ -440,13 +441,15 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
DisableWatermarks = !fallbackPreset.AllowWatermarks
|
||||
};
|
||||
|
||||
return await ValidatePlayoutItemPath(playoutItem);
|
||||
return await ValidatePlayoutItemPath(dbContext, playoutItem);
|
||||
}
|
||||
|
||||
return new UnableToLocatePlayoutItem();
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> ValidatePlayoutItemPath(PlayoutItem playoutItem)
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> ValidatePlayoutItemPath(
|
||||
TvContext dbContext,
|
||||
PlayoutItem playoutItem)
|
||||
{
|
||||
string path = await GetPlayoutItemPath(playoutItem);
|
||||
|
||||
@@ -455,14 +458,39 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
return new PlayoutItemWithPath(playoutItem, path);
|
||||
}
|
||||
|
||||
if (playoutItem.MediaItem.State == MediaItemState.RemoteOnly)
|
||||
{
|
||||
MediaFile file = playoutItem.MediaItem.GetHeadVersion().MediaFiles.Head();
|
||||
switch (file)
|
||||
{
|
||||
case PlexMediaFile pmf:
|
||||
Option<int> maybeId = await dbContext.Connection.QuerySingleOrDefaultAsync<int>(
|
||||
@"SELECT PMS.Id FROM PlexMediaSource PMS
|
||||
INNER JOIN Library L on PMS.Id = L.MediaSourceId
|
||||
INNER JOIN LibraryPath LP on L.Id = LP.LibraryId
|
||||
WHERE LP.Id = @LibraryPathId",
|
||||
new { playoutItem.MediaItem.LibraryPathId })
|
||||
.Map(Optional);
|
||||
|
||||
foreach (int plexMediaSourceId in maybeId)
|
||||
{
|
||||
return new PlayoutItemWithPath(
|
||||
playoutItem,
|
||||
$"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{pmf.Key}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new PlayoutItemDoesNotExistOnDisk(path);
|
||||
}
|
||||
|
||||
private async Task<string> GetPlayoutItemPath(PlayoutItem playoutItem)
|
||||
{
|
||||
MediaVersion version = playoutItem.MediaItem.GetHeadVersion();
|
||||
|
||||
MediaFile file = version.MediaFiles.Head();
|
||||
|
||||
string path = file.Path;
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
|
||||
@@ -4,5 +4,6 @@ public enum MediaItemState
|
||||
{
|
||||
Normal = 0,
|
||||
FileNotFound = 1,
|
||||
Unavailable = 2
|
||||
Unavailable = 2,
|
||||
RemoteOnly = 3
|
||||
}
|
||||
|
||||
@@ -140,10 +140,22 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None,
|
||||
playbackSettings.NormalizeLoudness);
|
||||
|
||||
IPixelFormat pixelFormat = await AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger)
|
||||
.IfNoneAsync(
|
||||
() =>
|
||||
{
|
||||
return videoStream.BitsPerRawSample switch
|
||||
{
|
||||
8 => new PixelFormatYuv420P(),
|
||||
10 => new PixelFormatYuv420P10Le(),
|
||||
_ => new PixelFormatUnknown(videoStream.BitsPerRawSample)
|
||||
};
|
||||
});
|
||||
|
||||
var ffmpegVideoStream = new VideoStream(
|
||||
videoStream.Index,
|
||||
videoStream.Codec,
|
||||
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
|
||||
Some(pixelFormat),
|
||||
new ColorParams(
|
||||
videoStream.ColorRange,
|
||||
videoStream.ColorSpace,
|
||||
|
||||
@@ -10,6 +10,7 @@ public interface IMediaServerMovieRepository<in TLibrary, TMovie, TEtag> where T
|
||||
Task<List<TEtag>> GetExistingMovies(TLibrary library);
|
||||
Task<bool> FlagNormal(TLibrary library, TMovie movie);
|
||||
Task<Option<int>> FlagUnavailable(TLibrary library, TMovie movie);
|
||||
Task<Option<int>> FlagRemoteOnly(TLibrary library, TMovie movie);
|
||||
Task<List<int>> FlagFileNotFound(TLibrary library, List<string> movieItemIds);
|
||||
Task<Either<BaseError, MediaItemScanResult<TMovie>>> GetOrAdd(TLibrary library, TMovie item);
|
||||
Task<Unit> SetEtag(TMovie movie, string etag);
|
||||
|
||||
@@ -25,4 +25,5 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T
|
||||
Task<List<int>> FlagFileNotFoundSeasons(TLibrary library, List<string> seasonItemIds);
|
||||
Task<List<int>> FlagFileNotFoundEpisodes(TLibrary library, List<string> episodeItemIds);
|
||||
Task<Option<int>> FlagUnavailable(TLibrary library, TEpisode episode);
|
||||
Task<Option<int>> FlagRemoteOnly(TLibrary library, TEpisode episode);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface IMetadataRepository
|
||||
Task<bool> RemoveActor(Actor actor);
|
||||
Task<bool> Update(Domain.Metadata metadata);
|
||||
Task<bool> Add(Domain.Metadata metadata);
|
||||
Task<bool> UpdateLocalStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true);
|
||||
Task<bool> UpdateStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true);
|
||||
Task<Unit> UpdateArtworkPath(Artwork artwork);
|
||||
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
|
||||
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
public class PixelFormatUnknown : IPixelFormat
|
||||
{
|
||||
public PixelFormatUnknown(int bitDepth = 8)
|
||||
{
|
||||
BitDepth = bitDepth;
|
||||
}
|
||||
|
||||
public string Name => "unknown";
|
||||
public string FFmpegName => "unknown";
|
||||
public int BitDepth => 8;
|
||||
public int BitDepth { get; }
|
||||
}
|
||||
|
||||
@@ -66,6 +66,28 @@ public class EmbyMovieRepository : IEmbyMovieRepository
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyMovie movie)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
movie.State = MediaItemState.RemoteOnly;
|
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
@"SELECT EmbyMovie.Id FROM EmbyMovie
|
||||
INNER JOIN MediaItem MI ON MI.Id = EmbyMovie.Id
|
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
|
||||
WHERE EmbyMovie.ItemId = @ItemId",
|
||||
new { LibraryId = library.Id, movie.ItemId });
|
||||
|
||||
foreach (int id in maybeId)
|
||||
{
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id",
|
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
|
||||
}
|
||||
|
||||
return None; }
|
||||
|
||||
public async Task<List<int>> FlagFileNotFound(EmbyLibrary library, List<string> movieItemIds)
|
||||
{
|
||||
if (movieItemIds.Count == 0)
|
||||
|
||||
@@ -338,6 +338,29 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyEpisode episode)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
episode.State = MediaItemState.RemoteOnly;
|
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
|
||||
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
|
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
|
||||
WHERE EmbyEpisode.ItemId = @ItemId",
|
||||
new { LibraryId = library.Id, episode.ItemId });
|
||||
|
||||
foreach (int id in maybeId)
|
||||
{
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id",
|
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private async Task UpdateShow(TvContext dbContext, EmbyShow existing, EmbyShow incoming)
|
||||
{
|
||||
// library path is used for search indexing later
|
||||
|
||||
@@ -67,6 +67,28 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinMovie movie)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
movie.State = MediaItemState.RemoteOnly;
|
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
@"SELECT JellyfinMovie.Id FROM JellyfinMovie
|
||||
INNER JOIN MediaItem MI ON MI.Id = JellyfinMovie.Id
|
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
|
||||
WHERE JellyfinMovie.ItemId = @ItemId",
|
||||
new { LibraryId = library.Id, movie.ItemId });
|
||||
|
||||
foreach (int id in maybeId)
|
||||
{
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id",
|
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
|
||||
}
|
||||
|
||||
return None; }
|
||||
|
||||
public async Task<List<int>> FlagFileNotFound(JellyfinLibrary library, List<string> movieItemIds)
|
||||
{
|
||||
if (movieItemIds.Count == 0)
|
||||
|
||||
@@ -342,6 +342,28 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinEpisode episode)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
episode.State = MediaItemState.RemoteOnly;
|
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
|
||||
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
|
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
|
||||
WHERE JellyfinEpisode.ItemId = @ItemId",
|
||||
new { LibraryId = library.Id, episode.ItemId });
|
||||
|
||||
foreach (int id in maybeId)
|
||||
{
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id",
|
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
|
||||
}
|
||||
|
||||
return None; }
|
||||
|
||||
private async Task UpdateShow(TvContext dbContext, JellyfinShow existing, JellyfinShow incoming)
|
||||
{
|
||||
// library path is used for search indexing later
|
||||
|
||||
@@ -119,7 +119,7 @@ public class MetadataRepository : IMetadataRepository
|
||||
return await dbContext.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateLocalStatistics(
|
||||
public async Task<bool> UpdateStatistics(
|
||||
MediaItem mediaItem,
|
||||
MediaVersion incoming,
|
||||
bool updateVersion = true)
|
||||
|
||||
@@ -66,6 +66,28 @@ public class PlexMovieRepository : IPlexMovieRepository
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexMovie movie)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
movie.State = MediaItemState.RemoteOnly;
|
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
@"SELECT PlexMovie.Id FROM PlexMovie
|
||||
INNER JOIN MediaItem MI ON MI.Id = PlexMovie.Id
|
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
|
||||
WHERE PlexMovie.Key = @Key",
|
||||
new { LibraryId = library.Id, movie.Key });
|
||||
|
||||
foreach (int id in maybeId)
|
||||
{
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id",
|
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
public async Task<List<int>> FlagFileNotFound(PlexLibrary library, List<string> plexMovieKeys)
|
||||
{
|
||||
if (plexMovieKeys.Count == 0)
|
||||
|
||||
@@ -84,6 +84,29 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexEpisode episode)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
episode.State = MediaItemState.RemoteOnly;
|
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
@"SELECT PlexEpisode.Id FROM PlexEpisode
|
||||
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
|
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
|
||||
WHERE PlexEpisode.Key = @Key",
|
||||
new { LibraryId = library.Id, episode.Key });
|
||||
|
||||
foreach (int id in maybeId)
|
||||
{
|
||||
return await dbContext.Connection.ExecuteAsync(
|
||||
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id",
|
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<List<PlexItemEtag>> GetExistingShows(PlexLibrary library)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
@@ -19,6 +19,9 @@ public class PlexStreamResponse
|
||||
[XmlAttribute("languageCode")]
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
[XmlAttribute("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[XmlAttribute("streamType")]
|
||||
public int StreamType { get; set; }
|
||||
|
||||
@@ -31,6 +34,12 @@ public class PlexStreamResponse
|
||||
[XmlAttribute("channels")]
|
||||
public int Channels { get; set; }
|
||||
|
||||
[XmlAttribute("width")]
|
||||
public int Width { get; set; }
|
||||
|
||||
[XmlAttribute("height")]
|
||||
public int Height { get; set; }
|
||||
|
||||
[XmlAttribute("anamorphic")]
|
||||
public bool Anamorphic { get; set; }
|
||||
|
||||
@@ -40,6 +49,24 @@ public class PlexStreamResponse
|
||||
[XmlAttribute("scanType")]
|
||||
public string ScanType { get; set; }
|
||||
|
||||
[XmlAttribute("frameRate")]
|
||||
public string FrameRate { get; set; }
|
||||
|
||||
[XmlAttribute("bitDepth")]
|
||||
public int BitDepth { get; set; }
|
||||
|
||||
[XmlAttribute("colorRange")]
|
||||
public string ColorRange { get; set; }
|
||||
|
||||
[XmlAttribute("colorSpace")]
|
||||
public string ColorSpace { get; set; }
|
||||
|
||||
[XmlAttribute("colorTrc")]
|
||||
public string ColorTrc { get; set; }
|
||||
|
||||
[XmlAttribute("colorPrimaries")]
|
||||
public string ColorPrimaries { get; set; }
|
||||
|
||||
[XmlAttribute("displayTitle")]
|
||||
public string DisplayTitle { get; set; }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Xml.Serialization;
|
||||
using System.Globalization;
|
||||
using System.Xml.Serialization;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Plex;
|
||||
@@ -541,7 +541,8 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
|
||||
private Option<MediaVersion> ProjectToMediaVersion(PlexXmlMetadataResponse response)
|
||||
{
|
||||
List<PlexStreamResponse> streams = response.Media.Head().Part.Head().Stream;
|
||||
PlexMediaResponse<PlexXmlPartResponse> media = response.Media.Head();
|
||||
List<PlexStreamResponse> streams = media.Part.Head().Stream;
|
||||
DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
||||
Option<PlexStreamResponse> maybeVideoStream = streams.Find(s => s.StreamType == 1);
|
||||
return maybeVideoStream.Map(
|
||||
@@ -549,7 +550,9 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
{
|
||||
var version = new MediaVersion
|
||||
{
|
||||
SampleAspectRatio = videoStream.PixelAspectRatio ?? "1:1",
|
||||
Duration = TimeSpan.FromMilliseconds(media.Duration),
|
||||
SampleAspectRatio = string.IsNullOrWhiteSpace(videoStream.PixelAspectRatio) ? "1:1"
|
||||
: videoStream.PixelAspectRatio,
|
||||
VideoScanKind = videoStream.ScanType switch
|
||||
{
|
||||
"interlaced" => VideoScanKind.Interlaced,
|
||||
@@ -557,21 +560,34 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
_ => VideoScanKind.Unknown
|
||||
},
|
||||
Streams = new List<MediaStream>(),
|
||||
DateUpdated = dateUpdated
|
||||
DateUpdated = dateUpdated,
|
||||
Width = videoStream.Width,
|
||||
Height = videoStream.Height,
|
||||
RFrameRate = videoStream.FrameRate,
|
||||
DisplayAspectRatio = media.AspectRatio == 0
|
||||
? string.Empty
|
||||
: media.AspectRatio.ToString("0.00###", CultureInfo.InvariantCulture),
|
||||
Chapters = new List<MediaChapter>() // TODO: `?includeChapters=1`
|
||||
};
|
||||
|
||||
version.Streams.Add(
|
||||
new MediaStream
|
||||
{
|
||||
MediaVersionId = version.Id,
|
||||
MediaStreamKind = MediaStreamKind.Video,
|
||||
Index = videoStream.Index,
|
||||
Codec = videoStream.Codec,
|
||||
Profile = (videoStream.Profile ?? string.Empty).ToLowerInvariant(),
|
||||
Default = videoStream.Default,
|
||||
Language = videoStream.LanguageCode,
|
||||
Forced = videoStream.Forced
|
||||
Forced = videoStream.Forced,
|
||||
BitsPerRawSample = videoStream.BitDepth,
|
||||
ColorRange = (videoStream.ColorRange ?? string.Empty).ToLowerInvariant(),
|
||||
ColorSpace = (videoStream.ColorSpace ?? string.Empty).ToLowerInvariant(),
|
||||
ColorTransfer = (videoStream.ColorTrc ?? string.Empty).ToLowerInvariant(),
|
||||
ColorPrimaries = (videoStream.ColorPrimaries ?? string.Empty).ToLowerInvariant()
|
||||
});
|
||||
|
||||
|
||||
foreach (PlexStreamResponse audioStream in streams.Filter(s => s.StreamType == 2))
|
||||
{
|
||||
var stream = new MediaStream
|
||||
@@ -584,9 +600,10 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
Channels = audioStream.Channels,
|
||||
Default = audioStream.Default,
|
||||
Forced = audioStream.Forced,
|
||||
Language = audioStream.LanguageCode
|
||||
Language = audioStream.LanguageCode,
|
||||
Title = audioStream.Title ?? string.Empty
|
||||
};
|
||||
|
||||
|
||||
version.Streams.Add(stream);
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ public class TranscodingTests
|
||||
|
||||
var metadataRepository = new Mock<IMetadataRepository>();
|
||||
metadataRepository
|
||||
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
||||
.Setup(r => r.UpdateStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
||||
.Callback<MediaItem, MediaVersion, bool>(
|
||||
(_, version, _) =>
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ public class EmbyMovieLibraryScanner :
|
||||
IEmbyMovieRepository embyMovieRepository,
|
||||
IEmbyPathReplacementService pathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMetadataRepository metadataRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILogger<EmbyMovieLibraryScanner> logger)
|
||||
@@ -35,6 +36,7 @@ public class EmbyMovieLibraryScanner :
|
||||
localStatisticsProvider,
|
||||
localSubtitlesProvider,
|
||||
localFileSystem,
|
||||
metadataRepository,
|
||||
mediator,
|
||||
logger)
|
||||
{
|
||||
@@ -103,6 +105,12 @@ public class EmbyMovieLibraryScanner :
|
||||
bool deepScan) =>
|
||||
Task.FromResult<Option<MovieMetadata>>(None);
|
||||
|
||||
protected override Task<Option<Tuple<MovieMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
EmbyConnectionParameters connectionParameters,
|
||||
EmbyLibrary library,
|
||||
MediaItemScanResult<EmbyMovie> result,
|
||||
EmbyMovie incoming) => Task.FromResult(Option<Tuple<MovieMetadata, MediaVersion>>.None);
|
||||
|
||||
protected override Task<Either<BaseError, MediaItemScanResult<EmbyMovie>>> UpdateMetadata(
|
||||
MediaItemScanResult<EmbyMovie> result,
|
||||
MovieMetadata fullMetadata) =>
|
||||
|
||||
@@ -27,6 +27,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
|
||||
IEmbyTelevisionRepository televisionRepository,
|
||||
IEmbyPathReplacementService pathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMetadataRepository metadataRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
IMediator mediator,
|
||||
@@ -35,6 +36,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
|
||||
localStatisticsProvider,
|
||||
localSubtitlesProvider,
|
||||
localFileSystem,
|
||||
metadataRepository,
|
||||
mediator,
|
||||
logger)
|
||||
{
|
||||
@@ -163,6 +165,12 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
|
||||
bool deepScan) =>
|
||||
Task.FromResult(Option<EpisodeMetadata>.None);
|
||||
|
||||
protected override Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
EmbyConnectionParameters connectionParameters,
|
||||
EmbyLibrary library,
|
||||
MediaItemScanResult<EmbyEpisode> result,
|
||||
EmbyEpisode incoming) => Task.FromResult(Option<Tuple<EpisodeMetadata, MediaVersion>>.None);
|
||||
|
||||
protected override Task<Either<BaseError, MediaItemScanResult<EmbyShow>>> UpdateMetadata(
|
||||
MediaItemScanResult<EmbyShow> result,
|
||||
ShowMetadata fullMetadata) =>
|
||||
|
||||
@@ -28,6 +28,7 @@ public class JellyfinMovieLibraryScanner :
|
||||
IJellyfinPathReplacementService pathReplacementService,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMetadataRepository metadataRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILogger<JellyfinMovieLibraryScanner> logger)
|
||||
@@ -35,6 +36,7 @@ public class JellyfinMovieLibraryScanner :
|
||||
localStatisticsProvider,
|
||||
localSubtitlesProvider,
|
||||
localFileSystem,
|
||||
metadataRepository,
|
||||
mediator,
|
||||
logger)
|
||||
{
|
||||
@@ -106,6 +108,12 @@ public class JellyfinMovieLibraryScanner :
|
||||
bool deepScan) =>
|
||||
Task.FromResult<Option<MovieMetadata>>(None);
|
||||
|
||||
protected override Task<Option<Tuple<MovieMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
JellyfinConnectionParameters connectionParameters,
|
||||
JellyfinLibrary library,
|
||||
MediaItemScanResult<JellyfinMovie> result,
|
||||
JellyfinMovie incoming) => Task.FromResult(Option<Tuple<MovieMetadata, MediaVersion>>.None);
|
||||
|
||||
protected override Task<Either<BaseError, MediaItemScanResult<JellyfinMovie>>> UpdateMetadata(
|
||||
MediaItemScanResult<JellyfinMovie> result,
|
||||
MovieMetadata fullMetadata) =>
|
||||
|
||||
@@ -28,6 +28,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
|
||||
IJellyfinTelevisionRepository televisionRepository,
|
||||
IJellyfinPathReplacementService pathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMetadataRepository metadataRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
IMediator mediator,
|
||||
@@ -36,6 +37,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
|
||||
localStatisticsProvider,
|
||||
localSubtitlesProvider,
|
||||
localFileSystem,
|
||||
metadataRepository,
|
||||
mediator,
|
||||
logger)
|
||||
{
|
||||
@@ -169,6 +171,12 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
|
||||
bool deepScan) =>
|
||||
Task.FromResult(Option<EpisodeMetadata>.None);
|
||||
|
||||
protected override Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
JellyfinConnectionParameters connectionParameters,
|
||||
JellyfinLibrary library,
|
||||
MediaItemScanResult<JellyfinEpisode> result,
|
||||
JellyfinEpisode incoming) => Task.FromResult(Option<Tuple<EpisodeMetadata, MediaVersion>>.None);
|
||||
|
||||
protected override Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> UpdateMetadata(
|
||||
MediaItemScanResult<JellyfinShow> result,
|
||||
ShowMetadata fullMetadata) =>
|
||||
|
||||
@@ -214,7 +214,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
|
||||
version.DateUpdated = _localFileSystem.GetLastWriteTime(filePath);
|
||||
|
||||
return await _metadataRepository.UpdateLocalStatistics(mediaItem, version) && durationChange;
|
||||
return await _metadataRepository.UpdateStatistics(mediaItem, version) && durationChange;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
|
||||
|
||||
@@ -19,6 +19,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
where TEtag : MediaServerItemEtag
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
|
||||
private readonly ILogger _logger;
|
||||
@@ -28,15 +29,20 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMetadataRepository metadataRepository,
|
||||
IMediator mediator,
|
||||
ILogger logger)
|
||||
{
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localSubtitlesProvider = localSubtitlesProvider;
|
||||
_localFileSystem = localFileSystem;
|
||||
_metadataRepository = metadataRepository;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected virtual bool ServerSupportsRemoteStreaming => false;
|
||||
protected virtual bool ServerReturnsStatisticsWithMetadata => false;
|
||||
|
||||
protected async Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
IMediaServerMovieRepository<TLibrary, TMovie, TEtag> movieRepository,
|
||||
@@ -121,18 +127,51 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Either<BaseError, MediaItemScanResult<TMovie>> maybeMovie;
|
||||
|
||||
Either<BaseError, MediaItemScanResult<TMovie>> maybeMovie = await movieRepository
|
||||
.GetOrAdd(library, incoming)
|
||||
.MapT(
|
||||
result =>
|
||||
{
|
||||
result.LocalPath = localPath;
|
||||
return result;
|
||||
})
|
||||
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan))
|
||||
.BindT(existing => UpdateStatistics(existing, incoming, ffmpegPath, ffprobePath))
|
||||
.BindT(UpdateSubtitles);
|
||||
if (ServerReturnsStatisticsWithMetadata)
|
||||
{
|
||||
maybeMovie = await movieRepository
|
||||
.GetOrAdd(library, incoming)
|
||||
.MapT(
|
||||
result =>
|
||||
{
|
||||
result.LocalPath = localPath;
|
||||
return result;
|
||||
})
|
||||
.BindT(
|
||||
existing => UpdateMetadataAndStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
existing,
|
||||
incoming,
|
||||
deepScan))
|
||||
.BindT(UpdateSubtitles);
|
||||
}
|
||||
else
|
||||
{
|
||||
maybeMovie = await movieRepository
|
||||
.GetOrAdd(library, incoming)
|
||||
.MapT(
|
||||
result =>
|
||||
{
|
||||
result.LocalPath = localPath;
|
||||
return result;
|
||||
})
|
||||
.BindT(
|
||||
existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan, None))
|
||||
.BindT(
|
||||
existing => UpdateStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
existing,
|
||||
incoming,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
None))
|
||||
.BindT(UpdateSubtitles);
|
||||
}
|
||||
|
||||
if (maybeMovie.IsLeft)
|
||||
{
|
||||
@@ -158,6 +197,14 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
else if (ServerSupportsRemoteStreaming)
|
||||
{
|
||||
Option<int> flagResult = await movieRepository.FlagRemoteOnly(library, result.Item);
|
||||
if (flagResult.IsSome)
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Option<int> flagResult = await movieRepository.FlagUnavailable(library, result.Item);
|
||||
@@ -218,6 +265,18 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
TMovie incoming,
|
||||
bool deepScan);
|
||||
|
||||
protected virtual Task<Option<MediaVersion>> GetMediaServerStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TMovie> result,
|
||||
TMovie incoming) => Task.FromResult(Option<MediaVersion>.None);
|
||||
|
||||
protected abstract Task<Option<Tuple<MovieMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TMovie> result,
|
||||
TMovie incoming);
|
||||
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TMovie>>> UpdateMetadata(
|
||||
MediaItemScanResult<TMovie> result,
|
||||
MovieMetadata fullMetadata);
|
||||
@@ -259,11 +318,23 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
// don't scan, but mark as unavailable
|
||||
if (!_localFileSystem.FileExists(localPath))
|
||||
{
|
||||
foreach (int id in await movieRepository.FlagUnavailable(library, incoming))
|
||||
if (ServerSupportsRemoteStreaming)
|
||||
{
|
||||
await _mediator.Publish(
|
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
|
||||
CancellationToken.None);
|
||||
foreach (int id in await movieRepository.FlagRemoteOnly(library, incoming))
|
||||
{
|
||||
await _mediator.Publish(
|
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (int id in await movieRepository.FlagUnavailable(library, incoming))
|
||||
{
|
||||
await _mediator.Publish(
|
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,19 +353,76 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TMovie>>> UpdateMetadata(
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TMovie>>> UpdateMetadataAndStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TMovie> result,
|
||||
TMovie incoming,
|
||||
bool deepScan)
|
||||
{
|
||||
foreach (MovieMetadata fullMetadata in await GetFullMetadata(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming,
|
||||
deepScan))
|
||||
Option<Tuple<MovieMetadata, MediaVersion>> maybeMetadataAndStatistics = await GetFullMetadataAndStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming);
|
||||
|
||||
foreach ((MovieMetadata fullMetadata, MediaVersion mediaVersion) in maybeMetadataAndStatistics)
|
||||
{
|
||||
Either<BaseError, MediaItemScanResult<TMovie>> metadataResult = await UpdateMetadata(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming,
|
||||
deepScan,
|
||||
fullMetadata);
|
||||
|
||||
foreach (BaseError error in metadataResult.LeftToSeq())
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
foreach (MediaItemScanResult<TMovie> r in metadataResult.RightToSeq())
|
||||
{
|
||||
result = r;
|
||||
}
|
||||
|
||||
Either<BaseError, MediaItemScanResult<TMovie>> statisticsResult = await UpdateStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
mediaVersion);
|
||||
|
||||
foreach (BaseError error in statisticsResult.LeftToSeq())
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
foreach (MediaItemScanResult<TMovie> r in metadataResult.RightToSeq())
|
||||
{
|
||||
result = r;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TMovie>>> UpdateMetadata(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TMovie> result,
|
||||
TMovie incoming,
|
||||
bool deepScan,
|
||||
Option<MovieMetadata> maybeFullMetadata)
|
||||
{
|
||||
if (maybeFullMetadata.IsNone)
|
||||
{
|
||||
maybeFullMetadata = await GetFullMetadata(connectionParameters, library, result, incoming, deepScan);
|
||||
}
|
||||
|
||||
foreach (MovieMetadata fullMetadata in maybeFullMetadata)
|
||||
{
|
||||
// TODO: move some of this code into this scanner
|
||||
// will have to merge JF, Emby, Plex logic
|
||||
@@ -305,17 +433,20 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TMovie>>> UpdateStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TMovie> result,
|
||||
TMovie incoming,
|
||||
string ffmpegPath,
|
||||
string ffprobePath)
|
||||
string ffprobePath,
|
||||
Option<MediaVersion> maybeMediaVersion)
|
||||
{
|
||||
TMovie existing = result.Item;
|
||||
|
||||
if (result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) ||
|
||||
existing.MediaVersions.Head().Streams.Count == 0)
|
||||
{
|
||||
if (_localFileSystem.FileExists(result.LocalPath))
|
||||
if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath))
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath);
|
||||
Either<BaseError, bool> refreshResult =
|
||||
@@ -339,6 +470,25 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (maybeMediaVersion.IsNone)
|
||||
{
|
||||
maybeMediaVersion = await GetMediaServerStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming);
|
||||
}
|
||||
|
||||
foreach (MediaVersion mediaVersion in maybeMediaVersion)
|
||||
{
|
||||
if (await _metadataRepository.UpdateStatistics(result.Item, mediaVersion))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -21,6 +21,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
where TEtag : MediaServerItemEtag
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
|
||||
private readonly ILogger _logger;
|
||||
@@ -30,16 +31,21 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMetadataRepository metadataRepository,
|
||||
IMediator mediator,
|
||||
ILogger logger)
|
||||
{
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localSubtitlesProvider = localSubtitlesProvider;
|
||||
_localFileSystem = localFileSystem;
|
||||
_metadataRepository = metadataRepository;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected virtual bool ServerSupportsRemoteStreaming => false;
|
||||
protected virtual bool ServerReturnsStatisticsWithMetadata => false;
|
||||
|
||||
protected async Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository,
|
||||
TConnectionParameters connectionParameters,
|
||||
@@ -268,6 +274,18 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
TEpisode incoming,
|
||||
bool deepScan);
|
||||
|
||||
protected virtual Task<Option<MediaVersion>> GetMediaServerStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TEpisode> result,
|
||||
TEpisode incoming) => Task.FromResult(Option<MediaVersion>.None);
|
||||
|
||||
protected abstract Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TEpisode> result,
|
||||
TEpisode incoming);
|
||||
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TShow>>> UpdateMetadata(
|
||||
MediaItemScanResult<TShow> result,
|
||||
ShowMetadata fullMetadata);
|
||||
@@ -279,7 +297,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateMetadata(
|
||||
MediaItemScanResult<TEpisode> result,
|
||||
EpisodeMetadata fullMetadata);
|
||||
|
||||
|
||||
private async Task<Either<BaseError, Unit>> ScanSeasons(
|
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository,
|
||||
TLibrary library,
|
||||
@@ -436,17 +454,50 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
|
||||
incoming.SeasonId = season.Id;
|
||||
|
||||
Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode = await televisionRepository
|
||||
.GetOrAdd(library, incoming, deepScan)
|
||||
.MapT(
|
||||
result =>
|
||||
{
|
||||
result.LocalPath = localPath;
|
||||
return result;
|
||||
})
|
||||
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan))
|
||||
.BindT(existing => UpdateStatistics(existing, incoming, ffmpegPath, ffprobePath))
|
||||
.BindT(UpdateSubtitles);
|
||||
Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode;
|
||||
|
||||
if (ServerReturnsStatisticsWithMetadata)
|
||||
{
|
||||
maybeEpisode = await televisionRepository
|
||||
.GetOrAdd(library, incoming, deepScan)
|
||||
.MapT(
|
||||
result =>
|
||||
{
|
||||
result.LocalPath = localPath;
|
||||
return result;
|
||||
})
|
||||
.BindT(
|
||||
existing => UpdateMetadataAndStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
existing,
|
||||
incoming,
|
||||
deepScan))
|
||||
.BindT(UpdateSubtitles);
|
||||
}
|
||||
else
|
||||
{
|
||||
maybeEpisode = await televisionRepository
|
||||
.GetOrAdd(library, incoming, deepScan)
|
||||
.MapT(
|
||||
result =>
|
||||
{
|
||||
result.LocalPath = localPath;
|
||||
return result;
|
||||
})
|
||||
.BindT(
|
||||
existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan, None))
|
||||
.BindT(
|
||||
existing => UpdateStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
existing,
|
||||
incoming,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
None))
|
||||
.BindT(UpdateSubtitles);
|
||||
}
|
||||
|
||||
if (maybeEpisode.IsLeft)
|
||||
{
|
||||
@@ -474,6 +525,14 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
else if (ServerSupportsRemoteStreaming)
|
||||
{
|
||||
Option<int> flagResult = await televisionRepository.FlagRemoteOnly(library, result.Item);
|
||||
if (flagResult.IsSome)
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Option<int> flagResult = await televisionRepository.FlagUnavailable(library, result.Item);
|
||||
@@ -542,11 +601,22 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
// don't scan, but mark as unavailable
|
||||
if (!_localFileSystem.FileExists(localPath))
|
||||
{
|
||||
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming))
|
||||
if (ServerSupportsRemoteStreaming) {
|
||||
foreach (int id in await televisionRepository.FlagRemoteOnly(library, incoming))
|
||||
{
|
||||
await _mediator.Publish(
|
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mediator.Publish(
|
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
|
||||
CancellationToken.None);
|
||||
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming))
|
||||
{
|
||||
await _mediator.Publish(
|
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,19 +687,76 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateMetadata(
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateMetadataAndStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TEpisode> result,
|
||||
TEpisode incoming,
|
||||
bool deepScan)
|
||||
{
|
||||
foreach (EpisodeMetadata fullMetadata in await GetFullMetadata(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming,
|
||||
deepScan))
|
||||
Option<Tuple<EpisodeMetadata, MediaVersion>> maybeMetadataAndStatistics = await GetFullMetadataAndStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming);
|
||||
|
||||
foreach ((EpisodeMetadata fullMetadata, MediaVersion mediaVersion) in maybeMetadataAndStatistics)
|
||||
{
|
||||
Either<BaseError, MediaItemScanResult<TEpisode>> metadataResult = await UpdateMetadata(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming,
|
||||
deepScan,
|
||||
fullMetadata);
|
||||
|
||||
foreach (BaseError error in metadataResult.LeftToSeq())
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
foreach (MediaItemScanResult<TEpisode> r in metadataResult.RightToSeq())
|
||||
{
|
||||
result = r;
|
||||
}
|
||||
|
||||
Either<BaseError, MediaItemScanResult<TEpisode>> statisticsResult = await UpdateStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
mediaVersion);
|
||||
|
||||
foreach (BaseError error in statisticsResult.LeftToSeq())
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
foreach (MediaItemScanResult<TEpisode> r in metadataResult.RightToSeq())
|
||||
{
|
||||
result = r;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateMetadata(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TEpisode> result,
|
||||
TEpisode incoming,
|
||||
bool deepScan,
|
||||
Option<EpisodeMetadata> maybeFullMetadata)
|
||||
{
|
||||
if (maybeFullMetadata.IsNone)
|
||||
{
|
||||
maybeFullMetadata = await GetFullMetadata(connectionParameters, library, result, incoming, deepScan);
|
||||
}
|
||||
|
||||
foreach (EpisodeMetadata fullMetadata in maybeFullMetadata)
|
||||
{
|
||||
// TODO: move some of this code into this scanner
|
||||
// will have to merge JF, Emby, Plex logic
|
||||
@@ -640,17 +767,20 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
MediaItemScanResult<TEpisode> result,
|
||||
TEpisode incoming,
|
||||
string ffmpegPath,
|
||||
string ffprobePath)
|
||||
string ffprobePath,
|
||||
Option<MediaVersion> maybeMediaVersion)
|
||||
{
|
||||
TEpisode existing = result.Item;
|
||||
|
||||
if (result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) ||
|
||||
existing.MediaVersions.Head().Streams.Count == 0)
|
||||
{
|
||||
if (_localFileSystem.FileExists(result.LocalPath))
|
||||
if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath))
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath);
|
||||
Either<BaseError, bool> refreshResult =
|
||||
@@ -674,6 +804,25 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (maybeMediaVersion.IsNone)
|
||||
{
|
||||
maybeMediaVersion = await GetMediaServerStatistics(
|
||||
connectionParameters,
|
||||
library,
|
||||
result,
|
||||
incoming);
|
||||
}
|
||||
|
||||
foreach (MediaVersion mediaVersion in maybeMediaVersion)
|
||||
{
|
||||
if (await _metadataRepository.UpdateStatistics(result.Item, mediaVersion))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class PlexMovieLibraryScanner :
|
||||
localStatisticsProvider,
|
||||
localSubtitlesProvider,
|
||||
localFileSystem,
|
||||
metadataRepository,
|
||||
mediator,
|
||||
logger)
|
||||
{
|
||||
@@ -52,6 +53,9 @@ public class PlexMovieLibraryScanner :
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override bool ServerSupportsRemoteStreaming => true;
|
||||
protected override bool ServerReturnsStatisticsWithMetadata => true;
|
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
@@ -129,6 +133,53 @@ public class PlexMovieLibraryScanner :
|
||||
return None;
|
||||
}
|
||||
|
||||
protected override async Task<Option<MediaVersion>> GetMediaServerStatistics(
|
||||
PlexConnectionParameters connectionParameters,
|
||||
PlexLibrary library,
|
||||
MediaItemScanResult<PlexMovie> result,
|
||||
PlexMovie incoming)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Plex Statistics", result.LocalPath);
|
||||
|
||||
Either<BaseError, MediaVersion> maybeVersion =
|
||||
await _plexServerApiClient.GetMovieMetadataAndStatistics(
|
||||
library,
|
||||
incoming.Key.Split("/").Last(),
|
||||
connectionParameters.Connection,
|
||||
connectionParameters.Token)
|
||||
.MapT(tuple => tuple.Item2); // drop the metadata part
|
||||
|
||||
foreach (BaseError error in maybeVersion.LeftToSeq())
|
||||
{
|
||||
_logger.LogWarning("Failed to get movie statistics from Plex: {Error}", error.ToString());
|
||||
}
|
||||
|
||||
return maybeVersion.ToOption();
|
||||
}
|
||||
|
||||
protected override async Task<Option<Tuple<MovieMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
PlexConnectionParameters connectionParameters,
|
||||
PlexLibrary library,
|
||||
MediaItemScanResult<PlexMovie> result,
|
||||
PlexMovie incoming)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Plex Metadata and Statistics", result.LocalPath);
|
||||
|
||||
Either<BaseError, Tuple<MovieMetadata, MediaVersion>> maybeResult =
|
||||
await _plexServerApiClient.GetMovieMetadataAndStatistics(
|
||||
library,
|
||||
incoming.Key.Split("/").Last(),
|
||||
connectionParameters.Connection,
|
||||
connectionParameters.Token);
|
||||
|
||||
foreach (BaseError error in maybeResult.LeftToSeq())
|
||||
{
|
||||
_logger.LogWarning("Failed to get movie metadata and statistics from Plex: {Error}", error.ToString());
|
||||
}
|
||||
|
||||
return maybeResult.ToOption();
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateMetadata(
|
||||
MediaItemScanResult<PlexMovie> result,
|
||||
MovieMetadata fullMetadata)
|
||||
@@ -136,11 +187,6 @@ public class PlexMovieLibraryScanner :
|
||||
PlexMovie existing = result.Item;
|
||||
MovieMetadata existingMetadata = existing.MovieMetadata.Head();
|
||||
|
||||
_logger.LogDebug(
|
||||
"Refreshing {Attribute} for {Title}",
|
||||
"Plex Metadata",
|
||||
existingMetadata.Title);
|
||||
|
||||
if (existingMetadata.MetadataKind != MetadataKind.External)
|
||||
{
|
||||
existingMetadata.MetadataKind = MetadataKind.External;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class PlexTelevisionLibraryScanner :
|
||||
localStatisticsProvider,
|
||||
localSubtitlesProvider,
|
||||
localFileSystem,
|
||||
metadataRepository,
|
||||
mediator,
|
||||
logger)
|
||||
{
|
||||
@@ -52,6 +53,9 @@ public class PlexTelevisionLibraryScanner :
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override bool ServerSupportsRemoteStreaming => true;
|
||||
protected override bool ServerReturnsStatisticsWithMetadata => true;
|
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
@@ -259,6 +263,53 @@ public class PlexTelevisionLibraryScanner :
|
||||
return None;
|
||||
}
|
||||
|
||||
protected override async Task<Option<MediaVersion>> GetMediaServerStatistics(
|
||||
PlexConnectionParameters connectionParameters,
|
||||
PlexLibrary library,
|
||||
MediaItemScanResult<PlexEpisode> result,
|
||||
PlexEpisode incoming)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Plex Statistics", result.LocalPath);
|
||||
|
||||
Either<BaseError, MediaVersion> maybeVersion =
|
||||
await _plexServerApiClient.GetEpisodeMetadataAndStatistics(
|
||||
library,
|
||||
incoming.Key.Split("/").Last(),
|
||||
connectionParameters.Connection,
|
||||
connectionParameters.Token)
|
||||
.MapT(tuple => tuple.Item2); // drop the metadata part
|
||||
|
||||
foreach (BaseError error in maybeVersion.LeftToSeq())
|
||||
{
|
||||
_logger.LogWarning("Failed to get episode statistics from Plex: {Error}", error.ToString());
|
||||
}
|
||||
|
||||
return maybeVersion.ToOption();
|
||||
}
|
||||
|
||||
protected override async Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
|
||||
PlexConnectionParameters connectionParameters,
|
||||
PlexLibrary library,
|
||||
MediaItemScanResult<PlexEpisode> result,
|
||||
PlexEpisode incoming)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Plex Metadata and Statistics", result.LocalPath);
|
||||
|
||||
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeResult =
|
||||
await _plexServerApiClient.GetEpisodeMetadataAndStatistics(
|
||||
library,
|
||||
incoming.Key.Split("/").Last(),
|
||||
connectionParameters.Connection,
|
||||
connectionParameters.Token);
|
||||
|
||||
foreach (BaseError error in maybeResult.LeftToSeq())
|
||||
{
|
||||
_logger.LogWarning("Failed to get episode metadata and statistics from Plex: {Error}", error.ToString());
|
||||
}
|
||||
|
||||
return maybeResult.ToOption();
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, MediaItemScanResult<PlexShow>>> UpdateMetadata(
|
||||
MediaItemScanResult<PlexShow> result,
|
||||
ShowMetadata fullMetadata)
|
||||
|
||||
+4
-4
@@ -33,8 +33,8 @@ Global
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
|
||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
|
||||
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -79,10 +79,10 @@ Global
|
||||
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
|
||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
|
||||
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Application.Plex;
|
||||
using ErsatzTV.Application.Streaming;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Extensions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Flurl;
|
||||
|
||||
namespace ErsatzTV.Controllers;
|
||||
|
||||
@@ -14,10 +17,15 @@ public class InternalController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<InternalController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public InternalController(IMediator mediator, ILogger<InternalController> logger)
|
||||
public InternalController(
|
||||
IMediator mediator,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<InternalController> logger)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -79,4 +87,22 @@ public class InternalController : ControllerBase
|
||||
return BadRequest(error.Value);
|
||||
}
|
||||
));
|
||||
|
||||
[HttpGet("/media/plex/{plexMediaSourceId:int}/{*path}")]
|
||||
public async Task<IActionResult> GetPlexFanArt(
|
||||
int plexMediaSourceId,
|
||||
string path,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Either<BaseError, PlexConnectionParametersViewModel> connectionParameters =
|
||||
await _mediator.Send(new GetPlexConnectionParameters(plexMediaSourceId), cancellationToken);
|
||||
|
||||
return connectionParameters.Match<IActionResult>(
|
||||
Left: _ => new NotFoundResult(),
|
||||
Right: r =>
|
||||
{
|
||||
Url fullPath = new Uri(r.Uri, path).SetQueryParam("X-Plex-Token", r.AuthToken);
|
||||
return new RedirectResult(fullPath.ToString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user