mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* Allow Other Videos Library Type on Plex to be sync * Migrating database: Creating PlexOtherVideo table * Using Plex Media path to create tags for OtherVideos * missed these in the merge * Getting PlexLibrary for Tag set on OtherVideo * fix migrations * set tag metadata on plex other videos * update changelog --------- Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>pull/1771/head
27 changed files with 13147 additions and 23 deletions
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
public class PlexOtherVideo : OtherVideo |
||||
{ |
||||
public string Key { get; set; } |
||||
public string Etag { get; set; } |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Plex; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Plex; |
||||
|
||||
public interface IPlexOtherVideoLibraryScanner |
||||
{ |
||||
Task<Either<BaseError, Unit>> ScanLibrary( |
||||
PlexConnection connection, |
||||
PlexServerAuthToken token, |
||||
PlexLibrary library, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken); |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Metadata; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
public interface IMediaServerOtherVideoRepository<in TLibrary, TOtherVideo, TEtag> where TLibrary : Library |
||||
where TOtherVideo : OtherVideo |
||||
where TEtag : MediaServerItemEtag |
||||
{ |
||||
Task<List<TEtag>> GetExistingOtherVideos(TLibrary library); |
||||
Task<Option<int>> FlagNormal(TLibrary library, TOtherVideo otherVideo); |
||||
Task<Option<int>> FlagUnavailable(TLibrary library, TOtherVideo otherVideo); |
||||
Task<Option<int>> FlagRemoteOnly(TLibrary library, TOtherVideo otherVideo); |
||||
Task<List<int>> FlagFileNotFound(TLibrary library, List<string> movieItemIds); |
||||
Task<Either<BaseError, MediaItemScanResult<TOtherVideo>>> GetOrAdd(TLibrary library, TOtherVideo item, bool deepScan); |
||||
Task<Unit> SetEtag(TOtherVideo otherVideo, string etag); |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Plex; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
public interface IPlexOtherVideoRepository : IMediaServerOtherVideoRepository<PlexLibrary, PlexOtherVideo, PlexItemEtag> |
||||
{ |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlexOtherVideo : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "PlexOtherVideo", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false), |
||||
Key = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
Etag = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4") |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_PlexOtherVideo", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_PlexOtherVideo_OtherVideo_Id", |
||||
column: x => x.Id, |
||||
principalTable: "OtherVideo", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "PlexOtherVideo"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlexOtherVideo : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "PlexOtherVideo", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
Key = table.Column<string>(type: "TEXT", nullable: true), |
||||
Etag = table.Column<string>(type: "TEXT", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_PlexOtherVideo", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_PlexOtherVideo_OtherVideo_Id", |
||||
column: x => x.Id, |
||||
principalTable: "OtherVideo", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "PlexOtherVideo"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations; |
||||
|
||||
public class PlexOtherVideoConfiguration : IEntityTypeConfiguration<PlexOtherVideo> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<PlexOtherVideo> builder) => builder.ToTable("PlexOtherVideo"); |
||||
} |
@ -0,0 +1,271 @@
@@ -0,0 +1,271 @@
|
||||
using Dapper; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Errors; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Metadata; |
||||
using ErsatzTV.Core.Plex; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Repositories; |
||||
|
||||
public class PlexOtherVideoRepository : IPlexOtherVideoRepository |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly ILogger<PlexOtherVideoRepository> _logger; |
||||
|
||||
public PlexOtherVideoRepository(IDbContextFactory<TvContext> dbContextFactory, ILogger<PlexOtherVideoRepository> logger) |
||||
{ |
||||
_dbContextFactory = dbContextFactory; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<List<PlexItemEtag>> GetExistingOtherVideos(PlexLibrary library) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.Connection.QueryAsync<PlexItemEtag>( |
||||
@"SELECT `Key`, Etag, MI.State FROM PlexOtherVideo
|
||||
INNER JOIN OtherVideo O on PlexOtherVideo.Id = O.Id |
||||
INNER JOIN MediaItem MI on O.Id = MI.Id |
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id |
||||
WHERE LP.LibraryId = @LibraryId",
|
||||
new { LibraryId = library.Id }) |
||||
.Map(result => result.ToList()); |
||||
} |
||||
|
||||
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexOtherVideo otherVideo) |
||||
{ |
||||
if (otherVideo.State is MediaItemState.Normal) |
||||
{ |
||||
return Option<int>.None; |
||||
} |
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
otherVideo.State = MediaItemState.Normal; |
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT PlexOtherVideo.Id FROM PlexOtherVideo
|
||||
INNER JOIN MediaItem MI ON MI.Id = PlexOtherVideo.Id |
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId |
||||
WHERE PlexOtherVideo.Key = @Key",
|
||||
new { LibraryId = library.Id, otherVideo.Key }); |
||||
|
||||
foreach (int id in maybeId) |
||||
{ |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0", |
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None); |
||||
} |
||||
|
||||
return None; |
||||
} |
||||
|
||||
public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexOtherVideo otherVideo) |
||||
{ |
||||
if (otherVideo.State is MediaItemState.Unavailable) |
||||
{ |
||||
return Option<int>.None; |
||||
} |
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
otherVideo.State = MediaItemState.Unavailable; |
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT PlexOtherVideo.Id FROM PlexOtherVideo
|
||||
INNER JOIN MediaItem MI ON MI.Id = PlexOtherVideo.Id |
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId |
||||
WHERE PlexOtherVideo.Key = @Key",
|
||||
new { LibraryId = library.Id, otherVideo.Key }); |
||||
|
||||
foreach (int id in maybeId) |
||||
{ |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2", |
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None); |
||||
} |
||||
|
||||
return None; |
||||
} |
||||
|
||||
public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexOtherVideo otherVideo) |
||||
{ |
||||
if (otherVideo.State is MediaItemState.RemoteOnly) |
||||
{ |
||||
return Option<int>.None; |
||||
} |
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
otherVideo.State = MediaItemState.RemoteOnly; |
||||
|
||||
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT PlexOtherVideo.Id FROM PlexOtherVideo
|
||||
INNER JOIN MediaItem MI ON MI.Id = PlexOtherVideo.Id |
||||
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId |
||||
WHERE PlexOtherVideo.Key = @Key",
|
||||
new { LibraryId = library.Id, otherVideo.Key }); |
||||
|
||||
foreach (int id in maybeId) |
||||
{ |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3", |
||||
new { Id = id }).Map(count => count > 0 ? Some(id) : None); |
||||
} |
||||
|
||||
return None; |
||||
} |
||||
|
||||
public async Task<List<int>> FlagFileNotFound(PlexLibrary library, List<string> movieItemIds) |
||||
{ |
||||
if (movieItemIds.Count == 0) |
||||
{ |
||||
return []; |
||||
} |
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
List<int> ids = await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT M.Id
|
||||
FROM MediaItem M |
||||
INNER JOIN PlexOtherVideo ON PlexOtherVideo.Id = M.Id |
||||
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId |
||||
WHERE PlexOtherVideo.Key IN @OtherVideoKeys",
|
||||
new { LibraryId = library.Id, OtherVideoKeys = movieItemIds }) |
||||
.Map(result => result.ToList()); |
||||
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1", |
||||
new { Ids = ids }); |
||||
|
||||
return ids; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, MediaItemScanResult<PlexOtherVideo>>> GetOrAdd( |
||||
PlexLibrary library, |
||||
PlexOtherVideo item, |
||||
bool deepScan) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
Option<PlexOtherVideo> maybeExisting = await dbContext.PlexOtherVideos |
||||
.AsNoTracking() |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Genres) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Tags) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Studios) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Actors) |
||||
.ThenInclude(a => a.Artwork) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Artwork) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Directors) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Writers) |
||||
.Include(i => i.OtherVideoMetadata) |
||||
.ThenInclude(mm => mm.Guids) |
||||
.Include(i => i.MediaVersions) |
||||
.ThenInclude(mv => mv.MediaFiles) |
||||
.Include(i => i.MediaVersions) |
||||
.ThenInclude(mv => mv.Streams) |
||||
.Include(i => i.LibraryPath) |
||||
.ThenInclude(lp => lp.Library) |
||||
.Include(i => i.TraktListItems) |
||||
.ThenInclude(tli => tli.TraktList) |
||||
.SelectOneAsync(i => i.Key, i => i.Key == item.Key); |
||||
|
||||
foreach (PlexOtherVideo plexOtherVideo in maybeExisting) |
||||
{ |
||||
var result = new MediaItemScanResult<PlexOtherVideo>(plexOtherVideo) { IsAdded = false }; |
||||
if (plexOtherVideo.Etag != item.Etag || deepScan) |
||||
{ |
||||
await UpdateOtherVideoPath(dbContext, plexOtherVideo, item); |
||||
result.IsUpdated = true; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
return await AddOtherVideo(dbContext, library, item); |
||||
} |
||||
|
||||
public async Task<Unit> SetEtag(PlexOtherVideo otherVideo, string etag) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
"UPDATE PlexOtherVideo SET Etag = @Etag WHERE Id = @Id", |
||||
new { Etag = etag, otherVideo.Id }).Map(_ => Unit.Default); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<PlexOtherVideo>>> AddOtherVideo( |
||||
TvContext dbContext, |
||||
PlexLibrary library, |
||||
PlexOtherVideo item) |
||||
{ |
||||
try |
||||
{ |
||||
if (await MediaItemRepository.MediaFileAlreadyExists(item, library.Paths.Head().Id, dbContext, _logger)) |
||||
{ |
||||
return new MediaFileAlreadyExists(); |
||||
} |
||||
|
||||
// blank out etag for initial save in case stats/metadata/etc updates fail
|
||||
string etag = item.Etag; |
||||
item.Etag = string.Empty; |
||||
|
||||
item.LibraryPathId = library.Paths.Head().Id; |
||||
|
||||
await dbContext.PlexOtherVideos.AddAsync(item); |
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
// restore etag
|
||||
item.Etag = etag; |
||||
|
||||
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); |
||||
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(); |
||||
return new MediaItemScanResult<PlexOtherVideo>(item) { IsAdded = true }; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
|
||||
private static async Task UpdateOtherVideoPath(TvContext dbContext, PlexOtherVideo existing, PlexOtherVideo incoming) |
||||
{ |
||||
// library path is used for search indexing later
|
||||
incoming.LibraryPath = existing.LibraryPath; |
||||
incoming.Id = existing.Id; |
||||
|
||||
// version
|
||||
MediaVersion version = existing.MediaVersions.Head(); |
||||
MediaVersion incomingVersion = incoming.MediaVersions.Head(); |
||||
version.Name = incomingVersion.Name; |
||||
version.DateAdded = incomingVersion.DateAdded; |
||||
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"UPDATE MediaVersion SET Name = @Name, DateAdded = @DateAdded WHERE Id = @Id", |
||||
new { version.Name, version.DateAdded, version.Id }); |
||||
|
||||
// media file
|
||||
if (version.MediaFiles.Head() is PlexMediaFile file && |
||||
incomingVersion.MediaFiles.Head() is PlexMediaFile incomingFile) |
||||
{ |
||||
file.Path = incomingFile.Path; |
||||
file.Key = incomingFile.Key; |
||||
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"UPDATE MediaFile SET Path = @Path WHERE Id = @Id", |
||||
new { file.Path, file.Id }); |
||||
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"UPDATE PlexMediaFile SET `Key` = @Key WHERE Id = @Id", |
||||
new { file.Key, file.Id }); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models; |
||||
public class PlexLocationResponse |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Path { get; set; } |
||||
} |
@ -0,0 +1,471 @@
@@ -0,0 +1,471 @@
|
||||
using System.Collections.Immutable; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.MediaServer; |
||||
using ErsatzTV.Core.Errors; |
||||
using ErsatzTV.Core.Extensions; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.MediaSources; |
||||
using ErsatzTV.Core.Metadata; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Metadata; |
||||
|
||||
public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, TLibrary, TOtherVideo, TEtag> |
||||
where TConnectionParameters : MediaServerConnectionParameters |
||||
where TLibrary : Library |
||||
where TOtherVideo : OtherVideo |
||||
where TEtag : MediaServerItemEtag |
||||
{ |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
private readonly ILogger _logger; |
||||
private readonly IMediator _mediator; |
||||
private readonly IMetadataRepository _metadataRepository; |
||||
|
||||
protected MediaServerOtherVideoLibraryScanner( |
||||
ILocalFileSystem localFileSystem, |
||||
IMetadataRepository metadataRepository, |
||||
IMediator mediator, |
||||
ILogger logger) |
||||
{ |
||||
_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( |
||||
IMediaServerOtherVideoRepository<TLibrary, TOtherVideo, TEtag> otherVideoRepository, |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
Func<TOtherVideo, string> getLocalPath, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
try |
||||
{ |
||||
return await ScanLibrary( |
||||
otherVideoRepository, |
||||
connectionParameters, |
||||
library, |
||||
getLocalPath, |
||||
GetOtherVideoLibraryItems(connectionParameters, library), |
||||
deepScan, |
||||
cancellationToken); |
||||
} |
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
} |
||||
|
||||
private async Task<Either<BaseError, Unit>> ScanLibrary( |
||||
IMediaServerOtherVideoRepository<TLibrary, TOtherVideo, TEtag> otherVideoRepository, |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
Func<TOtherVideo, string> getLocalPath, |
||||
IAsyncEnumerable<Tuple<TOtherVideo, int>> otherVideoEntries, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var incomingItemIds = new List<string>(); |
||||
IReadOnlyDictionary<string, TEtag> existingOtherVideos = (await otherVideoRepository.GetExistingOtherVideos(library)) |
||||
.ToImmutableDictionary(e => e.MediaServerItemId, e => e); |
||||
|
||||
await foreach ((TOtherVideo incoming, int totalOtherVideoCount) in otherVideoEntries.WithCancellation(cancellationToken)) |
||||
{ |
||||
if (cancellationToken.IsCancellationRequested) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
|
||||
incomingItemIds.Add(MediaServerItemId(incoming)); |
||||
|
||||
decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalOtherVideoCount, 0, 1); |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
library.Id, |
||||
library.Name, |
||||
percentCompletion, |
||||
Array.Empty<int>(), |
||||
Array.Empty<int>()), |
||||
cancellationToken); |
||||
|
||||
string localPath = getLocalPath(incoming); |
||||
|
||||
if (await ShouldScanItem(otherVideoRepository, library, existingOtherVideos, incoming, localPath, deepScan) == false) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
Either<BaseError, MediaItemScanResult<TOtherVideo>> maybeOtherVideo; |
||||
|
||||
if (ServerReturnsStatisticsWithMetadata) |
||||
{ |
||||
maybeOtherVideo = await otherVideoRepository |
||||
.GetOrAdd(library, incoming, deepScan) |
||||
.MapT( |
||||
result => |
||||
{ |
||||
result.LocalPath = localPath; |
||||
return result; |
||||
}) |
||||
.BindT( |
||||
existing => UpdateMetadataAndStatistics( |
||||
connectionParameters, |
||||
library, |
||||
existing, |
||||
incoming, |
||||
deepScan)); |
||||
} |
||||
else |
||||
{ |
||||
maybeOtherVideo = await otherVideoRepository |
||||
.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, |
||||
deepScan, |
||||
None)) |
||||
.BindT(UpdateSubtitles); |
||||
} |
||||
|
||||
if (maybeOtherVideo.IsLeft) |
||||
{ |
||||
foreach (BaseError error in maybeOtherVideo.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Error processing other video {Title}: {Error}", |
||||
incoming.OtherVideoMetadata.Head().Title, |
||||
error.Value); |
||||
} |
||||
|
||||
continue; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<TOtherVideo> result in maybeOtherVideo.RightToSeq()) |
||||
{ |
||||
await otherVideoRepository.SetEtag(result.Item, MediaServerEtag(incoming)); |
||||
|
||||
if (_localFileSystem.FileExists(result.LocalPath)) |
||||
{ |
||||
Option<int> flagResult = await otherVideoRepository.FlagNormal(library, result.Item); |
||||
if (flagResult.IsSome) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
else if (ServerSupportsRemoteStreaming) |
||||
{ |
||||
Option<int> flagResult = await otherVideoRepository.FlagRemoteOnly(library, result.Item); |
||||
if (flagResult.IsSome) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
Option<int> flagResult = await otherVideoRepository.FlagUnavailable(library, result.Item); |
||||
if (flagResult.IsSome) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
if (result.IsAdded || result.IsUpdated) |
||||
{ |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
library.Id, |
||||
null, |
||||
null, |
||||
new[] { result.Item.Id }, |
||||
Array.Empty<int>()), |
||||
cancellationToken); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// trash OtherVideo that are no longer present on the media server
|
||||
var fileNotFoundItemIds = existingOtherVideos.Keys.Except(incomingItemIds).ToList(); |
||||
List<int> ids = await otherVideoRepository.FlagFileNotFound(library, fileNotFoundItemIds); |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()), |
||||
cancellationToken); |
||||
|
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
library.Id, |
||||
library.Name, |
||||
0, |
||||
Array.Empty<int>(), |
||||
Array.Empty<int>()), |
||||
cancellationToken); |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
protected abstract string MediaServerItemId(TOtherVideo otherVideo); |
||||
protected abstract string MediaServerEtag(TOtherVideo otherVideo); |
||||
|
||||
protected abstract IAsyncEnumerable<Tuple<TOtherVideo, int>> GetOtherVideoLibraryItems( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library); |
||||
|
||||
protected abstract Task<Option<OtherVideoMetadata>> GetFullMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
TOtherVideo incoming, |
||||
bool deepScan); |
||||
|
||||
protected virtual Task<Option<MediaVersion>> GetMediaServerStatistics( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
TOtherVideo incoming) => Task.FromResult(Option<MediaVersion>.None); |
||||
|
||||
protected abstract Task<Option<Tuple<OtherVideoMetadata, MediaVersion>>> GetFullMetadataAndStatistics( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
TOtherVideo incoming); |
||||
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TOtherVideo>>> UpdateMetadata( |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
OtherVideoMetadata fullMetadata); |
||||
|
||||
private async Task<bool> ShouldScanItem( |
||||
IMediaServerOtherVideoRepository<TLibrary, TOtherVideo, TEtag> otherVideoRepository, |
||||
TLibrary library, |
||||
IReadOnlyDictionary<string, TEtag> existingOtherVideos, |
||||
TOtherVideo incoming, |
||||
string localPath, |
||||
bool deepScan) |
||||
{ |
||||
// deep scan will always pull every OtherVideo
|
||||
if (deepScan) |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
string existingEtag = string.Empty; |
||||
MediaItemState existingState = MediaItemState.Normal; |
||||
if (existingOtherVideos.TryGetValue(MediaServerItemId(incoming), out TEtag? existingEntry)) |
||||
{ |
||||
existingEtag = existingEntry.Etag; |
||||
existingState = existingEntry.State; |
||||
} |
||||
|
||||
if (existingState is MediaItemState.Unavailable or MediaItemState.FileNotFound && |
||||
existingEtag == MediaServerEtag(incoming)) |
||||
{ |
||||
// skip scanning unavailable/file not found items that are unchanged and still don't exist locally
|
||||
if (!_localFileSystem.FileExists(localPath) && !ServerSupportsRemoteStreaming) |
||||
{ |
||||
return false; |
||||
} |
||||
} |
||||
else if (existingEtag == MediaServerEtag(incoming)) |
||||
{ |
||||
// item is unchanged, but file does not exist
|
||||
// don't scan, but mark as unavailable
|
||||
if (!_localFileSystem.FileExists(localPath)) |
||||
{ |
||||
if (ServerSupportsRemoteStreaming) |
||||
{ |
||||
if (existingState is not MediaItemState.RemoteOnly) |
||||
{ |
||||
foreach (int id in await otherVideoRepository.FlagRemoteOnly(library, incoming)) |
||||
{ |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()), |
||||
CancellationToken.None); |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
if (existingState is not MediaItemState.Unavailable) |
||||
{ |
||||
foreach (int id in await otherVideoRepository.FlagUnavailable(library, incoming)) |
||||
{ |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()), |
||||
CancellationToken.None); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
if (existingEntry is null) |
||||
{ |
||||
_logger.LogDebug("INSERT: new other video {OtherVideo}", incoming.OtherVideoMetadata.Head().Title); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug("UPDATE: Etag has changed for other video {OtherVideo}", incoming.OtherVideoMetadata.Head().Title); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TOtherVideo>>> UpdateMetadataAndStatistics( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
TOtherVideo incoming, |
||||
bool deepScan) |
||||
{ |
||||
Option<Tuple<OtherVideoMetadata, MediaVersion>> maybeMetadataAndStatistics = await GetFullMetadataAndStatistics( |
||||
connectionParameters, |
||||
library, |
||||
result, |
||||
incoming); |
||||
|
||||
foreach ((OtherVideoMetadata fullMetadata, MediaVersion mediaVersion) in maybeMetadataAndStatistics) |
||||
{ |
||||
Either<BaseError, MediaItemScanResult<TOtherVideo>> metadataResult = await UpdateMetadata( |
||||
connectionParameters, |
||||
library, |
||||
result, |
||||
incoming, |
||||
deepScan, |
||||
fullMetadata); |
||||
|
||||
foreach (BaseError error in metadataResult.LeftToSeq()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<TOtherVideo> r in metadataResult.RightToSeq()) |
||||
{ |
||||
result = r; |
||||
} |
||||
|
||||
Either<BaseError, MediaItemScanResult<TOtherVideo>> statisticsResult = await UpdateStatistics( |
||||
connectionParameters, |
||||
library, |
||||
result, |
||||
incoming, |
||||
deepScan, |
||||
mediaVersion); |
||||
|
||||
foreach (BaseError error in statisticsResult.LeftToSeq()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<TOtherVideo> r in metadataResult.RightToSeq()) |
||||
{ |
||||
result = r; |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TOtherVideo>>> UpdateMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
TOtherVideo incoming, |
||||
bool deepScan, |
||||
Option<OtherVideoMetadata> maybeFullMetadata) |
||||
{ |
||||
if (maybeFullMetadata.IsNone) |
||||
{ |
||||
maybeFullMetadata = await GetFullMetadata(connectionParameters, library, result, incoming, deepScan); |
||||
} |
||||
|
||||
foreach (OtherVideoMetadata fullMetadata in maybeFullMetadata) |
||||
{ |
||||
// TODO: move some of this code into this scanner
|
||||
// will have to merge JF, Emby, Plex logic
|
||||
return await UpdateMetadata(result, fullMetadata); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TOtherVideo>>> UpdateStatistics( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TOtherVideo> result, |
||||
TOtherVideo incoming, |
||||
bool deepScan, |
||||
Option<MediaVersion> maybeMediaVersion) |
||||
{ |
||||
TOtherVideo existing = result.Item; |
||||
|
||||
if (deepScan || result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) || |
||||
existing.MediaVersions.Head().Streams.Count == 0) |
||||
{ |
||||
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; |
||||
} |
||||
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TOtherVideo>>> UpdateSubtitles( |
||||
MediaItemScanResult<TOtherVideo> existing) |
||||
{ |
||||
try |
||||
{ |
||||
MediaVersion version = existing.Item.GetHeadVersion(); |
||||
Option<OtherVideoMetadata> maybeMetadata = existing.Item.OtherVideoMetadata.HeadOrNone(); |
||||
foreach (OtherVideoMetadata metadata in maybeMetadata) |
||||
{ |
||||
List<Subtitle> subtitles = version.Streams |
||||
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
||||
.Map(Subtitle.FromMediaStream) |
||||
.ToList(); |
||||
|
||||
if (await _metadataRepository.UpdateSubtitles(metadata, subtitles)) |
||||
{ |
||||
return existing; |
||||
} |
||||
} |
||||
|
||||
return BaseError.New("Failed to update media server subtitles"); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,413 @@
@@ -0,0 +1,413 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Extensions; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Plex; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Metadata; |
||||
using ErsatzTV.Core.Plex; |
||||
using ErsatzTV.Infrastructure.Data.Repositories; |
||||
using ErsatzTV.Scanner.Core.Metadata; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Plex; |
||||
|
||||
public class PlexOtherVideoLibraryScanner : |
||||
MediaServerOtherVideoLibraryScanner<PlexConnectionParameters, PlexLibrary, PlexOtherVideo, PlexItemEtag>, |
||||
IPlexOtherVideoLibraryScanner |
||||
{ |
||||
private readonly ILogger<PlexOtherVideoLibraryScanner> _logger; |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
private readonly IMetadataRepository _metadataRepository; |
||||
private readonly IOtherVideoRepository _otherVideoRepository; |
||||
private readonly IPlexOtherVideoRepository _plexOtherVideoRepository; |
||||
private readonly IPlexPathReplacementService _plexPathReplacementService; |
||||
private readonly IPlexServerApiClient _plexServerApiClient; |
||||
|
||||
public PlexOtherVideoLibraryScanner( |
||||
IPlexServerApiClient plexServerApiClient, |
||||
IOtherVideoRepository otherVideoRepository, |
||||
IMetadataRepository metadataRepository, |
||||
IMediator mediator, |
||||
IMediaSourceRepository mediaSourceRepository, |
||||
IPlexOtherVideoRepository plexOtherVideoRepository, |
||||
IPlexPathReplacementService plexPathReplacementService, |
||||
ILocalFileSystem localFileSystem, |
||||
ILogger<PlexOtherVideoLibraryScanner> logger) |
||||
: base( |
||||
localFileSystem, |
||||
metadataRepository, |
||||
mediator, |
||||
logger) |
||||
{ |
||||
_plexServerApiClient = plexServerApiClient; |
||||
_otherVideoRepository = otherVideoRepository; |
||||
_metadataRepository = metadataRepository; |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
_plexOtherVideoRepository = plexOtherVideoRepository; |
||||
_plexPathReplacementService = plexPathReplacementService; |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected override bool ServerSupportsRemoteStreaming => true; |
||||
protected override bool ServerReturnsStatisticsWithMetadata => true; |
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanLibrary( |
||||
PlexConnection connection, |
||||
PlexServerAuthToken token, |
||||
PlexLibrary library, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
List<PlexPathReplacement> pathReplacements = |
||||
await _mediaSourceRepository.GetPlexPathReplacements(library.MediaSourceId); |
||||
|
||||
string GetLocalPath(PlexOtherVideo otherVideo) |
||||
{ |
||||
return _plexPathReplacementService.GetReplacementPlexPath( |
||||
pathReplacements, |
||||
otherVideo.GetHeadVersion().MediaFiles.Head().Path, |
||||
false); |
||||
} |
||||
|
||||
return await ScanLibrary( |
||||
_plexOtherVideoRepository, |
||||
new PlexConnectionParameters(connection, token), |
||||
library, |
||||
GetLocalPath, |
||||
deepScan, |
||||
cancellationToken); |
||||
} |
||||
|
||||
protected override string MediaServerItemId(PlexOtherVideo otherVideo) => otherVideo.Key; |
||||
|
||||
protected override string MediaServerEtag(PlexOtherVideo otherVideo) => otherVideo.Etag; |
||||
|
||||
protected override IAsyncEnumerable<Tuple<PlexOtherVideo, int>> GetOtherVideoLibraryItems( |
||||
PlexConnectionParameters connectionParameters, |
||||
PlexLibrary library) => |
||||
_plexServerApiClient.GetOtherVideoLibraryContents( |
||||
library, |
||||
connectionParameters.Connection, |
||||
connectionParameters.Token); |
||||
|
||||
// this shouldn't be called anymore
|
||||
protected override Task<Option<OtherVideoMetadata>> GetFullMetadata( |
||||
PlexConnectionParameters connectionParameters, |
||||
PlexLibrary library, |
||||
MediaItemScanResult<PlexOtherVideo> result, |
||||
PlexOtherVideo incoming, |
||||
bool deepScan) |
||||
{ |
||||
if (result.IsAdded || result.Item.Etag != incoming.Etag || deepScan) |
||||
{ |
||||
throw new NotSupportedException("This shouldn't happen anymore"); |
||||
} |
||||
|
||||
return Task.FromResult<Option<OtherVideoMetadata>>(None); |
||||
} |
||||
|
||||
// this shouldn't be called anymore
|
||||
protected override async Task<Option<MediaVersion>> GetMediaServerStatistics( |
||||
PlexConnectionParameters connectionParameters, |
||||
PlexLibrary library, |
||||
MediaItemScanResult<PlexOtherVideo> result, |
||||
PlexOtherVideo incoming) |
||||
{ |
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Plex Statistics", result.LocalPath); |
||||
|
||||
Either<BaseError, MediaVersion> maybeVersion = |
||||
await _plexServerApiClient.GetOtherVideoMetadataAndStatistics( |
||||
library.MediaSourceId, |
||||
incoming.Key.Split("/").Last(), |
||||
connectionParameters.Connection, |
||||
connectionParameters.Token, |
||||
library) |
||||
.MapT(tuple => tuple.Item2); // drop the metadata part
|
||||
|
||||
foreach (BaseError error in maybeVersion.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Failed to get otherVideo statistics from Plex: {Error}", error.ToString()); |
||||
} |
||||
|
||||
return maybeVersion.ToOption(); |
||||
} |
||||
|
||||
protected override async Task<Option<Tuple<OtherVideoMetadata, MediaVersion>>> GetFullMetadataAndStatistics( |
||||
PlexConnectionParameters connectionParameters, |
||||
PlexLibrary library, |
||||
MediaItemScanResult<PlexOtherVideo> result, |
||||
PlexOtherVideo incoming) |
||||
{ |
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Plex Metadata and Statistics", result.LocalPath); |
||||
|
||||
Either<BaseError, Tuple<OtherVideoMetadata, MediaVersion>> maybeResult = |
||||
await _plexServerApiClient.GetOtherVideoMetadataAndStatistics( |
||||
library.MediaSourceId, |
||||
incoming.Key.Split("/").Last(), |
||||
connectionParameters.Connection, |
||||
connectionParameters.Token, |
||||
library); |
||||
|
||||
foreach (BaseError error in maybeResult.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Failed to get OtherVideo metadata and statistics from Plex: {Error}", error.ToString()); |
||||
} |
||||
|
||||
return maybeResult.ToOption(); |
||||
} |
||||
|
||||
protected override async Task<Either<BaseError, MediaItemScanResult<PlexOtherVideo>>> UpdateMetadata( |
||||
MediaItemScanResult<PlexOtherVideo> result, |
||||
OtherVideoMetadata fullMetadata) |
||||
{ |
||||
PlexOtherVideo existing = result.Item; |
||||
OtherVideoMetadata existingMetadata = existing.OtherVideoMetadata.Head(); |
||||
|
||||
if (existingMetadata.MetadataKind != MetadataKind.External) |
||||
{ |
||||
existingMetadata.MetadataKind = MetadataKind.External; |
||||
await _metadataRepository.MarkAsExternal(existingMetadata); |
||||
} |
||||
|
||||
if (existingMetadata.ContentRating != fullMetadata.ContentRating) |
||||
{ |
||||
existingMetadata.ContentRating = fullMetadata.ContentRating; |
||||
await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); |
||||
result.IsUpdated = true; |
||||
} |
||||
|
||||
foreach (Genre genre in existingMetadata.Genres |
||||
.Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Genres.Remove(genre); |
||||
if (await _metadataRepository.RemoveGenre(genre)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Genre genre in fullMetadata.Genres |
||||
.Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Genres.Add(genre); |
||||
if (await _otherVideoRepository.AddGenre(existingMetadata, genre)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Studio studio in existingMetadata.Studios |
||||
.Filter(s => fullMetadata.Studios.All(s2 => s2.Name != s.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Studios.Remove(studio); |
||||
if (await _metadataRepository.RemoveStudio(studio)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Studio studio in fullMetadata.Studios |
||||
.Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Studios.Add(studio); |
||||
if (await _otherVideoRepository.AddStudio(existingMetadata, studio)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Actor actor in existingMetadata.Actors |
||||
.Filter( |
||||
a => fullMetadata.Actors.All( |
||||
a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Actors.Remove(actor); |
||||
if (await _metadataRepository.RemoveActor(actor)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Actor actor in fullMetadata.Actors |
||||
.Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Actors.Add(actor); |
||||
if (await _otherVideoRepository.AddActor(existingMetadata, actor)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Director director in existingMetadata.Directors |
||||
.Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Directors.Remove(director); |
||||
if (await _metadataRepository.RemoveDirector(director)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Director director in fullMetadata.Directors |
||||
.Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Directors.Add(director); |
||||
if (await _otherVideoRepository.AddDirector(existingMetadata, director)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Writer writer in existingMetadata.Writers |
||||
.Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Writers.Remove(writer); |
||||
if (await _metadataRepository.RemoveWriter(writer)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Writer writer in fullMetadata.Writers |
||||
.Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Writers.Add(writer); |
||||
if (await _otherVideoRepository.AddWriter(existingMetadata, writer)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (MetadataGuid guid in existingMetadata.Guids |
||||
.Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Guids.Remove(guid); |
||||
if (await _metadataRepository.RemoveGuid(guid)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (MetadataGuid guid in fullMetadata.Guids |
||||
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Guids.Add(guid); |
||||
if (await _metadataRepository.AddGuid(existingMetadata, guid)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Tag tag in existingMetadata.Tags |
||||
.Filter(g => fullMetadata.Tags.All(g2 => g2.Name != g.Name)) |
||||
.Filter(g => g.ExternalCollectionId is null) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Tags.Remove(tag); |
||||
if (await _metadataRepository.RemoveTag(tag)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
foreach (Tag tag in fullMetadata.Tags |
||||
.Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) |
||||
.ToList()) |
||||
{ |
||||
existingMetadata.Tags.Add(tag); |
||||
if (await _otherVideoRepository.AddTag(existingMetadata, tag)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
if (await _metadataRepository.UpdateSubtitles(existingMetadata, fullMetadata.Subtitles)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
|
||||
/* |
||||
if (fullMetadata.SortTitle != existingMetadata.SortTitle) |
||||
{ |
||||
existingMetadata.SortTitle = fullMetadata.SortTitle; |
||||
// Not existing on IOtherVideoRepository
|
||||
if (await _otherVideoRepository.UpdateSortTitle(existingMetadata)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
*/ |
||||
|
||||
bool poster = await UpdateArtworkIfNeeded(existingMetadata, fullMetadata, ArtworkKind.Poster); |
||||
bool fanArt = await UpdateArtworkIfNeeded(existingMetadata, fullMetadata, ArtworkKind.FanArt); |
||||
if (poster || fanArt) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
|
||||
if (result.IsUpdated) |
||||
{ |
||||
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<bool> UpdateArtworkIfNeeded( |
||||
ErsatzTV.Core.Domain.Metadata existingMetadata, |
||||
ErsatzTV.Core.Domain.Metadata incomingMetadata, |
||||
ArtworkKind artworkKind) |
||||
{ |
||||
if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) |
||||
{ |
||||
Option<Artwork> maybeIncomingArtwork = Optional(incomingMetadata.Artwork).Flatten() |
||||
.Find(a => a.ArtworkKind == artworkKind); |
||||
|
||||
if (maybeIncomingArtwork.IsNone) |
||||
{ |
||||
existingMetadata.Artwork ??= new List<Artwork>(); |
||||
existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); |
||||
await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); |
||||
} |
||||
|
||||
foreach (Artwork incomingArtwork in maybeIncomingArtwork) |
||||
{ |
||||
_logger.LogDebug("Refreshing Plex {Attribute} from {Path}", artworkKind, incomingArtwork.Path); |
||||
|
||||
Option<Artwork> maybeExistingArtwork = Optional(existingMetadata.Artwork).Flatten() |
||||
.Find(a => a.ArtworkKind == artworkKind); |
||||
|
||||
if (maybeExistingArtwork.IsNone) |
||||
{ |
||||
existingMetadata.Artwork ??= new List<Artwork>(); |
||||
existingMetadata.Artwork.Add(incomingArtwork); |
||||
await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); |
||||
} |
||||
|
||||
foreach (Artwork existingArtwork in maybeExistingArtwork) |
||||
{ |
||||
existingArtwork.Path = incomingArtwork.Path; |
||||
existingArtwork.DateUpdated = incomingArtwork.DateUpdated; |
||||
await _metadataRepository.UpdateArtworkPath(existingArtwork); |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
Loading…
Reference in new issue