diff --git a/PlaylistShared.Api/Controllers/YandexSearchController.cs b/PlaylistShared.Api/Controllers/YandexSearchController.cs new file mode 100644 index 0000000..54d0cce --- /dev/null +++ b/PlaylistShared.Api/Controllers/YandexSearchController.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using PlaylistShared.Api.Entities; +using PlaylistShared.Api.Extensions; +using PlaylistShared.Api.Services; +using PlaylistShared.Shared; +using PlaylistShared.Shared.DTO; + +namespace PlaylistShared.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +[AllowAnonymous] +public class YandexSearchController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly YandexMusicService _yandexService; + private readonly SharedPlaylistService _sharedPlaylistService; + + public YandexSearchController(UserManager userManager, YandexMusicService yandexService, SharedPlaylistService sharedPlaylistService) + { + _userManager = userManager; + _yandexService = yandexService; + _sharedPlaylistService = sharedPlaylistService; + } + + [HttpGet("tracks")] + public async Task>>> SearchTracks( + [FromQuery] string query, + [FromQuery] int limit = 20, + [FromQuery] string? shared_id = null) + { + if (string.IsNullOrWhiteSpace(query)) + return BadRequest(ApiResponse>.Fail(new ErrorResponse + { + StatusCode = 400, + Message = "Поисковый запрос не может быть пустым." + })); + + ApplicationUser? user = null; + var userId = User.GetUserIdOrNull(); + if (userId.HasValue) + user = await _userManager.FindByIdAsync(userId.Value.ToString()); + + // Если нет пользователя или у него нет токена, пробуем через shared_id + if (user == null || string.IsNullOrEmpty(_yandexService.DecryptToken(user.YandexAccessToken))) + { + if (string.IsNullOrEmpty(shared_id)) + return Unauthorized("Не установлен яндекс токен."); + + var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shared_id); + if (playlist == null) return NotFound("Не найден плейлист."); + + if (!await _sharedPlaylistService.CanAddTrackAsync(playlist, userId)) + return StatusCode(403, "Нет доступа для добавления трека."); + + var owner = await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString()); + if (owner == null) return StatusCode(500, "Не удалось найти владельца плейлиста."); + user = owner; + } + + var decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken); + if (string.IsNullOrEmpty(decryptedToken)) + return BadRequest(ApiResponse>.Fail(new ErrorResponse + { + StatusCode = 400, + Message = "Токен Яндекс.Музыки не установлен или недействителен." + })); + + var results = await _yandexService.SearchTracksAsync(user, query, limit); + return Ok(ApiResponse>.Ok(results)); + } +} \ No newline at end of file diff --git a/PlaylistShared.Api/Services/YandexMusicService.cs b/PlaylistShared.Api/Services/YandexMusicService.cs index 97ccf6e..19fef84 100644 --- a/PlaylistShared.Api/Services/YandexMusicService.cs +++ b/PlaylistShared.Api/Services/YandexMusicService.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.DataProtection; using PlaylistShared.Api.Entities; +using PlaylistShared.Shared.DTO; using YandexMusic; using YandexMusic.API.Extensions.API; using YandexMusic.API.Models.Playlist; @@ -94,4 +95,22 @@ public class YandexMusicService return null; } } + + public async Task> SearchTracksAsync(ApplicationUser user, string query, int limit = 20) + { + var client = await CreateClientAsync(user); + if (client == null) return new List(); + + var searchResult = await client.SearchAsync(query, YandexMusic.API.Models.Common.YSearchType.Track, page: 0, pageSize: limit); + if (searchResult?.Tracks?.Results == null) return new List(); + + return searchResult.Tracks.Results.Select(t => new YandexTrackSearchResult + { + TrackId = t.Id, + Title = t.Title, + Artists = t.Artists?.Select(a => a.Name).ToList() ?? new List(), + CoverUri = t.CoverUri ?? string.Empty, + DurationMs = t.DurationMs, + }).ToList(); + } } \ No newline at end of file diff --git a/PlaylistShared.Pwa/Components/ShareButton.razor b/PlaylistShared.Pwa/Components/ShareButton.razor index 755b3c7..454986f 100644 --- a/PlaylistShared.Pwa/Components/ShareButton.razor +++ b/PlaylistShared.Pwa/Components/ShareButton.razor @@ -1,5 +1,6 @@ @inject IJSRuntime JS @inject ISnackbar Snackbar +@inject NavigationManager Navigation + Поиск трека + +
+ + + Искать + +
+ + @if (_isSearching) + { +
+ +
+ } + else if (_searchResults.Any()) + { +
+ @foreach (var track in _searchResults) + { +
+
+ +
+
+ @track.Title + @string.Join(", ", track.Artists) +
+
+ @FormatDuration(track.DurationMs) +
+
+ +
+
+ } +
+ } + else if (!string.IsNullOrEmpty(_searchQuery) && !_isSearching) + { + Ничего не найдено. Попробуйте изменить запрос. + } + + +@code { + [Parameter] public EventCallback OnAddTrack { get; set; } + [Parameter] public EventCallback OnPlayTrack { get; set; } + [Parameter] public string ShareToken { get; set; } = string.Empty; + [Parameter] public string? CurrentPlayingTrackId { get; set; } + [Parameter] public bool IsPlaying { get; set; } + + private string _searchQuery = ""; + private List _searchResults = new(); + private bool _isSearching; + private HashSet _addingTrackIds = new(); + private string? _currentPlayingTrackId; + private bool _isPlaying; + + protected override void OnParametersSet() + { + _currentPlayingTrackId = CurrentPlayingTrackId; + _isPlaying = IsPlaying; + } + + private async Task SearchTracks() + { + if (string.IsNullOrWhiteSpace(_searchQuery)) return; + + _isSearching = true; + try + { + var url = $"/api/yandexsearch/tracks?query={Uri.EscapeDataString(_searchQuery)}&limit=20"; + if (!string.IsNullOrEmpty(ShareToken)) + url += $"&shared_id={Uri.EscapeDataString(ShareToken)}"; + + var response = await Http.GetFromJsonAsync>>(url); + if (response?.Success == true) + _searchResults = response.Data ?? new(); + else + Snackbar.Add(response?.Error?.Message ?? "Ошибка поиска", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); + } + finally + { + _isSearching = false; + StateHasChanged(); + } + } + + private async Task AddTrack(YandexTrackSearchResult track) + { + if (_addingTrackIds.Contains(track.TrackId)) return; + _addingTrackIds.Add(track.TrackId); + try + { + await OnAddTrack.InvokeAsync(track.TrackId); + Snackbar.Add($"Трек \"{track.Title}\" добавлен", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка добавления: {ex.Message}", Severity.Error); + } + finally + { + _addingTrackIds.Remove(track.TrackId); + StateHasChanged(); + } + } + + private async Task PlayTrack(string trackId) + { + await OnPlayTrack.InvokeAsync(trackId); + } + + private string FormatDuration(long ms) + { + var seconds = ms / 1000; + var mins = seconds / 60; + var secs = seconds % 60; + return $"{mins}:{secs:D2}"; + } +} \ No newline at end of file diff --git a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor new file mode 100644 index 0000000..90c6760 --- /dev/null +++ b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor @@ -0,0 +1,112 @@ +@using PlaylistShared.Shared.Shared +@inject HttpClient Http +@inject ISnackbar Snackbar + + + + + + + + + + + @if (_addingTrack) + { + + } + else + { + Добавить + } + + + + + Поддерживаются ссылки вида: https://music.yandex.ru/album/12345/track/67890 + + + + + + + + +@code { + private int _activeTabIndex = 0; + private string _trackLink = ""; + private bool _addingTrack = false; + + [Parameter] public string ShareToken { get; set; } = string.Empty; + [Parameter] public EventCallback OnTrackAdded { get; set; } + [Parameter] public EventCallback OnPlayTrack { get; set; } + [Parameter] public string? CurrentPlayingTrackId { get; set; } + [Parameter] public bool IsPlaying { get; set; } + + private async Task AddTrackByLink() + { + if (string.IsNullOrWhiteSpace(_trackLink)) + { + Snackbar.Add("Введите ссылку на трек", Severity.Warning); + return; + } + + var trackId = ExtractTrackIdFromLink(_trackLink); + if (string.IsNullOrEmpty(trackId)) + { + Snackbar.Add("Неверный формат ссылки", Severity.Warning); + return; + } + + await AddTrackById(trackId); + if (!_addingTrack) // если не было ошибки + _trackLink = ""; + } + + private async Task AddTrackById(string trackId) + { + _addingTrack = true; + try + { + var request = new AddTracksRequest { TrackIds = new List { trackId } }; + var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/add-tracks", request); + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Трек успешно добавлен", Severity.Success); + await OnTrackAdded.InvokeAsync(); // уведомляем родителя, что список треков изменился + } + else + { + var error = await response.Content.ReadFromJsonAsync>(); + Snackbar.Add(error?.Error?.Message ?? "Ошибка добавления трека", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); + } + finally + { + _addingTrack = false; + StateHasChanged(); + } + } + + private async Task PlayTrack(string trackId) + { + OnPlayTrack.InvokeAsync(trackId); + } + + private string? ExtractTrackIdFromLink(string link) + { + var match = System.Text.RegularExpressions.Regex.Match(link, @"/track/(\d+)"); + return match.Success ? match.Groups[1].Value : null; + } +} \ No newline at end of file diff --git a/PlaylistShared.Pwa/Components/PermissionsDialog.razor b/PlaylistShared.Pwa/Components/SharedPlaylist/PermissionsDialog.razor similarity index 100% rename from PlaylistShared.Pwa/Components/PermissionsDialog.razor rename to PlaylistShared.Pwa/Components/SharedPlaylist/PermissionsDialog.razor diff --git a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor index a63abe8..97dc70f 100644 --- a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor +++ b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor @@ -1,4 +1,5 @@ @page "/shared/{token}" +@using PlaylistShared.Pwa.Components.SharedPlaylist @using PlaylistShared.Shared.DTO @using PlaylistShared.Shared.Enums @using PlaylistShared.Pwa.Services @@ -60,36 +61,13 @@ - @if (_canAdd) { - - Добавить трек - - - - - - - @if (_addingTrack) - { - - } - else - { - - Добавить - } - - - - - Поддерживаются ссылки вида: https://music.yandex.ru/album/12345/track/67890 - - + } @@ -171,6 +149,8 @@ @code { [Parameter] public string Token { get; set; } + private int _addTrackTabIndex = 0; // 0 - ссылка, 1 - поиск + private AudioPlayer? _audioPlayer; private string? _currentTrackId { get; set; } private bool _isPlaying = false; @@ -369,7 +349,7 @@ } } - private async Task AddTrack() + private async Task AddTrackByLink() { if (string.IsNullOrWhiteSpace(_trackLink)) { @@ -404,6 +384,65 @@ } } + private async Task OnTrackAdded(string trackId) + { + if (!_canAdd) return; + + try + { + var request = new AddTracksRequest { TrackIds = new List { trackId } }; + var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{Token}/add-tracks", request); + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Трек успешно добавлен", Severity.Success); + await LoadTracks(); + } + else + { + var error = await response.Content.ReadFromJsonAsync>(); + Snackbar.Add(error?.Error?.Message ?? "Ошибка добавления трека", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); + } + } + + private async Task AddTrackById(string trackId) + { + if (!_canAdd) + { + Snackbar.Add("У вас нет прав на добавление треков", Severity.Warning); + return; + } + + _addingTrack = true; + try + { + var request = new AddTracksRequest { TrackIds = new List { trackId } }; + var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{Token}/add-tracks", request); + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Трек успешно добавлен", Severity.Success); + await LoadTracks(); + } + else + { + var error = await response.Content.ReadFromJsonAsync>(); + Snackbar.Add(error?.Error?.Message ?? "Ошибка добавления трека", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); + } + finally + { + _addingTrack = false; + } + } + private async Task RemoveTrack(YandexTrackDisplay track) { var confirmed = await DialogService.ShowMessageBoxAsync( @@ -505,6 +544,4 @@ _isPlaying = false; StateHasChanged(); } - - } \ No newline at end of file diff --git a/PlaylistShared.Shared/DTO/YandexTrackSearchResult.cs b/PlaylistShared.Shared/DTO/YandexTrackSearchResult.cs new file mode 100644 index 0000000..34f32cc --- /dev/null +++ b/PlaylistShared.Shared/DTO/YandexTrackSearchResult.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace PlaylistShared.Shared.DTO; + +/// Результат поиска трека в Яндекс.Музыке. +public class YandexTrackSearchResult +{ + [JsonPropertyName("trackId")] + public string TrackId { get; set; } = string.Empty; + + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("artists")] + public List Artists { get; set; } = new(); + + [JsonPropertyName("coverUri")] + public string CoverUri { get; set; } = string.Empty; + + [JsonPropertyName("durationMs")] + public long DurationMs { get; set; } +} \ No newline at end of file