Доработан поиск артиста

This commit is contained in:
FrigaT
2026-04-16 18:53:14 +03:00
parent 5a8ae3d680
commit 280c164626
12 changed files with 203 additions and 119 deletions

View File

@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PlaylistShared.Api.Entities; using PlaylistShared.Api.Entities;
using PlaylistShared.Api.Extensions;
using PlaylistShared.Api.Services; using PlaylistShared.Api.Services;
using PlaylistShared.Shared; using PlaylistShared.Shared;
using PlaylistShared.Shared.Yandex; using PlaylistShared.Shared.Yandex;
@@ -87,7 +88,13 @@ public class AudioController : ControllerBase
{ {
Title = track.Title, Title = track.Title,
CoverUri = track.CoverUri, CoverUri = track.CoverUri,
Artists = track.Artists.Select(t => t.Name).ToList(), Artists = track.Artists.Select(a => new YandexArtist
{
Id = a.Id,
Name = a.Name,
CoverUrl = a.Cover.GetUrl(),
Description = a.Description?.Text ?? string.Empty,
}).ToList(),
DurationMs = track.DurationMs, DurationMs = track.DurationMs,
})); }));
} }

View File

@@ -168,13 +168,19 @@ public class SharedPlaylistController : ControllerBase
{ {
return new YandexPlaylistData return new YandexPlaylistData
{ {
Title = playlist.Title ?? "", Title = playlist.Title,
Description = playlist.Description ?? "", Description = playlist.Description,
Tracks = playlist.Tracks?.Select(t => new YandexTrack Tracks = playlist.Tracks.Select(t => new YandexTrack
{ {
TrackId = t.Track?.Id ?? "", TrackId = t.Track.Id,
Title = t.Track?.Title ?? "", Title = t.Track.Title,
Artists = t.Track?.Artists?.Select(a => a.Name).ToList() ?? new List<string>(), Artists = t.Track.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
DurationMs = (int)(t.Track?.DurationMs ?? 0), DurationMs = (int)(t.Track?.DurationMs ?? 0),
CoverUri = t.Track?.CoverUri ?? "" CoverUri = t.Track?.CoverUri ?? ""
}).ToList() ?? new List<YandexTrack>() }).ToList() ?? new List<YandexTrack>()

View File

@@ -30,7 +30,7 @@ public class YandexSearchController : ControllerBase
public async Task<ActionResult<ApiResponse<YandexSearchResult>>> SearchQuery( public async Task<ActionResult<ApiResponse<YandexSearchResult>>> SearchQuery(
[FromQuery] string query, [FromQuery] string query,
[FromQuery] int limit = 20, [FromQuery] int limit = 20,
[FromQuery] TrackSearchType? searchType = TrackSearchType.All, [FromQuery] TrackSearchType searchType = TrackSearchType.All,
[FromQuery] bool byId = false, [FromQuery] bool byId = false,
[FromQuery] string? shared_id = null) [FromQuery] string? shared_id = null)
{ {
@@ -75,7 +75,7 @@ public class YandexSearchController : ControllerBase
if (byId) if (byId)
{ {
results = await _yandexService.SearchTracksByIdAsync(user, query, searchType.Value, limit); results = await _yandexService.SearchTracksByIdAsync(user, query, searchType, limit);
} }
else else
{ {

View File

@@ -27,7 +27,7 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
<PackageReference Include="YandexMusic" Version="0.0.7" /> <PackageReference Include="YandexMusic" Version="0.0.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -137,7 +137,13 @@ public class YandexMusicService
{ {
TrackId = t.Id, TrackId = t.Id,
Title = t.Title, Title = t.Title,
Artists = t.Artists?.Select(a => a.Name).ToList() ?? new List<string>(), Artists = t.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = t.CoverUri, CoverUri = t.CoverUri,
DurationMs = t.DurationMs, DurationMs = t.DurationMs,
}).ToList(), }).ToList(),
@@ -149,7 +155,7 @@ public class YandexMusicService
OwnerUid = p.Owner?.Uid ?? string.Empty, OwnerUid = p.Owner?.Uid ?? string.Empty,
Title = p.Title, Title = p.Title,
Description = p.Description, Description = p.Description,
CoverUrl = p.CoverUri, CoverUrl = string.IsNullOrEmpty(p.CoverUri) ? p.Cover.GetUrl() : p.CoverUri,
TrackCount = p.TrackCount, TrackCount = p.TrackCount,
}).ToList(), }).ToList(),
@@ -185,43 +191,114 @@ public class YandexMusicService
int limit = 20 int limit = 20
) )
{ {
YandexSearchResult result = new();
var client = await CreateClientAsync(user); var client = await CreateClientAsync(user);
if (client == null) return new YandexSearchResult(); if (client == null) return result;
var ySerchType = searchType switch if (searchType == TrackSearchType.All)
{ {
TrackSearchType.Artist => YandexMusic.API.Models.Common.YSearchType.Artist, throw new Exception("Для поиска по ID необходимо указать конкретный тип (трек, альбом, исполнитель или плейлист).");
TrackSearchType.Album => YandexMusic.API.Models.Common.YSearchType.Album,
TrackSearchType.Playlist => YandexMusic.API.Models.Common.YSearchType.Playlist,
TrackSearchType.Track => YandexMusic.API.Models.Common.YSearchType.Track,
_ => YandexMusic.API.Models.Common.YSearchType.All
};
IEnumerable<YTrack> searchResult = searchType switch
{
TrackSearchType.Playlist => (await client.GetPlaylistAsync(id)).Tracks.Select(t => t.Track),
TrackSearchType.Track => (await client.GetTracksAsync([id])),
TrackSearchType.Album => (await client.GetAlbumAsync(id)).Volumes.SelectMany(t => t),
TrackSearchType.Artist => (await client.GetArtistAsync(id)).Albums.SelectMany(t => t.Volumes.SelectMany(v => v)),
_ => new List<YTrack>()
};
if (searchType != TrackSearchType.Track)
{
searchResult = searchResult.Distinct();
if (limit > 0) searchResult = searchResult.Take(limit);
} }
return new YandexSearchResult() else if (searchType == TrackSearchType.Track)
{ {
Tracks = searchResult.Select(t => new YandexTrack var track = await client.GetTrackAsync(id);
if (track != null)
{ {
TrackId = t.Id, result.Tracks = new List<YandexTrack>()
Title = t.Title, {
Artists = t.Artists?.Select(a => a.Name).ToList() ?? new List<string>(), new()
CoverUri = t.CoverUri ?? string.Empty, {
DurationMs = t.DurationMs, TrackId = track.Id,
}).ToList(), Title = track.Title,
}; Artists = track.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = track.CoverUri ?? string.Empty,
DurationMs = track.DurationMs,
}
};
}
}
else if (searchType == TrackSearchType.Album)
{
var album = await client.GetAlbumAsync(id);
if (album != null)
{
result.Tracks = album.Volumes.SelectMany(v => v).Select(t => new YandexTrack
{
TrackId = t.Id,
Title = t.Title,
Artists = t.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = t.CoverUri ?? string.Empty,
DurationMs = t.DurationMs,
}).ToList();
}
}
else if (searchType == TrackSearchType.Artist)
{
var artist = await client.GetArtistAsync(id);
if (artist != null)
{
result.Albums = artist.Albums.Select(a => new YandexAlbum()
{
Id = a.Id,
Title = a.Title,
Artists = a.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUrl = string.IsNullOrEmpty(a.CoverUri) ? a.Cover.GetUrl() : a.CoverUri,
Description = a.Description,
}).ToList();
result.Playlists = artist.Playlists.Select(p => new YandexPlaylist
{
Uuid = p.PlaylistUuid,
Kind = p.Kind,
OwnerUid = p.Owner?.Uid ?? string.Empty,
Title = p.Title,
Description = p.Description,
CoverUrl = p.Cover.GetUrl(),
TrackCount = p.TrackCount,
}).ToList();
result.Tracks = artist.PopularTracks.Select(t => new YandexTrack
{
TrackId = t.Id,
Title = t.Title,
Artists = t.Artists.Select(a => new YandexArtist()
{
Id = a.Id,
Name = a.Name,
CoverUrl = a.Cover.GetUrl(),
Description = a.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = t.CoverUri ?? string.Empty,
DurationMs = t.DurationMs,
}).ToList();
}
}
return result;
} }
} }

