mirror of https://github.com/ErsatzTV/ErsatzTV.git
49 changed files with 9538 additions and 68 deletions
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Emby; |
||||
|
||||
public record SynchronizeEmbyCollections(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>, |
||||
IEmbyBackgroundServiceRequest; |
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Emby; |
||||
using ErsatzTV.Core.Interfaces.Emby; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
namespace ErsatzTV.Application.Emby; |
||||
|
||||
public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmbyCollections, Either<BaseError, Unit>> |
||||
{ |
||||
private readonly IEmbySecretStore _embySecretStore; |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
private readonly IEmbyCollectionScanner _scanner; |
||||
|
||||
public SynchronizeEmbyCollectionsHandler( |
||||
IMediaSourceRepository mediaSourceRepository, |
||||
IEmbySecretStore embySecretStore, |
||||
IEmbyCollectionScanner scanner) |
||||
{ |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
_embySecretStore = embySecretStore; |
||||
_scanner = scanner; |
||||
} |
||||
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle( |
||||
SynchronizeEmbyCollections request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Validation<BaseError, ConnectionParameters> validation = await Validate(request); |
||||
return await validation.Match( |
||||
SynchronizeCollections, |
||||
error => Task.FromResult<Either<BaseError, Unit>>(error.Join())); |
||||
} |
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyCollections request) => |
||||
MediaSourceMustExist(request) |
||||
.BindT(MediaSourceMustHaveActiveConnection) |
||||
.BindT(MediaSourceMustHaveApiKey); |
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist( |
||||
SynchronizeEmbyCollections request) => |
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) |
||||
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist.")); |
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( |
||||
EmbyMediaSource embyMediaSource) |
||||
{ |
||||
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone(); |
||||
return maybeConnection.Map(connection => new ConnectionParameters(connection)) |
||||
.ToValidation<BaseError>("Emby media source requires an active connection"); |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( |
||||
ConnectionParameters connectionParameters) |
||||
{ |
||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets(); |
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) |
||||
.Where(match => match) |
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) |
||||
.ToValidation<BaseError>("Emby media source requires an api key"); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, Unit>> SynchronizeCollections(ConnectionParameters connectionParameters) => |
||||
await _scanner.ScanCollections( |
||||
connectionParameters.ActiveConnection.Address, |
||||
connectionParameters.ApiKey); |
||||
|
||||
private record ConnectionParameters(EmbyConnection ActiveConnection) |
||||
{ |
||||
public string ApiKey { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Jellyfin; |
||||
|
||||
public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId) : IRequest<Either<BaseError, Unit>>, |
||||
IJellyfinBackgroundServiceRequest; |
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Jellyfin; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Jellyfin; |
||||
|
||||
namespace ErsatzTV.Application.Jellyfin; |
||||
|
||||
public class SynchronizeJellyfinCollectionsHandler : |
||||
IRequestHandler<SynchronizeJellyfinCollections, Either<BaseError, Unit>> |
||||
{ |
||||
private readonly IJellyfinSecretStore _jellyfinSecretStore; |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
private readonly IJellyfinCollectionScanner _scanner; |
||||
|
||||
public SynchronizeJellyfinCollectionsHandler( |
||||
IMediaSourceRepository mediaSourceRepository, |
||||
IJellyfinSecretStore jellyfinSecretStore, |
||||
IJellyfinCollectionScanner scanner) |
||||
{ |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
_jellyfinSecretStore = jellyfinSecretStore; |
||||
_scanner = scanner; |
||||
} |
||||
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle( |
||||
SynchronizeJellyfinCollections request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Validation<BaseError, ConnectionParameters> validation = await Validate(request); |
||||
return await validation.Match( |
||||
SynchronizeCollections, |
||||
error => Task.FromResult<Either<BaseError, Unit>>(error.Join())); |
||||
} |
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeJellyfinCollections request) => |
||||
MediaSourceMustExist(request) |
||||
.BindT(MediaSourceMustHaveActiveConnection) |
||||
.BindT(MediaSourceMustHaveApiKey); |
||||
|
||||
private Task<Validation<BaseError, JellyfinMediaSource>> MediaSourceMustExist( |
||||
SynchronizeJellyfinCollections request) => |
||||
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) |
||||
.Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist.")); |
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( |
||||
JellyfinMediaSource jellyfinMediaSource) |
||||
{ |
||||
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); |
||||
return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) |
||||
.ToValidation<BaseError>("Jellyfin media source requires an active connection"); |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( |
||||
ConnectionParameters connectionParameters) |
||||
{ |
||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); |
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) |
||||
.Where(match => match) |
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) |
||||
.ToValidation<BaseError>("Jellyfin media source requires an api key"); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, Unit>> SynchronizeCollections(ConnectionParameters connectionParameters) => |
||||
await _scanner.ScanCollections( |
||||
connectionParameters.ActiveConnection.Address, |
||||
connectionParameters.ApiKey, |
||||
connectionParameters.JellyfinMediaSource.Id); |
||||
|
||||
private record ConnectionParameters( |
||||
JellyfinMediaSource JellyfinMediaSource, |
||||
JellyfinConnection ActiveConnection) |
||||
{ |
||||
public string ApiKey { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
public class EmbyCollection |
||||
{ |
||||
public int Id { get; set; } |
||||
public string ItemId { get; set; } |
||||
public string Etag { get; set; } |
||||
public string Name { get; set; } |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
public class JellyfinCollection |
||||
{ |
||||
public int Id { get; set; } |
||||
public string ItemId { get; set; } |
||||
public string Etag { get; set; } |
||||
public string Name { get; set; } |
||||
} |
@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Emby; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.Emby; |
||||
|
||||
public class EmbyCollectionScanner : IEmbyCollectionScanner |
||||
{ |
||||
private readonly IEmbyApiClient _embyApiClient; |
||||
private readonly IEmbyCollectionRepository _embyCollectionRepository; |
||||
private readonly ILogger<EmbyCollectionScanner> _logger; |
||||
private readonly ISearchIndex _searchIndex; |
||||
private readonly ISearchRepository _searchRepository; |
||||
|
||||
public EmbyCollectionScanner( |
||||
IEmbyCollectionRepository embyCollectionRepository, |
||||
IEmbyApiClient embyApiClient, |
||||
ISearchRepository searchRepository, |
||||
ISearchIndex searchIndex, |
||||
ILogger<EmbyCollectionScanner> logger) |
||||
{ |
||||
_embyCollectionRepository = embyCollectionRepository; |
||||
_embyApiClient = embyApiClient; |
||||
_searchRepository = searchRepository; |
||||
_searchIndex = searchIndex; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey) |
||||
{ |
||||
// get all collections from db (item id, etag)
|
||||
List<EmbyCollection> existingCollections = await _embyCollectionRepository.GetCollections(); |
||||
|
||||
// get all collections from emby
|
||||
Either<BaseError, List<EmbyCollection>> maybeIncomingCollections = |
||||
await _embyApiClient.GetCollectionLibraryItems(address, apiKey); |
||||
|
||||
foreach (BaseError error in maybeIncomingCollections.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Failed to get collections from Emby: {Error}", error.ToString()); |
||||
return error; |
||||
} |
||||
|
||||
foreach (List<EmbyCollection> incomingCollections in maybeIncomingCollections.RightToSeq()) |
||||
{ |
||||
// loop over collections
|
||||
foreach (EmbyCollection collection in incomingCollections) |
||||
{ |
||||
Option<EmbyCollection> maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId); |
||||
|
||||
// skip if unchanged (etag)
|
||||
if (await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) == collection.Etag) |
||||
{ |
||||
_logger.LogDebug("Emby collection {Name} is unchanged", collection.Name); |
||||
continue; |
||||
} |
||||
|
||||
// add if new
|
||||
if (maybeExisting.IsNone) |
||||
{ |
||||
_logger.LogDebug("Emby collection {Name} is new", collection.Name); |
||||
await _embyCollectionRepository.AddCollection(collection); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug("Emby collection {Name} has been updated", collection.Name); |
||||
} |
||||
|
||||
await SyncCollectionItems(address, apiKey, collection); |
||||
|
||||
// save collection etag
|
||||
await _embyCollectionRepository.SetEtag(collection); |
||||
} |
||||
|
||||
// remove missing collections (and remove any lingering tags from those collections)
|
||||
foreach (EmbyCollection collection in existingCollections |
||||
.Filter(e => incomingCollections.All(i => i.ItemId != e.ItemId))) |
||||
{ |
||||
await _embyCollectionRepository.RemoveCollection(collection); |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task SyncCollectionItems( |
||||
string address, |
||||
string apiKey, |
||||
EmbyCollection collection) |
||||
{ |
||||
// get collection items from JF
|
||||
Either<BaseError, List<MediaItem>> maybeItems = |
||||
await _embyApiClient.GetCollectionItems(address, apiKey, collection.ItemId); |
||||
|
||||
foreach (BaseError error in maybeItems.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Failed to get collection items from Emby: {Error}", error.ToString()); |
||||
return; |
||||
} |
||||
|
||||
List<int> removedIds = await _embyCollectionRepository.RemoveAllTags(collection); |
||||
|
||||
var embyItems = maybeItems.RightToSeq().Flatten().ToList(); |
||||
_logger.LogDebug("Emby collection {Name} contains {Count} items", collection.Name, embyItems.Count); |
||||
|
||||
// sync tags on items
|
||||
var addedIds = new List<int>(); |
||||
foreach (MediaItem item in embyItems) |
||||
{ |
||||
addedIds.Add(await _embyCollectionRepository.AddTag(item, collection)); |
||||
} |
||||
|
||||
var changedIds = removedIds.Except(addedIds).ToList(); |
||||
changedIds.AddRange(addedIds.Except(removedIds)); |
||||
|
||||
await _searchIndex.RebuildItems(_searchRepository, changedIds); |
||||
_searchIndex.Commit(); |
||||
} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Interfaces.Emby; |
||||
|
||||
public interface IEmbyCollectionScanner |
||||
{ |
||||
Task<Either<BaseError, Unit>> ScanCollections( |
||||
string address, |
||||
string apiKey); |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Core.Interfaces.Jellyfin; |
||||
|
||||
public interface IJellyfinCollectionScanner |
||||
{ |
||||
Task<Either<BaseError, Unit>> ScanCollections( |
||||
string address, |
||||
string apiKey, |
||||
int mediaSourceId); |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
public interface IEmbyCollectionRepository |
||||
{ |
||||
Task<List<EmbyCollection>> GetCollections(); |
||||
Task<bool> AddCollection(EmbyCollection collection); |
||||
Task<bool> RemoveCollection(EmbyCollection collection); |
||||
Task<List<int>> RemoveAllTags(EmbyCollection collection); |
||||
Task<int> AddTag(MediaItem item, EmbyCollection collection); |
||||
Task<bool> SetEtag(EmbyCollection collection); |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
public interface IJellyfinCollectionRepository |
||||
{ |
||||
Task<List<JellyfinCollection>> GetCollections(); |
||||
Task<bool> AddCollection(JellyfinCollection collection); |
||||
Task<bool> RemoveCollection(JellyfinCollection collection); |
||||
Task<List<int>> RemoveAllTags(JellyfinCollection collection); |
||||
Task<int> AddTag(MediaItem item, JellyfinCollection collection); |
||||
Task<bool> SetEtag(JellyfinCollection collection); |
||||
} |
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Jellyfin; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.Jellyfin; |
||||
|
||||
public class JellyfinCollectionScanner : IJellyfinCollectionScanner |
||||
{ |
||||
private readonly IJellyfinApiClient _jellyfinApiClient; |
||||
private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository; |
||||
private readonly ILogger<JellyfinCollectionScanner> _logger; |
||||
private readonly ISearchIndex _searchIndex; |
||||
private readonly ISearchRepository _searchRepository; |
||||
|
||||
public JellyfinCollectionScanner( |
||||
IJellyfinCollectionRepository jellyfinCollectionRepository, |
||||
IJellyfinApiClient jellyfinApiClient, |
||||
ISearchRepository searchRepository, |
||||
ISearchIndex searchIndex, |
||||
ILogger<JellyfinCollectionScanner> logger) |
||||
{ |
||||
_jellyfinCollectionRepository = jellyfinCollectionRepository; |
||||
_jellyfinApiClient = jellyfinApiClient; |
||||
_searchRepository = searchRepository; |
||||
_searchIndex = searchIndex; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey, int mediaSourceId) |
||||
{ |
||||
// get all collections from db (item id, etag)
|
||||
List<JellyfinCollection> existingCollections = await _jellyfinCollectionRepository.GetCollections(); |
||||
|
||||
// get all collections from jellyfin
|
||||
Either<BaseError, List<JellyfinCollection>> maybeIncomingCollections = |
||||
await _jellyfinApiClient.GetCollectionLibraryItems(address, apiKey, mediaSourceId); |
||||
|
||||
foreach (BaseError error in maybeIncomingCollections.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Failed to get collections from Jellyfin: {Error}", error.ToString()); |
||||
return error; |
||||
} |
||||
|
||||
foreach (List<JellyfinCollection> incomingCollections in maybeIncomingCollections.RightToSeq()) |
||||
{ |
||||
// loop over collections
|
||||
foreach (JellyfinCollection collection in incomingCollections) |
||||
{ |
||||
Option<JellyfinCollection> maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId); |
||||
|
||||
// skip if unchanged (etag)
|
||||
if (await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) == collection.Etag) |
||||
{ |
||||
_logger.LogDebug("Jellyfin collection {Name} is unchanged", collection.Name); |
||||
continue; |
||||
} |
||||
|
||||
// add if new
|
||||
if (maybeExisting.IsNone) |
||||
{ |
||||
_logger.LogDebug("Jellyfin collection {Name} is new", collection.Name); |
||||
await _jellyfinCollectionRepository.AddCollection(collection); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug("Jellyfin collection {Name} has been updated", collection.Name); |
||||
} |
||||
|
||||
await SyncCollectionItems(address, apiKey, mediaSourceId, collection); |
||||
|
||||
// save collection etag
|
||||
await _jellyfinCollectionRepository.SetEtag(collection); |
||||
} |
||||
|
||||
// remove missing collections (and remove any lingering tags from those collections)
|
||||
foreach (JellyfinCollection collection in existingCollections |
||||
.Filter(e => incomingCollections.All(i => i.ItemId != e.ItemId))) |
||||
{ |
||||
await _jellyfinCollectionRepository.RemoveCollection(collection); |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task SyncCollectionItems( |
||||
string address, |
||||
string apiKey, |
||||
int mediaSourceId, |
||||
JellyfinCollection collection) |
||||
{ |
||||
// get collection items from JF
|
||||
Either<BaseError, List<MediaItem>> maybeItems = |
||||
await _jellyfinApiClient.GetCollectionItems(address, apiKey, mediaSourceId, collection.ItemId); |
||||
|
||||
foreach (BaseError error in maybeItems.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Failed to get collection items from Jellyfin: {Error}", error.ToString()); |
||||
return; |
||||
} |
||||
|
||||
List<int> removedIds = await _jellyfinCollectionRepository.RemoveAllTags(collection); |
||||
|
||||
var jellyfinItems = maybeItems.RightToSeq().Flatten().ToList(); |
||||
_logger.LogDebug("Jellyfin collection {Name} contains {Count} items", collection.Name, jellyfinItems.Count); |
||||
|
||||
// sync tags on items
|
||||
var addedIds = new List<int>(); |
||||
foreach (MediaItem item in jellyfinItems) |
||||
{ |
||||
addedIds.Add(await _jellyfinCollectionRepository.AddTag(item, collection)); |
||||
} |
||||
|
||||
var changedIds = removedIds.Except(addedIds).ToList(); |
||||
changedIds.AddRange(addedIds.Except(removedIds)); |
||||
|
||||
await _searchIndex.RebuildItems(_searchRepository, changedIds); |
||||
_searchIndex.Commit(); |
||||
} |
||||
} |
@ -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 EmbyCollectionConfiguration : IEntityTypeConfiguration<EmbyCollection> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<EmbyCollection> builder) => builder.ToTable("EmbyCollection"); |
||||
} |
@ -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 JellyfinCollectionConfiguration : IEntityTypeConfiguration<JellyfinCollection> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<JellyfinCollection> builder) => builder.ToTable("JellyfinCollection"); |
||||
} |
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
using Dapper; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Repositories; |
||||
|
||||
public class EmbyCollectionRepository : IEmbyCollectionRepository |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public EmbyCollectionRepository(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<List<EmbyCollection>> GetCollections() |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.EmbyCollections.ToListAsync(); |
||||
} |
||||
|
||||
public async Task<bool> AddCollection(EmbyCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
await dbContext.AddAsync(collection); |
||||
return await dbContext.SaveChangesAsync() > 0; |
||||
} |
||||
|
||||
public async Task<bool> RemoveCollection(EmbyCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
dbContext.Remove(collection); |
||||
|
||||
// remove all tags that reference this collection
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"DELETE FROM Tag WHERE Name = @Name AND ExternalCollectionId = @ItemId", |
||||
new { collection.Name, collection.ItemId }); |
||||
|
||||
return await dbContext.SaveChangesAsync() > 0; |
||||
} |
||||
|
||||
public async Task<List<int>> RemoveAllTags(EmbyCollection collection) |
||||
{ |
||||
var result = new List<int>(); |
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
// movies
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JM.Id FROM Tag T
|
||||
INNER JOIN MovieMetadata MM on T.MovieMetadataId = MM.Id |
||||
INNER JOIN EmbyMovie JM on JM.Id = MM.MovieId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// shows
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JS.Id FROM Tag T
|
||||
INNER JOIN ShowMetadata SM on T.ShowMetadataId = SM.Id |
||||
INNER JOIN EmbyShow JS on JS.Id = SM.ShowId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// seasons
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JS.Id FROM Tag T
|
||||
INNER JOIN SeasonMetadata SM on T.SeasonMetadataId = SM.Id |
||||
INNER JOIN EmbySeason JS on JS.Id = SM.SeasonId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// episodes
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JE.Id FROM Tag T
|
||||
INNER JOIN EpisodeMetadata EM on T.EpisodeMetadataId = EM.Id |
||||
INNER JOIN EmbyEpisode JE on JE.Id = EM.EpisodeId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// delete all tags
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"DELETE FROM Tag WHERE Name = @Name AND ExternalCollectionId = @ItemId", |
||||
new { collection.Name, collection.ItemId }); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public async Task<int> AddTag(MediaItem item, EmbyCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
switch (item) |
||||
{ |
||||
case EmbyMovie movie: |
||||
int movieId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM EmbyMovie WHERE ItemId = @ItemId", |
||||
new { movie.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, MovieMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM MovieMetadata WHERE MovieId = @MovieId)",
|
||||
new { collection.Name, collection.ItemId, MovieId = movieId }); |
||||
return movieId; |
||||
case EmbyShow show: |
||||
int showId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM EmbyShow WHERE ItemId = @ItemId", |
||||
new { show.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, ShowMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM ShowMetadata WHERE ShowId = @ShowId)",
|
||||
new { collection.Name, collection.ItemId, ShowId = showId }); |
||||
return showId; |
||||
case EmbySeason season: |
||||
int seasonId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM EmbySeason WHERE ItemId = @ItemId", |
||||
new { season.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, SeasonMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM SeasonMetadata WHERE SeasonId = @SeasonId)",
|
||||
new { collection.Name, collection.ItemId, SeasonId = seasonId }); |
||||
return seasonId; |
||||
case EmbyEpisode episode: |
||||
int episodeId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM EmbyEpisode WHERE ItemId = @ItemId", |
||||
new { episode.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, EpisodeMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM EpisodeMetadata WHERE EpisodeId = @EpisodeId)",
|
||||
new { collection.Name, collection.ItemId, EpisodeId = episodeId }); |
||||
return episodeId; |
||||
default: |
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
public async Task<bool> SetEtag(EmbyCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
@"UPDATE EmbyCollection SET Etag = @Etag WHERE ItemId = @ItemId", |
||||
new { collection.Etag, collection.ItemId }) > 0; |
||||
} |
||||
} |
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
using Dapper; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Repositories; |
||||
|
||||
public class JellyfinCollectionRepository : IJellyfinCollectionRepository |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public JellyfinCollectionRepository(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<List<JellyfinCollection>> GetCollections() |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.JellyfinCollections.ToListAsync(); |
||||
} |
||||
|
||||
public async Task<bool> AddCollection(JellyfinCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
await dbContext.AddAsync(collection); |
||||
return await dbContext.SaveChangesAsync() > 0; |
||||
} |
||||
|
||||
public async Task<bool> RemoveCollection(JellyfinCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
dbContext.Remove(collection); |
||||
|
||||
// remove all tags that reference this collection
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"DELETE FROM Tag WHERE Name = @Name AND ExternalCollectionId = @ItemId", |
||||
new { collection.Name, collection.ItemId }); |
||||
|
||||
return await dbContext.SaveChangesAsync() > 0; |
||||
} |
||||
|
||||
public async Task<List<int>> RemoveAllTags(JellyfinCollection collection) |
||||
{ |
||||
var result = new List<int>(); |
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
// movies
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JM.Id FROM Tag T
|
||||
INNER JOIN MovieMetadata MM on T.MovieMetadataId = MM.Id |
||||
INNER JOIN JellyfinMovie JM on JM.Id = MM.MovieId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// shows
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JS.Id FROM Tag T
|
||||
INNER JOIN ShowMetadata SM on T.ShowMetadataId = SM.Id |
||||
INNER JOIN JellyfinShow JS on JS.Id = SM.ShowId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// seasons
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JS.Id FROM Tag T
|
||||
INNER JOIN SeasonMetadata SM on T.SeasonMetadataId = SM.Id |
||||
INNER JOIN JellyfinSeason JS on JS.Id = SM.SeasonId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// episodes
|
||||
result.AddRange( |
||||
await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT JE.Id FROM Tag T
|
||||
INNER JOIN EpisodeMetadata EM on T.EpisodeMetadataId = EM.Id |
||||
INNER JOIN JellyfinEpisode JE on JE.Id = EM.EpisodeId |
||||
WHERE T.ExternalCollectionId = @ItemId",
|
||||
new { collection.ItemId })); |
||||
|
||||
// delete all tags
|
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"DELETE FROM Tag WHERE Name = @Name AND ExternalCollectionId = @ItemId", |
||||
new { collection.Name, collection.ItemId }); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public async Task<int> AddTag(MediaItem item, JellyfinCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
switch (item) |
||||
{ |
||||
case JellyfinMovie movie: |
||||
int movieId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM JellyfinMovie WHERE ItemId = @ItemId", |
||||
new { movie.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, MovieMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM MovieMetadata WHERE MovieId = @MovieId)",
|
||||
new { collection.Name, collection.ItemId, MovieId = movieId }); |
||||
return movieId; |
||||
case JellyfinShow show: |
||||
int showId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM JellyfinShow WHERE ItemId = @ItemId", |
||||
new { show.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, ShowMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM ShowMetadata WHERE ShowId = @ShowId)",
|
||||
new { collection.Name, collection.ItemId, ShowId = showId }); |
||||
return showId; |
||||
case JellyfinSeason season: |
||||
int seasonId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM JellyfinSeason WHERE ItemId = @ItemId", |
||||
new { season.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, SeasonMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM SeasonMetadata WHERE SeasonId = @SeasonId)",
|
||||
new { collection.Name, collection.ItemId, SeasonId = seasonId }); |
||||
return seasonId; |
||||
case JellyfinEpisode episode: |
||||
int episodeId = await dbContext.Connection.ExecuteScalarAsync<int>( |
||||
@"SELECT Id FROM JellyfinEpisode WHERE ItemId = @ItemId", |
||||
new { episode.ItemId }); |
||||
await dbContext.Connection.ExecuteAsync( |
||||
@"INSERT INTO Tag (Name, ExternalCollectionId, EpisodeMetadataId)
|
||||
SELECT @Name, @ItemId, Id FROM |
||||
(SELECT Id FROM EpisodeMetadata WHERE EpisodeId = @EpisodeId)",
|
||||
new { collection.Name, collection.ItemId, EpisodeId = episodeId }); |
||||
return episodeId; |
||||
default: |
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
public async Task<bool> SetEtag(JellyfinCollection collection) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
@"UPDATE JellyfinCollection SET Etag = @Etag WHERE ItemId = @ItemId", |
||||
new { collection.Etag, collection.ItemId }) > 0; |
||||
} |
||||
} |
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.Migrations |
||||
{ |
||||
public partial class Add_JellyfinCollection : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "ExternalCollectionId", |
||||
table: "Tag", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "JellyfinCollection", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
ItemId = table.Column<string>(type: "TEXT", nullable: true), |
||||
Etag = table.Column<string>(type: "TEXT", nullable: true), |
||||
Name = table.Column<string>(type: "TEXT", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_JellyfinCollection", x => x.Id); |
||||
}); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "JellyfinCollection"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "ExternalCollectionId", |
||||
table: "Tag"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_EmbyCollection : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "EmbyCollection", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
ItemId = table.Column<string>(type: "TEXT", nullable: true), |
||||
Etag = table.Column<string>(type: "TEXT", nullable: true), |
||||
Name = table.Column<string>(type: "TEXT", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_EmbyCollection", x => x.Id); |
||||
}); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "EmbyCollection"); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue