Проведен аудит. Добавлено переключение треков

This commit is contained in:
FrigaT
2026-05-21 20:49:55 +03:00
parent 38af6174fa
commit 9139d8ecfe
23 changed files with 351 additions and 222 deletions

View File

@@ -17,7 +17,13 @@
<MudItem>
<MudStack Spacing="0">
<MudText Typo="Typo.body1" Color="Color.Secondary">@Track.Title</MudText>
<MudText Typo="Typo.body2" >@string.Join(", ", Track.Artists.Select(a => a.Name))</MudText>
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<MudText Typo="Typo.body2">@string.Join(", ", Track.Artists.Select(a => a.Name))</MudText>
@if (!string.IsNullOrEmpty(AddedByName))
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" Class="pa-0 px-1" Style="height: 18px; font-size: 10px;">@AddedByName</MudChip>
}
</MudStack>
</MudStack>
</MudItem>
@@ -33,4 +39,5 @@
[Parameter] public YandexTrack Track { get; set; } = null!;
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
[Parameter] public bool CanPlay { get; set; } = true;
[Parameter] public string? AddedByName { get; set; }
}

View File

@@ -11,6 +11,12 @@
<MudStack Spacing="1" Row AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap">
<!-- Кнопки управления -->
<MudIconButton Icon="@Icons.Material.Filled.SkipPrevious"
Size="Size.Small"
Color="Color.Default"
Disabled="@(!AudioPlayerService.HasPrevious)"
OnClick="() => AudioPlayerService.PlayPreviousAsync()" />
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
@onmouseleave="() => { _isPlayHovered = false; }"
Class="relative d-inline-block rounded overflow-hidden cursor-pointer"
@@ -32,7 +38,13 @@
ToggledChanged="OnPlayClick" />
</MudItem>
</MudItem>
<MudIconButton Icon="@Icons.Material.Filled.SkipNext"
Size="Size.Small"
Color="Color.Default"
Disabled="@(!AudioPlayerService.HasNext)"
OnClick="() => AudioPlayerService.PlayNextAsync()" />
<!-- Название и прогресс -->
@if (AudioPlayerService.CurrentTrack != null)
{
@@ -170,15 +182,9 @@
#endregion
#region Обработка сервиса
private async Task OnServiceLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
private async Task OnServiceLoadAndPlay(string trackId, string? playToken, string? sharedPlaylistId)
{
if (string.IsNullOrWhiteSpace(accessToken))
{
var tokens = await TokenStorage.GetTokensAsync();
accessToken = tokens.token;
}
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
if (string.IsNullOrWhiteSpace(playToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
{
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
return;
@@ -186,7 +192,7 @@
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
await EnsureAudioModuleAsync();
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, sharedPlaylistId);
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, playToken, sharedPlaylistId);
}
private async Task OnServicePlay()

View File

@@ -1,6 +1,17 @@
@page "/shared/{token}"
<PageTitle>@_playlist?.Title - Playlist Share</PageTitle>
<HeadContent>
<meta property="og:title" content="@(_playlist?.Title ?? "Поделиться плейлистом")" />
<meta property="og:description" content="@($"Слушайте плейлист «{_playlist?.Title}» на Playlist Share")" />
@if (!string.IsNullOrEmpty(_playlist?.CoverUrl))
{
<meta property="og:image" content="@_playlist.CoverUrl" />
}
<meta property="og:url" content="@Navigation.Uri" />
<meta property="og:type" content="music.playlist" />
</HeadContent>
@using PlaylistShared.Pwa.Components.Common
@using PlaylistShared.Pwa.Components.Global
@using PlaylistShared.Pwa.Components.SharedPlaylist
@@ -245,7 +256,8 @@
Style="min-height: 0;">
<RowTemplate>
<MudTd Class="py-1 px-0" Style="width: 100%;">
<TrackItem Track="@context" PlaylistShareToken="@Token" CanPlay="@_canPlay" />
<TrackItem Track="@context" PlaylistShareToken="@Token" CanPlay="@_canPlay"
AddedByName="@(_trackAdditions.TryGetValue(context.TrackId, out var n) ? n : null)" />
</MudTd>
@if (_canRemove)
{
@@ -444,6 +456,10 @@
/// </summary>
private List<YandexTrack> _tracks = new();
/// <summary>
/// Словарь trackId → имя пользователя, добавившего трек.
/// </summary>
private Dictionary<string, string?> _trackAdditions = new();
/// <summary>
/// Продолжительность плейлиста.
/// </summary>
long _playlistDurationMs;
@@ -621,6 +637,15 @@
_existingTrackIds = _tracks.Select(t => t.TrackId).ToHashSet();
_playlistDurationMs = _tracks.Sum(t => t.DurationMs);
_playlistTrackCount = _tracks.Count();
AudioPlayerService.SetQueue(_tracks, shareToken: Token);
try
{
var additionsResp = await Http.GetFromJsonAsync<ApiResponse<Dictionary<string, string?>>>($"/api/sharedplaylist/{Token}/additions");
if (additionsResp?.Success == true && additionsResp.Data != null)
_trackAdditions = additionsResp.Data;
}
catch { }
}
else
{

View File

@@ -1,6 +1,7 @@
using MudBlazor;
using MudBlazor;
using PlaylistShared.Shared;
using PlaylistShared.Shared.Yandex;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace PlaylistShared.Pwa.Services;
@@ -22,9 +23,16 @@ public class AudioPlayerService : IAudioPlayerService
private string _currentTimeString = "0:00";
private string _totalTimeString = "0:00";
private List<YandexTrack> _queue = new();
private int _queueIndex = -1;
private string? _queueShareToken;
public string? CurrentTrackId => _currentTrackId;
public YandexTrack? CurrentTrack => _currentTrack;
public bool IsPlaying => _isPlaying;
public IReadOnlyList<YandexTrack> CurrentQueue => _queue;
public bool HasNext => _queueIndex >= 0 && _queueIndex < _queue.Count - 1;
public bool HasPrevious => _queueIndex > 0;
public double CurrentVolume
{
get => _currentVolume;
@@ -57,14 +65,10 @@ public class AudioPlayerService : IAudioPlayerService
private async Task LoadVolume()
{
var savedVolume = await _playerStorage.GetVolumeAsync();
if (savedVolume != null)
{
_currentVolume = savedVolume.Value;
}
}
// Внешние команды (вызываются из компонентов)
public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? playlistShareToken = null, YandexTrack? track = null)
{
if (_currentTrackId == trackId)
@@ -75,30 +79,33 @@ public class AudioPlayerService : IAudioPlayerService
_currentTrackId = trackId;
// Если accessToken не передан, пытаемся получить его из хранилища
var idx = _queue.FindIndex(t => t.TrackId == trackId);
if (idx >= 0) _queueIndex = idx;
if (string.IsNullOrWhiteSpace(accessToken))
{
var tokens = await _tokenStorage.GetTokensAsync();
accessToken = tokens.token;
}
// Проверяем, есть ли чем авторизоваться
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(playlistShareToken))
string? playToken = null;
if (!string.IsNullOrWhiteSpace(accessToken))
playToken = await FetchPlayTokenAsync(accessToken);
if (string.IsNullOrWhiteSpace(playToken) && string.IsNullOrWhiteSpace(playlistShareToken))
{
_snackbar.Add("Не удалось воспроизвести трек: отсутствует токен авторизации или идентификатор расшаренного плейлиста.", Severity.Error);
return;
}
// Если title и coverUrl не переданы, нужно запросить через API
if (track is null)
{
try
{
track = await GetTrackInfo(trackId, accessToken, playlistShareToken);
track = await GetTrackInfo(trackId, playToken, playlistShareToken);
}
catch (Exception ex)
{
// Логируем ошибку, но продолжаем без обложки/названия
Console.WriteLine($"Failed to fetch track info: {ex.Message}");
}
}
@@ -106,7 +113,7 @@ public class AudioPlayerService : IAudioPlayerService
_currentTrack = track;
_isPlaying = true;
OnStateChanged?.Invoke();
OnLoadAndPlayRequested?.Invoke(trackId, accessToken, playlistShareToken);
OnLoadAndPlayRequested?.Invoke(trackId, playToken, playlistShareToken);
OnStartedTrack?.Invoke();
}
@@ -138,14 +145,36 @@ public class AudioPlayerService : IAudioPlayerService
await _playerStorage.SetVolumeAsync(volume);
}
// События для связи с реальным AudioPlayer компонентом
public void SetQueue(IEnumerable<YandexTrack> tracks, int startIndex = 0, string? shareToken = null)
{
_queue = tracks.ToList();
_queueIndex = _queue.Count > 0 ? Math.Clamp(startIndex, 0, _queue.Count - 1) : -1;
_queueShareToken = shareToken;
OnStateChanged?.Invoke();
}
public async Task PlayNextAsync()
{
if (!HasNext) return;
_queueIndex++;
var track = _queue[_queueIndex];
await LoadAndPlayAsync(track.TrackId, playlistShareToken: _queueShareToken, track: track);
}
public async Task PlayPreviousAsync()
{
if (!HasPrevious) return;
_queueIndex--;
var track = _queue[_queueIndex];
await LoadAndPlayAsync(track.TrackId, playlistShareToken: _queueShareToken, track: track);
}
public event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
public event Func<Task>? OnPlayRequested;
public event Func<Task>? OnPauseRequested;
public event Func<double, Task>? OnSeekRequested;
public event Func<double, Task>? OnVolumeChangeRequested;
// Внутренние методы для обновления состояния из AudioPlayer
public void SetPlayingState(bool isPlaying)
{
_isPlaying = isPlaying;
@@ -172,37 +201,40 @@ public class AudioPlayerService : IAudioPlayerService
public void NotifyTrackEnded()
{
_isPlaying = false;
_currentTrackId = null;
_currentProgress = 0;
_currentTime = 0;
_currentTimeString = "0:00";
_totalTime = 0;
_currentTimeString = "0:00";
_totalTimeString = "0:00";
OnStateChanged?.Invoke();
OnEndedTrack?.Invoke();
if (HasNext)
_ = PlayNextAsync();
else
_currentTrackId = null;
}
/// <summary>
/// Вспомогательный метод для получения информации о треке через API
/// </summary>
/// <param name="trackId"></param>
/// <param name="accessToken"></param>
/// <param name="sharedPlaylistId"></param>
/// <returns></returns>
private async Task<YandexTrack?> GetTrackInfo(string trackId, string? accessToken, string? sharedPlaylistId)
private async Task<string?> FetchPlayTokenAsync(string jwt)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/audio/play-token");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
using var response = await _http.SendAsync(request);
if (!response.IsSuccessStatusCode) return null;
var result = await response.Content.ReadFromJsonAsync<ApiResponse<string>>();
return result?.Data;
}
private async Task<YandexTrack?> GetTrackInfo(string trackId, string? playToken, string? sharedPlaylistId)
{
var url = $"/api/audio/track-info/{trackId}";
if (!string.IsNullOrEmpty(accessToken))
url += $"?access_token={accessToken}";
if (!string.IsNullOrEmpty(playToken))
url += $"?play_token={playToken}";
else if (!string.IsNullOrEmpty(sharedPlaylistId))
url += $"?shared_id={sharedPlaylistId}";
var response = await _http.GetFromJsonAsync<ApiResponse<YandexTrack>>(url);
if (response?.Success == true)
{
return response.Data;
}
return null;
return response?.Success == true ? response.Data : null;
}
private string FormatDuration(double seconds)
@@ -211,4 +243,4 @@ public class AudioPlayerService : IAudioPlayerService
var secs = (int)(seconds % 60);
return $"{mins}:{secs:D2}";
}
}
}

