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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
LibraryMediaKind.OtherVideos => "Other Videos",
|
||||
LibraryMediaKind.Songs => "Songs",
|
||||
LibraryMediaKind.Images => "Images",
|
||||
LibraryMediaKind.RemoteStreams => "Remote Streams",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user