View File

@@ -12,7 +12,7 @@
@if (CanPlay && (_isHovered || IsCurrentTrackPlaying)) @if (CanPlay && (_isHovered || IsCurrentTrackPlaying))
{ {
<MudItem class="play-overlay" <MudItem class="play-overlay"
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; border-radius: 4px;"> style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: opacity 0.2s ease; cursor: pointer;">
<MudIconButton Icon="@(IsCurrentTrackPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)" <MudIconButton Icon="@(IsCurrentTrackPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
Color="Color.Inherit" Color="Color.Inherit"
Size="Size.Large" Size="Size.Large"

View File

@@ -40,14 +40,14 @@
{ {
<MudExpansionPanels> <MudExpansionPanels>
@* Секция исполнителей *@ @* Секция исполнителей *@
@if (ShouldShowSection(TrackSearchType.Artist)) @if (_searchResult?.Artists != null)
{ {
<MudExpansionPanel Text="Исполнители" Expanded="true"> <MudExpansionPanel Text="Исполнители" Expanded="true">
<MudGrid> <MudGrid>
@foreach (var artist in _searchResult.Artists!) @foreach (var artist in _searchResult.Artists)
{ {
<MudItem xs="12" sm="6" md="4" lg="3"> <MudItem xs="12" sm="6" md="3" lg="2">
<ArtistCard Item="artist" OnClick="() => SearchTracksByEntity(artist.Id, TrackSearchType.Artist)" /> <ArtistCard Item="artist" OnClick="() => SearchTracksByEntity(artist.Id, artist.Name, TrackSearchType.Artist)" />
</MudItem> </MudItem>
} }
</MudGrid> </MudGrid>
@@ -55,14 +55,14 @@
} }
@* Секция альбомов *@ @* Секция альбомов *@
@if (ShouldShowSection(TrackSearchType.Album)) @if (_searchResult?.Albums != null)
{ {
<MudExpansionPanel Text="Альбомы" Expanded="true"> <MudExpansionPanel Text="Альбомы" Expanded="true">
<MudGrid> <MudGrid>
@foreach (var album in _searchResult.Albums!) @foreach (var album in _searchResult.Albums)
{ {
<MudItem xs="12" sm="6" md="4" lg="3"> <MudItem xs="12" sm="6" md="3" lg="2">
<AddTrackSection Item="album" OnClick="() => SearchTracksByEntity(album.Id, TrackSearchType.Album)" /> <AlbumCard Item="album" OnClick="() => SearchTracksByEntity(album.Id, album.Title, TrackSearchType.Album)" />
</MudItem> </MudItem>
} }
</MudGrid> </MudGrid>
@@ -70,14 +70,14 @@
} }
@* Секция плейлистов *@ @* Секция плейлистов *@
@if (ShouldShowSection(TrackSearchType.Playlist)) @if (_searchResult?.Playlists != null)
{ {
<MudExpansionPanel Text="Плейлисты" Expanded="true"> <MudExpansionPanel Text="Плейлисты" Expanded="true">
<MudGrid> <MudGrid>
@foreach (var playlist in _searchResult.Playlists!) @foreach (var playlist in _searchResult.Playlists)
{ {
<MudItem xs="12" sm="6" md="4" lg="3"> <MudItem xs="12" sm="6" md="3" lg="2">
<PlaylistCard Item="playlist" OnClick="() => SearchTracksByEntity(playlist.Kind, TrackSearchType.Playlist)" /> <PlaylistCard Item="playlist" OnClick="() => SearchTracksByEntity(playlist.Uuid, playlist.Title, TrackSearchType.Playlist)" />
</MudItem> </MudItem>
} }
</MudGrid> </MudGrid>
@@ -85,7 +85,7 @@
} }
@* Секция треков *@ @* Секция треков *@
@if (ShouldShowSection(TrackSearchType.Track)) @if (_searchResult?.Tracks != null)
{ {
<MudExpansionPanel Text="Треки" Expanded="true"> <MudExpansionPanel Text="Треки" Expanded="true">
<MudTable Items="@_searchResult.Tracks" <MudTable Items="@_searchResult.Tracks"
@@ -123,22 +123,9 @@
private string _searchQuery = ""; private string _searchQuery = "";
private bool _isSearching = false; private bool _isSearching = false;
private bool _isFirstSearch = true;
private TrackSearchType _searchType = TrackSearchType.All; private TrackSearchType _searchType = TrackSearchType.All;
private YandexSearchResult? _searchResult = null; private YandexSearchResult? _searchResult = null;
private bool ShouldShowSection(TrackSearchType sectionType)
{
return sectionType switch
{
TrackSearchType.Track => _searchResult?.Tracks?.Any() == true,
TrackSearchType.Album => _searchResult?.Albums?.Any() == true,
TrackSearchType.Playlist => _searchResult?.Playlists?.Any() == true,
TrackSearchType.Artist => _searchResult?.Artists?.Any() == true,
_ => false
};
}
private async Task OnSearchQueryChanged() private async Task OnSearchQueryChanged()
{ {
await SearchTracks(byId: false); await SearchTracks(byId: false);
@@ -155,7 +142,6 @@
if (string.IsNullOrWhiteSpace(query)) if (string.IsNullOrWhiteSpace(query))
{ {
_searchResult = null; _searchResult = null;
_isFirstSearch = true;
return; return;
} }
@@ -176,7 +162,6 @@
} }
} }
_isFirstSearch = false;
_isSearching = true; _isSearching = true;
_searchResult = null; _searchResult = null;
StateHasChanged(); StateHasChanged();
@@ -212,10 +197,11 @@
} }
} }
private async Task SearchTracksByEntity(string entityId, TrackSearchType entityType) private async Task SearchTracksByEntity(string entityId, string title, TrackSearchType entityType)
{ {
// Переключаем тип на треки и ищем по ID // Переключаем тип и ищем по ID
_searchType = TrackSearchType.Track; _searchType = entityType;
_searchQuery = title;
await SearchTracks(byId: true, forcedQuery: entityId); await SearchTracks(byId: true, forcedQuery: entityId);
} }

