From 5c43ae47b106d69032cfcbcd0c42581330e30f2f Mon Sep 17 00:00:00 2001
From: Jason Dove <1695733+jasongdove@users.noreply.github.com>
Date: Sun, 20 Jul 2025 16:10:32 +0000
Subject: [PATCH] add basic remote stream library (#2175)
* initial remote stream library support; scanning seems to work ok
* flood schedule remote streams kind of works
* switch remote stream definitions to yaml files
* implement remote stream script playback
* update changelog
---
CHANGELOG.md | 8 +
.../ErsatzTV.Application.csproj.DotSettings | 1 +
.../CollectionCardResultsViewModel.cs | 3 +-
ErsatzTV.Application/MediaCards/Mapper.cs | 13 +-
.../Queries/GetCollectionCardsHandler.cs | 6 +
.../RemoteStreamCardResultsViewModel.cs | 5 +
.../MediaCards/RemoteStreamCardViewModel.cs | 20 +
.../Commands/AddItemsToCollection.cs | 3 +-
.../Commands/AddItemsToCollectionHandler.cs | 1 +
.../Commands/AddItemsToPlaylist.cs | 3 +-
.../Commands/AddMediaItemToCollection.cs | 5 +
.../AddMediaItemToCollectionHandler.cs | 83 +
ErsatzTV.Application/MediaItems/Mapper.cs | 3 +
.../Queries/GetMediaItemInfoHandler.cs | 6 +
.../MediaItems/Queries/GetRemoteStreamById.cs | 5 +
.../Queries/GetRemoteStreamByIdHandler.cs | 19 +
.../MediaItems/RemoteStreamViewModel.cs | 3 +
ErsatzTV.Application/Playouts/Mapper.cs | 2 +
.../GetFuturePlayoutItemsByIdHandler.cs | 4 +
.../Commands/PreviewBlockPlayoutHandler.cs | 2 +
.../QuerySearchIndexAllItemsHandler.cs | 17 +-
.../Queries/QuerySearchIndexRemoteStreams.cs | 5 +
.../QuerySearchIndexRemoteStreamsHandler.cs | 33 +
.../Search/SearchResultAllItemsViewModel.cs | 3 +-
...layoutItemProcessByChannelNumberHandler.cs | 18 +-
.../PrepareTroubleshootingPlaybackHandler.cs | 36 +-
.../Fakes/FakeMediaCollectionRepository.cs | 1 +
.../Domain/Library/LibraryMediaKind.cs | 3 +-
.../Domain/MediaItem/RemoteStream.cs | 14 +
.../Domain/Metadata/RemoteStreamMetadata.cs | 7 +
.../ProgramScheduleItemCollectionType.cs | 1 +
.../Extensions/MediaItemExtensions.cs | 1 +
.../Metadata/IFallbackMetadataProvider.cs | 1 +
.../IMediaCollectionRepository.cs | 1 +
.../Repositories/IRemoteStreamRepository.cs | 18 +
.../Metadata/FallbackMetadataProvider.cs | 76 +-
ErsatzTV.Core/Metadata/LocalFileSystem.cs | 29 +-
.../Scheduling/MediaItemsForCollection.cs | 7 +
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 9 +-
.../Scheduling/PlayoutModeSchedulerBase.cs | 8 +
.../Scheduling/PlayoutModeSchedulerFlood.cs | 20 +-
.../Streaming/YamlRemoteStreamDefinition.cs | 12 +
.../OutputOption/TimeLimitOutputOption.cs | 16 +-
...0250720030144_Add_RemoteStream.Designer.cs | 6126 ++++++++++++++++
.../20250720030144_Add_RemoteStream.cs | 328 +
...3_Add_LocalLibraryRemoteStream.Designer.cs | 6126 ++++++++++++++++
...0720030313_Add_LocalLibraryRemoteStream.cs | 27 +
...0135059_Add_RemoteStreamFields.Designer.cs | 6138 +++++++++++++++++
.../20250720135059_Add_RemoteStreamFields.cs | 62 +
.../Migrations/TvContextModelSnapshot.cs | 185 +
...0250720030204_Add_RemoteStream.Designer.cs | 5963 ++++++++++++++++
.../20250720030204_Add_RemoteStream.cs | 323 +
...6_Add_LocalLibraryRemoteStream.Designer.cs | 5963 ++++++++++++++++
...0720030246_Add_LocalLibraryRemoteStream.cs | 27 +
...0135252_Add_RemoteStreamFields.Designer.cs | 5975 ++++++++++++++++
.../20250720135252_Add_RemoteStreamFields.cs | 59 +
.../Migrations/TvContextModelSnapshot.cs | 183 +
.../MediaItem/RemoteStreamConfiguration.cs | 22 +
.../RemoteStreamMetadataConfiguration.cs | 41 +
.../Repositories/MediaCollectionRepository.cs | 67 +-
.../Data/Repositories/MediaItemRepository.cs | 6 +-
.../Repositories/RemoteStreamRepository.cs | 172 +
.../Data/Repositories/SearchRepository.cs | 20 +
ErsatzTV.Infrastructure/Data/TvContext.cs | 2 +
.../Health/Checks/FileNotFoundHealthCheck.cs | 4 +
.../Metadata/LocalStatisticsProvider.cs | 24 +-
.../Search/LuceneSearchIndex.cs | 81 +
.../Commands/ScanLocalLibraryHandler.cs | 11 +
.../Metadata/ILocalMetadataProvider.cs | 2 +
.../Metadata/IRemoteStreamFolderScanner.cs | 15 +
.../Core/Metadata/LocalFolderScanner.cs | 5 +
.../Core/Metadata/LocalMetadataProvider.cs | 135 +
.../Metadata/RemoteStreamFolderScanner.cs | 375 +
ErsatzTV.Scanner/Program.cs | 13 +-
ErsatzTV/Controllers/InternalController.cs | 55 +
ErsatzTV/Pages/CollectionItems.razor | 43 +
ErsatzTV/Pages/Libraries.razor | 18 +-
ErsatzTV/Pages/LocalLibraries.razor | 1 +
ErsatzTV/Pages/LocalLibraryEditor.razor | 8 +-
ErsatzTV/Pages/MultiSelectBase.cs | 24 +-
ErsatzTV/Pages/RemoteStreamList.razor | 132 +
ErsatzTV/Pages/Search.razor | 82 +-
ErsatzTV/Pages/Trash.razor | 64 +-
.../RunOnce/DatabaseCleanerService.cs | 1 +
ErsatzTV/Shared/MainLayout.razor | 1 +
ErsatzTV/Startup.cs | 1 +
86 files changed, 39340 insertions(+), 101 deletions(-)
create mode 100644 ErsatzTV.Application/MediaCards/RemoteStreamCardResultsViewModel.cs
create mode 100644 ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs
create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollection.cs
create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollectionHandler.cs
create mode 100644 ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamById.cs
create mode 100644 ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamByIdHandler.cs
create mode 100644 ErsatzTV.Application/MediaItems/RemoteStreamViewModel.cs
create mode 100644 ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreams.cs
create mode 100644 ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreamsHandler.cs
create mode 100644 ErsatzTV.Core/Domain/MediaItem/RemoteStream.cs
create mode 100644 ErsatzTV.Core/Domain/Metadata/RemoteStreamMetadata.cs
create mode 100644 ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs
create mode 100644 ErsatzTV.Core/Streaming/YamlRemoteStreamDefinition.cs
create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20250720030144_Add_RemoteStream.Designer.cs
create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20250720030144_Add_RemoteStream.cs
create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20250720030313_Add_LocalLibraryRemoteStream.Designer.cs
create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20250720030313_Add_LocalLibraryRemoteStream.cs
create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20250720135059_Add_RemoteStreamFields.Designer.cs
create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20250720135059_Add_RemoteStreamFields.cs
create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20250720030204_Add_RemoteStream.Designer.cs
create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20250720030204_Add_RemoteStream.cs
create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20250720030246_Add_LocalLibraryRemoteStream.Designer.cs
create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20250720030246_Add_LocalLibraryRemoteStream.cs
create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20250720135252_Add_RemoteStreamFields.Designer.cs
create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20250720135252_Add_RemoteStreamFields.cs
create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MediaItem/RemoteStreamConfiguration.cs
create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Metadata/RemoteStreamMetadataConfiguration.cs
create mode 100644 ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs
create mode 100644 ErsatzTV.Scanner/Core/Interfaces/Metadata/IRemoteStreamFolderScanner.cs
create mode 100644 ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs
create mode 100644 ErsatzTV/Pages/RemoteStreamList.razor
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebd2855f9..410692721 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -102,6 +102,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add Trakt List option `Auto Refresh` to automatically update list from trakt.tv once each day
- Add Trakt List option `Generate Playlist` to automatically generate ETV Playlist from matched Trakt List items
- Read `country` field from movie NFO files and include in search index as `country`
+- Add *experimental* and *incomplete* `Remote Stream` library kind
+ - Remote Stream libraries have fallback metadata added like Other Video libraries (every folder is a tag)
+ - Remote Stream library items consist of YAML (`.yml`) files with the following fields
+ - `url`: the URL of the content that can be played directly by ffmpeg
+ - `script`: the process name and arguments for a command that will output content to stdout
+ - `duration`: when the content is "live" and does not have duration metadata, this must be provided to allow scheduling
+ - The remote stream definition (YAML file) may provide either a `url` or a `script`
+ - If both are provided, `url` will be used
### Changed
- Allow `Other Video` libraries and `Image` libraries to use the same folders
diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
index 660306c65..2138ffc30 100644
--- a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
+++ b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
@@ -45,6 +45,7 @@
True
True
True
+ True
True
True
True
\ No newline at end of file
diff --git a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs
index 8188c50f0..ebd730fd9 100644
--- a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs
+++ b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs
@@ -10,7 +10,8 @@ public record CollectionCardResultsViewModel(
List MusicVideoCards,
List OtherVideoCards,
List SongCards,
- List ImageCards)
+ List ImageCards,
+ List RemoteStreamCards)
{
public bool UseCustomPlaybackOrder { get; set; }
}
diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs
index edc98c522..3982f0709 100644
--- a/ErsatzTV.Application/MediaCards/Mapper.cs
+++ b/ErsatzTV.Application/MediaCards/Mapper.cs
@@ -155,6 +155,15 @@ internal static class Mapper
string.Empty, // TODO: thumbnail?
imageMetadata.Image.State);
+ internal static RemoteStreamCardViewModel ProjectToViewModel(RemoteStreamMetadata remoteStreamMetadata) =>
+ new(
+ remoteStreamMetadata.RemoteStreamId,
+ remoteStreamMetadata.Title,
+ remoteStreamMetadata.OriginalTitle,
+ remoteStreamMetadata.SortTitle,
+ string.Empty, // TODO: thumbnail?
+ remoteStreamMetadata.RemoteStream.State);
+
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
new(
artistMetadata.ArtistId,
@@ -199,7 +208,9 @@ internal static class Mapper
.ToList(),
collection.MediaItems.OfType().Map(s => ProjectToViewModel(s.SongMetadata.Head()))
.ToList(),
- collection.MediaItems.OfType().Map(i => ProjectToViewModel(i.ImageMetadata.Head())).ToList())
+ collection.MediaItems.OfType().Map(i => ProjectToViewModel(i.ImageMetadata.Head())).ToList(),
+ collection.MediaItems.OfType().Map(i => ProjectToViewModel(i.RemoteStreamMetadata.Head()))
+ .ToList())
{ UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
internal static ActorCardViewModel ProjectToViewModel(
diff --git a/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs
index 3f09be715..fb55ea352 100644
--- a/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs
+++ b/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs
@@ -105,6 +105,12 @@ public class GetCollectionCardsHandler :
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Image).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
+ .Include(c => c.MediaItems)
+ .ThenInclude(i => (i as RemoteStream).RemoteStreamMetadata)
+ .ThenInclude(ovm => ovm.Artwork)
+ .Include(c => c.MediaItems)
+ .ThenInclude(i => (i as RemoteStream).MediaVersions)
+ .ThenInclude(mv => mv.MediaFiles)
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
diff --git a/ErsatzTV.Application/MediaCards/RemoteStreamCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/RemoteStreamCardResultsViewModel.cs
new file mode 100644
index 000000000..d856f4241
--- /dev/null
+++ b/ErsatzTV.Application/MediaCards/RemoteStreamCardResultsViewModel.cs
@@ -0,0 +1,5 @@
+using ErsatzTV.Core.Search;
+
+namespace ErsatzTV.Application.MediaCards;
+
+public record RemoteStreamCardResultsViewModel(int Count, List Cards, SearchPageMap PageMap);
diff --git a/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs b/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs
new file mode 100644
index 000000000..9b5e5f393
--- /dev/null
+++ b/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs
@@ -0,0 +1,20 @@
+using ErsatzTV.Core.Domain;
+
+namespace ErsatzTV.Application.MediaCards;
+
+public record RemoteStreamCardViewModel(
+ int RemoteStreamId,
+ string Title,
+ string Subtitle,
+ string SortTitle,
+ string Poster,
+ MediaItemState State) : MediaCardViewModel(
+ RemoteStreamId,
+ Title,
+ Subtitle,
+ SortTitle,
+ Poster,
+ State)
+{
+ public int CustomIndex { get; set; }
+}
diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs
index 37c725d00..21b0adb8e 100644
--- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs
+++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs
@@ -12,4 +12,5 @@ public record AddItemsToCollection(
List MusicVideoIds,
List OtherVideoIds,
List SongIds,
- List ImageIds) : IRequest>;
+ List ImageIds,
+ List RemoteStreamIds) : IRequest>;
diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs
index 1cf0ec36c..09fabe2d3 100644
--- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs
+++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs
@@ -60,6 +60,7 @@ public class AddItemsToCollectionHandler :
.Append(request.OtherVideoIds)
.Append(request.SongIds)
.Append(request.ImageIds)
+ .Append(request.RemoteStreamIds)
.ToList();
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToPlaylist.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToPlaylist.cs
index 1f71fc0a9..6d1e88fcd 100644
--- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToPlaylist.cs
+++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToPlaylist.cs
@@ -12,4 +12,5 @@ public record AddItemsToPlaylist(
List MusicVideoIds,
List OtherVideoIds,
List SongIds,
- List ImageIds) : IRequest>;
+ List ImageIds,
+ List RemoteStreamIds) : IRequest>;
diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollection.cs
new file mode 100644
index 000000000..27badc055
--- /dev/null
+++ b/ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollection.cs
@@ -0,0 +1,5 @@
+using ErsatzTV.Core;
+
+namespace ErsatzTV.Application.MediaCollections;
+
+public record AddMediaItemToCollection(int CollectionId, int MediaItemId) : IRequest>;
diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollectionHandler.cs
new file mode 100644
index 000000000..aa003d8bc
--- /dev/null
+++ b/ErsatzTV.Application/MediaCollections/Commands/AddMediaItemToCollectionHandler.cs
@@ -0,0 +1,83 @@
+using System.Threading.Channels;
+using ErsatzTV.Application.Playouts;
+using ErsatzTV.Application.Search;
+using ErsatzTV.Core;
+using ErsatzTV.Core.Domain;
+using ErsatzTV.Core.Interfaces.Repositories;
+using ErsatzTV.Core.Scheduling;
+using ErsatzTV.Infrastructure.Data;
+using ErsatzTV.Infrastructure.Extensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace ErsatzTV.Application.MediaCollections;
+
+public class AddMediaItemToCollectionHandler :
+ IRequestHandler>
+{
+ private readonly ChannelWriter _channel;
+ private readonly IDbContextFactory _dbContextFactory;
+ private readonly IMediaCollectionRepository _mediaCollectionRepository;
+ private readonly ChannelWriter _searchChannel;
+
+ public AddMediaItemToCollectionHandler(
+ IDbContextFactory dbContextFactory,
+ IMediaCollectionRepository mediaCollectionRepository,
+ ChannelWriter channel,
+ ChannelWriter searchChannel)
+ {
+ _dbContextFactory = dbContextFactory;
+ _mediaCollectionRepository = mediaCollectionRepository;
+ _channel = channel;
+ _searchChannel = searchChannel;
+ }
+
+ public async Task> Handle(
+ AddMediaItemToCollection request,
+ CancellationToken cancellationToken)
+ {
+ await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
+ Validation validation = await Validate(dbContext, request);
+ return await validation.Apply(parameters => ApplyAddMediaItemRequest(dbContext, parameters));
+ }
+
+ private async Task ApplyAddMediaItemRequest(TvContext dbContext, Parameters parameters)
+ {
+ parameters.Collection.MediaItems.Add(parameters.MediaItem);
+ if (await dbContext.SaveChangesAsync() > 0)
+ {
+ await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.MediaItem.Id]));
+
+ // refresh all playouts that use this collection
+ foreach (int playoutId in await _mediaCollectionRepository
+ .PlayoutIdsUsingCollection(parameters.Collection.Id))
+ {
+ await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
+ }
+ }
+
+ return Unit.Default;
+ }
+
+ private static async Task> Validate(
+ TvContext dbContext,
+ AddMediaItemToCollection request) =>
+ (await CollectionMustExist(dbContext, request), await ValidateMediaItem(dbContext, request))
+ .Apply((collection, episode) => new Parameters(collection, episode));
+
+ private static Task> CollectionMustExist(
+ TvContext dbContext,
+ AddMediaItemToCollection request) =>
+ dbContext.Collections
+ .Include(c => c.MediaItems)
+ .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
+ .Map(o => o.ToValidation("Collection does not exist."));
+
+ private static Task> ValidateMediaItem(
+ TvContext dbContext,
+ AddMediaItemToCollection request) =>
+ dbContext.MediaItems
+ .SelectOneAsync(m => m.Id, e => e.Id == request.MediaItemId)
+ .Map(o => o.ToValidation("MediaItem does not exist"));
+
+ private sealed record Parameters(Collection Collection, MediaItem MediaItem);
+}
diff --git a/ErsatzTV.Application/MediaItems/Mapper.cs b/ErsatzTV.Application/MediaItems/Mapper.cs
index 018dfa15b..49f55b5eb 100644
--- a/ErsatzTV.Application/MediaItems/Mapper.cs
+++ b/ErsatzTV.Application/MediaItems/Mapper.cs
@@ -32,6 +32,9 @@ internal static class Mapper
internal static NamedMediaItemViewModel ProjectToViewModel(Image image) =>
new(image.Id, image.ImageMetadata.HeadOrNone().Match(i => i.Title, () => "???"));
+ internal static RemoteStreamViewModel ProjectToViewModel(RemoteStream remoteStream) =>
+ new(remoteStream.Id, remoteStream.Url, remoteStream.Script);
+
private static string MovieTitle(Movie movie)
{
var title = "???";
diff --git a/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs
index 4d284be95..0a402878e 100644
--- a/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs
+++ b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs
@@ -56,6 +56,12 @@ public class GetMediaItemInfoHandler : IRequestHandler mv.Chapters)
.Include(i => (i as Image).MediaVersions)
.ThenInclude(mv => mv.Streams)
+ .Include(i => (i as RemoteStream).RemoteStreamMetadata)
+ .ThenInclude(mv => mv.Subtitles)
+ .Include(i => (i as RemoteStream).MediaVersions)
+ .ThenInclude(mv => mv.Chapters)
+ .Include(i => (i as RemoteStream).MediaVersions)
+ .ThenInclude(mv => mv.Streams)
.Include(i => (i as Song).SongMetadata)
.ThenInclude(mv => mv.Subtitles)
.Include(i => (i as Song).MediaVersions)
diff --git a/ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamById.cs b/ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamById.cs
new file mode 100644
index 000000000..0ca058e7c
--- /dev/null
+++ b/ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamById.cs
@@ -0,0 +1,5 @@
+using ErsatzTV.Core;
+
+namespace ErsatzTV.Application.MediaItems;
+
+public record GetRemoteStreamById(int RemoteStreamId) : IRequest