wip: partial implementation salvaged from interrupted workflow run
Untrusted draft — no build/test had run yet. Review before building on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.Libraries;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public record GetLibraryScanStatus : IRequest<List<LibraryScanStatusResponseModel>>;
|
||||
@@ -0,0 +1,20 @@
|
||||
using ErsatzTV.Core.Api.Libraries;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public class GetLibraryScanStatusHandler(IScannerProxyService scannerProxyService)
|
||||
: IRequestHandler<GetLibraryScanStatus, List<LibraryScanStatusResponseModel>>
|
||||
{
|
||||
public Task<List<LibraryScanStatusResponseModel>> Handle(
|
||||
GetLibraryScanStatus request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<LibraryScanStatusResponseModel> result = scannerProxyService.GetActiveScans()
|
||||
.Select(scan => new LibraryScanStatusResponseModel(scan.LibraryId, scan.Progress))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.MediaSources;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources;
|
||||
|
||||
public record GetAllMediaSourcesForApi : IRequest<List<MediaSourceResponseModel>>;
|
||||
@@ -0,0 +1,133 @@
|
||||
#nullable enable
|
||||
using Dapper;
|
||||
using ErsatzTV.Core.Api.MediaSources;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources;
|
||||
|
||||
public class GetAllMediaSourcesForApiHandler(
|
||||
ILibraryRepository libraryRepository,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllMediaSourcesForApi, List<MediaSourceResponseModel>>
|
||||
{
|
||||
public async Task<List<MediaSourceResponseModel>> Handle(
|
||||
GetAllMediaSourcesForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<Library> libraries = (await libraryRepository.GetAll())
|
||||
.Filter(ShouldIncludeLibrary)
|
||||
.ToList();
|
||||
|
||||
Dictionary<int, int> itemCountsByLibrary = await GetItemCountsByLibrary(cancellationToken);
|
||||
Dictionary<int, string> addressByMediaSourceId = await GetConnectionAddresses(cancellationToken);
|
||||
|
||||
var result = new List<MediaSourceResponseModel>();
|
||||
foreach (IGrouping<int, Library> group in libraries.GroupBy(l => l.MediaSourceId))
|
||||
{
|
||||
MediaSource mediaSource = group.First().MediaSource;
|
||||
|
||||
List<MediaSourceLibraryResponseModel> libraryModels = group
|
||||
.OrderBy(l => l.MediaKind)
|
||||
.ThenBy(l => l.Name)
|
||||
.Map(l => new MediaSourceLibraryResponseModel(
|
||||
l.Id,
|
||||
l.Name,
|
||||
l.MediaKind,
|
||||
l.LastScan,
|
||||
itemCountsByLibrary.TryGetValue(l.Id, out int count) ? count : 0))
|
||||
.ToList();
|
||||
|
||||
string? address = addressByMediaSourceId.TryGetValue(group.Key, out string? a) ? a : null;
|
||||
|
||||
result.Add(
|
||||
new MediaSourceResponseModel(
|
||||
group.Key,
|
||||
GetKind(mediaSource),
|
||||
GetName(mediaSource),
|
||||
address,
|
||||
libraryModels));
|
||||
}
|
||||
|
||||
return result
|
||||
.OrderBy(s => s.Kind == "Local" ? 0 : 1)
|
||||
.ThenBy(s => s.Kind)
|
||||
.ThenBy(s => s.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<Dictionary<int, int>> GetItemCountsByLibrary(CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
IEnumerable<LibraryItemCount> counts = await dbContext.Connection.QueryAsync<LibraryItemCount>(
|
||||
@"SELECT LP.LibraryId AS LibraryId, COUNT(*) AS Count
|
||||
FROM MediaItem
|
||||
INNER JOIN LibraryPath LP on MediaItem.LibraryPathId = LP.Id
|
||||
GROUP BY LP.LibraryId");
|
||||
return counts.ToDictionary(c => c.LibraryId, c => c.Count);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<int, string>> GetConnectionAddresses(CancellationToken cancellationToken)
|
||||
{
|
||||
var addresses = new Dictionary<int, string>();
|
||||
|
||||
foreach (PlexMediaSource plex in await mediaSourceRepository.GetAllPlex())
|
||||
{
|
||||
foreach (PlexConnection connection in Optional(plex.Connections.SingleOrDefault(c => c.IsActive)))
|
||||
{
|
||||
addresses[plex.Id] = connection.Uri;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (JellyfinMediaSource jellyfin in await mediaSourceRepository.GetAllJellyfin(cancellationToken))
|
||||
{
|
||||
foreach (JellyfinConnection connection in jellyfin.Connections.HeadOrNone())
|
||||
{
|
||||
addresses[jellyfin.Id] = connection.Address;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (EmbyMediaSource emby in await mediaSourceRepository.GetAllEmby(cancellationToken))
|
||||
{
|
||||
foreach (EmbyConnection connection in emby.Connections.HeadOrNone())
|
||||
{
|
||||
addresses[emby.Id] = connection.Address;
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private static bool ShouldIncludeLibrary(Library library) =>
|
||||
library switch
|
||||
{
|
||||
LocalLibrary => library.Paths.Count > 0,
|
||||
PlexLibrary plex => plex.ShouldSyncItems,
|
||||
JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems,
|
||||
EmbyLibrary emby => emby.ShouldSyncItems,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private static string GetKind(MediaSource mediaSource) =>
|
||||
mediaSource switch
|
||||
{
|
||||
PlexMediaSource => "Plex",
|
||||
JellyfinMediaSource => "Jellyfin",
|
||||
EmbyMediaSource => "Emby",
|
||||
_ => "Local"
|
||||
};
|
||||
|
||||
private static string GetName(MediaSource mediaSource) =>
|
||||
mediaSource switch
|
||||
{
|
||||
PlexMediaSource plex => plex.ServerName,
|
||||
JellyfinMediaSource jellyfin => jellyfin.ServerName,
|
||||
EmbyMediaSource emby => emby.ServerName,
|
||||
_ => "Local"
|
||||
};
|
||||
|
||||
private sealed record LibraryItemCount(int LibraryId, int Count);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Core.Api.Libraries;
|
||||
|
||||
public record LibraryScanStatusResponseModel(int LibraryId, decimal Percent);
|
||||
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Api.MediaSources;
|
||||
|
||||
public record MediaSourceLibraryResponseModel(
|
||||
int Id,
|
||||
string Name,
|
||||
LibraryMediaKind MediaKind,
|
||||
DateTime? LastScan,
|
||||
int ItemCount);
|
||||
@@ -0,0 +1,9 @@
|
||||
#nullable enable
|
||||
namespace ErsatzTV.Core.Api.MediaSources;
|
||||
|
||||
public record MediaSourceResponseModel(
|
||||
int Id,
|
||||
string Kind,
|
||||
string Name,
|
||||
string? ConnectionAddress,
|
||||
List<MediaSourceLibraryResponseModel> Libraries);
|
||||
@@ -1,3 +1,5 @@
|
||||
using ErsatzTV.Core.Metadata;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata;
|
||||
|
||||
public interface IScannerProxyService
|
||||
@@ -7,4 +9,5 @@ public interface IScannerProxyService
|
||||
Task Progress(Guid scanId, decimal percentComplete);
|
||||
bool IsActive(Guid scanId);
|
||||
Option<decimal> GetProgress(int libraryId);
|
||||
IReadOnlyList<LibraryScanProgress> GetActiveScans();
|
||||
}
|
||||
|
||||
@@ -49,4 +49,7 @@ public class ScannerProxyService(IMediator mediator) : IScannerProxyService
|
||||
public Option<decimal> GetProgress(int libraryId) => _activeLibraries.TryGetValue(libraryId, out decimal progress)
|
||||
? progress
|
||||
: Option<decimal>.None;
|
||||
|
||||
public IReadOnlyList<LibraryScanProgress> GetActiveScans() =>
|
||||
_activeLibraries.Select(kvp => new LibraryScanProgress(kvp.Key, kvp.Value)).ToList();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Core.Api.Libraries;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
@@ -9,6 +11,13 @@ namespace ErsatzTV.Controllers.Api;
|
||||
[EndpointGroupName("general")]
|
||||
public class LibrariesController(ITelevisionRepository televisionRepository, IMediator mediator)
|
||||
{
|
||||
[HttpGet("/api/libraries/scan-status")]
|
||||
[Tags("Libraries")]
|
||||
[EndpointSummary("Get active library scan status")]
|
||||
[ProducesResponseType(typeof(List<LibraryScanStatusResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<List<LibraryScanStatusResponseModel>> GetScanStatus(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetLibraryScanStatus(), cancellationToken);
|
||||
|
||||
[HttpPost("/api/libraries/{id:int}/scan")]
|
||||
[Tags("Libraries")]
|
||||
[EndpointSummary("Scan library")]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
using ErsatzTV.Core.Api.MediaSources;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
|
||||
[ApiController]
|
||||
public class MediaSourcesController(IMediator mediator)
|
||||
{
|
||||
[HttpGet("/api/media-sources")]
|
||||
[Tags("Media Sources")]
|
||||
[EndpointSummary("Get all media sources with their libraries")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(List<MediaSourceResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<List<MediaSourceResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetAllMediaSourcesForApi(), cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user