View File

@@ -1,9 +1,11 @@
@using PlaylistShared.Shared.Yandex @using PlaylistShared.Shared.Yandex
<MudPaper Class="d-flex flex-column align-center pa-2 cursor-pointer" Elevation="0" OnClick="OnClick.InvokeAsync"> <MudItem Class="d-flex flex-column align-center pa-2 cursor-pointer" Elevation="0" @onclick="HandleClick">
@if (!string.IsNullOrEmpty(Item.CoverUrl)) @if (!string.IsNullOrEmpty(Item.CoverUrl))
{ {
<MudAvatar Image="@Item.CoverUrl.FormatCoverUrl(Size, Size)" Size="MudBlazor.Size.Large" /> <MudAvatar Size="MudBlazor.Size.Large">
<MudImage Src="@Item.CoverUrl.FormatCoverUrl(Size, Size)" />
</MudAvatar>
} }
else else
{ {
@@ -15,10 +17,18 @@
<MudText Typo="Typo.caption" Align="Align.Center" Color="Color.Secondary"> <MudText Typo="Typo.caption" Align="Align.Center" Color="Color.Secondary">
@string.Join(", ", Item.Artists.Select(a => a.Name)) @string.Join(", ", Item.Artists.Select(a => a.Name))
</MudText> </MudText>
</MudPaper> </MudItem>
@code { @code {
[Parameter] public YandexAlbum Item { get; set; } = null!; [Parameter] public YandexAlbum Item { get; set; } = null!;
[Parameter] public EventCallback OnClick { get; set; } [Parameter] public EventCallback OnClick { get; set; }
[Parameter] public int Size { get; set; } = 50; [Parameter] public int Size { get; set; } = 50;
private async Task HandleClick()
{
if (OnClick.HasDelegate)
{
await OnClick.InvokeAsync();
}
}
} }

View File

@@ -1,9 +1,11 @@
@using PlaylistShared.Shared.Yandex @using PlaylistShared.Shared.Yandex
<MudPaper Class="d-flex flex-column align-center pa-2 cursor-pointer" Elevation="0" OnClick="OnClick.InvokeAsync"> <MudItem Class="d-flex flex-column align-center pa-2 cursor-pointer" @onclick="HandleClick">
@if (!string.IsNullOrEmpty(Item.CoverUrl)) @if (!string.IsNullOrEmpty(Item.CoverUrl))
{ {
<MudAvatar Image="@Item.CoverUrl.FormatCoverUrl(Size, Size)" Size="MudBlazor.Size.Large" /> <MudAvatar Size="MudBlazor.Size.Large">
<MudImage Src="@Item.CoverUrl.FormatCoverUrl(Size, Size)" />
</MudAvatar>
} }
else else
{ {
@@ -12,10 +14,18 @@
</MudAvatar> </MudAvatar>
} }
<MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Name</MudText> <MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Name</MudText>
</MudPaper> </MudItem>
@code { @code {
[Parameter] public YandexArtist Item { get; set; } = null!; [Parameter] public YandexArtist Item { get; set; } = null!;
[Parameter] public EventCallback OnClick { get; set; } [Parameter] public EventCallback OnClick { get; set; }
[Parameter] public int Size { get; set; } = 50; [Parameter] public int Size { get; set; } = 50;
private async Task HandleClick()
{
if (OnClick.HasDelegate)
{
await OnClick.InvokeAsync();
}
}
} }

View File

@@ -1,9 +1,11 @@
@using PlaylistShared.Shared.Yandex @using PlaylistShared.Shared.Yandex
<MudPaper Class="d-flex flex-column align-center pa-2 cursor-pointer" Elevation="0" OnClick="OnClick.InvokeAsync"> <MudItem Class="d-flex flex-column align-center pa-2 cursor-pointer" Elevation="0" onclick="HandleClick">
@if (!string.IsNullOrEmpty(Item.CoverUrl)) @if (!string.IsNullOrEmpty(Item.CoverUrl))
{ {
<MudAvatar Image="@Item.CoverUrl.FormatCoverUrl(Size, Size)" Size="MudBlazor.Size.Large" /> <MudAvatar Size="MudBlazor.Size.Large">
<MudImage Src="@Item.CoverUrl.FormatCoverUrl(Size, Size)" />
</MudAvatar>
} }
else else
{ {
@@ -12,10 +14,18 @@
</MudAvatar> </MudAvatar>
} }
<MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Title</MudText> <MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Title</MudText>
</MudPaper> </MudItem>
@code { @code {
[Parameter] public YandexPlaylist Item { get; set; } = null!; [Parameter] public YandexPlaylist Item { get; set; } = null!;
[Parameter] public EventCallback OnClick { get; set; } [Parameter] public EventCallback OnClick { get; set; }
[Parameter] public int Size { get; set; } = 50; [Parameter] public int Size { get; set; } = 50;
private async Task HandleClick()
{
if (OnClick.HasDelegate)
{
await OnClick.InvokeAsync();
}
}
} }

View File

@@ -109,41 +109,19 @@ code {
text-align: start; text-align: start;
} }
.track-cover-container { /* Горизонтальный скролинг */
border-radius: 4px; .horizontal-scroll {
overflow: hidden; overflow-x: auto;
transition: transform 0.2s ease; scroll-snap-type: x mandatory;
overflow-y: hidden; /* отключаем вертикальный скролл */
cursor: grab;
} }
.track-cover-container:hover { .horizontal-scroll:active {
transform: scale(1.05); cursor: grabbing;
}
.play-overlay {
transition: opacity 0.2s ease;
cursor: pointer;
}
/* Фиксированный плеер внизу */
.fixed-player {
position: sticky;
display: flex;
bottom: 0;
width: 100%;
right: 0;
justify-content: center;
background-color: var(--mud-palette-background);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
/* Отступ снизу, когда плеер виден */
.page-with-player {
padding-bottom: 80px; /* Высота плеера (подберите под свою тему) */
}
/* На мобильных устройствах можно уменьшить отступ */
@media (max-width: 600px) {
.page-with-player {
padding-bottom: 100px; /* если плеер выше на мобильных */
} }
/* Для WebKit (Chrome, Edge, Safari) можно включить горизонтальный скролл мышью */
.horizontal-scroll {
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
} }

View File

@@ -12,7 +12,7 @@ public class YandexTrack
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
[JsonPropertyName("artists")] [JsonPropertyName("artists")]
public List<string> Artists { get; set; } = new(); public List<YandexArtist> Artists { get; set; } = new();
[JsonPropertyName("coverUri")] [JsonPropertyName("coverUri")]
public string CoverUri { get; set; } = string.Empty; public string CoverUri { get; set; } = string.Empty;