add songs libraries (#490)
* first pass at adding song libraries * start handling optional video * fix song playback * fix song transitions * add songs page to UI
This commit is contained in:
@@ -63,6 +63,11 @@
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#other_videos")">@_data.OtherVideoCards.Count Other Videos</MudLink>
|
||||
}
|
||||
|
||||
@if (_data.SongCards.Any())
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#songs")">@_data.SongCards.Count Songs</MudLink>
|
||||
}
|
||||
@if (SupportsCustomOrdering())
|
||||
{
|
||||
<div style="margin-left: auto">
|
||||
@@ -248,6 +253,30 @@
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_data.SongCards.Any())
|
||||
{
|
||||
<MudText GutterBottom="true"
|
||||
Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
|
||||
Songs
|
||||
</MudText>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (SongCardViewModel card in _data.SongCards.OrderBy(e => e.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
DeleteClicked="@RemoveSongFromCollection"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
@@ -307,6 +336,7 @@
|
||||
.Append(_data.ArtistCards.OrderBy(a => a.SortTitle))
|
||||
.Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle))
|
||||
.Append(_data.OtherVideoCards.OrderBy(ov => ov.SortTitle))
|
||||
.Append(_data.SongCards.OrderBy(s => s.SortTitle))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -404,6 +434,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveSongFromCollection(MediaCardViewModel vm)
|
||||
{
|
||||
if (vm is SongCardViewModel song)
|
||||
{
|
||||
var request = new RemoveItemsFromCollection(Id)
|
||||
{
|
||||
MediaItemIds = new List<int> { song.SongId }
|
||||
};
|
||||
|
||||
await RemoveItemsWithConfirmation("song", $"{song.Title}", request);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveItemsWithConfirmation(
|
||||
string entityType,
|
||||
string entityName,
|
||||
|
||||
@@ -99,7 +99,8 @@ namespace ErsatzTV.Pages
|
||||
_selectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId).ToList(),
|
||||
_selectedItems.OfType<ArtistCardViewModel>().Map(a => a.ArtistId).ToList(),
|
||||
_selectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(),
|
||||
_selectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList());
|
||||
_selectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList(),
|
||||
_selectedItems.OfType<SongCardViewModel>().Map(s => s.SongId).ToList());
|
||||
|
||||
protected async Task AddItemsToCollection(
|
||||
List<int> movieIds,
|
||||
@@ -109,10 +110,11 @@ namespace ErsatzTV.Pages
|
||||
List<int> artistIds,
|
||||
List<int> musicVideoIds,
|
||||
List<int> otherVideoIds,
|
||||
List<int> songIds,
|
||||
string entityName = "selected items")
|
||||
{
|
||||
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
|
||||
musicVideoIds.Count + otherVideoIds.Count;
|
||||
musicVideoIds.Count + otherVideoIds.Count + songIds.Count;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{ { "EntityType", count.ToString() }, { "EntityName", entityName } };
|
||||
@@ -130,7 +132,8 @@ namespace ErsatzTV.Pages
|
||||
episodeIds,
|
||||
artistIds,
|
||||
musicVideoIds,
|
||||
otherVideoIds);
|
||||
otherVideoIds,
|
||||
songIds);
|
||||
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink>
|
||||
}
|
||||
|
||||
if (_songs?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
|
||||
}
|
||||
<div style="margin-left: auto">
|
||||
<MudTooltip Text="Add All To Collection">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
@@ -282,6 +287,34 @@
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_songs?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
|
||||
Songs
|
||||
</MudText>
|
||||
@if (_songs.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
AddToCollectionClicked="@AddToCollection"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
@@ -292,6 +325,7 @@
|
||||
private TelevisionEpisodeCardResultsViewModel _episodes;
|
||||
private MusicVideoCardResultsViewModel _musicVideos;
|
||||
private OtherVideoCardResultsViewModel _otherVideos;
|
||||
private SongCardResultsViewModel _songs;
|
||||
private ArtistCardResultsViewModel _artists;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -305,6 +339,7 @@
|
||||
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50));
|
||||
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50));
|
||||
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50));
|
||||
_songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50));
|
||||
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50));
|
||||
}
|
||||
}
|
||||
@@ -320,6 +355,7 @@
|
||||
.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))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -532,6 +568,17 @@
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetSongsLink()
|
||||
{
|
||||
var uri = "/media/music/songs/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));
|
||||
@@ -543,6 +590,7 @@
|
||||
results.ArtistIds,
|
||||
results.MusicVideoIds,
|
||||
results.OtherVideoIds,
|
||||
results.SongIds,
|
||||
"search results");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
@page "/music/songs"
|
||||
@page "/music/songs/page/{PageNumber:int}"
|
||||
@using LanguageExt.UnsafeValueAccess
|
||||
@using ErsatzTV.Application.MediaCards
|
||||
@using ErsatzTV.Application.MediaCollections
|
||||
@using ErsatzTV.Application.MediaCollections.Commands
|
||||
@using ErsatzTV.Application.Search.Queries
|
||||
@using ErsatzTV.Extensions
|
||||
@using Unit = LanguageExt.Unit
|
||||
@inherits MultiSelectBase<SongList>
|
||||
@inject NavigationManager _navigationManager
|
||||
@inject ChannelWriter<IBackgroundServiceRequest> _channel
|
||||
|
||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
|
||||
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
|
||||
@if (IsSelectMode())
|
||||
{
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
|
||||
<div style="margin-left: auto">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(_ => AddSelectionToCollection())">
|
||||
Add To Collection
|
||||
</MudButton>
|
||||
<MudButton Class="ml-3"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Check"
|
||||
OnClick="@(_ => ClearSelection())">
|
||||
Clear Selection
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Style="margin-bottom: auto; margin-top: auto; width: 33%">@_query</MudText>
|
||||
<div style="max-width: 300px; width: 33%;">
|
||||
<MudPaper Style="align-items: center; display: flex; justify-content: center;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
|
||||
OnClick="@PrevPage"
|
||||
Disabled="@(PageNumber <= 1)">
|
||||
</MudIconButton>
|
||||
<MudText Style="flex-grow: 1"
|
||||
Align="Align.Center">
|
||||
@Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
|
||||
OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)">
|
||||
</MudIconButton>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</MudPaper>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px">
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
<FragmentLetterAnchor TCard="SongCardViewModel" Cards="@_data.Cards">
|
||||
<MediaCard Data="@context"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
AddToCollectionClicked="@AddToCollection"
|
||||
SelectClicked="@(e => SelectClicked(context, e))"
|
||||
IsSelected="@IsSelected(context)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
</FragmentLetterAnchor>
|
||||
</MudContainer>
|
||||
</MudContainer>
|
||||
@if (_data.PageMap.IsSome)
|
||||
{
|
||||
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()"
|
||||
BaseUri="/music/songs"
|
||||
Query="@_query"/>
|
||||
}
|
||||
|
||||
@code {
|
||||
private static int PageSize => 100;
|
||||
|
||||
[Parameter]
|
||||
public int PageNumber { get; set; }
|
||||
|
||||
private SongCardResultsViewModel _data;
|
||||
private string _query;
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
if (PageNumber == 0)
|
||||
{
|
||||
PageNumber = 1;
|
||||
}
|
||||
|
||||
_query = _navigationManager.Uri.GetSearchQuery();
|
||||
return RefreshData();
|
||||
}
|
||||
|
||||
protected override async Task RefreshData()
|
||||
{
|
||||
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:song" : $"type:song AND ({_query})";
|
||||
_data = await Mediator.Send(new QuerySearchIndexSongs(searchQuery, PageNumber, PageSize));
|
||||
}
|
||||
|
||||
private void PrevPage()
|
||||
{
|
||||
var uri = $"/music/songs/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 = $"/music/songs/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 SongCardViewModel song)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "song" }, { "EntityName", song.Title } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
||||
{
|
||||
var request = new AddSongToCollection(collection.Id, song.SongId);
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
Left: error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error adding song to collection: {error.Value}");
|
||||
Logger.LogError("Unexpected error adding song to collection: {Error}", error.Value);
|
||||
},
|
||||
Right: _ => Snackbar.Add($"Added {song.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,6 +54,7 @@
|
||||
<MudNavLink Href="/media/movies">Movies</MudNavLink>
|
||||
<MudNavLink Href="/media/music/artists">Music</MudNavLink>
|
||||
<MudNavLink Href="/media/other/videos">Other Videos</MudNavLink>
|
||||
<MudNavLink Href="/music/songs">Songs</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="Lists" Expanded="true">
|
||||
<MudNavLink Href="/media/collections">Collections</MudNavLink>
|
||||
|
||||
@@ -272,6 +272,7 @@ namespace ErsatzTV
|
||||
services.AddScoped<IArtistRepository, ArtistRepository>();
|
||||
services.AddScoped<IMusicVideoRepository, MusicVideoRepository>();
|
||||
services.AddScoped<IOtherVideoRepository, OtherVideoRepository>();
|
||||
services.AddScoped<ISongRepository, SongRepository>();
|
||||
services.AddScoped<ILibraryRepository, LibraryRepository>();
|
||||
services.AddScoped<IMetadataRepository, MetadataRepository>();
|
||||
services.AddScoped<IArtworkRepository, ArtworkRepository>();
|
||||
@@ -286,6 +287,7 @@ namespace ErsatzTV
|
||||
services.AddScoped<ITelevisionFolderScanner, TelevisionFolderScanner>();
|
||||
services.AddScoped<IMusicVideoFolderScanner, MusicVideoFolderScanner>();
|
||||
services.AddScoped<IOtherVideoFolderScanner, OtherVideoFolderScanner>();
|
||||
services.AddScoped<ISongFolderScanner, SongFolderScanner>();
|
||||
services.AddScoped<IPlexMovieLibraryScanner, PlexMovieLibraryScanner>();
|
||||
services.AddScoped<IPlexTelevisionLibraryScanner, PlexTelevisionLibraryScanner>();
|
||||
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();
|
||||
|
||||
Reference in New Issue
Block a user