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
This commit is contained in:
Jason Dove
2025-07-20 16:10:32 +00:00
committed by GitHub
parent c29788bc3f
commit 5c43ae47b1
86 changed files with 39340 additions and 101 deletions
@@ -2,6 +2,7 @@
using CliWrap;
using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Plex;
using ErsatzTV.Application.Streaming;
using ErsatzTV.Application.Subtitles.Queries;
@@ -54,6 +55,60 @@ public class InternalController : ControllerBase
}
}
[HttpGet("ffmpeg/remote-stream/{remoteStreamId}")]
public async Task<IActionResult> GetRemoteStream(int remoteStreamId, CancellationToken cancellationToken)
{
Option<RemoteStreamViewModel> maybeRemoteStream =
await _mediator.Send(new GetRemoteStreamById(remoteStreamId), cancellationToken);
foreach (RemoteStreamViewModel remoteStream in maybeRemoteStream)
{
if (!string.IsNullOrWhiteSpace(remoteStream.Url))
{
return new RedirectResult(remoteStream.Url);
}
if (!string.IsNullOrWhiteSpace(remoteStream.Script))
{
string[] split = remoteStream.Script.Split(" ");
if (split.Length > 0)
{
Command command = Cli.Wrap(split.Head());
if (split.Length > 1)
{
command = command.WithArguments(split.Tail());
}
var process = new FFmpegProcess
{
StartInfo = new ProcessStartInfo
{
FileName = command.TargetFilePath,
Arguments = command.Arguments,
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true
}
};
HttpContext.Response.RegisterForDispose(process);
foreach ((string key, string value) in command.EnvironmentVariables)
{
process.StartInfo.Environment[key] = value;
}
process.Start();
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
}
}
}
return NotFound();
}
[HttpGet("/media/plex/{plexMediaSourceId:int}/{*path}")]
public async Task<IActionResult> GetPlexMedia(
int plexMediaSourceId,
+43
View File
@@ -86,6 +86,11 @@
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")">@_data.ImageCards.Count Images</MudLink>
}
@if (_data?.RemoteStreamCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#remote_streams")">@_data.RemoteStreamCards.Count Remote Streams</MudLink>
}
</div>
@if (SupportsCustomOrdering())
{
@@ -329,6 +334,31 @@
}
</MudStack>
}
@if (_data?.RemoteStreamCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "remote_streams" } })">
Remote Streams
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (RemoteStreamCardViewModel card in _data.RemoteStreamCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveRemoteStreamFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
</MudContainer>
</div>
</MudForm>
@@ -522,6 +552,19 @@
}
}
private async Task RemoveRemoteStreamFromCollection(MediaCardViewModel vm)
{
if (vm is RemoteStreamCardViewModel remoteStream)
{
var request = new RemoveItemsFromCollection(Id)
{
MediaItemIds = [remoteStream.RemoteStreamId]
};
await RemoveItemsWithConfirmation("remote stream", $"{remoteStream.Title}", request);
}
}
private async Task RemoveItemsWithConfirmation(
string entityType,
string entityName,
+17 -1
View File
@@ -48,7 +48,23 @@
<MudTd DataLabel="Server Name">@context.MediaSourceName</MudTd>
}
<MudTd DataLabel="Library Name">@context.Name</MudTd>
<MudTd DataLabel="Media Kind">@context.MediaKind</MudTd>
<MudTd DataLabel="Media Kind">
@switch (context.MediaKind)
{
case LibraryMediaKind.MusicVideos:
@:Music Videos
break;
case LibraryMediaKind.OtherVideos:
@:Other Videos
break;
case LibraryMediaKind.RemoteStreams:
@:Remote Streams
break;
default:
@(context.MediaKind)
break;
}
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
@if (Locker.IsLibraryLocked(context.Id))
+1
View File
@@ -97,6 +97,7 @@
LibraryMediaKind.OtherVideos => "Other Videos",
LibraryMediaKind.Songs => "Songs",
LibraryMediaKind.Images => "Images",
LibraryMediaKind.RemoteStreams => "Remote Streams",
_ => "Unknown"
};
+5 -3
View File
@@ -9,9 +9,9 @@
@inject ISnackbar Snackbar
@inject NavigationManager NavigationManager
<MudForm Model="@_model" @bind-IsValid="@_success" Style="max-height: 100%">
<MudForm @ref="_form" Model="@_model" @bind-IsValid="@_success" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="SaveChangesAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Local Library" : "Add Local Library")</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="@SaveChangesAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Local Library" : "Add Local Library")</MudButton>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@@ -91,6 +91,7 @@
private readonly LocalLibraryEditViewModel _model = new();
private readonly LocalLibraryPathEditViewModel _newPath = new();
private MudForm _form;
private bool _success;
private bool IsEdit => Id != 0;
@@ -120,7 +121,7 @@
_model.HasChanges = true;
_model.Name = "New Local Library";
_model.MediaKind = LibraryMediaKind.Movies;
_model.Paths = new List<LocalLibraryPathEditViewModel>();
_model.Paths = [];
}
}
@@ -219,6 +220,7 @@
private async Task SaveChangesAsync()
{
await _form.Validate();
if (_success)
{
Either<BaseError, LocalLibraryViewModel> result = IsEdit
+12 -12
View File
@@ -13,13 +13,7 @@ namespace ErsatzTV.Pages;
public class MultiSelectBase<T> : FragmentNavigationBase
{
private Option<MediaCardViewModel> _recentlySelected;
public MultiSelectBase()
{
_recentlySelected = None;
SelectedItems = [];
}
private Option<MediaCardViewModel> _recentlySelected = None;
[Inject]
protected IDialogService Dialog { get; set; }
@@ -33,7 +27,7 @@ public class MultiSelectBase<T> : FragmentNavigationBase
[Inject]
protected IMediator Mediator { get; set; }
protected System.Collections.Generic.HashSet<MediaCardViewModel> SelectedItems { get; }
protected System.Collections.Generic.HashSet<MediaCardViewModel> SelectedItems { get; } = [];
protected bool IsSelected(MediaCardViewModel card) =>
SelectedItems.Contains(card);
@@ -91,7 +85,8 @@ public class MultiSelectBase<T> : FragmentNavigationBase
SelectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(),
SelectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList(),
SelectedItems.OfType<SongCardViewModel>().Map(s => s.SongId).ToList(),
SelectedItems.OfType<ImageCardViewModel>().Map(i => i.ImageId).ToList());
SelectedItems.OfType<ImageCardViewModel>().Map(i => i.ImageId).ToList(),
SelectedItems.OfType<RemoteStreamCardViewModel>().Map(i => i.RemoteStreamId).ToList());
protected Task AddSelectionToPlaylist() => AddItemsToPlaylist(
SelectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
@@ -102,7 +97,8 @@ public class MultiSelectBase<T> : FragmentNavigationBase
SelectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(),
SelectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList(),
SelectedItems.OfType<SongCardViewModel>().Map(s => s.SongId).ToList(),
SelectedItems.OfType<ImageCardViewModel>().Map(i => i.ImageId).ToList());
SelectedItems.OfType<ImageCardViewModel>().Map(i => i.ImageId).ToList(),
SelectedItems.OfType<RemoteStreamCardViewModel>().Map(i => i.RemoteStreamId).ToList());
protected async Task AddItemsToCollection(
List<int> movieIds,
@@ -114,6 +110,7 @@ public class MultiSelectBase<T> : FragmentNavigationBase
List<int> otherVideoIds,
List<int> songIds,
List<int> imageIds,
List<int> remoteStreamIds,
string entityName = "selected items")
{
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
@@ -138,7 +135,8 @@ public class MultiSelectBase<T> : FragmentNavigationBase
musicVideoIds,
otherVideoIds,
songIds,
imageIds);
imageIds,
remoteStreamIds);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match(
@@ -197,6 +195,7 @@ public class MultiSelectBase<T> : FragmentNavigationBase
List<int> otherVideoIds,
List<int> songIds,
List<int> imageIds,
List<int> remoteStreamIds,
string entityName = "selected items")
{
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
@@ -221,7 +220,8 @@ public class MultiSelectBase<T> : FragmentNavigationBase
musicVideoIds,
otherVideoIds,
songIds,
imageIds);
imageIds,
remoteStreamIds);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match(
+132
View File
@@ -0,0 +1,132 @@
@page "/media/remote/streams"
@page "/media/remote/streams/page/{PageNumber:int}"
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.Search
@using ErsatzTV.Extensions
@inherits MultiSelectBase<RemoteStreamList>
@inject NavigationManager NavigationManager
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;">
<MediaCardPager Query="@_query"
PageNumber="@PageNumber"
PageSize="@PageSize"
TotalCount="@_data.Count"
NextPage="@NextPage"
PrevPage="@PrevPage"
AddSelectionToCollection="@AddSelectionToCollection"
AddSelectionToPlaylist="@AddSelectionToPlaylist"
ClearSelection="@ClearSelection"
IsSelectMode="@IsSelectMode"
SelectionLabel="@SelectionLabel"/>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudStack Row="true" Wrap="Wrap.Wrap">
<FragmentLetterAnchor TCard="RemoteStreamCardViewModel" Cards="@_data.Cards">
<MediaCard Data="@context"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(context, e))"
IsSelected="@IsSelected(context)"
IsSelectMode="@IsSelectMode()"/>
</FragmentLetterAnchor>
</MudStack>
</MudContainer>
</div>
</MudForm>
@if (_data.PageMap is not null)
{
<LetterBar PageMap="@_data.PageMap"
BaseUri="media/remote/streams"
Query="@_query"/>
}
@code {
private static int PageSize => 100;
[Parameter]
public int PageNumber { get; set; }
private RemoteStreamCardResultsViewModel _data = new(0, new List<RemoteStreamCardViewModel>(), null);
private string _query;
protected override async Task OnParametersSetAsync()
{
if (PageNumber == 0)
{
PageNumber = 1;
}
_query = NavigationManager.Uri.GetSearchQuery();
await RefreshData();
}
protected override async Task RefreshData()
{
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:remote_stream" : $"type:remote_stream AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexRemoteStreams(searchQuery, PageNumber, PageSize), CancellationToken);
}
private void PrevPage()
{
var uri = $"media/remote/streams/page/{PageNumber - 1}";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
NavigationManager.NavigateTo(uri);
}
private void NextPage()
{
var uri = $"media/remote/streams/page/{PageNumber + 1}";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
NavigationManager.NavigateTo(uri);
}
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{
List<MediaCardViewModel> GetSortedItems()
{
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
}
SelectClicked(GetSortedItems, card, e);
}
private async Task AddToCollection(MediaCardViewModel card)
{
if (card is RemoteStreamCardViewModel remoteStream)
{
var parameters = new DialogParameters { { "EntityType", "remote stream" }, { "EntityName", remoteStream.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddMediaItemToCollection(collection.Id, remoteStream.RemoteStreamId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match(
Left: error =>
{
Snackbar.Add($"Unexpected error adding remote stream to collection: {error.Value}");
Logger.LogError("Unexpected error adding remote stream to collection: {Error}", error.Value);
},
Right: _ => Snackbar.Add($"Added {remoteStream.Title} to collection {collection.Name}", Severity.Success));
}
}
}
}
+81 -1
View File
@@ -95,6 +95,11 @@
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink>
}
@if (_remoteStreams?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#remote_streams")" Style="margin-bottom: auto; margin-top: auto">@_remoteStreams.Count Remote Streams</MudLink>
}
<div class="flex-grow-1 d-none d-md-flex"></div>
<div>
<MudTooltip Text="Add All To Collection">
@@ -376,6 +381,33 @@
}
</MudStack>
}
@if (_remoteStreams?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "remote_streams" } })">
Remote Streams
</MudText>
@if (_remoteStreams.Count > 50)
{
<MudLink Href="@GetRemoteStreamsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (RemoteStreamCardViewModel card in _remoteStreams.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
</MudContainer>
</div>
</MudForm>
@@ -390,6 +422,7 @@
private OtherVideoCardResultsViewModel _otherVideos;
private SongCardResultsViewModel _songs;
private ImageCardResultsViewModel _images;
private RemoteStreamCardResultsViewModel _remoteStreams;
private ArtistCardResultsViewModel _artists;
private PersistingComponentStateSubscription _persistingSubscription;
@@ -472,6 +505,15 @@
_images = restoredImages;
}
if (!ApplicationState.TryTakeFromJson("_remoteStreams", out RemoteStreamCardResultsViewModel restoredRemoteStreams))
{
_remoteStreams = await Mediator.Send(new QuerySearchIndexRemoteStreams($"type:remote_stream AND ({_query})", 1, 50), CancellationToken);
}
else
{
_remoteStreams = restoredRemoteStreams;
}
if (!ApplicationState.TryTakeFromJson("_artists", out ArtistCardResultsViewModel restoredArtists))
{
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken);
@@ -492,6 +534,8 @@
ApplicationState.PersistAsJson("_musicVideos", _musicVideos);
ApplicationState.PersistAsJson("_otherVideos", _otherVideos);
ApplicationState.PersistAsJson("_songs", _songs);
ApplicationState.PersistAsJson("_images", _images);
ApplicationState.PersistAsJson("_remoteStreams", _remoteStreams);
ApplicationState.PersistAsJson("_artists", _artists);
return Task.CompletedTask;
@@ -514,7 +558,8 @@
.Append(_artists.Cards.OrderBy(a => a.SortTitle))
.Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle))
.Append(_otherVideos.Cards.OrderBy(ov => ov.SortTitle))
.Append(_songs.Cards.OrderBy(ov => ov.SortTitle))
.Append(_images.Cards.OrderBy(ov => ov.SortTitle))
.Append(_remoteStreams.Cards.OrderBy(ov => ov.SortTitle))
.ToList();
}
@@ -711,6 +756,27 @@
Right: _ => Snackbar.Add($"Added {image.Title} to collection {collection.Name}", Severity.Success));
}
}
if (card is RemoteStreamCardViewModel remoteStream)
{
var parameters = new DialogParameters { { "EntityType", "remote stream" }, { "EntityName", remoteStream.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddMediaItemToCollection(collection.Id, remoteStream.RemoteStreamId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match(
Left: error =>
{
Snackbar.Add($"Unexpected error adding remote stream to collection: {error.Value}");
Logger.LogError("Unexpected error adding remote stream to collection: {Error}", error.Value);
},
Right: _ => Snackbar.Add($"Added {remoteStream.Title} to collection {collection.Name}", Severity.Success));
}
}
}
private string GetMoviesLink()
@@ -821,6 +887,18 @@
return uri;
}
private string GetRemoteStreamsLink()
{
var uri = "media/remote/streams/page/1";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
return uri;
}
private async Task AddAllToCollection(MouseEventArgs _)
{
SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query), CancellationToken);
@@ -834,6 +912,7 @@
results.OtherVideoIds,
results.SongIds,
results.ImageIds,
results.RemoteStreamIds,
"search results");
}
@@ -850,6 +929,7 @@
results.OtherVideoIds,
results.SongIds,
results.ImageIds,
results.RemoteStreamIds,
"search results");
}
+62 -2
View File
@@ -85,6 +85,11 @@
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink>
}
@if (_remoteStreams?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#remote_streams")" Style="margin-bottom: auto; margin-top: auto">@_remoteStreams.Count Remote Streams</MudLink>
}
<div class="flex-grow-1 d-none d-md-flex"></div>
<div>
<MudButton Variant="@Variant.Filled"
@@ -340,7 +345,7 @@
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Songs
Images
</MudText>
@if (_images.Count > 50)
{
@@ -363,6 +368,34 @@
}
</MudStack>
}
@if (_remoteStreams?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "remote_streams" } })">
Remote Streams
</MudText>
@if (_remoteStreams.Count > 50)
{
<MudLink Href="@GetRemoteStreamsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (RemoteStreamCardViewModel card in _remoteStreams.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
</MudContainer>
</div>
</MudForm>
@@ -379,6 +412,7 @@
private OtherVideoCardResultsViewModel _otherVideos;
private SongCardResultsViewModel _songs;
private ImageCardResultsViewModel _images;
private RemoteStreamCardResultsViewModel _remoteStreams;
private ArtistCardResultsViewModel _artists;
private PersistingComponentStateSubscription _persistingSubscription;
@@ -412,6 +446,7 @@
ApplicationState.PersistAsJson("_otherVideos", _otherVideos);
ApplicationState.PersistAsJson("_songs", _songs);
ApplicationState.PersistAsJson("_images", _images);
ApplicationState.PersistAsJson("_remoteStreams", _remoteStreams);
ApplicationState.PersistAsJson("_artists", _artists);
return Task.CompletedTask;
@@ -468,6 +503,11 @@
_images = await Mediator.Send(new QuerySearchIndexImages($"type:image AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_remoteStreams", out _remoteStreams))
{
_remoteStreams = await Mediator.Send(new QuerySearchIndexRemoteStreams($"type:remote_stream AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_artists", out _artists))
{
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), _cts.Token);
@@ -491,6 +531,7 @@
.Append(_otherVideos.Cards.OrderBy(ov => ov.SortTitle))
.Append(_songs.Cards.OrderBy(ov => ov.SortTitle))
.Append(_images.Cards.OrderBy(i => i.SortTitle))
.Append(_remoteStreams.Cards.OrderBy(rs => rs.SortTitle))
.ToList();
}
@@ -605,6 +646,18 @@
return uri;
}
private string GetRemoteStreamsLink()
{
var uri = "media/remote/streams/page/1";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
return uri;
}
private Task DeleteFromDatabase() => DeleteItemsFromDatabase(
SelectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
SelectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(),
@@ -614,7 +667,8 @@
SelectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(),
SelectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList(),
SelectedItems.OfType<SongCardViewModel>().Map(s => s.SongId).ToList(),
SelectedItems.OfType<ImageCardViewModel>().Map(i => i.ImageId).ToList());
SelectedItems.OfType<ImageCardViewModel>().Map(i => i.ImageId).ToList(),
SelectedItems.OfType<RemoteStreamCardViewModel>().Map(i => i.RemoteStreamId).ToList());
private async Task DeleteItemsFromDatabase(
List<int> movieIds,
@@ -626,6 +680,7 @@
List<int> otherVideoIds,
List<int> songIds,
List<int> imageIds,
List<int> remoteStreamIds,
string entityName = "selected items")
{
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
@@ -648,6 +703,7 @@
.Append(otherVideoIds)
.Append(songIds)
.Append(imageIds)
.Append(remoteStreamIds)
.ToList());
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);
@@ -709,6 +765,10 @@
request = new DeleteItemsFromDatabase([image.ImageId]);
await DeleteItemsWithConfirmation("image", $"{image.Title} ({image.Subtitle})", request);
break;
case RemoteStreamCardViewModel remoteStream:
request = new DeleteItemsFromDatabase([remoteStream.RemoteStreamId]);
await DeleteItemsWithConfirmation("remote stream", $"{remoteStream.Title} ({remoteStream.Subtitle})", request);
break;
}
}
@@ -54,6 +54,7 @@ public class DatabaseCleanerService(
and Id not in (select Id from `Song`)
and Id not in (select Id from `Artist`)
and Id not in (select Id from `Image`)
and Id not in (select Id from `RemoteStream`)
""");
private static async Task GenerateFallbackMetadata(
+1
View File
@@ -123,6 +123,7 @@
<MudNavLink Href="media/other/videos">Other Videos</MudNavLink>
<MudNavLink Href="media/music/songs">Songs</MudNavLink>
<MudNavLink Href="media/browser/images">Images</MudNavLink>
<MudNavLink Href="media/remote/streams">Remote Streams</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Lists">
<MudNavLink Href="media/collections">Collections</MudNavLink>
+1
View File
@@ -668,6 +668,7 @@ public class Startup
services.AddScoped<IOtherVideoRepository, OtherVideoRepository>();
services.AddScoped<ISongRepository, SongRepository>();
services.AddScoped<IImageRepository, ImageRepository>();
services.AddScoped<IRemoteStreamRepository, RemoteStreamRepository>();
services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddScoped<IMetadataRepository, MetadataRepository>();
services.AddScoped<IArtworkRepository, ArtworkRepository>();