View File

@@ -12,6 +12,15 @@ public interface IAudioPlayerService
/// <summary>ID текущего воспроизводимого трека (null, если ничего не играет).</summary>
string? CurrentTrackId { get; }
/// <summary>Очередь треков.</summary>
IReadOnlyList<YandexTrack> CurrentQueue { get; }
/// <summary>Есть ли следующий трек в очереди.</summary>
bool HasNext { get; }
/// <summary>Есть ли предыдущий трек в очереди.</summary>
bool HasPrevious { get; }
/// <summary>Играет ли в данный момент (true) или приостановлен (false).</summary>
bool IsPlaying { get; }
@@ -57,6 +66,15 @@ public interface IAudioPlayerService
/// <summary>Установить громкость (0100).</summary>
Task SetVolumeAsync(double volume);
/// <summary>Установить очередь треков и начать воспроизведение с указанного индекса.</summary>
void SetQueue(IEnumerable<YandexTrack> tracks, int startIndex = 0, string? shareToken = null);
/// <summary>Перейти к следующему треку в очереди.</summary>
Task PlayNextAsync();
/// <summary>Перейти к предыдущему треку в очереди.</summary>
Task PlayPreviousAsync();
#endregion
#region События для подписки на изменения состояния

View File

@@ -12,7 +12,7 @@
const loadAndPlay = (src, token, sharedPlaylistId) => {
const url = new URL(src, window.location.href);
if (token) url.searchParams.set('access_token', token);
if (token) url.searchParams.set('play_token', token);
if (sharedPlaylistId) url.searchParams.set('shared_id', sharedPlaylistId);
audio.src = url.toString();
audio.load();