@@ -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,148 @@
|
||||
#nullable enable
|
||||
using Dapper;
|
||||
using ErsatzTV.Core.Api.MediaSources;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources;
|
||||
|
||||
public class GetAllMediaSourcesForApiHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllMediaSourcesForApi, List<MediaSourceResponseModel>>
|
||||
{
|
||||
public async Task<List<MediaSourceResponseModel>> Handle(
|
||||
GetAllMediaSourcesForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<MediaSource> mediaSources = await dbContext.MediaSources
|
||||
.AsNoTracking()
|
||||
.Include(s => s.Libraries)
|
||||
.ThenInclude(l => l.Paths)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
Dictionary<int, int> itemCountsByLibrary = await GetItemCountsByLibrary(dbContext, cancellationToken);
|
||||
Dictionary<int, string> addressByMediaSourceId = await GetConnectionAddresses(dbContext, cancellationToken);
|
||||
|
||||
var result = new List<MediaSourceResponseModel>();
|
||||
foreach (MediaSource mediaSource in mediaSources)
|
||||
{
|
||||
List<MediaSourceLibraryResponseModel> libraryModels = mediaSource.Libraries
|
||||
.Filter(ShouldIncludeLibrary)
|
||||
.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(mediaSource.Id, out string? a) ? a : null;
|
||||
|
||||
result.Add(
|
||||
new MediaSourceResponseModel(
|
||||
mediaSource.Id,
|
||||
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 static async Task<Dictionary<int, int>> GetItemCountsByLibrary(
|
||||
TvContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<LibraryItemCount> counts = await dbContext.Connection.QueryAsync<LibraryItemCount>(
|
||||
new CommandDefinition(
|
||||
@"SELECT LP.LibraryId AS LibraryId, COUNT(*) AS Count
|
||||
FROM MediaItem
|
||||
INNER JOIN LibraryPath LP on MediaItem.LibraryPathId = LP.Id
|
||||
GROUP BY LP.LibraryId",
|
||||
cancellationToken: cancellationToken));
|
||||
|
||||
return counts.ToDictionary(c => (int)c.LibraryId, c => (int)c.Count);
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<int, string>> GetConnectionAddresses(
|
||||
TvContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var addresses = new Dictionary<int, string>();
|
||||
|
||||
foreach (PlexMediaSource plex in await dbContext.PlexMediaSources
|
||||
.AsNoTracking()
|
||||
.Include(s => s.Connections)
|
||||
.ToListAsync(cancellationToken))
|
||||
{
|
||||
foreach (PlexConnection connection in Optional(plex.Connections.SingleOrDefault(c => c.IsActive)))
|
||||
{
|
||||
addresses[plex.Id] = connection.Uri;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (JellyfinMediaSource jellyfin in await dbContext.JellyfinMediaSources
|
||||
.AsNoTracking()
|
||||
.Include(s => s.Connections)
|
||||
.ToListAsync(cancellationToken))
|
||||
{
|
||||
foreach (JellyfinConnection connection in jellyfin.Connections.HeadOrNone())
|
||||
{
|
||||
addresses[jellyfin.Id] = connection.Address;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (EmbyMediaSource emby in await dbContext.EmbyMediaSources
|
||||
.AsNoTracking()
|
||||
.Include(s => s.Connections)
|
||||
.ToListAsync(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(long LibraryId, long Count);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Core.Api.Libraries;
|
||||
|
||||
public record LibraryScanStatusResponseModel(int LibraryId, decimal Percent);
|
||||
@@ -0,0 +1,11 @@
|
||||
#nullable enable
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Core.Api.Libraries;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using MediatR;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Application.Libraries;
|
||||
|
||||
[TestFixture]
|
||||
public class GetLibraryScanStatusHandlerTests
|
||||
{
|
||||
[Test]
|
||||
public async Task Handle_Should_Return_Active_Scans()
|
||||
{
|
||||
var mediator = Substitute.For<IMediator>();
|
||||
var scannerProxyService = new ScannerProxyService(mediator);
|
||||
Guid scanId = scannerProxyService.StartScan(42)
|
||||
.Match(
|
||||
Some: id => id,
|
||||
None: () => throw new AssertionException("Expected scan to start"));
|
||||
await scannerProxyService.Progress(scanId, 62.5m);
|
||||
|
||||
var handler = new GetLibraryScanStatusHandler(scannerProxyService);
|
||||
|
||||
List<LibraryScanStatusResponseModel> result =
|
||||
await handler.Handle(new GetLibraryScanStatus(), CancellationToken.None);
|
||||
|
||||
result.ShouldBe([new LibraryScanStatusResponseModel(42, 62.5m)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Tests.Support;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Application.MediaSources;
|
||||
|
||||
[TestFixture]
|
||||
public class GetAllMediaSourcesForApiHandlerTests
|
||||
{
|
||||
private InMemoryTvContext _db = null!;
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
||||
|
||||
[TearDown]
|
||||
public async Task TearDown() => await _db.DisposeAsync();
|
||||
|
||||
[Test]
|
||||
public async Task Handle_Should_Group_Configured_Libraries_By_Source_With_Item_Counts()
|
||||
{
|
||||
await SeedMediaSources();
|
||||
var handler = new GetAllMediaSourcesForApiHandler(_db.Factory);
|
||||
|
||||
var result = await handler.Handle(new GetAllMediaSourcesForApi(), CancellationToken.None);
|
||||
|
||||
result.Count.ShouldBe(4);
|
||||
|
||||
result[0].Kind.ShouldBe("Local");
|
||||
result[0].Name.ShouldBe("Local");
|
||||
result[0].ConnectionAddress.ShouldBeNull();
|
||||
result[0].Libraries.Single().Name.ShouldBe("Local Movies");
|
||||
result[0].Libraries.Single().LastScan.ShouldBe(new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
result[0].Libraries.Single().ItemCount.ShouldBe(2);
|
||||
|
||||
result[1].Kind.ShouldBe("Emby");
|
||||
result[1].Name.ShouldBe("Emby Server");
|
||||
result[1].ConnectionAddress.ShouldBe("http://emby.local");
|
||||
result[1].Libraries.Single().Name.ShouldBe("Emby Shows");
|
||||
result[1].Libraries.Single().ItemCount.ShouldBe(1);
|
||||
|
||||
result[2].Kind.ShouldBe("Jellyfin");
|
||||
result[2].Name.ShouldBe("Jellyfin Server");
|
||||
result[2].ConnectionAddress.ShouldBe("http://jellyfin.local");
|
||||
result[2].Libraries.ShouldBeEmpty();
|
||||
|
||||
result[3].Kind.ShouldBe("Plex");
|
||||
result[3].Name.ShouldBe("Plex Server");
|
||||
result[3].ConnectionAddress.ShouldBe("http://plex.local");
|
||||
result[3].Libraries.Single().Name.ShouldBe("Plex Movies");
|
||||
result[3].Libraries.Single().ItemCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Handle_Should_Exclude_Unconfigured_And_Not_Synced_Libraries()
|
||||
{
|
||||
await SeedMediaSources();
|
||||
var handler = new GetAllMediaSourcesForApiHandler(_db.Factory);
|
||||
|
||||
var result = await handler.Handle(new GetAllMediaSourcesForApi(), CancellationToken.None);
|
||||
|
||||
result.SelectMany(s => s.Libraries).Select(l => l.Name)
|
||||
.ShouldNotContain("Empty Local");
|
||||
result.SelectMany(s => s.Libraries).Select(l => l.Name)
|
||||
.ShouldNotContain("Disabled Jellyfin");
|
||||
}
|
||||
|
||||
private async Task SeedMediaSources()
|
||||
{
|
||||
await using TvContext context = _db.CreateContext();
|
||||
|
||||
var localSource = new LocalMediaSource
|
||||
{
|
||||
Libraries =
|
||||
[
|
||||
new LocalLibrary
|
||||
{
|
||||
Name = "Local Movies",
|
||||
MediaKind = LibraryMediaKind.Movies,
|
||||
LastScan = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Paths = [MakePath("/media/movies", 2)]
|
||||
},
|
||||
new LocalLibrary
|
||||
{
|
||||
Name = "Empty Local",
|
||||
MediaKind = LibraryMediaKind.Shows,
|
||||
Paths = []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var plexSource = new PlexMediaSource
|
||||
{
|
||||
ServerName = "Plex Server",
|
||||
ProductVersion = "1",
|
||||
Platform = "Linux",
|
||||
PlatformVersion = "1",
|
||||
ClientIdentifier = "plex",
|
||||
Connections = [new PlexConnection { IsActive = true, Uri = "http://plex.local" }],
|
||||
PathReplacements = [],
|
||||
Libraries =
|
||||
[
|
||||
new PlexLibrary
|
||||
{
|
||||
Name = "Plex Movies",
|
||||
MediaKind = LibraryMediaKind.Movies,
|
||||
Key = "1",
|
||||
ShouldSyncItems = true,
|
||||
Paths = []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var embySource = new EmbyMediaSource
|
||||
{
|
||||
ServerName = "Emby Server",
|
||||
OperatingSystem = "Linux",
|
||||
Connections = [new EmbyConnection { Address = "http://emby.local" }],
|
||||
PathReplacements = [],
|
||||
Libraries =
|
||||
[
|
||||
new EmbyLibrary
|
||||
{
|
||||
Name = "Emby Shows",
|
||||
MediaKind = LibraryMediaKind.Shows,
|
||||
ItemId = "emby-shows",
|
||||
ShouldSyncItems = true,
|
||||
PathInfos = [],
|
||||
Paths = [MakePath("/emby/shows", 1)]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var jellyfinSource = new JellyfinMediaSource
|
||||
{
|
||||
ServerName = "Jellyfin Server",
|
||||
OperatingSystem = "Linux",
|
||||
Connections = [new JellyfinConnection { Address = "http://jellyfin.local" }],
|
||||
PathReplacements = [],
|
||||
Libraries =
|
||||
[
|
||||
new JellyfinLibrary
|
||||
{
|
||||
Name = "Disabled Jellyfin",
|
||||
MediaKind = LibraryMediaKind.Movies,
|
||||
ItemId = "jellyfin-movies",
|
||||
ShouldSyncItems = false,
|
||||
PathInfos = [],
|
||||
Paths = [MakePath("/jellyfin/movies", 1)]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
context.MediaSources.AddRange(localSource, plexSource, embySource, jellyfinSource);
|
||||
await context.SaveChangesAsync();
|
||||
context.ChangeTracker.Clear();
|
||||
}
|
||||
|
||||
private static LibraryPath MakePath(string path, int mediaItemCount) =>
|
||||
new()
|
||||
{
|
||||
Path = path,
|
||||
LibraryFolders = [],
|
||||
MediaItems = Enumerable.Range(0, mediaItemCount)
|
||||
.Select(_ => new Movie
|
||||
{
|
||||
MovieMetadata = [],
|
||||
MediaVersions = [],
|
||||
Collections = [],
|
||||
CollectionItems = [],
|
||||
TraktListItems = []
|
||||
})
|
||||
.Cast<MediaItem>()
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Controllers.Api;
|
||||
using ErsatzTV.Core.Api.Libraries;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
public class LibrariesControllerTests
|
||||
{
|
||||
private LibrariesController _controller = null!;
|
||||
private IMediator _mediator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
_controller = new LibrariesController(Substitute.For<ITelevisionRepository>(), _mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ScanStatus_Should_Expose_Idiomatic_Rest_Route()
|
||||
{
|
||||
MethodInfo action = typeof(LibrariesController).GetMethod(nameof(LibrariesController.GetScanStatus))
|
||||
?? throw new AssertionException("Missing action GetScanStatus");
|
||||
|
||||
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
||||
attribute.HttpMethods.ShouldContain("GET");
|
||||
attribute.Template.ShouldBe("/api/libraries/scan-status");
|
||||
attribute.Name.ShouldBe("GetLibraryScanStatus");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetScanStatus_Should_Return_Results_From_Mediator()
|
||||
{
|
||||
var expected = new List<LibraryScanStatusResponseModel>
|
||||
{
|
||||
new(1, 42.5m),
|
||||
new(2, 99m)
|
||||
};
|
||||
|
||||
_mediator.Send(Arg.Any<GetLibraryScanStatus>(), Arg.Any<CancellationToken>())
|
||||
.Returns(expected);
|
||||
|
||||
List<LibraryScanStatusResponseModel> result = await _controller.GetScanStatus(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
using ErsatzTV.Controllers.Api;
|
||||
using ErsatzTV.Core.Api.MediaSources;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
public class MediaSourcesControllerTests
|
||||
{
|
||||
private MediaSourcesController _controller = null!;
|
||||
private IMediator _mediator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
_controller = new MediaSourcesController(_mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Controller_Should_Expose_Idiomatic_Rest_Route()
|
||||
{
|
||||
MethodInfo action = typeof(MediaSourcesController).GetMethod(nameof(MediaSourcesController.GetAll))
|
||||
?? throw new AssertionException("Missing action GetAll");
|
||||
|
||||
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
||||
attribute.HttpMethods.ShouldContain("GET");
|
||||
attribute.Template.ShouldBe("/api/media-sources");
|
||||
attribute.Name.ShouldBe("GetMediaSources");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Results_From_Mediator()
|
||||
{
|
||||
var expected = new List<MediaSourceResponseModel>
|
||||
{
|
||||
new(
|
||||
1,
|
||||
"Local",
|
||||
"Local",
|
||||
null,
|
||||
[new MediaSourceLibraryResponseModel(10, "Movies", LibraryMediaKind.Movies, null, 3)])
|
||||
};
|
||||
|
||||
_mediator.Send(Arg.Any<GetAllMediaSourcesForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns(expected);
|
||||
|
||||
List<MediaSourceResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
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;
|
||||
|
||||
[ApiController]
|
||||
[EndpointGroupName("general")]
|
||||
public class LibrariesController(ITelevisionRepository televisionRepository, IMediator mediator)
|
||||
public class LibrariesController(ITelevisionRepository televisionRepository, IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/libraries/scan-status", Name = "GetLibraryScanStatus")]
|
||||
[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) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/media-sources", Name = "GetMediaSources")]
|
||||
[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);
|
||||
}
|
||||
@@ -1481,6 +1481,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/libraries/scan-status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Libraries"
|
||||
],
|
||||
"summary": "Get active library scan status",
|
||||
"operationId": "GetLibraryScanStatus",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LibraryScanStatusResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LibraryScanStatusResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LibraryScanStatusResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/libraries/{id}/scan": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1603,6 +1643,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/media-sources": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Media Sources"
|
||||
],
|
||||
"summary": "Get all media sources with their libraries",
|
||||
"operationId": "GetMediaSources",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaSourceResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaSourceResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaSourceResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4647,6 +4727,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LibraryMediaKind": {
|
||||
"enum": [
|
||||
"Movies",
|
||||
"Shows",
|
||||
"MusicVideos",
|
||||
"OtherVideos",
|
||||
"Songs",
|
||||
"Images",
|
||||
"RemoteStreams"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LibraryScanStatusResponseModel": {
|
||||
"required": [
|
||||
"libraryId",
|
||||
"percent"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"libraryId": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"percent": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MarathonGroupBy": {
|
||||
"enum": [
|
||||
"None",
|
||||
@@ -4729,6 +4838,73 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MediaSourceLibraryResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"mediaKind",
|
||||
"lastScan",
|
||||
"itemCount"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"mediaKind": {
|
||||
"$ref": "#/components/schemas/LibraryMediaKind"
|
||||
},
|
||||
"lastScan": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
],
|
||||
"format": "date-time"
|
||||
},
|
||||
"itemCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaSourceResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"kind",
|
||||
"name",
|
||||
"connectionAddress",
|
||||
"libraries"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"connectionAddress": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"libraries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaSourceLibraryResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MultiCollectionItemViewModel": {
|
||||
"required": [
|
||||
"multiCollectionId",
|
||||
@@ -6388,6 +6564,9 @@
|
||||
{
|
||||
"name": "Maintenance"
|
||||
},
|
||||
{
|
||||
"name": "Media Sources"
|
||||
},
|
||||
{
|
||||
"name": "Playouts"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user