Compare commits
71 Commits
4b3036364b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38af6174fa | ||
|
|
2fe20c804a | ||
|
|
3c83a83396 | ||
|
|
14fcd7dff9 | ||
|
|
ecb12a7d4a | ||
|
|
2cd80c8082 | ||
|
|
78808ea525 | ||
|
|
d6da8460cc | ||
|
|
362762a813 | ||
|
|
7c05940dbf | ||
|
|
b3f19045fa | ||
|
|
b1febfc9dc | ||
|
|
0f2755281e | ||
|
|
d17ed30175 | ||
|
|
0f9dd1a8d8 | ||
|
|
45b8a168a1 | ||
|
|
c32eee0954 | ||
|
|
e2e117a539 | ||
|
|
64cc0e68a1 | ||
|
|
d2df57ca6e | ||
|
|
832363df57 | ||
|
|
d1e3e23e93 | ||
|
|
1c32b2e997 | ||
|
|
f9bbd895c4 | ||
|
|
8a809c9e7d | ||
|
|
e0c6b4119c | ||
|
|
eb323e874f | ||
| 58f21da19c | |||
|
|
9c95e6b189 | ||
|
|
12241639dc | ||
|
|
4324b86512 | ||
|
|
6b399f7fb7 | ||
|
|
ab56c34646 | ||
|
|
07a52b12d6 | ||
|
|
bb50bcbf22 | ||
|
|
dec6bc4dd1 | ||
|
|
280c164626 | ||
|
|
5a8ae3d680 | ||
|
|
68d7c7fc12 | ||
|
|
974fb0f538 | ||
|
|
a634986ac0 | ||
|
|
41a9d27005 | ||
|
|
35140b71b7 | ||
|
|
3e18537a0e | ||
|
|
9da342be3e | ||
|
|
203506be28 | ||
|
|
0732eab325 | ||
|
|
d03bd193c2 | ||
|
|
ba13199ca2 | ||
|
|
cbb0cb8c8e | ||
|
|
50ed75b413 | ||
|
|
e82dcdeaa4 | ||
|
|
629f14cbb9 | ||
|
|
76c9b11a68 | ||
|
|
e00b7a735c | ||
|
|
c7bd97462a | ||
|
|
abf1906173 | ||
|
|
6ae49faf15 | ||
|
|
0381ef74ab | ||
|
|
acf02c85a7 | ||
|
|
e0fca7e55e | ||
|
|
65efb9ff76 | ||
|
|
0369f0af07 | ||
|
|
b012fe37cc | ||
|
|
3a42a17ce7 | ||
|
|
b46e3a0715 | ||
|
|
68887284c1 | ||
|
|
9e8bb0db75 | ||
|
|
dcb2efbedb | ||
|
|
8230951839 | ||
|
|
164cf455fd |
@@ -2,7 +2,10 @@
|
|||||||
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.Yandex;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Controllers;
|
namespace PlaylistShared.Api.Controllers;
|
||||||
@@ -13,15 +16,18 @@ public class AudioController : ControllerBase
|
|||||||
{
|
{
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly YandexMusicService _yandexService;
|
private readonly YandexMusicService _yandexService;
|
||||||
|
private readonly SharedPlaylistService _sharedService;
|
||||||
private readonly JwtService _jwtService;
|
private readonly JwtService _jwtService;
|
||||||
|
|
||||||
public AudioController(
|
public AudioController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
YandexMusicService yandexService,
|
YandexMusicService yandexService,
|
||||||
|
SharedPlaylistService sharedService,
|
||||||
JwtService jwtService)
|
JwtService jwtService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_yandexService = yandexService;
|
_yandexService = yandexService;
|
||||||
|
_sharedService = sharedService;
|
||||||
_jwtService = jwtService;
|
_jwtService = jwtService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,17 +35,18 @@ public class AudioController : ControllerBase
|
|||||||
/// Потоковое воспроизведение трека из Яндекс.Музыки.
|
/// Потоковое воспроизведение трека из Яндекс.Музыки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="trackId">ID трека (например, "21696942").</param>
|
/// <param name="trackId">ID трека (например, "21696942").</param>
|
||||||
|
/// <param name="access_token">gwt пользователя</param>
|
||||||
|
/// <param name="shared_id">ID расшаренного плейлиста</param>
|
||||||
[HttpGet("track/{trackId}")]
|
[HttpGet("track/{trackId}")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? access_token = null)
|
public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? access_token = null, [FromQuery] string? shared_id = null)
|
||||||
{
|
{
|
||||||
var user = await GetUserFromToken(access_token);
|
var user = await GetUserFromToken(access_token);
|
||||||
if (user == null)
|
if (user == null || user.YandexAccessToken is null) user = await GetUserFromSharedPlaylistId(shared_id);
|
||||||
return Unauthorized();
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId);
|
var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId);
|
||||||
if (string.IsNullOrEmpty(streamUrl))
|
if (string.IsNullOrEmpty(streamUrl)) return NotFound();
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var httpClient = new HttpClient();
|
var httpClient = new HttpClient();
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, streamUrl);
|
var request = new HttpRequestMessage(HttpMethod.Get, streamUrl);
|
||||||
@@ -56,29 +63,66 @@ public class AudioController : ControllerBase
|
|||||||
Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
|
Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
|
||||||
|
|
||||||
if (response.Content.Headers.Contains("Content-Range"))
|
if (response.Content.Headers.Contains("Content-Range"))
|
||||||
Response.Headers.Add("Content-Range", response.Content.Headers.ContentRange?.ToString());
|
Response.Headers.Append("Content-Range", response.Content.Headers.ContentRange?.ToString());
|
||||||
if (response.Headers.Contains("Accept-Ranges"))
|
if (response.Headers.Contains("Accept-Ranges"))
|
||||||
Response.Headers.Add("Accept-Ranges", response.Headers.AcceptRanges?.ToString());
|
Response.Headers.Append("Accept-Ranges", response.Headers.AcceptRanges?.ToString());
|
||||||
if (response.Content.Headers.Contains("Content-Length"))
|
if (response.Content.Headers.Contains("Content-Length"))
|
||||||
Response.Headers.Add("Content-Length", response.Content.Headers.ContentLength?.ToString());
|
Response.Headers.Append("Content-Length", response.Content.Headers.ContentLength?.ToString());
|
||||||
|
|
||||||
await response.Content.CopyToAsync(Response.Body);
|
await response.Content.CopyToAsync(Response.Body);
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("track-info/{trackId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult<ApiResponse<YandexTrack>>> GetTrackInfo(string trackId, [FromQuery] string? access_token = null, [FromQuery] string? shared_id = null)
|
||||||
|
{
|
||||||
|
var user = await GetUserFromToken(access_token);
|
||||||
|
if (user == null || user.YandexAccessToken is null) user = await GetUserFromSharedPlaylistId(shared_id);
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var track = await _yandexService.GetYTrackAsync(user, trackId);
|
||||||
|
if (track == null) return NotFound();
|
||||||
|
|
||||||
|
return Ok(ApiResponse<YandexTrack>.Ok(new YandexTrack
|
||||||
|
{
|
||||||
|
Title = track.Title,
|
||||||
|
CoverUri = track.CoverUri,
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<ApplicationUser?> GetUserFromToken(string? token)
|
private async Task<ApplicationUser?> GetUserFromToken(string? token)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token)) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
var principal = _jwtService.ValidateToken(token);
|
var principal = _jwtService.ValidateToken(token);
|
||||||
if (principal == null)
|
if (principal == null) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
if (string.IsNullOrEmpty(userId))
|
if (string.IsNullOrEmpty(userId)) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
return await _userManager.FindByIdAsync(userId);
|
return await _userManager.FindByIdAsync(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ApplicationUser?> GetUserFromSharedPlaylistId(string? sharedId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sharedId)) return null;
|
||||||
|
|
||||||
|
var playlist = await _sharedService.GetEntityByTokenAsync(sharedId);
|
||||||
|
|
||||||
|
if (playlist == null) return null;
|
||||||
|
|
||||||
|
if (!await _sharedService.CanPlayEveryoneAsync(playlist)) return null;
|
||||||
|
|
||||||
|
return await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString());
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
82
PlaylistShared.Api/Controllers/FavoritesController.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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.SharedPlaylist;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class FavoritesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly FavoritesService _favoritesService;
|
||||||
|
private readonly SharedPlaylistService _sharedPlaylistService;
|
||||||
|
|
||||||
|
public FavoritesController(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
FavoritesService favoritesService,
|
||||||
|
SharedPlaylistService sharedPlaylistService)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_favoritesService = favoritesService;
|
||||||
|
_sharedPlaylistService = sharedPlaylistService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получить список избранных плейлистов текущего пользователя.</summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<ApiResponse<List<SharedPlaylistDto>>>> GetFavorites()
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var favorites = await _favoritesService.GetUserFavoritesAsync(userId);
|
||||||
|
return Ok(ApiResponse<List<SharedPlaylistDto>>.Ok(favorites));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Проверить, добавлен ли плейлист в избранное.</summary>
|
||||||
|
[HttpGet("{shareToken}/check")]
|
||||||
|
public async Task<ActionResult<ApiResponse<bool>>> CheckFavorite(string shareToken)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var isFavorite = await _favoritesService.IsFavoriteAsync(userId, shareToken);
|
||||||
|
return Ok(ApiResponse<bool>.Ok(isFavorite));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Добавить плейлист в избранное.</summary>
|
||||||
|
[HttpPost("{shareToken}")]
|
||||||
|
public async Task<ActionResult<ApiResponse<object>>> AddFavorite(string shareToken)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
|
||||||
|
if (playlist == null)
|
||||||
|
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
||||||
|
|
||||||
|
await _favoritesService.AddFavoriteAsync(userId, shareToken);
|
||||||
|
return Ok(ApiResponse<object>.Ok(new { message = "Плейлист добавлен в избранное" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Удалить плейлист из избранного.</summary>
|
||||||
|
[HttpDelete("{shareToken}")]
|
||||||
|
public async Task<ActionResult<ApiResponse<object>>> RemoveFavorite(string shareToken)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
await _favoritesService.RemoveFavoriteAsync(userId, shareToken);
|
||||||
|
return Ok(ApiResponse<object>.Ok(new { message = "Плейлист удалён из избранного" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,8 @@ using PlaylistShared.Api.Extensions;
|
|||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
using PlaylistShared.Shared;
|
using PlaylistShared.Shared;
|
||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using PlaylistShared.Shared.Playlist;
|
using PlaylistShared.Shared.SharedPlaylist;
|
||||||
using PlaylistShared.Shared.Shared;
|
using PlaylistShared.Shared.Yandex;
|
||||||
using YandexMusic;
|
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Controllers;
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|
||||||
@@ -20,39 +19,41 @@ public class PlaylistsController : ControllerBase
|
|||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly SharedPlaylistService _sharedService;
|
private readonly SharedPlaylistService _sharedService;
|
||||||
private readonly YandexMusicService _yandexService;
|
private readonly YandexMusicService _yandexService;
|
||||||
|
private readonly YandexApiService _yandexApiService;
|
||||||
|
|
||||||
public PlaylistsController(
|
public PlaylistsController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
SharedPlaylistService sharedService,
|
SharedPlaylistService sharedService,
|
||||||
YandexMusicService yandexService)
|
YandexMusicService yandexService,
|
||||||
|
YandexApiService yandexApiService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_sharedService = sharedService;
|
_sharedService = sharedService;
|
||||||
_yandexService = yandexService;
|
_yandexService = yandexService;
|
||||||
|
_yandexApiService = yandexApiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<ApiResponse<List<YandexPlaylistInfo>>>> GetMyPlaylists()
|
public async Task<ActionResult<ApiResponse<List<YandexPlaylistShare>>>> GetMyPlaylists()
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
if (user == null) return Unauthorized();
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
var decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken);
|
var decryptedToken = _yandexApiService.DecryptToken(user.YandexAccessToken);
|
||||||
if (string.IsNullOrEmpty(decryptedToken))
|
if (string.IsNullOrEmpty(decryptedToken))
|
||||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" }));
|
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" }));
|
||||||
|
|
||||||
var yandexClient = new YandexMusicClient();
|
var authSuccess = await _yandexApiService.AuthAsync(decryptedToken);
|
||||||
var authSuccess = await yandexClient.Authorize(decryptedToken);
|
|
||||||
if (!authSuccess)
|
if (!authSuccess)
|
||||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Не удалось авторизоваться в Яндекс.Музыке. Проверьте токен." }));
|
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Не удалось авторизоваться в Яндекс.Музыке. Проверьте токен." }));
|
||||||
|
|
||||||
var favorites = await yandexClient.GetFavoritesAsync();
|
var favorites = await _yandexApiService.Client.Api.Playlist.FavoritesAsync();
|
||||||
var ownPlaylists = favorites.Where(p => p.Owner.Uid == yandexClient.Account.Uid).ToList();
|
var ownPlaylists = favorites.Where(p => p.Owner.Uid == _yandexApiService.Client.Account.Uid).ToList();
|
||||||
|
|
||||||
var sharedPlaylists = await _sharedService.GetAllByUserAsync(userId);
|
var sharedPlaylists = await _sharedService.GetAllByUserAsync(userId);
|
||||||
|
|
||||||
var result = ownPlaylists.Select(p => new YandexPlaylistInfo
|
var result = ownPlaylists.Select(p => new YandexPlaylistShare
|
||||||
{
|
{
|
||||||
Kind = p.Kind,
|
Kind = p.Kind,
|
||||||
OwnerUid = p.Owner.Uid,
|
OwnerUid = p.Owner.Uid,
|
||||||
@@ -63,7 +64,7 @@ public class PlaylistsController : ControllerBase
|
|||||||
ShareToken = sharedPlaylists.FirstOrDefault(s => s.YandexPlaylistKind == p.Kind && s.YandexPlaylistOwnerUid == p.Owner.Uid)?.ShareToken,
|
ShareToken = sharedPlaylists.FirstOrDefault(s => s.YandexPlaylistKind == p.Kind && s.YandexPlaylistOwnerUid == p.Owner.Uid)?.ShareToken,
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
return Ok(ApiResponse<List<YandexPlaylistInfo>>.Ok(result));
|
return Ok(ApiResponse<List<YandexPlaylistShare>>.Ok(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("share")]
|
[HttpPost("share")]
|
||||||
@@ -74,21 +75,21 @@ public class PlaylistsController : ControllerBase
|
|||||||
if (user == null) return Unauthorized();
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
// Проверяем, что плейлист действительно принадлежит пользователю
|
// Проверяем, что плейлист действительно принадлежит пользователю
|
||||||
var yandexClient = new YandexMusicClient();
|
var playlist = await _yandexService.GetPlaylistAsync(user, request.OwnerUid, request.Kind);
|
||||||
await yandexClient.Authorize(_yandexService.DecryptToken(user.YandexAccessToken));
|
if (playlist == null)
|
||||||
var playlist = await yandexClient.GetPlaylistAsync(request.OwnerUid, request.Kind);
|
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
||||||
if (playlist == null || playlist.Owner.Uid != yandexClient.Account.Uid)
|
|
||||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Плейлист не принадлежит вам" }));
|
|
||||||
|
|
||||||
var dto = new SharePlaylistDto
|
var dto = new SharePlaylistDto
|
||||||
{
|
{
|
||||||
|
YandexPlaylistUuid = playlist.PlaylistUuid,
|
||||||
YandexPlaylistKind = request.Kind,
|
YandexPlaylistKind = request.Kind,
|
||||||
YandexPlaylistOwnerUid = request.OwnerUid,
|
YandexPlaylistOwnerUid = request.OwnerUid,
|
||||||
Title = playlist.Title,
|
Title = playlist.Title,
|
||||||
Description = playlist.Description,
|
Description = playlist.Description,
|
||||||
ViewPermission = ViewPermission.Everyone,
|
ViewPermission = ViewPermission.Everyone,
|
||||||
|
PlayPermission = ViewPermission.Everyone,
|
||||||
AddPermission = EditPermission.AuthorizedOnly,
|
AddPermission = EditPermission.AuthorizedOnly,
|
||||||
RemovePermission = EditPermission.AddedByUserOnly
|
RemovePermission = EditPermission.AddedByUserOnly,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _sharedService.CreateAsync(userId, dto);
|
var result = await _sharedService.CreateAsync(userId, dto);
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ using PlaylistShared.Api.Entities;
|
|||||||
using PlaylistShared.Api.Extensions;
|
using PlaylistShared.Api.Extensions;
|
||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
using PlaylistShared.Shared;
|
using PlaylistShared.Shared;
|
||||||
using PlaylistShared.Shared.DTO;
|
using PlaylistShared.Shared.SharedPlaylist;
|
||||||
using PlaylistShared.Shared.Shared;
|
using PlaylistShared.Shared.Yandex;
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@@ -101,7 +101,7 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
|
|
||||||
// POST /api/sharedplaylist/{token}/add-tracks
|
// POST /api/sharedplaylist/{token}/add-tracks
|
||||||
[HttpPost("{token}/add-tracks")]
|
[HttpPost("{token}/add-tracks")]
|
||||||
public async Task<ActionResult<ApiResponse<object>>> AddTracks(string token, [FromBody] AddTracksRequest request)
|
public async Task<ActionResult<ApiResponse<object>>> AddTracks(string token, [FromBody] UpdateTrackListRequest request)
|
||||||
{
|
{
|
||||||
var currentUserId = User.GetUserIdOrNull();
|
var currentUserId = User.GetUserIdOrNull();
|
||||||
var playlist = await _sharedService.GetEntityByTokenAsync(token);
|
var playlist = await _sharedService.GetEntityByTokenAsync(token);
|
||||||
@@ -131,7 +131,7 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
|
|
||||||
// POST /api/sharedplaylist/{token}/remove-tracks
|
// POST /api/sharedplaylist/{token}/remove-tracks
|
||||||
[HttpPost("{token}/remove-tracks")]
|
[HttpPost("{token}/remove-tracks")]
|
||||||
public async Task<ActionResult<ApiResponse<object>>> RemoveTracks(string token, [FromBody] RemoveTracksRequest request)
|
public async Task<ActionResult<ApiResponse<object>>> RemoveTracks(string token, [FromBody] UpdateTrackListRequest request)
|
||||||
{
|
{
|
||||||
var currentUserId = User.GetUserIdOrNull();
|
var currentUserId = User.GetUserIdOrNull();
|
||||||
var playlist = await _sharedService.GetEntityByTokenAsync(token);
|
var playlist = await _sharedService.GetEntityByTokenAsync(token);
|
||||||
@@ -164,34 +164,23 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" }));
|
return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/sharedplaylist/{token}/add-track-by-link
|
|
||||||
[HttpPost("{token}/add-track-by-link")]
|
|
||||||
public async Task<ActionResult<ApiResponse<object>>> AddTrackByLink(string token, [FromBody] AddTrackByLinkRequest request)
|
|
||||||
{
|
|
||||||
var trackId = ExtractTrackIdFromLink(request.Link);
|
|
||||||
if (string.IsNullOrEmpty(trackId))
|
|
||||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Неверный формат ссылки" }));
|
|
||||||
|
|
||||||
return await AddTracks(token, new AddTracksRequest { TrackIds = new List<string> { trackId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? ExtractTrackIdFromLink(string link)
|
|
||||||
{
|
|
||||||
var match = System.Text.RegularExpressions.Regex.Match(link, @"/track/(\d+)");
|
|
||||||
return match.Success ? match.Groups[1].Value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private YandexPlaylistData MapToYandexPlaylistData(YPlaylist playlist)
|
private YandexPlaylistData MapToYandexPlaylistData(YPlaylist playlist)
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
Id = 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>()
|
||||||
|
|||||||
@@ -6,31 +6,32 @@ using PlaylistShared.Api.Extensions;
|
|||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
using PlaylistShared.Shared;
|
using PlaylistShared.Shared;
|
||||||
using PlaylistShared.Shared.Profile;
|
using PlaylistShared.Shared.Profile;
|
||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Controllers;
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class YandexTokenController : ControllerBase
|
public class YandexAccountController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly YandexMusicService _yandexService;
|
private readonly YandexAuthService _yandexService;
|
||||||
|
|
||||||
public YandexTokenController(UserManager<ApplicationUser> userManager, YandexMusicService yandexService)
|
public YandexAccountController(UserManager<ApplicationUser> userManager, YandexAuthService yandexService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_yandexService = yandexService;
|
_yandexService = yandexService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("set")]
|
[HttpPost("token")]
|
||||||
public async Task<ActionResult<ApiResponse<object>>> SetToken([FromBody] SetYandexTokenRequest request)
|
public async Task<ActionResult<ApiResponse<object>>> SetToken([FromBody] SetYandexTokenRequest request)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
if (user == null) return Unauthorized();
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
user.YandexAccessToken = _yandexService.EncryptToken(request.Token);
|
user.YandexAccessToken = _yandexService.Service.EncryptToken(request.Token);
|
||||||
// Не храним refresh-токен, так как пользователь вводит только access-токен
|
// Не храним refresh-токен, так как пользователь вводит только access-токен
|
||||||
user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен
|
user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен
|
||||||
await _userManager.UpdateAsync(user);
|
await _userManager.UpdateAsync(user);
|
||||||
@@ -55,4 +56,35 @@ public class YandexTokenController : ControllerBase
|
|||||||
ExpiryUtc = user.YandexTokenExpiryUtc
|
ExpiryUtc = user.YandexTokenExpiryUtc
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("qr")]
|
||||||
|
public async Task<ActionResult<ApiResponse<YandexAuthQr>>> GetQr()
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var qr = await _yandexService.GetQrOrGenerate(user);
|
||||||
|
|
||||||
|
return Ok(ApiResponse<YandexAuthQr>.Ok(qr));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("qr/{sessionId}")]
|
||||||
|
public async Task<IActionResult> CheckQr(int sessionId)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var checkResult = await _yandexService.CheckQrAsync(sessionId);
|
||||||
|
if (checkResult == null) return NotFound();
|
||||||
|
|
||||||
|
if (checkResult.Status == Shared.Enums.YandexAuthQrStatus.Authorized)
|
||||||
|
{
|
||||||
|
await SetToken(new() { Token = _yandexService.Service.Client.AuthStorage.Token });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ApiResponse<YandexAuthQrCheck>.Ok(checkResult));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
99
PlaylistShared.Api/Controllers/YandexSearchController.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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.Enums;
|
||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class YandexSearchController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly YandexMusicService _yandexService;
|
||||||
|
private readonly SharedPlaylistService _sharedPlaylistService;
|
||||||
|
|
||||||
|
public YandexSearchController(UserManager<ApplicationUser> userManager, YandexMusicService yandexService, SharedPlaylistService sharedPlaylistService)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_yandexService = yandexService;
|
||||||
|
_sharedPlaylistService = sharedPlaylistService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<ActionResult<ApiResponse<YandexSearchResult>>> SearchQuery(
|
||||||
|
[FromQuery] string query = "",
|
||||||
|
[FromQuery] int limit = 40,
|
||||||
|
[FromQuery] TrackSearchType searchType = TrackSearchType.All,
|
||||||
|
[FromQuery] bool byId = false,
|
||||||
|
[FromQuery] string? shared_id = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query) && searchType != TrackSearchType.MyPlaylists)
|
||||||
|
return BadRequest(ApiResponse<YandexSearchResult>.Fail(new ErrorResponse
|
||||||
|
{
|
||||||
|
StatusCode = 400,
|
||||||
|
Message = "Поисковый запрос не может быть пустым."
|
||||||
|
}));
|
||||||
|
|
||||||
|
ApplicationUser? user = null;
|
||||||
|
var userId = User.GetUserIdOrNull();
|
||||||
|
if (userId.HasValue)
|
||||||
|
user = await _userManager.FindByIdAsync(userId.Value.ToString());
|
||||||
|
|
||||||
|
var byShareId = false;
|
||||||
|
|
||||||
|
// Если нет пользователя или у него нет токена, пробуем через shared_id
|
||||||
|
if (user == null || string.IsNullOrEmpty(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;
|
||||||
|
|
||||||
|
byShareId = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(user.YandexAccessToken))
|
||||||
|
return BadRequest(ApiResponse<YandexSearchResult>.Fail(new ErrorResponse
|
||||||
|
{
|
||||||
|
StatusCode = 400,
|
||||||
|
Message = "Токен Яндекс.Музыки не установлен или недействителен."
|
||||||
|
}));
|
||||||
|
|
||||||
|
YandexSearchResult? results = null;
|
||||||
|
|
||||||
|
if (byId)
|
||||||
|
{
|
||||||
|
results = await _yandexService.SearchTracksByIdAsync(user, query, searchType);
|
||||||
|
}
|
||||||
|
else if (searchType == TrackSearchType.MyPlaylists)
|
||||||
|
{
|
||||||
|
if (byShareId)
|
||||||
|
{
|
||||||
|
return Unauthorized("Необходимо подключение профиля к яндекс музыке.");
|
||||||
|
}
|
||||||
|
|
||||||
|
results = await _yandexService.SearchMyPlaylists(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
results = await _yandexService.SearchAsync(user, query, searchType, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ApiResponse<YandexSearchResult>.Ok(results));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,34 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Data;
|
namespace PlaylistShared.Api.Data;
|
||||||
|
|
||||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
|
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>, IDataProtectionKeyContext
|
||||||
{
|
{
|
||||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
|
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<FavoritePlaylist> FavoritePlaylists => Set<FavoritePlaylist>();
|
||||||
|
|
||||||
public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
|
public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
|
||||||
public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>();
|
public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>();
|
||||||
public DbSet<UserSession> UserSessions => Set<UserSession>();
|
|
||||||
public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>();
|
public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>();
|
||||||
|
public DbSet<UserSession> UserSessions => Set<UserSession>();
|
||||||
|
public DbSet<YandexAuthSession> YandexAuthSessions => Set<YandexAuthSession>();
|
||||||
|
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
builder.Entity<DataProtectionKey>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.FriendlyName).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
builder.Entity<SharedPlaylist>(entity =>
|
builder.Entity<SharedPlaylist>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
@@ -26,6 +37,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
.WithMany(u => u.OwnedPlaylists)
|
.WithMany(u => u.OwnedPlaylists)
|
||||||
.HasForeignKey(e => e.CreatorUserId)
|
.HasForeignKey(e => e.CreatorUserId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
entity.Property(e => e.YandexPlaylistUuid).IsRequired().HasMaxLength(50);
|
||||||
entity.Property(e => e.YandexPlaylistKind).IsRequired().HasMaxLength(50);
|
entity.Property(e => e.YandexPlaylistKind).IsRequired().HasMaxLength(50);
|
||||||
entity.Property(e => e.YandexPlaylistOwnerUid).IsRequired().HasMaxLength(50);
|
entity.Property(e => e.YandexPlaylistOwnerUid).IsRequired().HasMaxLength(50);
|
||||||
entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
|
entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
|
||||||
@@ -77,5 +89,56 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
.HasForeignKey(e => e.SessionId)
|
.HasForeignKey(e => e.SessionId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Entity<FavoritePlaylist>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => new { e.UserId, e.SharedPlaylistId });
|
||||||
|
entity.HasOne(e => e.User)
|
||||||
|
.WithMany(u => u.FavoritePlaylists)
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
entity.HasOne(e => e.SharedPlaylist)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.SharedPlaylistId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
entity.Property(e => e.AddedAtUtc).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
builder.Entity<YandexAuthSession>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id)
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
entity.HasOne(e => e.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
entity.Property(e => e.QrCodeUrl)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500);
|
||||||
|
entity.Property(e => e.SerializedCookies)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
entity.Property(e => e.ConfirmedAt)
|
||||||
|
.IsRequired(false);
|
||||||
|
entity.Property(e => e.IsConfirmed)
|
||||||
|
.IsRequired()
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
entity.Property(e => e.TrackId)
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.IsRequired(false);
|
||||||
|
entity.Property(e => e.CsfrToken)
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.IsRequired(false);
|
||||||
|
entity.Property(e => e.HeaderCsfrToken)
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.IsRequired(false);
|
||||||
|
entity.Property(e => e.HeaderProcessId)
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.IsRequired(false);
|
||||||
|
entity.HasIndex(e => e.UserId)
|
||||||
|
.HasDatabaseName("IX_YandexAuthSessions_UserId");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
547
PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.Designer.cs
generated
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260414094124_AddSharedPermissions")]
|
||||||
|
partial class AddSharedPermissions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RefreshTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexAccessToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexRefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("YandexTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AddPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("CoverUrl")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("PlayPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RemovePermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShareToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("ViewPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistOwnerUid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ShareToken")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SharedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AddedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssociatedUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ClientIpAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FirstSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("AssociatedUserId");
|
||||||
|
|
||||||
|
b.ToTable("UserSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RemovedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RemovedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RemovedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
|
.WithMany("OwnedPlaylists")
|
||||||
|
.HasForeignKey("CreatorUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AddedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssociatedUserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RemovedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackRemovalLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RemovedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("OwnedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
|
||||||
|
b.Navigation("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSharedPermissions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "PlayPermission",
|
||||||
|
table: "SharedPlaylists",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PlayPermission",
|
||||||
|
table: "SharedPlaylists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
591
PlaylistShared.Api/Data/Migrations/20260414111229_AddUserFavorites.Designer.cs
generated
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260414111229_AddUserFavorites")]
|
||||||
|
partial class AddUserFavorites
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RefreshTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexAccessToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexRefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("YandexTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "SharedPlaylistId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId");
|
||||||
|
|
||||||
|
b.ToTable("FavoritePlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AddPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("CoverUrl")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("PlayPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RemovePermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShareToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("ViewPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistOwnerUid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistUuid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ShareToken")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SharedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AddedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssociatedUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ClientIpAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FirstSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("AssociatedUserId");
|
||||||
|
|
||||||
|
b.ToTable("UserSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RemovedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RemovedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RemovedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany("FavoritePlaylists")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
|
.WithMany("OwnedPlaylists")
|
||||||
|
.HasForeignKey("CreatorUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AddedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssociatedUserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RemovedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackRemovalLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RemovedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("FavoritePlaylists");
|
||||||
|
|
||||||
|
b.Navigation("OwnedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
|
||||||
|
b.Navigation("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserFavorites : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "YandexPlaylistUuid",
|
||||||
|
table: "SharedPlaylists",
|
||||||
|
type: "nvarchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FavoritePlaylists",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
SharedPlaylistId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
AddedAtUtc = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FavoritePlaylists", x => new { x.UserId, x.SharedPlaylistId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FavoritePlaylists_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FavoritePlaylists_SharedPlaylists_SharedPlaylistId",
|
||||||
|
column: x => x.SharedPlaylistId,
|
||||||
|
principalTable: "SharedPlaylists",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FavoritePlaylists_SharedPlaylistId",
|
||||||
|
table: "FavoritePlaylists",
|
||||||
|
column: "SharedPlaylistId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FavoritePlaylists");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "YandexPlaylistUuid",
|
||||||
|
table: "SharedPlaylists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
611
PlaylistShared.Api/Data/Migrations/20260414121754_AddDataProtectionKeys.Designer.cs
generated
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260414121754_AddDataProtectionKeys")]
|
||||||
|
partial class AddDataProtectionKeys
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Xml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DataProtectionKeys");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RefreshTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexAccessToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexRefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("YandexTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "SharedPlaylistId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId");
|
||||||
|
|
||||||
|
b.ToTable("FavoritePlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AddPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("CoverUrl")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("PlayPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RemovePermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShareToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("ViewPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistOwnerUid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistUuid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ShareToken")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SharedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AddedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssociatedUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ClientIpAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FirstSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("AssociatedUserId");
|
||||||
|
|
||||||
|
b.ToTable("UserSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RemovedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RemovedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RemovedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany("FavoritePlaylists")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
|
.WithMany("OwnedPlaylists")
|
||||||
|
.HasForeignKey("CreatorUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AddedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssociatedUserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RemovedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackRemovalLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RemovedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("FavoritePlaylists");
|
||||||
|
|
||||||
|
b.Navigation("OwnedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
|
||||||
|
b.Navigation("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddDataProtectionKeys : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DataProtectionKeys",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
FriendlyName = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Xml = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DataProtectionKeys", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DataProtectionKeys");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
668
PlaylistShared.Api/Data/Migrations/20260419180136_AddYandexAuthSessions.Designer.cs
generated
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260419180136_AddYandexAuthSessions")]
|
||||||
|
partial class AddYandexAuthSessions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.6")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Xml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DataProtectionKeys");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RefreshTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexAccessToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexRefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("YandexTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "SharedPlaylistId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId");
|
||||||
|
|
||||||
|
b.ToTable("FavoritePlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AddPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("CoverUrl")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("PlayPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RemovePermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShareToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("ViewPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistOwnerUid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistUuid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ShareToken")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SharedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AddedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssociatedUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ClientIpAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FirstSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("AssociatedUserId");
|
||||||
|
|
||||||
|
b.ToTable("UserSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConfirmedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CsfrToken")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsConfirmed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("QrCodeUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("SerializedCookies")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("IX_YandexAuthSessions_UserId");
|
||||||
|
|
||||||
|
b.ToTable("YandexAuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RemovedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RemovedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RemovedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany("FavoritePlaylists")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
|
.WithMany("OwnedPlaylists")
|
||||||
|
.HasForeignKey("CreatorUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AddedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssociatedUserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RemovedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackRemovalLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RemovedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("FavoritePlaylists");
|
||||||
|
|
||||||
|
b.Navigation("OwnedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
|
||||||
|
b.Navigation("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddYandexAuthSessions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "YandexAuthSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
QrCodeUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
|
SerializedCookies = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
ConfirmedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
IsConfirmed = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
TrackId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
CsfrToken = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_YandexAuthSessions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_YandexAuthSessions_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_YandexAuthSessions_UserId",
|
||||||
|
table: "YandexAuthSessions",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "YandexAuthSessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
676
PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.Designer.cs
generated
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260420123450_AddYandexAuthSessions_Header")]
|
||||||
|
partial class AddYandexAuthSessions_Header
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.6")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Xml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DataProtectionKeys");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RefreshTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexAccessToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexId")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexRefreshToken")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("YandexTokenExpiryUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "SharedPlaylistId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId");
|
||||||
|
|
||||||
|
b.ToTable("FavoritePlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AddPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("CoverUrl")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("PlayPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RemovePermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShareToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("ViewPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistOwnerUid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistUuid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ShareToken")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SharedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AddedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssociatedUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ClientIpAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FirstSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("AssociatedUserId");
|
||||||
|
|
||||||
|
b.ToTable("UserSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConfirmedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CsfrToken")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderCsfrToken")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderProcessId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsConfirmed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("QrCodeUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("SerializedCookies")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("IX_YandexAuthSessions_UserId");
|
||||||
|
|
||||||
|
b.ToTable("YandexAuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RemovedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RemovedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RemovedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany("FavoritePlaylists")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
|
.WithMany("OwnedPlaylists")
|
||||||
|
.HasForeignKey("CreatorUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AddedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssociatedUserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RemovedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackRemovalLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RemovedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Session");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("FavoritePlaylists");
|
||||||
|
|
||||||
|
b.Navigation("OwnedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
|
||||||
|
b.Navigation("TrackRemovalLogs");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddYandexAuthSessions_Header : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "HeaderCsfrToken",
|
||||||
|
table: "YandexAuthSessions",
|
||||||
|
type: "nvarchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "HeaderProcessId",
|
||||||
|
table: "YandexAuthSessions",
|
||||||
|
type: "nvarchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "HeaderCsfrToken",
|
||||||
|
table: "YandexAuthSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "HeaderProcessId",
|
||||||
|
table: "YandexAuthSessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,31 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
.HasAnnotation("ProductVersion", "10.0.6")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Xml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DataProtectionKeys");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -237,6 +257,24 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.ToTable("AspNetUsers", (string)null);
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "SharedPlaylistId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId");
|
||||||
|
|
||||||
|
b.ToTable("FavoritePlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -261,6 +299,9 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("PlayPermission")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("RemovePermission")
|
b.Property<int>("RemovePermission")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -289,6 +330,11 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("nvarchar(50)");
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("YandexPlaylistUuid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CreatorUserId");
|
b.HasIndex("CreatorUserId");
|
||||||
@@ -361,6 +407,61 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.ToTable("UserSessions");
|
b.ToTable("UserSessions");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConfirmedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CsfrToken")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderCsfrToken")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderProcessId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsConfirmed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("QrCodeUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("SerializedCookies")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("IX_YandexAuthSessions_UserId");
|
||||||
|
|
||||||
|
b.ToTable("YandexAuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TrackRemovalLog", b =>
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -446,6 +547,25 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany("FavoritePlaylists")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
@@ -493,6 +613,16 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TrackRemovalLog", b =>
|
modelBuilder.Entity("TrackRemovalLog", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
|
||||||
@@ -521,6 +651,8 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("FavoritePlaylists");
|
||||||
|
|
||||||
b.Navigation("OwnedPlaylists");
|
b.Navigation("OwnedPlaylists");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ public class ApplicationUser : IdentityUser<Guid>
|
|||||||
|
|
||||||
/// <summary>Плейлисты, созданные пользователем.</summary>
|
/// <summary>Плейлисты, созданные пользователем.</summary>
|
||||||
public ICollection<SharedPlaylist> OwnedPlaylists { get; set; } = new List<SharedPlaylist>();
|
public ICollection<SharedPlaylist> OwnedPlaylists { get; set; } = new List<SharedPlaylist>();
|
||||||
|
|
||||||
|
/// <summary>Избранные плейлисты.</summary>
|
||||||
|
public ICollection<FavoritePlaylist> FavoritePlaylists { get; set; } = new List<FavoritePlaylist>();
|
||||||
}
|
}
|
||||||
13
PlaylistShared.Api/Entities/FavoritePlaylist.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
|
/// <summary>Избранный расшаренный плейлист пользователя.</summary>
|
||||||
|
public class FavoritePlaylist
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public Guid SharedPlaylistId { get; set; }
|
||||||
|
public DateTime AddedAtUtc { get; set; }
|
||||||
|
|
||||||
|
// Навигационные свойства
|
||||||
|
public ApplicationUser User { get; set; } = null!;
|
||||||
|
public SharedPlaylist SharedPlaylist { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ public class SharedPlaylist
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid CreatorUserId { get; set; }
|
public Guid CreatorUserId { get; set; }
|
||||||
|
public string YandexPlaylistUuid { get; set; } = null!;
|
||||||
public string YandexPlaylistKind { get; set; } = null!;
|
public string YandexPlaylistKind { get; set; } = null!;
|
||||||
public string YandexPlaylistOwnerUid { get; set; } = null!;
|
public string YandexPlaylistOwnerUid { get; set; } = null!;
|
||||||
public string Title { get; set; } = null!;
|
public string Title { get; set; } = null!;
|
||||||
@@ -17,6 +18,7 @@ public class SharedPlaylist
|
|||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
public string ShareToken { get; set; } = null!;
|
public string ShareToken { get; set; } = null!;
|
||||||
public ViewPermission ViewPermission { get; set; }
|
public ViewPermission ViewPermission { get; set; }
|
||||||
|
public ViewPermission PlayPermission { get; set; }
|
||||||
public EditPermission AddPermission { get; set; }
|
public EditPermission AddPermission { get; set; }
|
||||||
public EditPermission RemovePermission { get; set; }
|
public EditPermission RemovePermission { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ public class UserSession
|
|||||||
public ApplicationUser? User { get; set; }
|
public ApplicationUser? User { get; set; }
|
||||||
public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
|
public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
|
||||||
public ICollection<TrackRemovalLog> TrackRemovalLogs { get; set; } = new List<TrackRemovalLog>();
|
public ICollection<TrackRemovalLog> TrackRemovalLogs { get; set; } = new List<TrackRemovalLog>();
|
||||||
}
|
}
|
||||||
|
|||||||
18
PlaylistShared.Api/Entities/YandexAuthSession.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
|
public class YandexAuthSession
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
public string QrCodeUrl { get; set; }
|
||||||
|
public string SerializedCookies { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? ConfirmedAt { get; set; }
|
||||||
|
public bool IsConfirmed { get; set; }
|
||||||
|
public string? TrackId { get; set; }
|
||||||
|
public string? CsfrToken { get; set; }
|
||||||
|
public string? HeaderProcessId { get; set; }
|
||||||
|
public string? HeaderCsfrToken { get; set; }
|
||||||
|
|
||||||
|
public ApplicationUser? User { get; set; }
|
||||||
|
}
|
||||||
@@ -4,14 +4,16 @@ namespace PlaylistShared.Api.Extensions;
|
|||||||
|
|
||||||
public static class YCoverExtensions
|
public static class YCoverExtensions
|
||||||
{
|
{
|
||||||
public static string GetUrl(this YCover cover, string size = "200x200")
|
public static string GetUrl(this YCover cover)
|
||||||
{
|
{
|
||||||
switch (cover)
|
switch (cover)
|
||||||
{
|
{
|
||||||
case YCoverImage img when !string.IsNullOrEmpty(img.Uri):
|
case YCoverImage img when !string.IsNullOrEmpty(img.Uri):
|
||||||
return $"https://{img.Uri.Replace("%%", size)}";
|
return img.Uri;
|
||||||
case YCoverPic pic when !string.IsNullOrEmpty(pic.Uri):
|
case YCoverPic pic when !string.IsNullOrEmpty(pic.Uri):
|
||||||
return $"https://{pic.Uri.Replace("%%", size)}";
|
return pic.Uri;
|
||||||
|
case YCoverMosaic mosaic when mosaic.ItemsUri.Any():
|
||||||
|
return mosaic.ItemsUri.First();
|
||||||
default:
|
default:
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using PlaylistShared.Api.Entities;
|
|
||||||
using PlaylistShared.Shared.Auth;
|
|
||||||
using PlaylistShared.Shared.Shared;
|
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Mapping;
|
|
||||||
|
|
||||||
public class AppMappingProfile : Profile
|
|
||||||
{
|
|
||||||
public AppMappingProfile()
|
|
||||||
{
|
|
||||||
CreateMap<SharedPlaylist, SharedPlaylistDto>()
|
|
||||||
.ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator));
|
|
||||||
CreateMap<ApplicationUser, ApplicationUserDto>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,24 +10,24 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
|
||||||
<PackageReference Include="AspNet.Security.OAuth.Yandex" Version="10.0.0" />
|
<PackageReference Include="AspNet.Security.OAuth.Yandex" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
||||||
<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.6" />
|
<PackageReference Include="YandexMusic" Version="0.0.16" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using PlaylistShared.Api.Data;
|
using PlaylistShared.Api.Data;
|
||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
using PlaylistShared.Api.Mapping;
|
|
||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Api;
|
namespace PlaylistShared.Api;
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ public class Program
|
|||||||
|
|
||||||
// DbContext
|
// DbContext
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));// Identity
|
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
// Identity
|
||||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
|
builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
|
||||||
{
|
{
|
||||||
options.User.RequireUniqueEmail = true;
|
options.User.RequireUniqueEmail = true;
|
||||||
@@ -88,37 +90,39 @@ public class Program
|
|||||||
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
builder.Services.AddAutoMapper(t => t.AddProfile<AppMappingProfile>());
|
|
||||||
builder.Services.AddScoped<JwtService>();
|
builder.Services.AddScoped<JwtService>();
|
||||||
builder.Services.AddScoped<UserSessionService>();
|
builder.Services.AddScoped<UserSessionService>();
|
||||||
|
|
||||||
|
builder.Services.AddDataProtection()
|
||||||
|
.PersistKeysToDbContext<ApplicationDbContext>()
|
||||||
|
.SetApplicationName("PlaylistShared.Api");
|
||||||
|
|
||||||
|
builder.Services.AddScoped<YandexApiService>();
|
||||||
builder.Services.AddScoped<YandexMusicService>();
|
builder.Services.AddScoped<YandexMusicService>();
|
||||||
|
builder.Services.AddScoped<YandexAuthService>();
|
||||||
builder.Services.AddScoped<SharedPlaylistService>();
|
builder.Services.AddScoped<SharedPlaylistService>();
|
||||||
builder.Services.AddScoped<TrackAdditionLogService>();
|
builder.Services.AddScoped<TrackAdditionLogService>();
|
||||||
builder.Services.AddScoped<TrackRemovalLogService>();
|
builder.Services.AddScoped<TrackRemovalLogService>();
|
||||||
builder.Services.AddDataProtection();
|
builder.Services.AddScoped<FavoritesService>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("Development", policy =>
|
|
||||||
{
|
|
||||||
policy.WithOrigins("http://localhost:5053", "https://localhost:7225", "http://localhost:5181", "https://api.playlistshare.frigat.duckdns.org", "https://playlistshare.frigat.duckdns.org")
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowCredentials();
|
|
||||||
});
|
|
||||||
|
|
||||||
options.AddPolicy("Production", policy =>
|
options.AddPolicy("Production", policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins("https://api.playlistshare.frigat.duckdns.org", "https://playlistshare.frigat.duckdns.org")
|
policy.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>())
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowCredentials();
|
.AllowCredentials();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
});
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
@@ -131,17 +135,14 @@ public class Program
|
|||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
app.UseCors("Production");
|
||||||
{
|
|
||||||
app.UseCors("Development");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseCors("Production");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.UseSession();
|
app.UseSession();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
100
PlaylistShared.Api/Services/FavoritesService.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
using PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Services;
|
||||||
|
|
||||||
|
public class FavoritesService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
private readonly SharedPlaylistService _sharedPlaylistService;
|
||||||
|
|
||||||
|
public FavoritesService(ApplicationDbContext db, SharedPlaylistService sharedPlaylistService)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_sharedPlaylistService = sharedPlaylistService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsFavoriteAsync(Guid userId, string shareToken)
|
||||||
|
{
|
||||||
|
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
|
||||||
|
if (playlist == null) return false;
|
||||||
|
return await _db.FavoritePlaylists
|
||||||
|
.AnyAsync(f => f.UserId == userId && f.SharedPlaylistId == playlist.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddFavoriteAsync(Guid userId, string shareToken)
|
||||||
|
{
|
||||||
|
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
|
||||||
|
if (playlist == null)
|
||||||
|
throw new ArgumentException("Playlist not found");
|
||||||
|
|
||||||
|
var exists = await _db.FavoritePlaylists
|
||||||
|
.AnyAsync(f => f.UserId == userId && f.SharedPlaylistId == playlist.Id);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
|
var favorite = new FavoritePlaylist
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
SharedPlaylistId = playlist.Id,
|
||||||
|
AddedAtUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_db.FavoritePlaylists.Add(favorite);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveFavoriteAsync(Guid userId, string shareToken)
|
||||||
|
{
|
||||||
|
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
|
||||||
|
if (playlist == null) return;
|
||||||
|
|
||||||
|
var favorite = await _db.FavoritePlaylists
|
||||||
|
.FirstOrDefaultAsync(f => f.UserId == userId && f.SharedPlaylistId == playlist.Id);
|
||||||
|
if (favorite != null)
|
||||||
|
{
|
||||||
|
_db.FavoritePlaylists.Remove(favorite);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SharedPlaylistDto>> GetUserFavoritesAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var favoritePlaylists = await _db.FavoritePlaylists
|
||||||
|
.Include(f => f.SharedPlaylist)
|
||||||
|
.ThenInclude(sp => sp.Creator)
|
||||||
|
.Where(f => f.UserId == userId)
|
||||||
|
.OrderByDescending(f => f.AddedAtUtc)
|
||||||
|
.Select(f => f.SharedPlaylist)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Маппинг в DTO (можно использовать AutoMapper, но для простоты сделаем вручную)
|
||||||
|
return favoritePlaylists.Select(sp => new SharedPlaylistDto
|
||||||
|
{
|
||||||
|
Id = sp.Id,
|
||||||
|
CreatorUserId = sp.CreatorUserId,
|
||||||
|
YandexPlaylistUuid = sp.YandexPlaylistUuid,
|
||||||
|
YandexPlaylistKind = sp.YandexPlaylistKind,
|
||||||
|
YandexPlaylistOwnerUid = sp.YandexPlaylistOwnerUid,
|
||||||
|
Title = sp.Title,
|
||||||
|
Description = sp.Description,
|
||||||
|
CoverUrl = sp.CoverUrl,
|
||||||
|
CreatedAt = sp.CreatedAt,
|
||||||
|
UpdatedAt = sp.UpdatedAt,
|
||||||
|
IsDeleted = sp.IsDeleted,
|
||||||
|
ShareToken = sp.ShareToken,
|
||||||
|
ViewPermission = sp.ViewPermission,
|
||||||
|
PlayPermission = sp.PlayPermission,
|
||||||
|
AddPermission = sp.AddPermission,
|
||||||
|
RemovePermission = sp.RemovePermission,
|
||||||
|
Creator = sp.Creator != null ? new Shared.Auth.ApplicationUserDto
|
||||||
|
{
|
||||||
|
Id = sp.Creator.Id,
|
||||||
|
UserName = sp.Creator.UserName,
|
||||||
|
Email = sp.Creator.Email,
|
||||||
|
YandexId = sp.Creator.YandexId,
|
||||||
|
DisplayName = sp.Creator.UserName
|
||||||
|
} : null
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
using AutoMapper;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using PlaylistShared.Api.Data;
|
using PlaylistShared.Api.Data;
|
||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
|
using PlaylistShared.Shared.Auth;
|
||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using PlaylistShared.Shared.Playlist;
|
using PlaylistShared.Shared.SharedPlaylist;
|
||||||
using PlaylistShared.Shared.Shared;
|
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Services;
|
namespace PlaylistShared.Api.Services;
|
||||||
|
|
||||||
public class SharedPlaylistService
|
public class SharedPlaylistService
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _db;
|
private readonly ApplicationDbContext _db;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly TrackAdditionLogService _trackLogService;
|
private readonly TrackAdditionLogService _trackLogService;
|
||||||
|
|
||||||
public SharedPlaylistService(ApplicationDbContext db, IMapper mapper, TrackAdditionLogService trackLogService)
|
public SharedPlaylistService(ApplicationDbContext db, TrackAdditionLogService trackLogService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_mapper = mapper;
|
|
||||||
_trackLogService = trackLogService;
|
_trackLogService = trackLogService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +24,7 @@ public class SharedPlaylistService
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
CreatorUserId = creatorUserId,
|
CreatorUserId = creatorUserId,
|
||||||
|
YandexPlaylistUuid = dto.YandexPlaylistUuid,
|
||||||
YandexPlaylistKind = dto.YandexPlaylistKind,
|
YandexPlaylistKind = dto.YandexPlaylistKind,
|
||||||
YandexPlaylistOwnerUid = dto.YandexPlaylistOwnerUid,
|
YandexPlaylistOwnerUid = dto.YandexPlaylistOwnerUid,
|
||||||
Title = dto.Title,
|
Title = dto.Title,
|
||||||
@@ -34,13 +32,14 @@ public class SharedPlaylistService
|
|||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
ShareToken = GenerateToken(),
|
ShareToken = GenerateToken(),
|
||||||
|
PlayPermission = dto.PlayPermission,
|
||||||
ViewPermission = dto.ViewPermission,
|
ViewPermission = dto.ViewPermission,
|
||||||
AddPermission = dto.AddPermission,
|
AddPermission = dto.AddPermission,
|
||||||
RemovePermission = dto.RemovePermission
|
RemovePermission = dto.RemovePermission
|
||||||
};
|
};
|
||||||
_db.SharedPlaylists.Add(entity);
|
_db.SharedPlaylists.Add(entity);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
return _mapper.Map<SharedPlaylistDto>(entity);
|
return MapToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SharedPlaylistDto?> GetByTokenAsync(string token)
|
public async Task<SharedPlaylistDto?> GetByTokenAsync(string token)
|
||||||
@@ -48,7 +47,7 @@ public class SharedPlaylistService
|
|||||||
var entity = await _db.SharedPlaylists
|
var entity = await _db.SharedPlaylists
|
||||||
.Include(sp => sp.Creator)
|
.Include(sp => sp.Creator)
|
||||||
.FirstOrDefaultAsync(sp => sp.ShareToken == token && !sp.IsDeleted);
|
.FirstOrDefaultAsync(sp => sp.ShareToken == token && !sp.IsDeleted);
|
||||||
return entity == null ? null : _mapper.Map<SharedPlaylistDto>(entity);
|
return entity == null ? null : MapToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SharedPlaylist?> GetEntityByTokenAsync(string token)
|
public async Task<SharedPlaylist?> GetEntityByTokenAsync(string token)
|
||||||
@@ -63,11 +62,12 @@ public class SharedPlaylistService
|
|||||||
var entity = await _db.SharedPlaylists.FindAsync(playlistId);
|
var entity = await _db.SharedPlaylists.FindAsync(playlistId);
|
||||||
if (entity == null) return null;
|
if (entity == null) return null;
|
||||||
entity.ViewPermission = dto.ViewPermission;
|
entity.ViewPermission = dto.ViewPermission;
|
||||||
|
entity.PlayPermission = dto.PlayPermission;
|
||||||
entity.AddPermission = dto.AddPermission;
|
entity.AddPermission = dto.AddPermission;
|
||||||
entity.RemovePermission = dto.RemovePermission;
|
entity.RemovePermission = dto.RemovePermission;
|
||||||
entity.UpdatedAt = DateTime.UtcNow;
|
entity.UpdatedAt = DateTime.UtcNow;
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
return _mapper.Map<SharedPlaylistDto>(entity);
|
return MapToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteAsync(Guid playlistId)
|
public async Task<bool> DeleteAsync(Guid playlistId)
|
||||||
@@ -80,6 +80,18 @@ public class SharedPlaylistService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanPlayAsync(SharedPlaylist playlist, Guid? currentUserId)
|
||||||
|
{
|
||||||
|
if (currentUserId == playlist.CreatorUserId) return true;
|
||||||
|
return playlist.PlayPermission == ViewPermission.Everyone ||
|
||||||
|
(playlist.PlayPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanPlayEveryoneAsync(SharedPlaylist playlist)
|
||||||
|
{
|
||||||
|
return playlist.PlayPermission == ViewPermission.Everyone;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> CanViewAsync(SharedPlaylist playlist, Guid? currentUserId)
|
public async Task<bool> CanViewAsync(SharedPlaylist playlist, Guid? currentUserId)
|
||||||
{
|
{
|
||||||
if (currentUserId == playlist.CreatorUserId) return true;
|
if (currentUserId == playlist.CreatorUserId) return true;
|
||||||
@@ -129,4 +141,36 @@ public class SharedPlaylistService
|
|||||||
.Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted)
|
.Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ручное маппинг сущности в DTO
|
||||||
|
private SharedPlaylistDto MapToDto(SharedPlaylist entity)
|
||||||
|
{
|
||||||
|
return new SharedPlaylistDto
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
CreatorUserId = entity.CreatorUserId,
|
||||||
|
YandexPlaylistUuid = entity.YandexPlaylistUuid,
|
||||||
|
YandexPlaylistKind = entity.YandexPlaylistKind,
|
||||||
|
YandexPlaylistOwnerUid = entity.YandexPlaylistOwnerUid,
|
||||||
|
Title = entity.Title,
|
||||||
|
Description = entity.Description,
|
||||||
|
CoverUrl = entity.CoverUrl,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
|
UpdatedAt = entity.UpdatedAt,
|
||||||
|
IsDeleted = entity.IsDeleted,
|
||||||
|
ShareToken = entity.ShareToken,
|
||||||
|
ViewPermission = entity.ViewPermission,
|
||||||
|
PlayPermission = entity.PlayPermission,
|
||||||
|
AddPermission = entity.AddPermission,
|
||||||
|
RemovePermission = entity.RemovePermission,
|
||||||
|
Creator = entity.Creator != null ? new ApplicationUserDto
|
||||||
|
{
|
||||||
|
Id = entity.Creator.Id,
|
||||||
|
UserName = entity.Creator.UserName ?? string.Empty,
|
||||||
|
Email = entity.Creator.Email,
|
||||||
|
YandexId = entity.Creator.YandexId,
|
||||||
|
DisplayName = entity.Creator.UserName
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
128
PlaylistShared.Api/Services/Yandex/YandexApiService.cs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
using System.Net;
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис для работы с API Яндекс Музыки в ASP.NET Core.
|
||||||
|
/// </summary>
|
||||||
|
public class YandexApiService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDataProtector _dataProtector;
|
||||||
|
private readonly YandexMusicClient _client;
|
||||||
|
private readonly CookieContainer _cookieContainer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Экземпляр клиента Яндекс Музыки.
|
||||||
|
/// </summary>
|
||||||
|
public YandexMusicClient Client => _client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Контейнер кук, используемый клиентом.
|
||||||
|
/// </summary>
|
||||||
|
public CookieContainer CookieContainer => _cookieContainer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт сервис с автоматическим созданием HttpClient (рекомендуется).
|
||||||
|
/// </summary>
|
||||||
|
public YandexApiService(IDataProtectionProvider provider, IWebProxy? proxy = null, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
_dataProtector = provider.CreateProtector("YandexTokens");
|
||||||
|
_cookieContainer = new();
|
||||||
|
_client = new YandexMusicClient(_cookieContainer, proxy, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool?> AuthAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(user.YandexAccessToken))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var decryptedToken = DecryptToken(user.YandexAccessToken);
|
||||||
|
if (decryptedToken == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await AuthorizeAsync(decryptedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Авторизуется с помощью OAuth-токена.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> AuthAsync(string token)
|
||||||
|
{
|
||||||
|
return await AuthorizeAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> AuthorizeAsync(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _client.Api.Auth.AuthorizeAsync(token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Засшифровывает и возвращает токен для хранения в базе данных.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string EncryptToken(string token) => _dataProtector.Protect(token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расшифровывает ключ из базы данных. Если токен повреждён или недействителен, возвращает null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encryptedToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string DecryptToken(string encryptedToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _dataProtector.Unprotect(encryptedToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает куки из строки для указанного домена.
|
||||||
|
/// </summary>
|
||||||
|
public void SetCookies(string cookieString, string domain)
|
||||||
|
{
|
||||||
|
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
|
||||||
|
_cookieContainer.SetCookies(uri, cookieString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает все куки для указанного домена в виде строки.
|
||||||
|
/// </summary>
|
||||||
|
public string GetCookies(string domain)
|
||||||
|
{
|
||||||
|
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
|
||||||
|
var cookies = _cookieContainer.GetCookies(uri);
|
||||||
|
return string.Join("; ", cookies.Cast<Cookie>().Select(c => $"{c.Name}={c.Value}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает значение конкретной куки.
|
||||||
|
/// </summary>
|
||||||
|
public string? GetCookie(string domain, string cookieName)
|
||||||
|
{
|
||||||
|
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
|
||||||
|
var cookie = _cookieContainer.GetCookies(uri)[cookieName];
|
||||||
|
return cookie?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
152
PlaylistShared.Api/Services/Yandex/YandexAuthService.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using YandexMusic.API;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Services;
|
||||||
|
|
||||||
|
public class YandexAuthService
|
||||||
|
{
|
||||||
|
private readonly YandexApiService _apiService;
|
||||||
|
private readonly ApplicationDbContext _dbContext;
|
||||||
|
|
||||||
|
public YandexApiService Service => _apiService;
|
||||||
|
public YandexMusicApi Api => _apiService.Client.Api;
|
||||||
|
|
||||||
|
public YandexAuthService(YandexApiService apiService, ApplicationDbContext dbContext)
|
||||||
|
{
|
||||||
|
_apiService = apiService;
|
||||||
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<YandexAuthQr> GetQrOrGenerate(ApplicationUser user)
|
||||||
|
{
|
||||||
|
var existingSession = _dbContext.YandexAuthSessions
|
||||||
|
.Where(s => s.UserId == user.Id && !s.IsConfirmed && s.CreatedAt > DateTime.UtcNow.AddMinutes(-5))
|
||||||
|
.OrderByDescending(s => s.CreatedAt)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (existingSession != null)
|
||||||
|
{
|
||||||
|
return new YandexAuthQr
|
||||||
|
{
|
||||||
|
QrLink = existingSession.QrCodeUrl,
|
||||||
|
SessionId = existingSession.Id.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GenerateQrAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<YandexAuthQr> GenerateQrAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
var qr = await Api.Passport.GetAuthQRLinkAsync();
|
||||||
|
var trackId = Service.Client.AuthStorage.AuthToken.TrackId;
|
||||||
|
var csfrToken = Service.Client.AuthStorage.AuthToken.CsfrToken;
|
||||||
|
var headerProcessUuid = Service.Client.AuthStorage.HeaderToken.ProcessUuid;
|
||||||
|
var headerCsfrToken = Service.Client.AuthStorage.HeaderToken.CsfrToken;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(qr))
|
||||||
|
throw new Exception("Не удалось получить QR-ссылку");
|
||||||
|
|
||||||
|
var cookiesJson = SerializeCookies(_apiService.CookieContainer);
|
||||||
|
|
||||||
|
var session = new YandexAuthSession
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
QrCodeUrl = qr,
|
||||||
|
SerializedCookies = cookiesJson,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
IsConfirmed = false,
|
||||||
|
TrackId = trackId,
|
||||||
|
CsfrToken = csfrToken,
|
||||||
|
HeaderCsfrToken = headerCsfrToken,
|
||||||
|
HeaderProcessId = headerProcessUuid,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
_dbContext.YandexAuthSessions.Add(session);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new YandexAuthQr
|
||||||
|
{
|
||||||
|
QrLink = qr,
|
||||||
|
SessionId = session.Id.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<YandexAuthQrCheck?> CheckQrAsync(int sessionId)
|
||||||
|
{
|
||||||
|
var session = await _dbContext.YandexAuthSessions.FindAsync(sessionId);
|
||||||
|
if (session == null) return null;
|
||||||
|
|
||||||
|
RestoreCookies(Service.CookieContainer, session.SerializedCookies);
|
||||||
|
if (Service.Client.AuthStorage.AuthToken is null)
|
||||||
|
{
|
||||||
|
Service.Client.AuthStorage.AuthToken = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
Service.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? "";
|
||||||
|
Service.Client.AuthStorage.AuthToken.TrackId = session?.TrackId ?? "";
|
||||||
|
Service.Client.AuthStorage.HeaderToken.CsfrToken = session?.HeaderCsfrToken ?? "";
|
||||||
|
Service.Client.AuthStorage.HeaderToken.ProcessUuid = session?.HeaderProcessId ?? "";
|
||||||
|
|
||||||
|
var status = await Api.Passport.CheckQRStatusAsync();
|
||||||
|
|
||||||
|
if (status?.State == "otp_auth_finished")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var auth = await Api.Passport.AuthorizeByQRAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new() { Status = Shared.Enums.YandexAuthQrStatus.Error, };
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbContext.YandexAuthSessions.Where(t => t.UserId == session.UserId).ExecuteDelete();
|
||||||
|
_dbContext.SaveChanges();
|
||||||
|
|
||||||
|
return new() { Status = Shared.Enums.YandexAuthQrStatus.Authorized, };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Status = Shared.Enums.YandexAuthQrStatus.Pending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string SerializeCookies(CookieContainer container)
|
||||||
|
{
|
||||||
|
var allCookies = new List<object>();
|
||||||
|
|
||||||
|
var cookies = container.GetAllCookies();
|
||||||
|
foreach (Cookie cookie in cookies)
|
||||||
|
{
|
||||||
|
allCookies.Add(new { cookie.Name, cookie.Value, cookie.Domain, cookie.Path });
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(allCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreCookies(CookieContainer container, string serializedCookies)
|
||||||
|
{
|
||||||
|
var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies);
|
||||||
|
foreach (var c in cookies)
|
||||||
|
{
|
||||||
|
container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CookieData
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Domain { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
342
PlaylistShared.Api/Services/Yandex/YandexMusicService.cs
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
using PlaylistShared.Api.Extensions;
|
||||||
|
using PlaylistShared.Shared.Enums;
|
||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
using YandexMusic.API;
|
||||||
|
using YandexMusic.API.Models.Playlist;
|
||||||
|
using YandexMusic.API.Models.Track;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Services;
|
||||||
|
|
||||||
|
public class YandexMusicService
|
||||||
|
{
|
||||||
|
private readonly YandexApiService _yandexApiService;
|
||||||
|
private YandexMusicApi Api => _yandexApiService.Client.Api;
|
||||||
|
private bool IsAuthorized => _yandexApiService.Client.IsAuthorized;
|
||||||
|
|
||||||
|
public YandexMusicService(YandexApiService yandexApiService)
|
||||||
|
{
|
||||||
|
_yandexApiService = yandexApiService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AuthorizeIfNot(ApplicationUser user)
|
||||||
|
{
|
||||||
|
if (!IsAuthorized)
|
||||||
|
{
|
||||||
|
var authResult = await _yandexApiService.AuthAsync(user);
|
||||||
|
if (authResult == null || authResult == false)
|
||||||
|
throw new Exception("Не удалось авторизоваться в Яндекс.Музыке. Проверьте токен.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> GetPlaylistAsync(ApplicationUser user, string ownerUid, string kind)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
return await Api.Playlist.GetAsync(ownerUid, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> CreatePlaylistAsync(ApplicationUser user, string title)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
return await Api.Playlist.CreateAsync(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> AddTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
var playlist = await Api.Playlist.GetAsync(ownerUid, kind);
|
||||||
|
if (playlist == null) return null;
|
||||||
|
|
||||||
|
var tracks = await Api.Track.GetAsync(trackIds);
|
||||||
|
if (tracks == null || !tracks.Any()) return null;
|
||||||
|
|
||||||
|
var insertedTracks = tracks.Where(t => !playlist.Tracks.Any(p => p.Track.Id == t.Id)).ToArray();
|
||||||
|
|
||||||
|
return await playlist.InsertTracksAsync(insertedTracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> RemoveTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
var tracks = await Api.Track.GetAsync(trackIds);
|
||||||
|
if (tracks == null || !tracks.Any()) return null;
|
||||||
|
|
||||||
|
var playlist = await Api.Playlist.GetAsync(ownerUid, kind);
|
||||||
|
if (playlist == null) return null;
|
||||||
|
|
||||||
|
return await playlist.RemoveTracksAsync(tracks.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetTrackFileUrlAsync(ApplicationUser user, string trackId)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
var track = await GetYTrackAsync(user, trackId);
|
||||||
|
if (track == null) return null;
|
||||||
|
|
||||||
|
return await track.GetLinkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YTrack?> GetYTrackAsync(ApplicationUser user, string trackId)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
var track = await Api.Track.GetAsync(trackId);
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YandexSearchResult> SearchAsync(
|
||||||
|
ApplicationUser user,
|
||||||
|
string query,
|
||||||
|
TrackSearchType? searchType = TrackSearchType.All,
|
||||||
|
int limit = 20
|
||||||
|
)
|
||||||
|
{
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
var ySerchType = searchType switch
|
||||||
|
{
|
||||||
|
TrackSearchType.Artist => YandexMusic.API.Models.Common.YSearchType.Artist,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
var searchResult = await Api.Search.SearchAsync(query, ySerchType, page: 0, pageSize: limit);
|
||||||
|
if (searchResult == null) return new YandexSearchResult();
|
||||||
|
|
||||||
|
return new YandexSearchResult
|
||||||
|
{
|
||||||
|
Tracks = searchResult.Tracks?.Results.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,
|
||||||
|
DurationMs = t.DurationMs,
|
||||||
|
}).ToList(),
|
||||||
|
|
||||||
|
Playlists = searchResult.Playlists?.Results.Select(p => new YandexPlaylist
|
||||||
|
{
|
||||||
|
Uuid = p.PlaylistUuid,
|
||||||
|
Kind = p.Kind,
|
||||||
|
OwnerUid = p.Owner?.Uid ?? string.Empty,
|
||||||
|
Title = p.Title,
|
||||||
|
Description = p.Description,
|
||||||
|
CoverUrl = string.IsNullOrEmpty(p.CoverUri) ? p.Cover.GetUrl() : p.CoverUri,
|
||||||
|
TrackCount = p.TrackCount,
|
||||||
|
}).ToList(),
|
||||||
|
|
||||||
|
Artists = searchResult.Artists?.Results.Select(a => new YandexArtist
|
||||||
|
{
|
||||||
|
Id = a.Id,
|
||||||
|
Name = a.Name,
|
||||||
|
CoverUrl = a.Cover.GetUrl(),
|
||||||
|
Description = a.Description?.Text ?? string.Empty,
|
||||||
|
}).ToList(),
|
||||||
|
|
||||||
|
Albums = searchResult.Albums?.Results.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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YandexSearchResult> SearchMyPlaylists(ApplicationUser user)
|
||||||
|
{
|
||||||
|
YandexSearchResult result = new();
|
||||||
|
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
|
||||||
|
var favoritesPlaylist = await Api.Playlist.FavoritesAsync();
|
||||||
|
result.Playlists = favoritesPlaylist?.Select(t => new YandexPlaylist
|
||||||
|
{
|
||||||
|
Uuid = t.PlaylistUuid,
|
||||||
|
Kind = t.Kind,
|
||||||
|
OwnerUid = t.Owner?.Uid ?? string.Empty,
|
||||||
|
Title = t.Title,
|
||||||
|
Description = t.Description,
|
||||||
|
CoverUrl = t.Cover.GetUrl(),
|
||||||
|
TrackCount = t.TrackCount,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var personalPlaylists = await Api.Playlist.GetPersonalPlaylistsAsync();
|
||||||
|
result.PersonalPlaylists = personalPlaylists?.Select(t => new YandexPlaylist
|
||||||
|
{
|
||||||
|
Uuid = t.PlaylistUuid,
|
||||||
|
Kind = t.Kind,
|
||||||
|
OwnerUid = t.Owner?.Uid ?? string.Empty,
|
||||||
|
Title = t.Title,
|
||||||
|
Description = t.Description,
|
||||||
|
CoverUrl = t.Cover.GetUrl(),
|
||||||
|
TrackCount = t.TrackCount,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var likedPlaylists = (await Api.Library.GetLikedPlaylistsAsync())?.Select(t => t.Playlist).ToList();
|
||||||
|
result.LikedPlaylists = likedPlaylists?.Select(t => new YandexPlaylist
|
||||||
|
{
|
||||||
|
Uuid = t.PlaylistUuid,
|
||||||
|
Kind = t.Kind,
|
||||||
|
OwnerUid = t.Owner?.Uid ?? string.Empty,
|
||||||
|
Title = t.Title,
|
||||||
|
Description = t.Description,
|
||||||
|
CoverUrl = t.Cover.GetUrl(),
|
||||||
|
TrackCount = t.TrackCount,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YandexSearchResult> SearchTracksByIdAsync(
|
||||||
|
ApplicationUser user,
|
||||||
|
string id,
|
||||||
|
TrackSearchType searchType
|
||||||
|
)
|
||||||
|
{
|
||||||
|
YandexSearchResult result = new();
|
||||||
|
|
||||||
|
await AuthorizeIfNot(user);
|
||||||
|
|
||||||
|
if (searchType == TrackSearchType.All)
|
||||||
|
{
|
||||||
|
throw new Exception("Для поиска по ID необходимо указать конкретный тип (трек, альбом, исполнитель или плейлист).");
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (searchType == TrackSearchType.Track)
|
||||||
|
{
|
||||||
|
var track = await Api.Track.GetAsync(id);
|
||||||
|
|
||||||
|
if (track != null)
|
||||||
|
{
|
||||||
|
result.Tracks = new List<YandexTrack>()
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
TrackId = track.Id,
|
||||||
|
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 Api.Album.GetAsync(id);
|
||||||
|
|
||||||
|
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 Api.Artist.GetAsync(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();
|
||||||
|
|
||||||
|
var allTraks = await artist.Artist.GetAllTracksAsync();
|
||||||
|
result.Tracks = allTraks?.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (searchType == TrackSearchType.Playlist)
|
||||||
|
{
|
||||||
|
var playlist = await Api.Playlist.GetAsync(id);
|
||||||
|
|
||||||
|
result.Tracks = playlist?.Tracks.Select(p => new YandexTrack
|
||||||
|
{
|
||||||
|
TrackId = p.Track.Id,
|
||||||
|
CoverUri = p.Track.CoverUri,
|
||||||
|
Artists = p.Track.Artists.Select(a => new YandexArtist
|
||||||
|
{
|
||||||
|
Id = a.Id,
|
||||||
|
Name = a.Name,
|
||||||
|
CoverUrl = a.Cover.GetUrl(),
|
||||||
|
Description = a.Description?.Text ?? string.Empty,
|
||||||
|
}).ToList(),
|
||||||
|
Title = p.Track.Title,
|
||||||
|
DurationMs = p.Track.DurationMs,
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.DataProtection;
|
|
||||||
using PlaylistShared.Api.Entities;
|
|
||||||
using YandexMusic;
|
|
||||||
using YandexMusic.API.Extensions.API;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Services;
|
|
||||||
|
|
||||||
public class YandexMusicService
|
|
||||||
{
|
|
||||||
private readonly IDataProtector _dataProtector;
|
|
||||||
|
|
||||||
public YandexMusicService(IDataProtectionProvider provider)
|
|
||||||
{
|
|
||||||
_dataProtector = provider.CreateProtector("YandexTokens");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<YandexMusicClient?> CreateClientAsync(ApplicationUser user)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(user.YandexAccessToken))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
string decryptedToken;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
decryptedToken = _dataProtector.Unprotect(user.YandexAccessToken);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = new YandexMusicClient();
|
|
||||||
var success = await client.Authorize(decryptedToken);
|
|
||||||
return success ? client : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<YPlaylist?> GetPlaylistAsync(ApplicationUser user, string ownerUid, string kind)
|
|
||||||
{
|
|
||||||
var client = await CreateClientAsync(user);
|
|
||||||
if (client == null) return null;
|
|
||||||
return await client.GetPlaylistAsync(ownerUid, kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<YPlaylist?> CreatePlaylistAsync(ApplicationUser user, string title)
|
|
||||||
{
|
|
||||||
var client = await CreateClientAsync(user);
|
|
||||||
if (client == null) return null;
|
|
||||||
return await client.CreatePlaylistAsync(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<YPlaylist?> AddTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
|
|
||||||
{
|
|
||||||
var client = await CreateClientAsync(user);
|
|
||||||
if (client == null) return null;
|
|
||||||
// Получаем треки по ID
|
|
||||||
var tracks = await client.GetTracksAsync(trackIds);
|
|
||||||
if (tracks == null || !tracks.Any()) return null;
|
|
||||||
var playlist = await client.GetPlaylistAsync(ownerUid, kind);
|
|
||||||
if (playlist == null) return null;
|
|
||||||
return await playlist.InsertTracksAsync(tracks.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<YPlaylist?> RemoveTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
|
|
||||||
{
|
|
||||||
var client = await CreateClientAsync(user);
|
|
||||||
if (client == null) return null;
|
|
||||||
var tracks = await client.GetTracksAsync(trackIds);
|
|
||||||
if (tracks == null || !tracks.Any()) return null;
|
|
||||||
var playlist = await client.GetPlaylistAsync(ownerUid, kind);
|
|
||||||
if (playlist == null) return null;
|
|
||||||
return await playlist.RemoveTracksAsync(tracks.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> GetTrackFileUrlAsync(ApplicationUser user, string trackId)
|
|
||||||
{
|
|
||||||
using var client = await CreateClientAsync(user);
|
|
||||||
if (client == null) return null;
|
|
||||||
var track = await client.GetTrackAsync(trackId);
|
|
||||||
if (track == null) return null;
|
|
||||||
return await track.GetLinkAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string EncryptToken(string token) => _dataProtector.Protect(token);
|
|
||||||
|
|
||||||
public string DecryptToken(string encryptedToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _dataProtector.Unprotect(encryptedToken);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,12 @@
|
|||||||
"Issuer": "PlaylistShared.Api",
|
"Issuer": "PlaylistShared.Api",
|
||||||
"Audience": "PlaylistShared.Client"
|
"Audience": "PlaylistShared.Client"
|
||||||
},
|
},
|
||||||
|
"Cors": {
|
||||||
|
"Origins": [
|
||||||
|
"https://api.playlistshare.frigat.duckdns.org",
|
||||||
|
"https://playlistshare.frigat.duckdns.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
"Yandex": {
|
"Yandex": {
|
||||||
"ClientId": "0916685f8a3641ca8fc382dbccf77236",
|
"ClientId": "0916685f8a3641ca8fc382dbccf77236",
|
||||||
"ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698"
|
"ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<CascadingAuthenticationState>
|
@using PlaylistShared.Pwa.Components.Common
|
||||||
|
|
||||||
|
<CascadingAuthenticationState>
|
||||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)" >
|
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)" >
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p role="alert">You are not authorized to access this resource.</p>
|
<p role="alert">У вас нет прав доступа к этому ресурсу.</p>
|
||||||
}
|
}
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeRouteView>
|
</AuthorizeRouteView>
|
||||||
|
|||||||
@@ -1,300 +0,0 @@
|
|||||||
@using Microsoft.JSInterop
|
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
|
||||||
@namespace PlaylistShared.Pwa.Components
|
|
||||||
@inject HttpClient Http
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<MudIconButton Icon="@(_isPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
|
|
||||||
Size="Size.Medium"
|
|
||||||
Color="Color.Primary"
|
|
||||||
OnClick="TogglePlayPause" />
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Stop"
|
|
||||||
Size="Size.Medium"
|
|
||||||
Color="Color.Default"
|
|
||||||
OnClick="Stop" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="flex-grow: 1; min-width: 150px;">
|
|
||||||
<MudSlider @bind-Value="_currentProgress"
|
|
||||||
@bind-Value:event="oninput"
|
|
||||||
Min="0"
|
|
||||||
Max="100"
|
|
||||||
Size="Size.Small"
|
|
||||||
ValueChanged="@((double newValue) => SeekTo(newValue))" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
|
||||||
<MudText Typo="Typo.body2">@_currentTime / @_totalTime</MudText>
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px; width: 120px;">
|
|
||||||
<MudIconButton Icon="@(_currentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)"
|
|
||||||
Size="Size.Small"
|
|
||||||
Color="Color.Default"
|
|
||||||
OnClick="ToggleMute" />
|
|
||||||
<MudSlider @bind-Value="_currentVolume"
|
|
||||||
@bind-Value:event="oninput"
|
|
||||||
Min="0"
|
|
||||||
Max="100"
|
|
||||||
Size="Size.Small"
|
|
||||||
ValueChanged="@((double newValue) => ChangeVolume(newValue))" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
<audio id="@_audioId" style="display: none;"></audio>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private string _audioId = $"audio_{Guid.NewGuid():N}";
|
|
||||||
private IJSObjectReference? _audioModule;
|
|
||||||
private IJSObjectReference? _audioElement;
|
|
||||||
private double _currentProgress;
|
|
||||||
private double _currentVolume = 70;
|
|
||||||
private string _currentTime = "0:00";
|
|
||||||
private string _totalTime = "0:00";
|
|
||||||
private bool _isPlaying;
|
|
||||||
private Timer? _progressTimer;
|
|
||||||
private bool _isMuted;
|
|
||||||
|
|
||||||
[Inject] protected IJSRuntime JS { get; set; } = null!;
|
|
||||||
[Inject] private TokenStorage TokenStorage { get; set; } = null!;
|
|
||||||
[Inject] private AuthenticationStateProvider AuthProvider { get; set; } = null!;
|
|
||||||
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>Требовать ли авторизацию для воспроизведения (по умолчанию true).</summary>
|
|
||||||
[Parameter] public bool RequireAuth { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>Событие при завершении трека.</summary>
|
|
||||||
[Parameter] public EventCallback OnTrackEnded { get; set; }
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
await EnsureAudioModuleAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnAudioEnded()
|
|
||||||
{
|
|
||||||
_isPlaying = false;
|
|
||||||
_currentProgress = 0;
|
|
||||||
StopProgressTimer();
|
|
||||||
if (OnTrackEnded.HasDelegate)
|
|
||||||
await OnTrackEnded.InvokeAsync();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnTimeUpdate(double currentTime, double duration)
|
|
||||||
{
|
|
||||||
// Защита от некорректных значений
|
|
||||||
if (double.IsNaN(currentTime) || double.IsNaN(duration) || double.IsInfinity(currentTime) || double.IsInfinity(duration))
|
|
||||||
return;
|
|
||||||
if (duration <= 0) return;
|
|
||||||
|
|
||||||
_currentProgress = (currentTime / duration) * 100;
|
|
||||||
_currentTime = FormatTime(currentTime);
|
|
||||||
// Длительность не обновляем здесь
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnDurationReady(double duration)
|
|
||||||
{
|
|
||||||
if (double.IsNaN(duration) || double.IsInfinity(duration) || duration <= 0) return;
|
|
||||||
_totalTime = FormatTime(duration);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureAudioModuleAsync()
|
|
||||||
{
|
|
||||||
if (_audioModule == null)
|
|
||||||
_audioModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/AudioPlayer.js");
|
|
||||||
if (_audioElement == null)
|
|
||||||
_audioElement = await _audioModule.InvokeAsync<IJSObjectReference>("init", _audioId, DotNetObjectReference.Create(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CheckAuthAsync()
|
|
||||||
{
|
|
||||||
if (!RequireAuth) return true;
|
|
||||||
|
|
||||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
|
||||||
var isAuthenticated = authState.User.Identity?.IsAuthenticated == true;
|
|
||||||
|
|
||||||
if (!isAuthenticated)
|
|
||||||
{
|
|
||||||
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadAndPlayAsync(string trackId)
|
|
||||||
{
|
|
||||||
if (!await CheckAuthAsync()) return;
|
|
||||||
|
|
||||||
var tokens = await TokenStorage.GetTokensAsync();
|
|
||||||
var accessToken = tokens.token;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(accessToken))
|
|
||||||
{
|
|
||||||
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamUrl = new Uri(Http.BaseAddress, $"/api/audio/track/{trackId}").ToString();
|
|
||||||
|
|
||||||
await EnsureAudioModuleAsync();
|
|
||||||
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken);
|
|
||||||
_isPlaying = true;
|
|
||||||
StartProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PlayAsync()
|
|
||||||
{
|
|
||||||
if (!await CheckAuthAsync()) return;
|
|
||||||
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
await _audioElement.InvokeVoidAsync("play");
|
|
||||||
_isPlaying = true;
|
|
||||||
StartProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PauseAsync()
|
|
||||||
{
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
await _audioElement.InvokeVoidAsync("pause");
|
|
||||||
_isPlaying = false;
|
|
||||||
StopProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StopAsync()
|
|
||||||
{
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
await _audioElement.InvokeVoidAsync("stop");
|
|
||||||
_isPlaying = false;
|
|
||||||
_currentProgress = 0;
|
|
||||||
StopProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TogglePlayPause()
|
|
||||||
{
|
|
||||||
if (_isPlaying)
|
|
||||||
await PauseAsync();
|
|
||||||
else
|
|
||||||
await PlayAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Stop()
|
|
||||||
{
|
|
||||||
await StopAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SeekTo(double value)
|
|
||||||
{
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
|
||||||
if (duration > 0 && !double.IsNaN(duration))
|
|
||||||
{
|
|
||||||
var newTime = (value / 100) * duration;
|
|
||||||
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"SeekTo error: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ChangeVolume(double value)
|
|
||||||
{
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var volume = value / 100;
|
|
||||||
await _audioElement.InvokeVoidAsync("setVolume", volume);
|
|
||||||
_isMuted = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"ChangeVolume error: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ToggleMute()
|
|
||||||
{
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
_isMuted = !_isMuted;
|
|
||||||
var newVolume = _isMuted ? 0 : (_currentVolume / 100);
|
|
||||||
await _audioElement.InvokeVoidAsync("setVolume", newVolume);
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartProgressTimer()
|
|
||||||
{
|
|
||||||
StopProgressTimer();
|
|
||||||
_progressTimer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
await UpdateProgress();
|
|
||||||
}, null, 0, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StopProgressTimer() => _progressTimer?.Dispose();
|
|
||||||
|
|
||||||
private async Task UpdateProgress()
|
|
||||||
{
|
|
||||||
if (_audioElement == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("UpdateProgress: _audioElement is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var current = await _audioElement.InvokeAsync<double>("getCurrentTime");
|
|
||||||
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
|
||||||
|
|
||||||
if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current))
|
|
||||||
{
|
|
||||||
_currentProgress = (current / duration) * 100;
|
|
||||||
_currentTime = FormatTime(current);
|
|
||||||
_totalTime = FormatTime(duration);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"UpdateProgress error: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FormatTime(double seconds)
|
|
||||||
{
|
|
||||||
var total = (int)seconds;
|
|
||||||
var mins = total / 60;
|
|
||||||
var secs = total % 60;
|
|
||||||
return $"{mins}:{secs:D2}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
StopProgressTimer();
|
|
||||||
if (_audioElement != null)
|
|
||||||
await _audioElement.DisposeAsync();
|
|
||||||
if (_audioModule != null)
|
|
||||||
await _audioModule.DisposeAsync();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("login");
|
||||||
|
}
|
||||||
|
}
|
||||||
84
PlaylistShared.Pwa/Components/Common/ShareButton.razor
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Share"
|
||||||
|
Color="Color.Default"
|
||||||
|
OnClick="@TogglePopover"
|
||||||
|
Size="Size.Medium" />
|
||||||
|
|
||||||
|
<MudPopover Open="@_popoverOpen"
|
||||||
|
AnchorOrigin="Origin.BottomCenter"
|
||||||
|
TransformOrigin="Origin.TopCenter"
|
||||||
|
RelativeWidth="DropdownWidth.Adaptive"
|
||||||
|
Paper="true">
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-2">Ссылка для приглашения:</MudText>
|
||||||
|
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
|
||||||
|
<MudTextField @bind-Value="_shareUrl"
|
||||||
|
ReadOnly="true"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
FullWidth="true" />
|
||||||
|
<MudIconButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Medium"
|
||||||
|
OnClick="CopyLink"
|
||||||
|
Icon="@Icons.Material.Filled.ContentCopy">
|
||||||
|
</MudIconButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudPopover>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _popoverOpen;
|
||||||
|
private string _shareUrl = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ссылка для копирования. Если не указана, используется текущий URL страницы.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public string ShareUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ShareUrl))
|
||||||
|
ShareUrl = Navigation.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(ShareUrl))
|
||||||
|
_shareUrl = ShareUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TogglePopover()
|
||||||
|
{
|
||||||
|
if (_popoverOpen)
|
||||||
|
{
|
||||||
|
_popoverOpen = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ShareUrl))
|
||||||
|
{
|
||||||
|
Snackbar.Add("Ссылка недоступна", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_shareUrl = ShareUrl;
|
||||||
|
_popoverOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyLink()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("navigator.clipboard.writeText", _shareUrl);
|
||||||
|
Snackbar.Add("Ссылка скопирована в буфер обмена", Severity.Success);
|
||||||
|
_popoverOpen = false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Не удалось скопировать ссылку: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
PlaylistShared.Pwa/Components/Common/ShareDialog.razor
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">Поделиться плейлистом</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText>Скопируйте ссылку и отправьте её друзьям:</MudText>
|
||||||
|
<MudTextField @bind-Value="ShareUrl"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
ReadOnly="true"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Class="mt-2" />
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
OnClick="CopyToClipboard">
|
||||||
|
Скопировать ссылку
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Default"
|
||||||
|
OnClick="Close">
|
||||||
|
Закрыть
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ShareUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private async Task CopyToClipboard()
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("navigator.clipboard.writeText", ShareUrl);
|
||||||
|
Snackbar.Add("Ссылка скопирована в буфер обмена!", Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close() => MudDialog.Cancel();
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using PlaylistShared.Shared.DTO
|
||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
@inject IAudioPlayerService AudioPlayerService
|
||||||
|
|
||||||
|
<MudItem @onmouseenter="HandleMouseEnter"
|
||||||
|
@onmouseleave="HandleMouseLeave"
|
||||||
|
style="position: relative; display: inline-block; cursor: pointer; border-radius: 4px; overflow: hidden;">
|
||||||
|
|
||||||
|
<MudImage Src="@Track?.CoverUri.FormatCoverUrl(Width, Height)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
|
||||||
|
|
||||||
|
@if (CanPlay && (_isHovered || IsCurrentTrackPlaying))
|
||||||
|
{
|
||||||
|
<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; transition: opacity 0.2s ease; cursor: pointer;">
|
||||||
|
<MudIconButton Icon="@(IsCurrentTrackPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
|
||||||
|
Color="Color.Inherit"
|
||||||
|
Size="Size.Large"
|
||||||
|
OnClick="OnPlayClick" />
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public YandexTrack? Track { get; set; } = null;
|
||||||
|
[Parameter] public string TrackId { get; set; } = string.Empty;
|
||||||
|
[Parameter] public int Height { get; set; } = 50;
|
||||||
|
[Parameter] public int Width { get; set; } = 50;
|
||||||
|
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
|
||||||
|
[Parameter] public bool CanPlay { get; set; } = false;
|
||||||
|
|
||||||
|
private bool IsCurrentTrackPlaying => AudioPlayerService.IsPlaying && AudioPlayerService.CurrentTrackId == TrackId;
|
||||||
|
|
||||||
|
private bool _isHovered;
|
||||||
|
|
||||||
|
private void HandleMouseEnter() => _isHovered = true;
|
||||||
|
private void HandleMouseLeave() => _isHovered = false;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
AudioPlayerService.OnStateChanged += OnPlayerStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPlayClick()
|
||||||
|
{
|
||||||
|
var playlistShareToken = string.IsNullOrWhiteSpace(PlaylistShareToken) ? null : PlaylistShareToken;
|
||||||
|
|
||||||
|
if (IsCurrentTrackPlaying)
|
||||||
|
{
|
||||||
|
await AudioPlayerService.PauseAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await AudioPlayerService.LoadAndPlayAsync(
|
||||||
|
trackId: TrackId,
|
||||||
|
playlistShareToken: playlistShareToken,
|
||||||
|
track: Track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerStateChanged()
|
||||||
|
{
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
PlaylistShared.Pwa/Components/Common/TrackItem.razor
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@using PlaylistShared.Shared.DTO
|
||||||
|
@using PlaylistShared.Pwa.Components.Common
|
||||||
|
@using PlaylistShared.Pwa.Extensions
|
||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
|
||||||
|
<MudStack Row AlignItems="AlignItems.Center">
|
||||||
|
<!-- Обложка с фиксированной шириной -->
|
||||||
|
<MudItem>
|
||||||
|
<TrackCoverWithPlay TrackId="@Track.TrackId"
|
||||||
|
Track="@Track"
|
||||||
|
PlaylistShareToken="@PlaylistShareToken"
|
||||||
|
CanPlay="@CanPlay"
|
||||||
|
Width="40" Height="40" />
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Информация о треке (занимает всё доступное место) -->
|
||||||
|
<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>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudSpacer />
|
||||||
|
|
||||||
|
<!-- Длительность (фиксированная ширина по содержимому) -->
|
||||||
|
<MudItem>
|
||||||
|
<MudText Typo="Typo.body2">@Track.DurationMs.FormatDuration()</MudText>
|
||||||
|
</MudItem>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public YandexTrack Track { get; set; } = null!;
|
||||||
|
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
|
||||||
|
[Parameter] public bool CanPlay { get; set; } = true;
|
||||||
|
}
|
||||||
23
PlaylistShared.Pwa/Components/Common/TrackItemSkeleton.razor
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@using PlaylistShared.Pwa.Components.Common
|
||||||
|
|
||||||
|
<MudStack Class="py-2 px-0" Row AlignItems="AlignItems.Center">
|
||||||
|
<!-- Обложка-скелет -->
|
||||||
|
<MudItem>
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Width="40px" Height="40px" />
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Информация о треке (две строки текста) -->
|
||||||
|
<MudItem>
|
||||||
|
<MudStack Spacing="0">
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Text" Width="180px" Class="my-0" />
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Text" Width="120px" Class="my-0" />
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudSpacer />
|
||||||
|
|
||||||
|
<!-- Длительность-скелет -->
|
||||||
|
<MudItem>
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Text" Width="30px" />
|
||||||
|
</MudItem>
|
||||||
|
</MudStack>
|
||||||
315
PlaylistShared.Pwa/Components/Global/AudioPlayer.razor
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
@using Microsoft.JSInterop
|
||||||
|
@inject IAudioPlayerService AudioPlayerService
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject TokenStorage TokenStorage
|
||||||
|
@inject PlayerStorage PlayerStorage
|
||||||
|
@inject AuthenticationStateProvider AuthProvider
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@inject HttpClient Http
|
||||||
|
@implements IDisposable
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
<MudStack Spacing="1" Row AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap">
|
||||||
|
<!-- Кнопки управления -->
|
||||||
|
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
|
||||||
|
@onmouseleave="() => { _isPlayHovered = false; }"
|
||||||
|
Class="relative d-inline-block rounded overflow-hidden cursor-pointer"
|
||||||
|
Style="width: 50px; height: 50px;">
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrack?.CoverUri))
|
||||||
|
{
|
||||||
|
<MudImage Src="@AudioPlayerService.CurrentTrack.CoverUri.FormatCoverUrl(50, 50)" Height="50" Width="50" Class="rounded d-block" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudItem Class="absolute d-flex align-center justify-center rounded"
|
||||||
|
Style="top: 0; left: 0; right: 0; bottom: 0; background: transparent;">
|
||||||
|
<MudToggleIconButton Toggled="@AudioPlayerService.IsPlaying"
|
||||||
|
Icon="@Icons.Material.Filled.PlayArrow"
|
||||||
|
Color="@Color.Primary"
|
||||||
|
Size="Size.Large"
|
||||||
|
ToggledIcon="@Icons.Material.Filled.Pause"
|
||||||
|
ToggledColor="@Color.Primary"
|
||||||
|
ToggledChanged="OnPlayClick" />
|
||||||
|
</MudItem>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Название и прогресс -->
|
||||||
|
@if (AudioPlayerService.CurrentTrack != null)
|
||||||
|
{
|
||||||
|
<MudStack Spacing="0" AlignItems="AlignItems.Stretch" Class="d-flex flex-grow-1 relative overflow-hidden align-center rounded" Style="height: 50px;">
|
||||||
|
<MudItem Class="absolute" style="top: 0; left: 0; right: 0; bottom: 0; z-index: 1;">
|
||||||
|
<TrackProgress Value="@AudioPlayerService.CurrentTime"
|
||||||
|
Min="0" Max="@AudioPlayerService.TotalTime"
|
||||||
|
Height="50"
|
||||||
|
BufferValue="@_bufferSecond"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Icon=""
|
||||||
|
Step="0.1"
|
||||||
|
Opacity="0.3"
|
||||||
|
Buffer
|
||||||
|
ValueChanged="SeekTo" />
|
||||||
|
</MudItem>
|
||||||
|
<MudStack Spacing="0" Row AlignItems="AlignItems.Center" Class="px-3 relative pointer-events-none" Style="z-index: 2; width: 100%; height: 100%;">
|
||||||
|
<MudStack AlignItems="AlignItems.Start" Spacing="0" Style="min-width: 0; width: 100%;">
|
||||||
|
<MudText Typo="Typo.body2" Inline Color="Color.Default" Style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;">
|
||||||
|
@AudioPlayerService.CurrentTrack.Title
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.body2" Inline Color="Color.Default" Style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;">
|
||||||
|
@string.Join(", ", AudioPlayerService.CurrentTrack.Artists.Select(a => a.Name))
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudSpacer />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
@AudioPlayerService.CurrentTimeString
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudSpacer />
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudHidden Breakpoint="Breakpoint.SmAndDown">
|
||||||
|
<!-- Громкость -->
|
||||||
|
<MudItem @onmouseenter="() => _volumeIsOpen = true"
|
||||||
|
@onmouseleave="() => _volumeIsOpen = false"
|
||||||
|
@onwheel="OnVolumeHandleWheel"
|
||||||
|
Style="position: relative; display: flex; align-items: center;">
|
||||||
|
|
||||||
|
<MudIconButton Icon="@(AudioPlayerService.CurrentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Default"
|
||||||
|
OnClick="ToggleMute" />
|
||||||
|
|
||||||
|
<MudPopover Open="@_volumeIsOpen"
|
||||||
|
AnchorOrigin="Origin.TopCenter"
|
||||||
|
TransformOrigin="Origin.BottomCenter"
|
||||||
|
Fixed
|
||||||
|
Class="pa-0 mt-n5"
|
||||||
|
Style="height:120px; width: 10px; background-color: transparent !important; overflow: visible !important;">
|
||||||
|
<MudProgressLinear Vertical Color="Color.Primary" Size="Size.Medium" Value="@AudioPlayerService.CurrentVolume" />
|
||||||
|
|
||||||
|
</MudPopover>
|
||||||
|
</MudItem>
|
||||||
|
</MudHidden>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<audio id="@_audioId" style="display: none;"></audio>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private const double _volumeDefault = 50;
|
||||||
|
|
||||||
|
// Генерируем уникальный ID для аудиоэлемента, чтобы избежать конфликтов при множественных экземплярах
|
||||||
|
private string _audioId = $"audio_{Guid.NewGuid():N}";
|
||||||
|
private IJSObjectReference? _audioModule;
|
||||||
|
private IJSObjectReference? _audioElement;
|
||||||
|
|
||||||
|
// Громкость
|
||||||
|
private bool _volumeIsOpen;
|
||||||
|
private double _volumeBeforeMute;
|
||||||
|
private double _bufferSecond;
|
||||||
|
|
||||||
|
private bool _isPlayHovered;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
AudioPlayerService.OnLoadAndPlayRequested += OnServiceLoadAndPlay;
|
||||||
|
AudioPlayerService.OnPlayRequested += OnServicePlay;
|
||||||
|
AudioPlayerService.OnPauseRequested += OnServicePause;
|
||||||
|
AudioPlayerService.OnSeekRequested += OnServiceSeek;
|
||||||
|
AudioPlayerService.OnVolumeChangeRequested += OnServiceVolumeChange;
|
||||||
|
AudioPlayerService.OnStateChanged += OnServiceStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
await EnsureAudioModuleAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureAudioModuleAsync()
|
||||||
|
{
|
||||||
|
if (_audioModule == null)
|
||||||
|
_audioModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/AudioPlayer.js");
|
||||||
|
if (_audioElement == null)
|
||||||
|
_audioElement = await _audioModule.InvokeAsync<IJSObjectReference>("init", _audioId, DotNetObjectReference.Create(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Обработка JS
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnAudioEnded()
|
||||||
|
{
|
||||||
|
AudioPlayerService.NotifyTrackEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnTimeUpdate(double currentTime, double duration)
|
||||||
|
{
|
||||||
|
if (!double.IsNaN(duration) && !double.IsNaN(currentTime) && duration > 0)
|
||||||
|
{
|
||||||
|
AudioPlayerService.UpdateProgress(currentTime, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnDownloadProgress(double second)
|
||||||
|
{
|
||||||
|
_bufferSecond = second;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Обработка сервиса
|
||||||
|
private async Task OnServiceLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken))
|
||||||
|
{
|
||||||
|
var tokens = await TokenStorage.GetTokensAsync();
|
||||||
|
accessToken = tokens.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
|
||||||
|
{
|
||||||
|
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
|
||||||
|
await EnsureAudioModuleAsync();
|
||||||
|
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, sharedPlaylistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnServicePlay()
|
||||||
|
{
|
||||||
|
if (_audioElement == null) return;
|
||||||
|
await _audioElement.InvokeVoidAsync("play");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnServicePause()
|
||||||
|
{
|
||||||
|
if (_audioElement == null) return;
|
||||||
|
await _audioElement.InvokeVoidAsync("pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnServiceSeek(double time)
|
||||||
|
{
|
||||||
|
if (_audioElement == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _audioElement.InvokeVoidAsync("setCurrentTime", time);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Seek error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnServiceVolumeChange(double volume)
|
||||||
|
{
|
||||||
|
if (_audioElement == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Volume change error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServiceStateChanged()
|
||||||
|
{
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task<bool> CheckAuthAsync()
|
||||||
|
{
|
||||||
|
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
||||||
|
if (!authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnVolumeHandleWheel(WheelEventArgs e)
|
||||||
|
{
|
||||||
|
// Изменяем громкость на 5 единиц за один тик колесика
|
||||||
|
double step = 5;
|
||||||
|
double newVolume = e.DeltaY < 0
|
||||||
|
? Math.Min(AudioPlayerService.CurrentVolume + step, 100)
|
||||||
|
: Math.Max(AudioPlayerService.CurrentVolume - step, 0);
|
||||||
|
|
||||||
|
await ChangeVolume(newVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeekTo(double value)
|
||||||
|
{
|
||||||
|
await AudioPlayerService.SeekToAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ChangeVolume(double value)
|
||||||
|
{
|
||||||
|
await AudioPlayerService.SetVolumeAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleMute()
|
||||||
|
{
|
||||||
|
if (AudioPlayerService.CurrentVolume > 0)
|
||||||
|
{
|
||||||
|
_volumeBeforeMute = AudioPlayerService.CurrentVolume;
|
||||||
|
await AudioPlayerService.SetVolumeAsync(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await AudioPlayerService.SetVolumeAsync(_volumeBeforeMute);
|
||||||
|
_volumeBeforeMute = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPlayClick()
|
||||||
|
{
|
||||||
|
if (AudioPlayerService.IsPlaying)
|
||||||
|
await AudioPlayerService.PauseAsync();
|
||||||
|
else
|
||||||
|
await AudioPlayerService.PlayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AudioPlayerService.OnLoadAndPlayRequested -= OnServiceLoadAndPlay;
|
||||||
|
AudioPlayerService.OnPlayRequested -= OnServicePlay;
|
||||||
|
AudioPlayerService.OnPauseRequested -= OnServicePause;
|
||||||
|
AudioPlayerService.OnSeekRequested -= OnServiceSeek;
|
||||||
|
AudioPlayerService.OnVolumeChangeRequested -= OnServiceVolumeChange;
|
||||||
|
AudioPlayerService.OnStateChanged -= OnServiceStateChanged;
|
||||||
|
|
||||||
|
if (_audioElement != null)
|
||||||
|
await _audioElement.DisposeAsync();
|
||||||
|
if (_audioModule != null)
|
||||||
|
await _audioModule.DisposeAsync();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeAsync().AsTask().Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
PlaylistShared.Pwa/Components/Global/ContextualBarContent.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Components.Global;
|
||||||
|
|
||||||
|
public class ContextualBarContent : ComponentBase, IDisposable
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
public ContextualActionBarService ContextualActionBarService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public ContextualActionBarPosition Position { get; set; } = ContextualActionBarPosition.Default;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
ContextualActionBarService.Content = ChildContent;
|
||||||
|
ContextualActionBarService.Position = Position;
|
||||||
|
ContextualActionBarService.ChangeParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ContextualActionBarService.Content = null;
|
||||||
|
ContextualActionBarService.Position = ContextualActionBarPosition.Default;
|
||||||
|
ContextualActionBarService.ChangeParameters();
|
||||||
|
}
|
||||||
|
}
|
||||||
127
PlaylistShared.Pwa/Components/Global/TrackProgress.razor
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
@using MudBlazor
|
||||||
|
|
||||||
|
<div class="track-progress-container @ColorClass"
|
||||||
|
@onwheel="HandleWheel"
|
||||||
|
style="--track-height: @(Height)px; height: @(Height)px; --track-opacity: @(Opacity.ToString(System.Globalization.CultureInfo.InvariantCulture));">
|
||||||
|
|
||||||
|
<div class="progress-base-track">
|
||||||
|
@if (Buffer)
|
||||||
|
{
|
||||||
|
<div class="progress-buffer-bar" style="width:@(CalculatePercentage(BufferValue).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture))%;"></div>
|
||||||
|
}
|
||||||
|
<div class="progress-active-bar" style="width:@(CalculatePercentage(Value).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture))%;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Icon))
|
||||||
|
{
|
||||||
|
<div class="track-icon-thumb" style="left: @(CalculatePercentage(Value).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture))%;">
|
||||||
|
<MudIcon Icon="@Icon" Size="Size.Small" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<input type="range"
|
||||||
|
min="@Min.ToString(System.Globalization.CultureInfo.InvariantCulture)"
|
||||||
|
max="@Max.ToString(System.Globalization.CultureInfo.InvariantCulture)"
|
||||||
|
step="@Step.ToString(System.Globalization.CultureInfo.InvariantCulture)"
|
||||||
|
value="@Value.ToString(System.Globalization.CultureInfo.InvariantCulture)"
|
||||||
|
height="@Height"
|
||||||
|
@oninput="OnInput"
|
||||||
|
class="progress-input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public double Value { get; set; }
|
||||||
|
[Parameter] public double BufferValue { get; set; }
|
||||||
|
[Parameter] public bool Buffer { get; set; } = true;
|
||||||
|
[Parameter] public double Min { get; set; } = 0;
|
||||||
|
[Parameter] public double Max { get; set; } = 100;
|
||||||
|
[Parameter] public double Step { get; set; } = 1;
|
||||||
|
[Parameter] public double Opacity { get; set; } = 1.0;
|
||||||
|
[Parameter] public int Height { get; set; } = 4;
|
||||||
|
[Parameter] public Color Color { get; set; } = Color.Primary;
|
||||||
|
[Parameter] public string Icon { get; set; } = "";
|
||||||
|
[Parameter] public EventCallback<double> ValueChanged { get; set; }
|
||||||
|
|
||||||
|
private string ColorClass => $"track-color-{Color.ToString().ToLower()}";
|
||||||
|
|
||||||
|
private double CalculatePercentage(double val) => Max <= Min ? 0 : ((Math.Clamp(val, Min, Max) - Min) / (Max - Min)) * 100;
|
||||||
|
|
||||||
|
private async Task OnInput(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (double.TryParse(e.Value?.ToString(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var newValue))
|
||||||
|
await ValueChanged.InvokeAsync(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleWheel(WheelEventArgs e)
|
||||||
|
{
|
||||||
|
double range = Max - Min;
|
||||||
|
double wheelStep = range * 0.02;
|
||||||
|
var newValue = e.DeltaY < 0 ? Math.Min(Value + wheelStep, Max) : Math.Max(Value - wheelStep, Min);
|
||||||
|
await ValueChanged.InvokeAsync(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.track-progress-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
--track-color: var(--mud-palette-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-color-primary { --track-color: var(--mud-palette-primary); }
|
||||||
|
.track-color-secondary { --track-color: var(--mud-palette-secondary); }
|
||||||
|
.track-color-success { --track-color: var(--mud-palette-success); }
|
||||||
|
.track-color-info { --track-color: var(--mud-palette-info); }
|
||||||
|
.track-color-warning { --track-color: var(--mud-palette-warning); }
|
||||||
|
.track-color-error { --track-color: var(--mud-palette-error); }
|
||||||
|
|
||||||
|
.progress-base-track {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--track-height);
|
||||||
|
background-color: var(--mud-palette-action-disabled-background, rgba(0,0,0,0.1));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-active-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
background-color: var(--track-color);
|
||||||
|
opacity: var(--track-opacity); /* Применяем opacity */
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-buffer-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
background-color: var(--mud-palette-action-default-hover);
|
||||||
|
opacity: calc(var(--track-opacity) * 0.5); /* Буфер чуть прозрачнее основного прогресса */
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-icon-thumb {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 11;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--track-color);
|
||||||
|
opacity: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-progress-container:hover .track-icon-thumb { opacity: 1; }
|
||||||
|
|
||||||
|
.progress-input {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-input::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; }
|
||||||
|
.progress-input::-moz-range-thumb { opacity: 0; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
@using System.Threading
|
||||||
|
@using PlaylistShared.Shared.DTO
|
||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">Авторизация Яндекс.Музыки по QR</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
@if (_qrUrl != null)
|
||||||
|
{
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-2">Отсканируйте QR-код приложением Яндекс</MudText>
|
||||||
|
<MudImage Src="@_qrUrl" Style="max-width: 250px; border-radius: 12px; background-color: white;" />
|
||||||
|
<MudText Typo="Typo.body2" Class="mt-2" Color="Color.Secondary">
|
||||||
|
Статус: @_statusText
|
||||||
|
</MudText>
|
||||||
|
@if (_isWaiting)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Indeterminate Class="mt-2" Size="Size.Small" />
|
||||||
|
}
|
||||||
|
@if (_isError)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error" Class="mt-4">
|
||||||
|
@_errorMessage
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudProgressCircular Indeterminate />
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" OnClick="Cancel">Отмена</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||||
|
|
||||||
|
private string _qrUrl;
|
||||||
|
private string _sessionId;
|
||||||
|
private string _statusText = "Ожидание сканирования";
|
||||||
|
private bool _isWaiting = true;
|
||||||
|
private bool _isError = false;
|
||||||
|
private string _errorMessage = "";
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await StartQrFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartQrFlow()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. Получить QR и sessionId
|
||||||
|
var response = await Http.GetFromJsonAsync<ApiResponse<YandexAuthQr>>("/api/yandexaccount/qr");
|
||||||
|
if (!response.Success || response.Data == null)
|
||||||
|
{
|
||||||
|
ShowError("Не удалось получить QR-код");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_qrUrl = response.Data.QrLink;
|
||||||
|
_sessionId = response.Data.SessionId;
|
||||||
|
|
||||||
|
// 2. Начать опрос статуса
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_ = PollStatus(_cts.Token);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowError(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PollStatus(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(500, token);
|
||||||
|
var statusResponse = await Http.GetFromJsonAsync<ApiResponse<YandexAuthQrCheck>>($"/api/yandexaccount/qr/{_sessionId}", token);
|
||||||
|
if (statusResponse?.Data != null)
|
||||||
|
{
|
||||||
|
switch (statusResponse.Data.Status)
|
||||||
|
{
|
||||||
|
case Shared.Enums.YandexAuthQrStatus.Pending:
|
||||||
|
_statusText = "Ожидание подтверждения...";
|
||||||
|
break;
|
||||||
|
case Shared.Enums.YandexAuthQrStatus.Authorized:
|
||||||
|
_statusText = "Авторизация успешна!";
|
||||||
|
_isWaiting = false;
|
||||||
|
Snackbar.Add("Авторизация выполнена", Severity.Success);
|
||||||
|
_cts?.Cancel();
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
return;
|
||||||
|
case Shared.Enums.YandexAuthQrStatus.Expired:
|
||||||
|
_cts?.Cancel();
|
||||||
|
ShowError("Срок действия QR-кода истёк");
|
||||||
|
return;
|
||||||
|
case Shared.Enums.YandexAuthQrStatus.Error:
|
||||||
|
_cts?.Cancel();
|
||||||
|
ShowError("Ошибка авторизации");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) { break; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowError(ex.Message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowError(string message)
|
||||||
|
{
|
||||||
|
_isError = true;
|
||||||
|
_errorMessage = message;
|
||||||
|
_isWaiting = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
MudDialog.Close(DialogResult.Cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
@using System.Text.RegularExpressions
|
||||||
|
@using PlaylistShared.Shared.DTO
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">Подключение Яндекс.Музыки</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStepper @bind-ActiveIndex="_index" Vertical CenterLabels CompletedStepColor="Color.Success" @onwheel="HandleWheel">
|
||||||
|
<ChildContent>
|
||||||
|
<MudStep Title="Вход" Skippable>
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-4">Нажмите на кнопку и разрешите доступ приложению.</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@PlaylistShared.Pwa.CustomIcons.Yandex"
|
||||||
|
Href="https://oauth.yandex.ru/authorize?response_type=token&client_id=23cabbbdc6cd418abb4b39c32c41195d"
|
||||||
|
Target="_blank"
|
||||||
|
@onclick="() => {_index++;}"
|
||||||
|
FullWidth>
|
||||||
|
Войти в Яндекс
|
||||||
|
</MudButton>
|
||||||
|
</MudStep>
|
||||||
|
|
||||||
|
<MudStep Title="Добавление токена">
|
||||||
|
Скопируйте значение <code>access_token</code> или <code>весь URL</code> из адресной строки после перенаправления.
|
||||||
|
<MudAlert Severity="Severity.Info" Class="mt-4">
|
||||||
|
https://music.yandex.ru/#access_token=<code>ВАШ_ТОКЕН</code>&...
|
||||||
|
</MudAlert>
|
||||||
|
<MudTextField @bind-Value="_rawInput" @bind-Value:after="Submit" Label="Вставьте скопированную ссылку или токен" Variant="Variant.Outlined" Margin="Margin.Dense" Error="_tokenErr" />
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="Submit" FullWidth Class="mt-4">
|
||||||
|
Сохранить
|
||||||
|
</MudButton>
|
||||||
|
</MudStep>
|
||||||
|
</ChildContent>
|
||||||
|
<ActionContent Context="stepper">
|
||||||
|
<MudIconButton OnClick="@(() => stepper.PreviousStepAsync())" Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Disabled="@(_index <= 0)" />
|
||||||
|
<MudSpacer />
|
||||||
|
<MudIconButton OnClick="@(() => stepper.NextStepAsync())" Icon="@Icons.Material.Filled.ArrowForward" Color="Color.Primary" Disabled="@(_index >= 1)" />
|
||||||
|
</ActionContent>
|
||||||
|
</MudStepper>
|
||||||
|
</DialogContent>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||||
|
private string _rawInput = "";
|
||||||
|
private int _index;
|
||||||
|
private bool _tokenErr = false;
|
||||||
|
|
||||||
|
private async Task HandleWheel(WheelEventArgs e)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (e.DeltaY > 0 && _index < 1) // Прокрутка вниз -> Вперед
|
||||||
|
{
|
||||||
|
_index++;
|
||||||
|
}
|
||||||
|
else if (e.DeltaY < 0 && _index > 0) // Прокрутка вверх -> Назад
|
||||||
|
{
|
||||||
|
_index--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
var token = ExtractToken(_rawInput);
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
_tokenErr = true;
|
||||||
|
Snackbar.Add("Токен не найден", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tokenErr = false;
|
||||||
|
|
||||||
|
var response = await Http.PostAsJsonAsync("/api/yandexaccount/token", new SetYandexTokenRequest { Token = token });
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Snackbar.Add("Токен успешно обновлен", Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add("Ошибка обновления токена. Повторите позже.", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractToken(string input) =>
|
||||||
|
input.Contains("access_token=") ? Regex.Match(input, @"access_token=([^&]+)").Groups[1].Value : input.Trim();
|
||||||
|
|
||||||
|
public class SetYandexTokenRequest { public string Token { get; set; } }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
|
||||||
|
<MudItem Class="d-flex flex-column align-center pa-2 cursor-pointer" @onclick="HandleClick">
|
||||||
|
@if (!string.IsNullOrEmpty(Item.CoverUrl))
|
||||||
|
{
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large">
|
||||||
|
<MudImage Src="@Item.CoverUrl.FormatCoverUrl(Size, Size)" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large" Variant="Variant.Filled">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.AccountCircle" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
<MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Title</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Align="Align.Center" Color="Color.Secondary">
|
||||||
|
@string.Join(", ", Item.Artists.Select(a => a.Name))
|
||||||
|
</MudText>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public YandexAlbum Item { get; set; } = null!;
|
||||||
|
[Parameter] public EventCallback OnClick { get; set; }
|
||||||
|
[Parameter] public int Size { get; set; } = 50;
|
||||||
|
|
||||||
|
private async Task HandleClick()
|
||||||
|
{
|
||||||
|
if (OnClick.HasDelegate)
|
||||||
|
{
|
||||||
|
await OnClick.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
|
||||||
|
<MudItem Class="d-flex flex-column align-center pa-2 cursor-pointer" @onclick="HandleClick">
|
||||||
|
@if (!string.IsNullOrEmpty(Item.CoverUrl))
|
||||||
|
{
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large">
|
||||||
|
<MudImage Src="@Item.CoverUrl.FormatCoverUrl(Size, Size)" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large" Variant="Variant.Filled">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Album" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
<MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Name</MudText>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public YandexArtist Item { get; set; } = null!;
|
||||||
|
[Parameter] public EventCallback OnClick { get; set; }
|
||||||
|
[Parameter] public int Size { get; set; } = 50;
|
||||||
|
|
||||||
|
private async Task HandleClick()
|
||||||
|
{
|
||||||
|
if (OnClick.HasDelegate)
|
||||||
|
{
|
||||||
|
await OnClick.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@using PlaylistShared.Pwa.Components.Common
|
||||||
|
|
||||||
|
<MudItem Class="d-flex flex-column align-center pa-2">
|
||||||
|
<!-- Аватар-скелет -->
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large">
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Circle" Width="@Size.ToString()" Height="@Size.ToString()" />
|
||||||
|
</MudAvatar>
|
||||||
|
|
||||||
|
<!-- Текст-скелет -->
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Text" Width="50px" Class="mt-2" />
|
||||||
|
<MudSkeleton SkeletonType="SkeletonType.Text" Width="30px" Class="ma-0" />
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public int Size { get; set; } = 50;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
|
||||||
|
<MudItem Class="d-flex flex-column align-center pa-2 cursor-pointer" @onclick="HandleClick">
|
||||||
|
@if (!string.IsNullOrEmpty(Item.CoverUrl))
|
||||||
|
{
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large">
|
||||||
|
<MudImage Src="@Item.CoverUrl.FormatCoverUrl(Size, Size)" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAvatar Size="MudBlazor.Size.Large" Variant="Variant.Filled">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.PlaylistPlay" />
|
||||||
|
</MudAvatar>
|
||||||
|
}
|
||||||
|
<MudText Typo="Typo.body2" Align="Align.Center" Class="mt-2">@Item.Title</MudText>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public YandexPlaylist Item { get; set; } = null!;
|
||||||
|
[Parameter] public EventCallback OnClick { get; set; }
|
||||||
|
[Parameter] public int Size { get; set; } = 50;
|
||||||
|
|
||||||
|
private async Task HandleClick()
|
||||||
|
{
|
||||||
|
if (OnClick.HasDelegate)
|
||||||
|
{
|
||||||
|
await OnClick.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
@using PlaylistShared.Shared.Enums
|
||||||
|
@using PlaylistShared.Shared.SharedPlaylist
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">Настройки доступа</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudSelect T="ViewPermission" Label="Просмотр" @bind-Value="_permissions.ViewPermission" Variant="Variant.Outlined" FullWidth="true">
|
||||||
|
<MudSelectItem Value="ViewPermission.Everyone">Все</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ViewPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudSelect T="ViewPermission" Label="Воспроизведение" @bind-Value="_permissions.PlayPermission" Variant="Variant.Outlined" FullWidth="true">
|
||||||
|
<MudSelectItem Value="ViewPermission.Everyone">Все</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ViewPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudSelect T="EditPermission" Label="Добавление треков" @bind-Value="_permissions.AddPermission" Variant="Variant.Outlined" FullWidth="true">
|
||||||
|
<MudSelectItem Value="EditPermission.Everyone">Все</MudSelectItem>
|
||||||
|
<MudSelectItem Value="EditPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
|
||||||
|
<MudSelectItem Value="EditPermission.AddedByUserOnly">Только добавивший</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudSelect T="EditPermission" Label="Удаление треков" @bind-Value="_permissions.RemovePermission" Variant="Variant.Outlined" FullWidth="true">
|
||||||
|
<MudSelectItem Value="EditPermission.Everyone">Все</MudSelectItem>
|
||||||
|
<MudSelectItem Value="EditPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
|
||||||
|
<MudSelectItem Value="EditPermission.AddedByUserOnly">Только добавивший</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">Отмена</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Save" Disabled="_saving">
|
||||||
|
@if (_saving)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Size="Size.Small" Indeterminate />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Сохранить</span>
|
||||||
|
}
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter] private IMudDialogInstance? MudDialog { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string ShareToken { get; set; } = string.Empty;
|
||||||
|
[Parameter] public UpdatePermissionsDto InitialPermissions { get; set; } = new();
|
||||||
|
|
||||||
|
[Parameter] public EventCallback<UpdatePermissionsDto> OnPermissionsUpdated { get; set; }
|
||||||
|
|
||||||
|
private UpdatePermissionsDto _permissions = new();
|
||||||
|
private bool _saving;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_permissions = new UpdatePermissionsDto
|
||||||
|
{
|
||||||
|
ViewPermission = InitialPermissions.ViewPermission,
|
||||||
|
PlayPermission = InitialPermissions.PlayPermission,
|
||||||
|
AddPermission = InitialPermissions.AddPermission,
|
||||||
|
RemovePermission = InitialPermissions.RemovePermission
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Save()
|
||||||
|
{
|
||||||
|
_saving = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await Http.PutAsJsonAsync($"/api/sharedplaylist/{ShareToken}/permissions", _permissions);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ApiResponse<SharedPlaylistDto>>();
|
||||||
|
if (result?.Success == true)
|
||||||
|
{
|
||||||
|
Snackbar.Add("Настройки доступа сохранены", Severity.Success);
|
||||||
|
await OnPermissionsUpdated.InvokeAsync(_permissions);
|
||||||
|
MudDialog?.Close(DialogResult.Ok(_permissions));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add(result?.Error?.Message ?? "Ошибка сохранения", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add("Ошибка сохранения прав", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel() => MudDialog?.Cancel();
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
|
|
||||||
<div class="track-cover-container"
|
|
||||||
@onmouseenter="HandleMouseEnter"
|
|
||||||
@onmouseleave="HandleMouseLeave"
|
|
||||||
style="position: relative; display: inline-block; cursor: pointer;">
|
|
||||||
|
|
||||||
<MudImage Src="@FormatCoverUrl(CoverUrl)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
|
|
||||||
|
|
||||||
@if (_isHovered || IsPlaying)
|
|
||||||
{
|
|
||||||
<div 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;">
|
|
||||||
<MudIconButton Icon="@(IsPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
|
|
||||||
Color="Color.Inherit"
|
|
||||||
Size="Size.Large"
|
|
||||||
OnClick="OnPlayClick" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public string CoverUrl { get; set; } = string.Empty;
|
|
||||||
[Parameter] public string TrackId { get; set; } = string.Empty;
|
|
||||||
[Parameter] public bool IsPlaying { get; set; } = false;
|
|
||||||
[Parameter] public EventCallback<string> OnPlay { get; set; }
|
|
||||||
[Parameter] public int Height { get; set; } = 50;
|
|
||||||
[Parameter] public int Width { get; set; } = 50;
|
|
||||||
|
|
||||||
private bool _isHovered;
|
|
||||||
|
|
||||||
private void HandleMouseEnter() => _isHovered = true;
|
|
||||||
private void HandleMouseLeave() => _isHovered = false;
|
|
||||||
|
|
||||||
private async Task OnPlayClick()
|
|
||||||
{
|
|
||||||
await OnPlay.InvokeAsync(TrackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FormatCoverUrl(string? url)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(url)) return "";
|
|
||||||
return "https://" + url.Replace("%%", $"{Width}x{Height}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
PlaylistShared.Pwa/Extensions/LongExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace PlaylistShared.Pwa.Extensions;
|
||||||
|
|
||||||
|
public static class LongExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Преобразует миллисекунды в формат Минуты:Секунды
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatDuration(this long ms, FormatDurationType format = FormatDurationType.mmss)
|
||||||
|
{
|
||||||
|
var seconds = ms / 1000;
|
||||||
|
|
||||||
|
var mm = seconds / 60;
|
||||||
|
var ss = seconds % 60;
|
||||||
|
|
||||||
|
if (format == FormatDurationType.mmss || mm < 60)
|
||||||
|
{
|
||||||
|
return $"{mm}:{ss:D2}";
|
||||||
|
}
|
||||||
|
else if (format == FormatDurationType.hhmmss)
|
||||||
|
{
|
||||||
|
var hh = mm / 60;
|
||||||
|
mm = mm / 60;
|
||||||
|
|
||||||
|
return $"{hh}:{mm:D2}:{ss:D2}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return $"{mm}:{ss:D2}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FormatDurationType
|
||||||
|
{
|
||||||
|
mmss,
|
||||||
|
hhmmss,
|
||||||
|
}
|
||||||
|
}
|
||||||
19
PlaylistShared.Pwa/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace PlaylistShared.Pwa.Extensions;
|
||||||
|
|
||||||
|
public static class StringExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Преобразует шаблон URL обложки Яндекс.Музыки в полный URL с указанным размером.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="coverUri">Шаблон URL (например, "avatars.yandex.net/get-music-content/.../%%")</param>
|
||||||
|
/// <param name="width">Желаемая ширина обложки (по умолчанию 200)</param>
|
||||||
|
/// <param name="height">Желаемая высота обложки (по умолчанию 200)</param>
|
||||||
|
/// <returns>Полный URL обложки или пустую строку, если входная строка null или пуста.</returns>
|
||||||
|
public static string FormatCoverUrl(this string? coverUri, int width = 200, int height = 200)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(coverUri))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return "https://" + coverUri.Replace("%%", $"{width}x{height}");
|
||||||
|
}
|
||||||
|
}
|
||||||
7
PlaylistShared.Pwa/Icons/CustomIcons.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PlaylistShared.Pwa;
|
||||||
|
|
||||||
|
public static class CustomIcons
|
||||||
|
{
|
||||||
|
public const string Yandex = @"<path d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm.72 15.79h-2.14v-1.58c-.37.49-.87.89-1.48 1.18-.61.29-1.29.44-2.03.44-1.2 0-2.13-.34-2.8-.1-1.02-.66-1.52-1.61-1.52-2.84 0-1.25.43-2.22 1.28-2.91.85-.69 2.05-1.04 3.59-1.04h1.1v-.84c0-.62-.15-1.07-.46-1.34-.31-.27-.79-.41-1.44-.41-.53 0-1.02.08-1.48.24-.46.16-.9.41-1.32.74v-1.8c.48-.25 1.01-.45 1.58-.59.57-.14 1.15-.21 1.74-.21 1.45 0 2.53.33 3.23 1 .7.67 1.05 1.66 1.05 2.97v6.29zm-2.14-5.18h-.9c-.8 0-1.4.15-1.8.44-.4.29-.6.74-.6 1.34 0 .55.16.96.48 1.23.32.27.76.41 1.32.41.51 0 .97-.13 1.37-.39.4-.26.6-.64.6-1.14v-1.89z'/>";
|
||||||
|
public const string YandexMusic = "<path d='M23.8 9.4l-.1-.5-3.9-.9 2-3-.2-.3-3.1 1.5.3-4.2-.3-.2-2 3.4L14.3 0h-.4l.6 4.9-5.7-4.5-.5.1 4.4 5.5-8.7-2.9-.4.4 7.8 4.4-10.7.9-.1.7 11.2 1.2-9.3 7.6.4.6 11.1-6.1-2.2 10.6h.7l4.3-10 2.6 7.8.4-.3-.9-7.8 3.9 4.5.2-.4-2.9-5.5 4.2 1.5.1-.5-3.5-2.8 3.3-.7z'/>";
|
||||||
|
}
|
||||||
@@ -2,18 +2,23 @@
|
|||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<MudText Typo="Typo.body2" Class="d-inline mr-2">Здравствуйте, @context.User.Identity?.Name!</MudText>
|
<MudMenu Label="@context.User.Identity?.Name" Variant="Variant.Text" Color="Color.Inherit" Class="user-menu">
|
||||||
<MudButton Variant="Variant.Text" Color="Color.Inherit" OnClick="BeginLogOut">Выйти</MudButton>
|
<MudMenuItem OnClick="GoToProfile">Профиль</MudMenuItem>
|
||||||
|
<MudMenuItem OnClick="BeginLogOut">Выйти</MudMenuItem>
|
||||||
|
</MudMenu>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<MudLink Href="/login" Color="Color.Inherit" Underline="Underline.Hover" Typo="Typo.body2">Вход</MudLink>
|
<MudLink Href="/login" Color="Color.Inherit" Underline="Underline.Hover" Typo="Typo.body2">Вход</MudLink>
|
||||||
<MudText Class="d-inline mx-1">|</MudText>
|
|
||||||
<MudLink Href="/register" Color="Color.Inherit" Underline="Underline.Hover" Typo="Typo.body2">Регистрация</MudLink>
|
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public void BeginLogOut()
|
private void GoToProfile()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginLogOut()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo("/logout");
|
Navigation.NavigateTo("/logout");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
@using MudBlazor.Services
|
||||||
|
@using PlaylistShared.Pwa.Components.Global
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@inject PwaUpdateService PwaUpdateService
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject ContextualActionBarService ContextualActionBarService
|
||||||
|
@inject IBrowserViewportService BrowserViewportService
|
||||||
|
@implements IBrowserViewportObserver
|
||||||
|
|
||||||
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
|
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
|
||||||
<MudPopoverProvider />
|
<MudPopoverProvider />
|
||||||
@@ -6,31 +13,49 @@
|
|||||||
<MudSnackbarProvider />
|
<MudSnackbarProvider />
|
||||||
|
|
||||||
<MudLayout>
|
<MudLayout>
|
||||||
<MudAppBar Elevation="1">
|
<MudAppBar Elevation="1" Contextual Bottom = "@_actionBarBottom" Fixed>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@DrawerToggle" />
|
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@DrawerToggle" />
|
||||||
<MudText Typo="Typo.h6" Class="ml-2">Playlist share</MudText>
|
@if (_actionBarContent != null)
|
||||||
<MudSpacer />
|
{
|
||||||
<LoginDisplay />
|
@_actionBarContent
|
||||||
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle" Class="ml-2" />
|
}
|
||||||
<MudLink Href="https://git.frigat.duckdns.org/FrigaT/PlaylistShared" Target="_blank" Color="Color.Inherit" Underline="Underline.None" Class="ml-4">
|
else
|
||||||
<MudIcon Icon="@Icons.Custom.Brands.GitHub" Size="Size.Small" Class="mr-1" /> Git
|
{
|
||||||
</MudLink>
|
<MudSpacer />
|
||||||
|
<LoginDisplay />
|
||||||
|
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle" Class="ml-2" />
|
||||||
|
<MudLink Href="https://git.frigat.duckdns.org/FrigaT/PlaylistShared" Target="_blank" Color="Color.Inherit" Underline="Underline.None" Class="ml-4">
|
||||||
|
<MudIcon Icon="@Icons.Custom.Brands.GitHub" Size="Size.Small" Class="mr-1" /> Git
|
||||||
|
</MudLink>
|
||||||
|
}
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
|
|
||||||
<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
|
<MudDrawer @bind-Open="_drawerOpen" Class="@(_actionBarBottom ? " pt-0 pb-16" : "")" ClipMode="DrawerClipMode.Always" Elevation="2">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
|
|
||||||
<MudMainContent Class="pt-16 pa-4">
|
<MudMainContent Class="@("d-flex flex-column" + (_actionBarBottom ? " pt-0 pb-16" : ""))" Style="height: 100dvh;">
|
||||||
@Body
|
@Body
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
</MudLayout>
|
</MudLayout>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private RenderFragment? _actionBarContent;
|
||||||
|
private bool _actionBarBottom => _contextualPosition switch
|
||||||
|
{
|
||||||
|
ContextualActionBarPosition.Bottom => true,
|
||||||
|
ContextualActionBarPosition.Top => false,
|
||||||
|
_ => _isMobile,
|
||||||
|
};
|
||||||
|
private bool _isMobile = false;
|
||||||
|
private ContextualActionBarPosition _contextualPosition = ContextualActionBarPosition.Default;
|
||||||
|
|
||||||
private bool _drawerOpen = true;
|
private bool _drawerOpen = true;
|
||||||
private bool _isDarkMode = true;
|
private bool _isDarkMode = true;
|
||||||
private MudTheme? _theme;
|
private MudTheme? _theme;
|
||||||
|
|
||||||
|
private DotNetObjectReference<PwaUpdateService>? _dotNetRef;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
@@ -41,6 +66,25 @@
|
|||||||
PaletteDark = _darkPalette,
|
PaletteDark = _darkPalette,
|
||||||
LayoutProperties = new LayoutProperties()
|
LayoutProperties = new LayoutProperties()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ContextualActionBarService.OnChanged += OnContextualChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
_dotNetRef = DotNetObjectReference.Create(PwaUpdateService);
|
||||||
|
await JSRuntime.InvokeVoidAsync("registerSWMessageHandler", _dotNetRef);
|
||||||
|
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnContextualChangedHandler()
|
||||||
|
{
|
||||||
|
_actionBarContent = ContextualActionBarService.Content;
|
||||||
|
_contextualPosition = ContextualActionBarService.Position;
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawerToggle()
|
private void DrawerToggle()
|
||||||
@@ -97,4 +141,21 @@
|
|||||||
true => Icons.Material.Rounded.AutoMode,
|
true => Icons.Material.Rounded.AutoMode,
|
||||||
false => Icons.Material.Outlined.DarkMode,
|
false => Icons.Material.Outlined.DarkMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Guid IBrowserViewportObserver.Id { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
|
||||||
|
{
|
||||||
|
ReportRate = 250,
|
||||||
|
NotifyOnBreakpointOnly = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Task IBrowserViewportObserver.NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
|
||||||
|
{
|
||||||
|
_isMobile = browserViewportEventArgs.Breakpoint <= Breakpoint.Sm;
|
||||||
|
|
||||||
|
return InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Главная</MudNavLink>
|
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Главная</MudNavLink>
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<MudNavLink Href="/my-playlists" Icon="@Icons.Material.Filled.QueueMusic">Мои плейлисты</MudNavLink>
|
|
||||||
<MudNavLink Href="/profile" Icon="@Icons.Material.Filled.Person">Профиль</MudNavLink>
|
<MudNavLink Href="/profile" Icon="@Icons.Material.Filled.Person">Профиль</MudNavLink>
|
||||||
|
<MudNavLink Href="/my-playlists" Icon="@Icons.Material.Filled.QueueMusic">Мои плейлисты</MudNavLink>
|
||||||
|
<MudNavLink Href="/favorites" Icon="@Icons.Material.Filled.Star">Избранное</MudNavLink>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
112
PlaylistShared.Pwa/Pages/Favorites.razor
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
@page "/favorites"
|
||||||
|
<PageTitle>Избранное - Playlist Share</PageTitle>
|
||||||
|
|
||||||
|
@using PlaylistShared.Shared.SharedPlaylist
|
||||||
|
@attribute [Authorize]
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.h5">Избранные плейлисты</MudText>
|
||||||
|
<MudText Typo="Typo.body2">Расшаренные плейлисты, которые вы добавили в избранное</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
<CardHeaderActions>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadFavorites" />
|
||||||
|
</CardHeaderActions>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Indeterminate />
|
||||||
|
}
|
||||||
|
else if (_favorites == null || !_favorites.Any())
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info">
|
||||||
|
У вас пока нет избранных плейлистов. Перейдите на страницу расшаренного плейлиста и нажмите ★, чтобы добавить.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudTable Items="@_favorites">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Название</MudTh>
|
||||||
|
<MudTh>Владелец</MudTh>
|
||||||
|
<MudTh></MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Название">
|
||||||
|
<MudLink Href="@($"/shared/{context.ShareToken}")" Underline="Underline.Hover">
|
||||||
|
@context.Title
|
||||||
|
</MudLink>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Владелец">@context.Creator?.UserName</MudTd>
|
||||||
|
<MudTd DataLabel="">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Color="Color.Error"
|
||||||
|
OnClick="() => RemoveFromFavorites(context)"
|
||||||
|
Title="Удалить из избранного" />
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudContainer>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<SharedPlaylistDto> _favorites = new();
|
||||||
|
private bool _loading = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadFavorites()
|
||||||
|
{
|
||||||
|
_loading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await Http.GetFromJsonAsync<ApiResponse<List<SharedPlaylistDto>>>("/api/favorites");
|
||||||
|
if (response?.Success == true)
|
||||||
|
_favorites = response.Data ?? new();
|
||||||
|
else
|
||||||
|
Snackbar.Add(response?.Error?.Message ?? "Ошибка загрузки избранного", Severity.Error);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveFromFavorites(SharedPlaylistDto playlist)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await Http.DeleteAsync($"/api/favorites/{playlist.ShareToken}");
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Плейлист \"{playlist.Title}\" удалён из избранного", Severity.Success);
|
||||||
|
await LoadFavorites();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||||
|
Snackbar.Add(error?.Error?.Message ?? "Ошибка удаления", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,91 +1,74 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@using PlaylistShared.Pwa.Services
|
@using PlaylistShared.Pwa.Services
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject AuthenticationStateProvider AuthProvider
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
<MudText Typo="Typo.h4" GutterBottom>🎵 Playlist share</MudText>
|
<MudText Typo="Typo.h4" GutterBottom>🎵 Playlist Share</MudText>
|
||||||
<MudText Typo="Typo.body1">Делитесь плейлистами Яндекс.Музыки с друзьями и управляйте треками вместе!</MudText>
|
<MudText Typo="Typo.body1">
|
||||||
|
Делитесь плейлистами Яндекс.Музыки с друзьями и управляйте треками вместе!
|
||||||
|
</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
|
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<MudText Typo="Typo.h6" GutterBottom>🚀 Как начать</MudText>
|
<MudText Typo="Typo.body2" Class="mb-4">
|
||||||
|
Playlist share — это веб-приложение, которое позволяет создавать совместные плейлисты,
|
||||||
|
предоставлять доступ к ним по ссылке и слушать музыку прямо в браузере.
|
||||||
|
Для работы требуется аккаунт Яндекс.Музыки (подписка не обязательна).
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<!-- Блок с требованием регистрации для расшаривания -->
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="my-4">
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Share" Size="Size.Medium" />
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<MudText Typo="Typo.body1" FontWeight="FontWeight.Bold">
|
||||||
|
Чтобы расшаривать плейлисты, необходимо зарегистрироваться
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
Создайте аккаунт или войдите в существующий — это займёт всего минуту.
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/register">
|
||||||
|
Зарегистрироваться
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/login">
|
||||||
|
Войти
|
||||||
|
</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<!-- Краткие преимущества -->
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" md="6">
|
<MudItem xs="12" sm="4">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
<MudPaper Class="pa-3 text-center" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
||||||
<MudText Typo="Typo.h6" GutterBottom>1️⃣ Регистрация и вход</MudText>
|
<MudIcon Icon="@Icons.Material.Filled.Link" Size="Size.Medium" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">
|
<MudText Typo="Typo.body2" Class="mt-2">Создавайте ссылки-приглашения</MudText>
|
||||||
• Нажмите <MudLink Href="/register" Style="font-weight:bold;">«Регистрация»</MudLink> и создайте аккаунт.<br />
|
|
||||||
• Или <MudLink Href="/login" Style="font-weight:bold;">войдите</MudLink> в систему, если уже зарегистрированы.
|
|
||||||
</MudText>
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
<MudItem xs="12" md="6">
|
<MudPaper Class="pa-3 text-center" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Medium" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.h6" GutterBottom>2️⃣ Получение токена Яндекс.Музыки</MudText>
|
<MudText Typo="Typo.body2" Class="mt-2">Совместное управление треками</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">
|
|
||||||
Токен нужен для доступа к вашим плейлистам. Получите его один раз:
|
|
||||||
</MudText>
|
|
||||||
<ol style="margin-left: 1.2rem;">
|
|
||||||
<li>Перейдите по <MudLink Href="https://oauth.yandex.ru/authorize?response_type=token&client_id=23cabbbdc6cd418abb4b39c32c41195d" Target="_blank">ссылке</MudLink></li>
|
|
||||||
<li>Авторизуйтесь в Яндексе (если ещё не вошли)</li>
|
|
||||||
<li>Нажмите «Разрешить»</li>
|
|
||||||
<li>Скопируйте <strong>access_token</strong> из адресной строки после перенаправления</li>
|
|
||||||
</ol>
|
|
||||||
<MudAlert Severity="Severity.Info" Class="mt-2">
|
|
||||||
Пример: <code>https://music.yandex.ru/#access_token=ВАШ_ТОКЕН&...</code>
|
|
||||||
</MudAlert>
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
<MudItem xs="12" md="6">
|
<MudPaper Class="pa-3 text-center" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Medium" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.h6" GutterBottom>3️⃣ Добавление токена в профиле</MudText>
|
<MudText Typo="Typo.body2" Class="mt-2">Гибкие настройки доступа</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">
|
|
||||||
• Перейдите в <MudLink Href="/profile" Style="font-weight:bold;">Профиль</MudLink><br />
|
|
||||||
• Вставьте скопированный токен в поле «Токен Яндекс.Музыки»<br />
|
|
||||||
• Нажмите «Сохранить»
|
|
||||||
</MudText>
|
|
||||||
<MudAlert Severity="Severity.Success" Class="mt-2">✅ Токен сохраняется в зашифрованном виде.</MudAlert>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12" md="6">
|
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
|
||||||
<MudText Typo="Typo.h6" GutterBottom>4️⃣ Расшаривание плейлиста</MudText>
|
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">
|
|
||||||
• Откройте <MudLink Href="/my-playlists" Style="font-weight:bold;">Мои плейлисты</MudLink><br />
|
|
||||||
• Нажмите «Поделиться» рядом с нужным плейлистом<br />
|
|
||||||
• Скопируйте полученную ссылку и отправьте друзьям
|
|
||||||
</MudText>
|
|
||||||
<MudAlert Severity="Severity.Info" Class="mt-2">
|
|
||||||
Вы можете настроить права на добавление/удаление треков для гостей.
|
|
||||||
</MudAlert>
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|
||||||
<MudDivider Class="my-6" />
|
<MudDivider Class="my-4" />
|
||||||
|
|
||||||
<MudText Typo="Typo.h6" GutterBottom>📌 Важно</MudText>
|
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||||
<MudGrid>
|
🔐 Все данные передаются по защищённому соединению, токены хранятся в зашифрованном виде.
|
||||||
<MudItem xs="12" sm="6">
|
</MudText>
|
||||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
|
|
||||||
🔐 Токен даёт доступ к вашим плейлистам. Никому его не сообщайте.
|
|
||||||
</MudAlert>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" sm="6">
|
|
||||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
|
||||||
🎧 Для работы с плейлистами нужна активная подписка Яндекс.Плюс?<br />
|
|
||||||
<MudText Typo="Typo.body2">Нет, достаточно обычного аккаунта.</MudText>
|
|
||||||
</MudAlert>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardContent Class="text-center">
|
<MudCardContent Class="text-center">
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">Вход в PlaylistShared</MudText>
|
<MudText Typo="Typo.h5" Class="mb-4">Вход в PlaylistShared</MudText>
|
||||||
|
@*
|
||||||
<MudText Typo="Typo.body2" Class="mb-6">
|
<MudText Typo="Typo.body2" Class="mb-6">
|
||||||
Войдите через учётную запись Keycloak или используйте локальный аккаунт.
|
Войдите через учётную запись Keycloak или используйте локальный аккаунт.
|
||||||
</MudText>
|
</MudText>
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
|
|
||||||
<MudDivider Class="my-4">или</MudDivider>
|
<MudDivider Class="my-4">или</MudDivider>
|
||||||
|
*@
|
||||||
<!-- Локальная форма входа -->
|
<!-- Локальная форма входа -->
|
||||||
<MudTextField @bind-Value="_loginModel.Username" Label="Имя пользователя" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
|
<MudTextField @bind-Value="_loginModel.Username" Label="Имя пользователя" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
|
||||||
<MudTextField @bind-Value="_loginModel.Password" Label="Пароль" Variant="Variant.Outlined" FullWidth="true" InputType="InputType.Password" />
|
<MudTextField @bind-Value="_loginModel.Password" Label="Пароль" Variant="Variant.Outlined" FullWidth="true" InputType="InputType.Password" @bind-Value:after="LocalLogin" />
|
||||||
|
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="LocalLogin" FullWidth="true" Class="mt-4">
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="LocalLogin" FullWidth="true" Class="mt-4">
|
||||||
Войти (локально)
|
Войти (локально)
|
||||||
@@ -48,6 +48,12 @@
|
|||||||
|
|
||||||
private async Task LocalLogin()
|
private async Task LocalLogin()
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_loginModel.Username) || string.IsNullOrWhiteSpace(_loginModel.Password))
|
||||||
|
{
|
||||||
|
Snackbar.Add("Пожалуйста, заполните все поля", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var response = await Http.PostAsJsonAsync("/api/account/login", _loginModel);
|
var response = await Http.PostAsJsonAsync("/api/account/login", _loginModel);
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
@page "/my-playlists"
|
@page "/my-playlists"
|
||||||
|
<PageTitle>Мои плейлисты - Playlist Share</PageTitle>
|
||||||
|
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using PlaylistShared.Shared.DTO
|
@using PlaylistShared.Shared.DTO
|
||||||
@using PlaylistShared.Shared.Playlist
|
@using PlaylistShared.Shared.SharedPlaylist
|
||||||
|
@using PlaylistShared.Shared.Yandex
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@@ -68,11 +71,11 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<YandexPlaylistInfo> _playlists;
|
private List<YandexPlaylistShare> _playlists;
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private bool _showOnlyShared = false;
|
private bool _showOnlyShared = false;
|
||||||
|
|
||||||
private List<YandexPlaylistInfo> FilteredPlaylists => _showOnlyShared ? _playlists?.Where(p => p.IsShared).ToList() : _playlists;
|
private List<YandexPlaylistShare> FilteredPlaylists => _showOnlyShared ? _playlists?.Where(p => p.IsShared).ToList() : _playlists;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -84,7 +87,7 @@
|
|||||||
_loading = true;
|
_loading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await Http.GetFromJsonAsync<ApiResponse<List<YandexPlaylistInfo>>>("/api/playlists");
|
var response = await Http.GetFromJsonAsync<ApiResponse<List<YandexPlaylistShare>>>("/api/playlists");
|
||||||
if (response?.Success == true)
|
if (response?.Success == true)
|
||||||
_playlists = response.Data;
|
_playlists = response.Data;
|
||||||
else
|
else
|
||||||
@@ -101,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SharePlaylist(YandexPlaylistInfo playlist)
|
private async Task SharePlaylist(YandexPlaylistShare playlist)
|
||||||
{
|
{
|
||||||
var request = new SharePlaylistRequest { Kind = playlist.Kind, OwnerUid = playlist.OwnerUid };
|
var request = new SharePlaylistRequest { Kind = playlist.Kind, OwnerUid = playlist.OwnerUid };
|
||||||
var response = await Http.PostAsJsonAsync("/api/playlists/share", request);
|
var response = await Http.PostAsJsonAsync("/api/playlists/share", request);
|
||||||
@@ -116,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GoToShared(YandexPlaylistInfo playlist)
|
private void GoToShared(YandexPlaylistShare playlist)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(playlist.ShareToken))
|
if (!string.IsNullOrEmpty(playlist.ShareToken))
|
||||||
Navigation.NavigateTo($"/shared/{playlist.ShareToken}");
|
Navigation.NavigateTo($"/shared/{playlist.ShareToken}");
|
||||||
|
|||||||
@@ -1,72 +1,88 @@
|
|||||||
@page "/profile"
|
@page "/profile"
|
||||||
@using Microsoft.AspNetCore.Authorization
|
|
||||||
@using PlaylistShared.Shared.DTO
|
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@inject ISnackbar Snackbar
|
@inject IDialogService DialogService
|
||||||
|
@using PlaylistShared.Pwa.Components.Profile.YandexAccount
|
||||||
|
@using PlaylistShared.Shared.Profile
|
||||||
|
|
||||||
|
<PageTitle>Профиль</PageTitle>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-8">
|
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-8">
|
||||||
<MudCard>
|
<MudText Typo="Typo.h4" Class="mb-6">Профиль</MudText>
|
||||||
<MudCardHeader>
|
|
||||||
<CardHeaderContent>
|
<MudStack Spacing="4">
|
||||||
<MudText Typo="Typo.h5">Личный кабинет</MudText>
|
@*
|
||||||
</CardHeaderContent>
|
<!-- Секция почты -->
|
||||||
</MudCardHeader>
|
<MudCard>
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<MudText Typo="Typo.body2" Class="mb-4">Здесь вы можете указать токен доступа к Яндекс.Музыке.</MudText>
|
<MudText Typo="Typo.h6" GutterBottom="true">Данные аккаунта</MudText>
|
||||||
<MudTextField @bind-Value="_token" Label="Токен Яндекс.Музыки" Variant="Variant.Outlined" FullWidth="true" />
|
<MudTextField @bind-Value="_email" Label="Электронная почта" ReadOnly="true" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveToken" Class="mt-4" FullWidth="true">Сохранить токен</MudButton>
|
<MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-2" Disabled="true">Сменить почту</MudButton>
|
||||||
<MudText Class="mt-4" Typo="Typo.body2">Статус: @_statusText</MudText>
|
</MudCardContent>
|
||||||
</MudCardContent>
|
</MudCard>
|
||||||
</MudCard>
|
*@
|
||||||
|
|
||||||
|
<!-- Секция Яндекс.Музыки -->
|
||||||
|
<MudCard>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudStack Spacing="0">
|
||||||
|
<MudText Typo="Typo.h6">Яндекс.Музыка</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="@(_hasToken? Color.Success: Color.Error)">
|
||||||
|
@_statusText
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudMenu EndIcon="@Icons.Material.Filled.ArrowDropDown"
|
||||||
|
Label="@(_hasToken ? "Переподключить" : "Подключить")"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Dense
|
||||||
|
Color="Color.Primary">
|
||||||
|
<MudMenuItem OnClick="OpenTokenDialog">Token</MudMenuItem>
|
||||||
|
<MudMenuItem OnClick="OpenQrServerDialog">QR</MudMenuItem>
|
||||||
|
</MudMenu>
|
||||||
|
</MudStack>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudStack>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string _token = "";
|
private string _email = "user@example.com";
|
||||||
private string _statusText = "Загрузка...";
|
private string _statusText = "Загрузка...";
|
||||||
|
private bool _hasToken;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync() => await LoadStatus();
|
||||||
{
|
|
||||||
await LoadStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadStatus()
|
private async Task LoadStatus()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await Http.GetFromJsonAsync<ApiResponse<YandexTokenStatus>>("/api/yandextoken/status");
|
var response = await Http.GetFromJsonAsync<ApiResponse<YandexTokenStatus>>("/api/yandexaccount/status");
|
||||||
if (response?.Success == true)
|
if (response?.Success == true)
|
||||||
{
|
{
|
||||||
_statusText = response.Data.HasToken
|
_hasToken = response.Data.HasToken;
|
||||||
? $"Токен установлен{(response.Data.IsValid ? "" : " (просрочен)")}"
|
_statusText = _hasToken ? "Аккаунт подключен" : "Аккаунт не подключен";
|
||||||
: "Токен не установлен";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { _statusText = "Не удалось загрузить статус"; }
|
catch { _statusText = "Ошибка загрузки статуса"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveToken()
|
private async Task OpenTokenDialog()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_token))
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
{
|
var dialog = await DialogService.ShowAsync<YandexTokenDialog>("", options);
|
||||||
Snackbar.Add("Введите токен", Severity.Warning);
|
var result = await dialog.Result;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new SetYandexTokenRequest { Token = _token };
|
if (!result.Canceled) await LoadStatus();
|
||||||
var response = await Http.PostAsJsonAsync("/api/yandextoken/set", request);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
Snackbar.Add("Токен сохранён", Severity.Success);
|
|
||||||
await LoadStatus();
|
|
||||||
_token = "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Snackbar.Add("Ошибка сохранения токена", Severity.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class YandexTokenStatus { public bool HasToken { get; set; } public bool IsValid { get; set; } }
|
private async Task OpenQrServerDialog()
|
||||||
public class SetYandexTokenRequest { public string Token { get; set; } }
|
{
|
||||||
}
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
|
var dialog = await DialogService.ShowAsync<YandexQrServerDialog>("", options);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
|
||||||
|
if (!result.Canceled) await LoadStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@
|
|||||||
<Content Update="wwwroot\js\AudioPlayer.js">
|
<Content Update="wwwroot\js\AudioPlayer.js">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Update="wwwroot\js\shareUtils.js">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ internal class Program
|
|||||||
return new HttpClient { BaseAddress = new Uri(apiUrl) };
|
return new HttpClient { BaseAddress = new Uri(apiUrl) };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<PwaUpdateService>();
|
||||||
builder.Services.AddScoped<TokenStorage>();
|
builder.Services.AddScoped<TokenStorage>();
|
||||||
|
builder.Services.AddScoped<PlayerStorage>();
|
||||||
builder.Services.AddScoped<AuthStateProvider>();
|
builder.Services.AddScoped<AuthStateProvider>();
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AuthStateProvider>());
|
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AuthStateProvider>());
|
||||||
|
builder.Services.AddScoped<ContextualActionBarService>();
|
||||||
builder.Services.AddScoped<ApiClient>();
|
builder.Services.AddScoped<ApiClient>();
|
||||||
|
builder.Services.AddScoped<IAudioPlayerService, AudioPlayerService>();
|
||||||
|
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
|||||||
@@ -10,6 +10,23 @@
|
|||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"https (silent)": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"applicationUrl": "https://localhost:7225;http://localhost:5181",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"https (prod)": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "https://localhost:7225;http://localhost:5181"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
PlaylistShared.Pwa/Services/Api/SharedPlaylistClient.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using PlaylistShared.Shared;
|
||||||
|
using PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services.Api;
|
||||||
|
|
||||||
|
public class SharedPlaylistClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
|
public SharedPlaylistClient(HttpClient http)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GET /api/sharedplaylist/{token}
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ApiResponse<SharedPlaylistDto>> GetAsync(string token)
|
||||||
|
{
|
||||||
|
var response = await _http.GetFromJsonAsync<ApiResponse<SharedPlaylistDto>>($"/api/sharedplaylist/{token}");
|
||||||
|
return response ?? ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { Message = "Invalid response" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GET /api/sharedplaylist/{token}/tracks
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ApiResponse<YandexPlaylistData>> GetTracksAsync(string token)
|
||||||
|
{
|
||||||
|
var response = await _http.GetFromJsonAsync<ApiResponse<YandexPlaylistData>>($"/api/sharedplaylist/{token}/tracks");
|
||||||
|
return response ?? ApiResponse<YandexPlaylistData>.Fail(new ErrorResponse { Message = "Invalid response" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PUT /api/sharedplaylist/{token}/permissions
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ApiResponse<SharedPlaylistDto>> UpdatePermissionsAsync(string token, UpdatePermissionsDto permissions)
|
||||||
|
{
|
||||||
|
var response = await _http.PutAsJsonAsync($"/api/sharedplaylist/{token}/permissions", permissions);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ApiResponse<SharedPlaylistDto>>();
|
||||||
|
return result ?? ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { Message = "Invalid response" });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadFromJsonAsync<ApiResponse<SharedPlaylistDto>>();
|
||||||
|
return error ?? ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse
|
||||||
|
{
|
||||||
|
StatusCode = (int)response.StatusCode,
|
||||||
|
Message = response.ReasonPhrase ?? "Unknown error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POST /api/sharedplaylist/{token}/add-tracks
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ApiResponse<object>> AddTracksAsync(string token, List<string> trackIds)
|
||||||
|
{
|
||||||
|
var request = new UpdateTrackListRequest { TrackIds = trackIds };
|
||||||
|
var response = await _http.PostAsJsonAsync($"/api/sharedplaylist/{token}/add-tracks", request);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||||
|
return result ?? ApiResponse<object>.Fail(new ErrorResponse { Message = "Invalid response" });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||||
|
return error ?? ApiResponse<object>.Fail(new ErrorResponse
|
||||||
|
{
|
||||||
|
StatusCode = (int)response.StatusCode,
|
||||||
|
Message = response.ReasonPhrase ?? "Unknown error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POST /api/sharedplaylist/{token}/remove-tracks
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ApiResponse<object>> RemoveTracksAsync(string token, List<string> trackIds)
|
||||||
|
{
|
||||||
|
var request = new UpdateTrackListRequest { TrackIds = trackIds };
|
||||||
|
var response = await _http.PostAsJsonAsync($"/api/sharedplaylist/{token}/remove-tracks", request);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||||
|
return result ?? ApiResponse<object>.Fail(new ErrorResponse { Message = "Invalid response" });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||||
|
return error ?? ApiResponse<object>.Fail(new ErrorResponse
|
||||||
|
{
|
||||||
|
StatusCode = (int)response.StatusCode,
|
||||||
|
Message = response.ReasonPhrase ?? "Unknown error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
PlaylistShared.Pwa/Services/AudioPlayerService.cs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
using MudBlazor;
|
||||||
|
using PlaylistShared.Shared;
|
||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
public class AudioPlayerService : IAudioPlayerService
|
||||||
|
{
|
||||||
|
private readonly TokenStorage _tokenStorage;
|
||||||
|
private readonly ISnackbar _snackbar;
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly PlayerStorage _playerStorage;
|
||||||
|
|
||||||
|
private string? _currentTrackId;
|
||||||
|
private YandexTrack? _currentTrack;
|
||||||
|
private bool _isPlaying;
|
||||||
|
private double _currentVolume = 50;
|
||||||
|
private double _currentProgress;
|
||||||
|
private double _currentTime = 0;
|
||||||
|
private double _totalTime = 0;
|
||||||
|
private string _currentTimeString = "0:00";
|
||||||
|
private string _totalTimeString = "0:00";
|
||||||
|
|
||||||
|
public string? CurrentTrackId => _currentTrackId;
|
||||||
|
public YandexTrack? CurrentTrack => _currentTrack;
|
||||||
|
public bool IsPlaying => _isPlaying;
|
||||||
|
public double CurrentVolume
|
||||||
|
{
|
||||||
|
get => _currentVolume;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_currentVolume = value;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public double CurrentProgress => _currentProgress;
|
||||||
|
public double CurrentTime => _currentTime;
|
||||||
|
public double TotalTime => _totalTime;
|
||||||
|
public string CurrentTimeString => _currentTimeString;
|
||||||
|
public string TotalTimeString => _totalTimeString;
|
||||||
|
|
||||||
|
public event Action? OnStateChanged;
|
||||||
|
public event Action? OnStartedTrack;
|
||||||
|
public event Action? OnEndedTrack;
|
||||||
|
|
||||||
|
public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient, PlayerStorage playerStorage)
|
||||||
|
{
|
||||||
|
_tokenStorage = tokenStorage;
|
||||||
|
_snackbar = snackbar;
|
||||||
|
_http = httpClient;
|
||||||
|
_playerStorage = playerStorage;
|
||||||
|
|
||||||
|
_ = LoadVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
await PlayAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentTrackId = trackId;
|
||||||
|
|
||||||
|
// Если accessToken не передан, пытаемся получить его из хранилища
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken))
|
||||||
|
{
|
||||||
|
var tokens = await _tokenStorage.GetTokensAsync();
|
||||||
|
accessToken = tokens.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли чем авторизоваться
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(playlistShareToken))
|
||||||
|
{
|
||||||
|
_snackbar.Add("Не удалось воспроизвести трек: отсутствует токен авторизации или идентификатор расшаренного плейлиста.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если title и coverUrl не переданы, нужно запросить через API
|
||||||
|
if (track is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
track = await GetTrackInfo(trackId, accessToken, playlistShareToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Логируем ошибку, но продолжаем без обложки/названия
|
||||||
|
Console.WriteLine($"Failed to fetch track info: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentTrack = track;
|
||||||
|
_isPlaying = true;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnLoadAndPlayRequested?.Invoke(trackId, accessToken, playlistShareToken);
|
||||||
|
OnStartedTrack?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PlayAsync()
|
||||||
|
{
|
||||||
|
_isPlaying = true;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnPlayRequested?.Invoke();
|
||||||
|
OnStartedTrack?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PauseAsync()
|
||||||
|
{
|
||||||
|
_isPlaying = false;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnPauseRequested?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SeekToAsync(double second)
|
||||||
|
{
|
||||||
|
OnSeekRequested?.Invoke(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetVolumeAsync(double volume)
|
||||||
|
{
|
||||||
|
_currentVolume = volume;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnVolumeChangeRequested?.Invoke(volume);
|
||||||
|
await _playerStorage.SetVolumeAsync(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// События для связи с реальным AudioPlayer компонентом
|
||||||
|
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;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCurrentTrack(string? trackId)
|
||||||
|
{
|
||||||
|
_currentTrackId = trackId;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateProgress(double currentTime, double totalTime)
|
||||||
|
{
|
||||||
|
var progress = (currentTime / totalTime) * 100;
|
||||||
|
_currentProgress = progress;
|
||||||
|
_currentTime = currentTime;
|
||||||
|
_currentTimeString = FormatDuration(currentTime);
|
||||||
|
_totalTime = totalTime;
|
||||||
|
_totalTimeString = FormatDuration(totalTime);
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyTrackEnded()
|
||||||
|
{
|
||||||
|
_isPlaying = false;
|
||||||
|
_currentTrackId = null;
|
||||||
|
_currentProgress = 0;
|
||||||
|
_currentTime = 0;
|
||||||
|
_currentTimeString = "0:00";
|
||||||
|
_totalTime = 0;
|
||||||
|
_currentTimeString = "0:00";
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnEndedTrack?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var url = $"/api/audio/track-info/{trackId}";
|
||||||
|
if (!string.IsNullOrEmpty(accessToken))
|
||||||
|
url += $"?access_token={accessToken}";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatDuration(double seconds)
|
||||||
|
{
|
||||||
|
var mins = (int)(seconds / 60);
|
||||||
|
var secs = (int)(seconds % 60);
|
||||||
|
return $"{mins}:{secs:D2}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ public class AuthStateProvider : AuthenticationStateProvider, IDisposable
|
|||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
private Timer? _refreshTimer;
|
private Timer? _refreshTimer;
|
||||||
private ClaimsPrincipal _currentUser = new(new ClaimsIdentity());
|
private ClaimsPrincipal _currentUser = new(new ClaimsIdentity());
|
||||||
|
private string? _currentToken;
|
||||||
|
private string? _currentRefreshToken;
|
||||||
|
|
||||||
public AuthStateProvider(TokenStorage tokenStorage, ApiClient apiClient, HttpClient http)
|
public AuthStateProvider(TokenStorage tokenStorage, ApiClient apiClient, HttpClient http)
|
||||||
{
|
{
|
||||||
@@ -26,14 +28,59 @@ public class AuthStateProvider : AuthenticationStateProvider, IDisposable
|
|||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
return new AuthenticationState(_currentUser);
|
return new AuthenticationState(_currentUser);
|
||||||
|
|
||||||
var principal = ParseToken(token);
|
var (isValid, principal) = await ValidateTokenAsync(token);
|
||||||
if (principal == null)
|
if (isValid && principal != null)
|
||||||
return new AuthenticationState(_currentUser);
|
{
|
||||||
|
_currentUser = principal;
|
||||||
|
_currentToken = token;
|
||||||
|
_currentRefreshToken = refreshToken;
|
||||||
|
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
ScheduleTokenRefresh(token, refreshToken);
|
||||||
|
return new AuthenticationState(principal);
|
||||||
|
}
|
||||||
|
|
||||||
_currentUser = principal;
|
// Токен невалиден – пробуем обновить
|
||||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
if (!string.IsNullOrEmpty(refreshToken))
|
||||||
ScheduleTokenRefresh(token, refreshToken);
|
{
|
||||||
return new AuthenticationState(principal);
|
var newTokenResponse = await _apiClient.RefreshTokenAsync(refreshToken);
|
||||||
|
if (newTokenResponse != null && !string.IsNullOrEmpty(newTokenResponse.Token))
|
||||||
|
{
|
||||||
|
await MarkUserAsAuthenticated(newTokenResponse.Token, newTokenResponse.RefreshToken);
|
||||||
|
// После MarkUserAsAuthenticated состояние обновится через NotifyAuthenticationStateChanged,
|
||||||
|
// но текущий вызов всё равно должен вернуть нового пользователя
|
||||||
|
var (newIsValid, newPrincipal) = await ValidateTokenAsync(newTokenResponse.Token);
|
||||||
|
if (newIsValid && newPrincipal != null)
|
||||||
|
return new AuthenticationState(newPrincipal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всё плохо — логаут
|
||||||
|
await MarkUserAsLoggedOut();
|
||||||
|
return new AuthenticationState(_currentUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательный метод проверки валидности токена (включая срок)
|
||||||
|
private async Task<(bool IsValid, ClaimsPrincipal? Principal)> ValidateTokenAsync(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
var jwt = handler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
// Проверяем, не истёк ли токен
|
||||||
|
if (jwt.ValidTo < DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(jwt.Claims, "jwt");
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
return (true, principal);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkUserAsAuthenticated(string token, string refreshToken)
|
public async Task MarkUserAsAuthenticated(string token, string refreshToken)
|
||||||
@@ -41,6 +88,8 @@ public class AuthStateProvider : AuthenticationStateProvider, IDisposable
|
|||||||
await _tokenStorage.SetTokensAsync(token, refreshToken);
|
await _tokenStorage.SetTokensAsync(token, refreshToken);
|
||||||
var principal = ParseToken(token);
|
var principal = ParseToken(token);
|
||||||
_currentUser = principal ?? new ClaimsPrincipal(new ClaimsIdentity());
|
_currentUser = principal ?? new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
_currentToken = token;
|
||||||
|
_currentRefreshToken = refreshToken;
|
||||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||||
}
|
}
|
||||||
@@ -49,10 +98,51 @@ public class AuthStateProvider : AuthenticationStateProvider, IDisposable
|
|||||||
{
|
{
|
||||||
await _tokenStorage.ClearTokensAsync();
|
await _tokenStorage.ClearTokensAsync();
|
||||||
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
|
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
_currentToken = null;
|
||||||
|
_currentRefreshToken = null;
|
||||||
_http.DefaultRequestHeaders.Authorization = null;
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
|
_refreshTimer?.Dispose();
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> TryRefreshTokenAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_currentRefreshToken))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newToken = await _apiClient.RefreshTokenAsync(_currentRefreshToken);
|
||||||
|
if (newToken != null && !string.IsNullOrEmpty(newToken.Token))
|
||||||
|
{
|
||||||
|
await MarkUserAsAuthenticated(newToken.Token, newToken.RefreshToken);
|
||||||
|
return newToken.Token;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await MarkUserAsLoggedOut();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await MarkUserAsLoggedOut();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTokenExpiringSoon()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_currentToken))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
var jwt = handler.ReadJwtToken(_currentToken);
|
||||||
|
var expiresAt = jwt.ValidTo;
|
||||||
|
var timeToExpiry = expiresAt - DateTime.UtcNow;
|
||||||
|
return timeToExpiry < TimeSpan.FromMinutes(1);
|
||||||
|
}
|
||||||
|
|
||||||
private ClaimsPrincipal? ParseToken(string token)
|
private ClaimsPrincipal? ParseToken(string token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -62,7 +152,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IDisposable
|
|||||||
var identity = new ClaimsIdentity(jwt.Claims, "jwt");
|
var identity = new ClaimsIdentity(jwt.Claims, "jwt");
|
||||||
return new ClaimsPrincipal(identity);
|
return new ClaimsPrincipal(identity);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -81,18 +171,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IDisposable
|
|||||||
_refreshTimer?.Dispose();
|
_refreshTimer?.Dispose();
|
||||||
_refreshTimer = new Timer(async _ =>
|
_refreshTimer = new Timer(async _ =>
|
||||||
{
|
{
|
||||||
try
|
await TryRefreshTokenAsync();
|
||||||
{
|
|
||||||
var newToken = await _apiClient.RefreshTokenAsync(refreshToken);
|
|
||||||
if (newToken != null)
|
|
||||||
await MarkUserAsAuthenticated(newToken.Token, newToken.RefreshToken);
|
|
||||||
else
|
|
||||||
await MarkUserAsLoggedOut();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MarkUserAsLoggedOut();
|
|
||||||
}
|
|
||||||
}, null, (int)refreshTime.TotalMilliseconds, Timeout.Infinite);
|
}, null, (int)refreshTime.TotalMilliseconds, Timeout.Infinite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
PlaylistShared.Pwa/Services/ContextualActionBarService.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
public class ContextualActionBarService
|
||||||
|
{
|
||||||
|
// Событие, которое будет вызываться при изменении содержимого панели
|
||||||
|
public event Action? OnChanged;
|
||||||
|
|
||||||
|
public RenderFragment? Content { get; set; } = null;
|
||||||
|
|
||||||
|
public ContextualActionBarPosition Position { get; set; } = ContextualActionBarPosition.Default;
|
||||||
|
|
||||||
|
public void ChangeParameters()
|
||||||
|
{
|
||||||
|
OnChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ContextualActionBarPosition
|
||||||
|
{
|
||||||
|
Default,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
105
PlaylistShared.Pwa/Services/IAudioPlayerService.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using PlaylistShared.Shared.Yandex;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Глобальный сервис управления аудиоплеером.
|
||||||
|
/// Позволяет управлять воспроизведением из любого компонента.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAudioPlayerService
|
||||||
|
{
|
||||||
|
#region Состояние плеера (для чтения)
|
||||||
|
/// <summary>ID текущего воспроизводимого трека (null, если ничего не играет).</summary>
|
||||||
|
string? CurrentTrackId { get; }
|
||||||
|
|
||||||
|
/// <summary>Играет ли в данный момент (true) или приостановлен (false).</summary>
|
||||||
|
bool IsPlaying { get; }
|
||||||
|
|
||||||
|
/// <summary>Текущая громкость (0–100).</summary>
|
||||||
|
double CurrentVolume { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Прогресс воспроизведения в процентах (0–100).</summary>
|
||||||
|
double CurrentProgress { get; }
|
||||||
|
|
||||||
|
/// <summary>Текущее время в секундах.</summary>
|
||||||
|
double CurrentTime { get; }
|
||||||
|
|
||||||
|
/// <summary>Общая длительность в секундах</summary>
|
||||||
|
double TotalTime { get; }
|
||||||
|
|
||||||
|
/// <summary>Отформатированное текущее время (мм:сс).</summary>
|
||||||
|
string CurrentTimeString { get; }
|
||||||
|
|
||||||
|
/// <summary>Отформатированная общая длительность (мм:сс).</summary>
|
||||||
|
string TotalTimeString { get; }
|
||||||
|
|
||||||
|
/// <summary>Текущий трек.</summary>
|
||||||
|
YandexTrack? CurrentTrack { get; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Команды управления (вызываются из компонентов)
|
||||||
|
/// <summary>Загрузить и начать воспроизведение трека.</summary>
|
||||||
|
/// <param name="trackId">ID трека.</param>
|
||||||
|
/// <param name="accessToken">Опциональный access-токен (если не указан, будет взят из хранилища).</param>
|
||||||
|
/// <param name="sharedPlaylistId">ID расшаренного плейлиста (для неавторизованного доступа).</param>
|
||||||
|
/// <param name="title">Название трека. (Если не передано, вызывает api для получения)</param>
|
||||||
|
/// <param name="coverUrl">URL обложки трека. (Если не передано, вызывает api для получения)</param>
|
||||||
|
Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? playlistShareToken = null, YandexTrack? track = null);
|
||||||
|
|
||||||
|
/// <summary>Воспроизвести (если трек загружен и на паузе).</summary>
|
||||||
|
Task PlayAsync();
|
||||||
|
|
||||||
|
/// <summary>Поставить на паузу.</summary>
|
||||||
|
Task PauseAsync();
|
||||||
|
|
||||||
|
/// <summary>Перемотать на секунды.</summary>
|
||||||
|
Task SeekToAsync(double second);
|
||||||
|
|
||||||
|
/// <summary>Установить громкость (0–100).</summary>
|
||||||
|
Task SetVolumeAsync(double volume);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region События для подписки на изменения состояния
|
||||||
|
/// <summary>
|
||||||
|
/// Событие, возникающее при любом изменении состояния плеера:
|
||||||
|
/// смена трека, старт/пауза/стоп, обновление прогресса, изменение громкости, окончание трека.
|
||||||
|
/// Подписывайтесь на него, чтобы перерисовывать UI (например, иконку "пауза/плей").
|
||||||
|
/// </summary>
|
||||||
|
event Action? OnStateChanged;
|
||||||
|
|
||||||
|
event Action? OnStartedTrack;
|
||||||
|
|
||||||
|
event Action? OnEndedTrack;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region События для связи с реальным компонентом AudioPlayer (Эти события вызываются сервисом)
|
||||||
|
/// <summary>Запрос на загрузку и воспроизведение трека.</summary>
|
||||||
|
event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на воспроизведение (снять с паузы).</summary>
|
||||||
|
event Func<Task>? OnPlayRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на паузу.</summary>
|
||||||
|
event Func<Task>? OnPauseRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на перемотку (секунды).</summary>
|
||||||
|
event Func<double, Task>? OnSeekRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на изменение громкости (0–100).</summary>
|
||||||
|
event Func<double, Task>? OnVolumeChangeRequested;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Методы для обновления состояния из AudioPlayer (Вызываются компонентом AudioPlayer, когда реальный аудиоэлемент меняет своё состояние.)
|
||||||
|
/// <summary>Уведомить сервис о том, что трек начал или прекратил играть.</summary>
|
||||||
|
void SetPlayingState(bool isPlaying);
|
||||||
|
|
||||||
|
/// <summary>Установить ID текущего трека.</summary>
|
||||||
|
void SetCurrentTrack(string? trackId);
|
||||||
|
|
||||||
|
/// <summary>Обновить прогресс и отображаемое время.</summary>
|
||||||
|
void UpdateProgress(double currentTime, double totalTime);
|
||||||
|
|
||||||
|
/// <summary>Уведомить об окончании трека.</summary>
|
||||||
|
void NotifyTrackEnded();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
30
PlaylistShared.Pwa/Services/PlayerStorage.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
public class PlayerStorage
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _js;
|
||||||
|
private const string VolumeKey = "audio_player_volume";
|
||||||
|
|
||||||
|
public PlayerStorage(IJSRuntime js) => _js = js;
|
||||||
|
|
||||||
|
public async Task SetVolumeAsync(double volume)
|
||||||
|
{
|
||||||
|
await _js.InvokeVoidAsync("localStorage.setItem", VolumeKey, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<double?> GetVolumeAsync()
|
||||||
|
{
|
||||||
|
var volume = await _js.InvokeAsync<string>("localStorage.getItem", VolumeKey);
|
||||||
|
|
||||||
|
if (double.TryParse(volume, out var result))
|
||||||
|
{
|
||||||
|
result = Math.Clamp(result, 0, 100);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
PlaylistShared.Pwa/Services/PwaUpdateService.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
public class PwaUpdateService
|
||||||
|
{
|
||||||
|
private readonly ISnackbar _snackbar;
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
|
||||||
|
public PwaUpdateService(ISnackbar snackbar, IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
_snackbar = snackbar;
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void OnNewVersionAvailable()
|
||||||
|
{
|
||||||
|
_snackbar.Add("Доступна новая версия! Обновите страницу.", Severity.Info, configure: options =>
|
||||||
|
{
|
||||||
|
options.Action = "Обновить";
|
||||||
|
options.ShowCloseIcon = false;
|
||||||
|
options.RequireInteraction = true;
|
||||||
|
options.OnClick = _ =>
|
||||||
|
{
|
||||||
|
_jsRuntime.InvokeVoidAsync("location.reload", true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
options.CloseAfterNavigation = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,4 +28,4 @@ public class TokenStorage
|
|||||||
await _js.InvokeVoidAsync("localStorage.removeItem", TokenKey);
|
await _js.InvokeVoidAsync("localStorage.removeItem", TokenKey);
|
||||||
await _js.InvokeVoidAsync("localStorage.removeItem", RefreshTokenKey);
|
await _js.InvokeVoidAsync("localStorage.removeItem", RefreshTokenKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@
|
|||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using PlaylistShared.Shared
|
@using PlaylistShared.Shared
|
||||||
@using PlaylistShared.Pwa.Components
|
@using PlaylistShared.Pwa.Components
|
||||||
|
@using PlaylistShared.Pwa.Extensions
|
||||||
@@ -5,6 +5,12 @@ events {
|
|||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Не раскрывайть версию Nginx в ответах.
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# Ограничение запросов от одного IP‑адреса, чтобы предотвратить DDoS‑атаки и злоупотребление ресурсами.
|
||||||
|
limit_req_zone $binary_remote_addr zone=one:10m rate=60r/s;
|
||||||
|
|
||||||
# Сжатие
|
# Сжатие
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -20,6 +26,8 @@ http {
|
|||||||
|
|
||||||
# Для Service Worker – запрещаем кэширование, чтобы он всегда был свежим
|
# Для Service Worker – запрещаем кэширование, чтобы он всегда был свежим
|
||||||
location = /service-worker.js {
|
location = /service-worker.js {
|
||||||
|
etag off;
|
||||||
|
add_header Last-Modified "";
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
add_header Pragma "no-cache";
|
add_header Pragma "no-cache";
|
||||||
add_header Expires "0";
|
add_header Expires "0";
|
||||||
@@ -28,6 +36,18 @@ http {
|
|||||||
|
|
||||||
# Для файла манифеста Service Worker assets – тоже не кэшируем
|
# Для файла манифеста Service Worker assets – тоже не кэшируем
|
||||||
location = /service-worker-assets.js {
|
location = /service-worker-assets.js {
|
||||||
|
etag off;
|
||||||
|
add_header Last-Modified "";
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Для файла index.html – тоже не кэшируем
|
||||||
|
location = /index.html {
|
||||||
|
etag off;
|
||||||
|
add_header Last-Modified "";
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
add_header Pragma "no-cache";
|
add_header Pragma "no-cache";
|
||||||
add_header Expires "0";
|
add_header Expires "0";
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.7 KiB |
28
PlaylistShared.Pwa/wwwroot/favicon.svg
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="40 40 432 432" fill="none">
|
||||||
|
<!-- Тёмный скруглённый фон -->
|
||||||
|
<rect width="512" height="512" rx="110" fill="#1a1a27"/>
|
||||||
|
|
||||||
|
<!-- Эффект "раздачи" (Ripple) – усиленная видимость -->
|
||||||
|
<g stroke="#7e6fff" fill="none">
|
||||||
|
<!-- Внутренняя волна (яркая, толстая) -->
|
||||||
|
<circle cx="256" cy="256" r="80" stroke-width="12" stroke-opacity="0.9"/>
|
||||||
|
<!-- Средняя волна -->
|
||||||
|
<circle cx="256" cy="256" r="145" stroke-width="10" stroke-opacity="0.7"/>
|
||||||
|
<!-- Внешняя волна (тонкая, но заметная) -->
|
||||||
|
<circle cx="256" cy="256" r="210" stroke-width="8" stroke-opacity="0.5"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Лёгкое свечение – уменьшено, чтобы не перебивать волны -->
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0" stop-color="#7e6fff" stop-opacity="0.25"/>
|
||||||
|
<stop offset="1" stop-color="#7e6fff" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="256" cy="256" r="150" fill="url(#glow)"/>
|
||||||
|
|
||||||
|
<!-- Центральная нота Material (крупно, без изменений) -->
|
||||||
|
<g fill="#7e6fff" transform="translate(256, 256) scale(16) translate(-12, -12)">
|
||||||
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
PlaylistShared.Pwa/wwwroot/icon-180.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 73 KiB |
@@ -10,89 +10,153 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<link href="PlaylistShared.Pwa.styles.css" rel="stylesheet" />
|
<link href="PlaylistShared.Pwa.styles.css" rel="stylesheet" />
|
||||||
<link href="manifest.webmanifest" rel="manifest" />
|
<link href="manifest.webmanifest" rel="manifest" />
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
|
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
|
||||||
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
|
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="icon-180.png" />
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
:root {
|
||||||
background-color: #1a1a27 !important;
|
--bg: #1a1a27;
|
||||||
|
--primary: #7e6fff;
|
||||||
|
--text: #92929f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--mud-palette-background, var(--bg));
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
align-items: center;
|
||||||
height: 100%;
|
font-family: 'Roboto', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Кастомный спиннер в стиле MudBlazor (тёмная тема) */
|
/* Создаем специальный контейнер для загрузки на весь экран */
|
||||||
.loading-progress {
|
#app:empty,
|
||||||
|
#app > .loader-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #1a1a27; /* Фиксированный фон на время загрузки */
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
width: 100px;
|
||||||
width: 64px;
|
height: 100px;
|
||||||
height: 64px;
|
display: flex;
|
||||||
margin: 0 auto;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-progress circle {
|
/* Центральная нота с неоновым свечением */
|
||||||
fill: none;
|
.note-icon {
|
||||||
stroke: #2a2833;
|
font-size: 50px;
|
||||||
stroke-width: 4;
|
color: var(--mud-palette-primary, var(--primary));
|
||||||
transform-origin: 50% 50%;
|
z-index: 10;
|
||||||
animation: spin 1.5s linear infinite;
|
filter: drop-shadow(0 0 10px rgba(126, 111, 255, 0.5));
|
||||||
}
|
animation: note-float 0.6s ease-in-out infinite;
|
||||||
|
|
||||||
.loading-progress circle:last-child {
|
|
||||||
stroke: #7e6fff;
|
|
||||||
stroke-dasharray: 126;
|
|
||||||
stroke-dashoffset: 126;
|
|
||||||
animation: dash 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes dash {
|
/* Эффект расходящихся волн (Sharing) */
|
||||||
|
.ripple {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid var(--mud-palette-primary, var(--primary));
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
animation: ripple-out 0.9s cubic-bezier(0.23, 1, 0.32, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple:nth-child(2) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple:nth-child(4) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple-out {
|
||||||
0% {
|
0% {
|
||||||
stroke-dashoffset: 126;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
opacity: 0.8;
|
||||||
|
border-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
opacity: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes note-float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
stroke-dashoffset: 63;
|
transform: translateY(-15px) scale(1.1);
|
||||||
transform: rotate(135deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 126;
|
|
||||||
transform: rotate(450deg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-progress-text {
|
.share-text {
|
||||||
text-align: center;
|
color: var(--mud-palette-text-secondary, var(--text));
|
||||||
margin-top: 16px;
|
text-transform: uppercase;
|
||||||
color: #b2b0bf;
|
letter-spacing: 5px;
|
||||||
font-family: 'Roboto', sans-serif;
|
font-size: 0.75rem;
|
||||||
font-size: 14px;
|
font-weight: 400;
|
||||||
|
animation: fade-text 0.8s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Убираем белые вспышки */
|
@keyframes fade-text {
|
||||||
#app {
|
0%, 100% {
|
||||||
background-color: #1a1a27;
|
opacity: 0.4;
|
||||||
min-height: 100vh;
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<svg class="loading-progress">
|
<div class="loader-wrapper">
|
||||||
<circle r="40%" cx="50%" cy="50%" />
|
<div class="ripple-container">
|
||||||
<circle r="40%" cx="50%" cy="50%" />
|
<div class="ripple"></div>
|
||||||
</svg>
|
<div class="ripple"></div>
|
||||||
<div class="loading-progress-text"></div>
|
<div class="ripple"></div>
|
||||||
|
<div class="note-icon">
|
||||||
|
<svg width="50" height="50" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z" "" ) />>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="share-text">Playlist Share</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
@@ -104,6 +168,15 @@
|
|||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||||
<script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script>
|
<script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script>
|
||||||
|
<script>
|
||||||
|
function registerSWMessageHandler(dotNetHelper) {
|
||||||
|
navigator.serviceWorker.addEventListener('message', event => {
|
||||||
|
if (event.data && event.data.type === 'SW_ACTIVATED') {
|
||||||
|
dotNetHelper.invokeMethodAsync('OnNewVersionAvailable');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
return isNaN(num) ? 0 : num;
|
return isNaN(num) ? 0 : num;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAndPlay = (src, token) => {
|
const loadAndPlay = (src, token, sharedPlaylistId) => {
|
||||||
const url = new URL(src, window.location.href);
|
const url = new URL(src, window.location.href);
|
||||||
if (token) url.searchParams.set('access_token', token);
|
if (token) url.searchParams.set('access_token', token);
|
||||||
|
if (sharedPlaylistId) url.searchParams.set('shared_id', sharedPlaylistId);
|
||||||
audio.src = url.toString();
|
audio.src = url.toString();
|
||||||
audio.load();
|
audio.load();
|
||||||
durationReady = false;
|
durationReady = false;
|
||||||
@@ -25,14 +26,13 @@
|
|||||||
const stop = () => { audio.pause(); audio.currentTime = 0; };
|
const stop = () => { audio.pause(); audio.currentTime = 0; };
|
||||||
const setVolume = (volume) => { audio.volume = toNumber(volume); };
|
const setVolume = (volume) => { audio.volume = toNumber(volume); };
|
||||||
const setCurrentTime = (time) => { audio.currentTime = toNumber(time); };
|
const setCurrentTime = (time) => { audio.currentTime = toNumber(time); };
|
||||||
const getDuration = () => durationReady ? durationValue : 0;
|
|
||||||
const getCurrentTime = () => toNumber(audio.currentTime);
|
|
||||||
|
|
||||||
audio.addEventListener('loadedmetadata', () => {
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
const current = toNumber(audio.currentTime);
|
||||||
durationValue = toNumber(audio.duration);
|
durationValue = toNumber(audio.duration);
|
||||||
durationReady = durationValue > 0;
|
durationReady = durationValue > 0;
|
||||||
if (dotNetHelper && durationReady) {
|
if (dotNetHelper && durationReady) {
|
||||||
dotNetHelper.invokeMethodAsync('OnDurationReady', durationValue);
|
dotNetHelper.invokeMethodAsync('OnTimeUpdate', current, durationValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,6 +49,15 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('progress', () => {
|
||||||
|
if (dotNetHelper) {
|
||||||
|
if (audio.buffered.length > 0 && audio.duration) {
|
||||||
|
const bufferedEnd = toNumber(audio.buffered.end(audio.buffered.length - 1));
|
||||||
|
dotNetHelper.invokeMethodAsync('OnDownloadProgress', bufferedEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Возвращаем все методы, которые будут вызываться из C#
|
// Возвращаем все методы, которые будут вызываться из C#
|
||||||
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime, getDuration, getCurrentTime };
|
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime };
|
||||||
}
|
}
|
||||||
19
PlaylistShared.Pwa/wwwroot/js/shareUtils.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function isSupported() {
|
||||||
|
return !!navigator.share;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shareLink(title, text, url) {
|
||||||
|
if (!navigator.share) {
|
||||||
|
return { success: false, error: 'Web Share API не поддерживается' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.share({ title, text, url });
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
return { success: false, cancelled: true };
|
||||||
|
}
|
||||||
|
console.error('Ошибка при шеринге:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,20 +3,28 @@
|
|||||||
"short_name": "PlaylistShare",
|
"short_name": "PlaylistShare",
|
||||||
"id": "./",
|
"id": "./",
|
||||||
"start_url": "./",
|
"start_url": "./",
|
||||||
|
"scope": "/",
|
||||||
|
"handle_links": "preferred",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#1a1a27",
|
||||||
"theme_color": "#03173d",
|
"theme_color": "#1a1a27",
|
||||||
|
"launch_handler": {
|
||||||
|
"client_mode": "focus-existing"
|
||||||
|
},
|
||||||
|
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "icon-512.png",
|
"src": "icon-512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512",
|
||||||
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icon-192.png",
|
"src": "icon-192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192",
|
||||||
|
"purpose": "any maskable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,17 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
|
|||||||
|
|
||||||
const cacheNamePrefix = 'offline-cache-';
|
const cacheNamePrefix = 'offline-cache-';
|
||||||
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
|
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
|
||||||
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.webmanifest$/ ];
|
const offlineAssetsInclude = [/\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.webmanifest$/];
|
||||||
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
|
// ИСКЛЮЧАЕМ также service-worker-assets.js
|
||||||
|
const offlineAssetsExclude = [/^service-worker\.js$/, /\/service-worker-assets\.js$/];
|
||||||
|
|
||||||
// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'.
|
|
||||||
const base = "/";
|
const base = "/";
|
||||||
const baseUrl = new URL(base, self.origin);
|
const baseUrl = new URL(base, self.origin);
|
||||||
const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href);
|
const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href);
|
||||||
|
|
||||||
async function onInstall(event) {
|
async function onInstall(event) {
|
||||||
console.info('Service worker: Install');
|
self.skipWaiting();
|
||||||
|
|
||||||
// Fetch and cache all matching items from the assets manifest
|
|
||||||
const assetsRequests = self.assetsManifest.assets
|
const assetsRequests = self.assetsManifest.assets
|
||||||
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
|
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
|
||||||
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
|
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
|
||||||
@@ -28,7 +27,13 @@ async function onInstall(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onActivate(event) {
|
async function onActivate(event) {
|
||||||
console.info('Service worker: Activate');
|
await self.clients.claim();
|
||||||
|
|
||||||
|
// НОВОЕ: Уведомляем все открытые вкладки о том, что новый SW активирован
|
||||||
|
const clientsList = await self.clients.matchAll();
|
||||||
|
clientsList.forEach(client => {
|
||||||
|
client.postMessage({ type: 'SW_ACTIVATED', version: self.assetsManifest.version });
|
||||||
|
});
|
||||||
|
|
||||||
// Delete unused caches
|
// Delete unused caches
|
||||||
const cacheKeys = await caches.keys();
|
const cacheKeys = await caches.keys();
|
||||||
@@ -38,13 +43,16 @@ async function onActivate(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onFetch(event) {
|
async function onFetch(event) {
|
||||||
|
// НОВОЕ: никогда не перехватываем файлы Service Worker
|
||||||
|
const url = event.request.url;
|
||||||
|
if (url.includes('/service-worker.js') || url.includes('/service-worker-assets.js')) {
|
||||||
|
return fetch(event.request);
|
||||||
|
}
|
||||||
|
|
||||||
let cachedResponse = null;
|
let cachedResponse = null;
|
||||||
if (event.request.method === 'GET') {
|
if (event.request.method === 'GET') {
|
||||||
// For all navigation requests, try to serve index.html from cache,
|
|
||||||
// unless that request is for an offline resource.
|
|
||||||
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
|
|
||||||
const shouldServeIndexHtml = event.request.mode === 'navigate'
|
const shouldServeIndexHtml = event.request.mode === 'navigate'
|
||||||
&& !manifestUrlList.some(url => url === event.request.url);
|
&& !manifestUrlList.some(u => u === event.request.url);
|
||||||
|
|
||||||
const request = shouldServeIndexHtml ? 'index.html' : event.request;
|
const request = shouldServeIndexHtml ? 'index.html' : event.request;
|
||||||
const cache = await caches.open(cacheName);
|
const cache = await caches.open(cacheName);
|
||||||
@@ -52,4 +60,4 @@ async function onFetch(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cachedResponse || fetch(event.request);
|
return cachedResponse || fetch(event.request);
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
|
||||||
|
|
||||||
public class RemoveTracksRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("trackIds")]
|
|
||||||
public List<string> TrackIds { get; set; } = new();
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace PlaylistShared.Shared.DTO;
|
|
||||||
|
|
||||||
public class YandexTrack
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
public string Title { get; set; } = "";
|
|
||||||
public List<string> Artists { get; set; } = new();
|
|
||||||
public int DurationMs { get; set; }
|
|
||||||
public string CoverUri { get; set; } = "";
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace PlaylistShared.Shared.Enums;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Shared.Enums;
|
||||||
|
|
||||||
/// <summary>Кто может выполнять действие (добавление/удаление).</summary>
|
/// <summary>Кто может выполнять действие (добавление/удаление).</summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public enum EditPermission
|
public enum EditPermission
|
||||||
{
|
{
|
||||||
/// <summary>Все, включая неавторизованных (но для выполнения действия нужна авторизация, так как API требует токен).</summary>
|
/// <summary>Все, включая неавторизованных (но для выполнения действия нужна авторизация, так как API требует токен).</summary>
|
||||||
@@ -14,4 +17,4 @@ public enum EditPermission
|
|||||||
|
|
||||||
/// <summary>Только тот пользователь, который добавил трек (актуально для удаления).</summary>
|
/// <summary>Только тот пользователь, который добавил трек (актуально для удаления).</summary>
|
||||||
AddedByUserOnly,
|
AddedByUserOnly,
|
||||||
}
|
}
|
||||||
|
|||||||
17
PlaylistShared.Shared/Enums/TrackSearchType.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Shared.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Типы поиска треков в Яндекс.Музыке, которые можно указать при поисковом запросе.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum TrackSearchType
|
||||||
|
{
|
||||||
|
All,
|
||||||
|
Artist,
|
||||||
|
Album,
|
||||||
|
Playlist,
|
||||||
|
Track,
|
||||||
|
MyPlaylists,
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace PlaylistShared.Shared.Enums;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Shared.Enums;
|
||||||
|
|
||||||
/// <summary>Кто может просматривать плейлист.</summary>
|
/// <summary>Кто может просматривать плейлист.</summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public enum ViewPermission
|
public enum ViewPermission
|
||||||
{
|
{
|
||||||
/// <summary>Все, включая неавторизованных.</summary>
|
/// <summary>Все, включая неавторизованных.</summary>
|
||||||
|
|||||||
12
PlaylistShared.Shared/Enums/YandexAuthQrStatus.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Shared.Enums;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum YandexAuthQrStatus
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
Authorized,
|
||||||
|
Expired,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace PlaylistShared.Shared.Shared;
|
namespace PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
|
||||||
public class AddTrackByLinkRequest
|
public class AddTrackByLinkRequest
|
||||||
{
|
{
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.Playlist;
|
namespace PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
|
||||||
/// <summary>Запрос на создание нового шеринг-плейлиста.</summary>
|
/// <summary>Запрос на создание нового шеринг-плейлиста.</summary>
|
||||||
public class SharePlaylistDto
|
public class SharePlaylistDto
|
||||||
{
|
{
|
||||||
|
/// <summary>Идентификатор плейлиста в Яндекс.Музыке (guid).</summary>
|
||||||
|
[JsonPropertyName("yandexPlaylistId")]
|
||||||
|
public string YandexPlaylistUuid { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>Идентификатор плейлиста в Яндекс.Музыке (kind).</summary>
|
/// <summary>Идентификатор плейлиста в Яндекс.Музыке (kind).</summary>
|
||||||
[JsonPropertyName("yandexPlaylistKind")]
|
[JsonPropertyName("yandexPlaylistKind")]
|
||||||
public string YandexPlaylistKind { get; set; } = null!;
|
public string YandexPlaylistKind { get; set; } = null!;
|
||||||
@@ -38,6 +42,10 @@ public class SharePlaylistDto
|
|||||||
[JsonPropertyName("viewPermission")]
|
[JsonPropertyName("viewPermission")]
|
||||||
public ViewPermission ViewPermission { get; set; }
|
public ViewPermission ViewPermission { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Права на воспроизведение.</summary>
|
||||||
|
[JsonPropertyName("playPermission")]
|
||||||
|
public ViewPermission PlayPermission { get; set; }
|
||||||
|
|
||||||
/// <summary>Права на добавление треков.</summary>
|
/// <summary>Права на добавление треков.</summary>
|
||||||
[JsonPropertyName("addPermission")]
|
[JsonPropertyName("addPermission")]
|
||||||
public EditPermission AddPermission { get; set; }
|
public EditPermission AddPermission { get; set; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace PlaylistShared.Shared.Playlist;
|
namespace PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
|
||||||
public class SharePlaylistRequest
|
public class SharePlaylistRequest
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.Shared;
|
namespace PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
|
||||||
/// <summary>DTO шеринг-плейлиста (без навигационных свойств).</summary>
|
/// <summary>DTO шеринг-плейлиста (без навигационных свойств).</summary>
|
||||||
public class SharedPlaylistDto
|
public class SharedPlaylistDto
|
||||||
@@ -15,6 +15,10 @@ public class SharedPlaylistDto
|
|||||||
[JsonPropertyName("creatorUserId")]
|
[JsonPropertyName("creatorUserId")]
|
||||||
public Guid CreatorUserId { get; set; }
|
public Guid CreatorUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Uuid на яндекс плейлист</summary>
|
||||||
|
[JsonPropertyName("yandexPlaylistUuid")]
|
||||||
|
public string? YandexPlaylistUuid { get; set; }
|
||||||
|
|
||||||
/// <summary>Идентификатор плейлиста в Яндекс.Музыке (kind).</summary>
|
/// <summary>Идентификатор плейлиста в Яндекс.Музыке (kind).</summary>
|
||||||
[JsonPropertyName("yandexPlaylistKind")]
|
[JsonPropertyName("yandexPlaylistKind")]
|
||||||
public string YandexPlaylistKind { get; set; } = null!;
|
public string YandexPlaylistKind { get; set; } = null!;
|
||||||
@@ -55,6 +59,10 @@ public class SharedPlaylistDto
|
|||||||
[JsonPropertyName("viewPermission")]
|
[JsonPropertyName("viewPermission")]
|
||||||
public ViewPermission ViewPermission { get; set; }
|
public ViewPermission ViewPermission { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Права на воспроизведение.</summary>
|
||||||
|
[JsonPropertyName("playPermission")]
|
||||||
|
public ViewPermission PlayPermission { get; set; }
|
||||||
|
|
||||||
/// <summary>Права на добавление треков.</summary>
|
/// <summary>Права на добавление треков.</summary>
|
||||||
[JsonPropertyName("addPermission")]
|
[JsonPropertyName("addPermission")]
|
||||||
public EditPermission AddPermission { get; set; }
|
public EditPermission AddPermission { get; set; }
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.Shared;
|
namespace PlaylistShared.Shared.SharedPlaylist;
|
||||||
|
|
||||||
/// <summary>Запрос на обновление прав доступа шеринг-плейлиста.</summary>
|
/// <summary>Запрос на обновление прав доступа шеринг-плейлиста.</summary>
|
||||||
public class UpdatePermissionsDto
|
public class UpdatePermissionsDto
|
||||||
{
|
{
|
||||||
/// <summary>Новые права на просмотр.</summary>
|
/// <summary>Права на просмотр.</summary>
|
||||||
[JsonPropertyName("viewPermission")]
|
[JsonPropertyName("viewPermission")]
|
||||||
public ViewPermission ViewPermission { get; set; }
|
public ViewPermission ViewPermission { get; set; }
|
||||||
|
|
||||||
/// <summary>Новые права на добавление треков.</summary>
|
/// <summary>Права на воспроизведение треков.</summary>
|
||||||
|
[JsonPropertyName("playPermission")]
|
||||||
|
public ViewPermission PlayPermission { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Права на добавление треков.</summary>
|
||||||
[JsonPropertyName("addPermission")]
|
[JsonPropertyName("addPermission")]
|
||||||
public EditPermission AddPermission { get; set; }
|
public EditPermission AddPermission { get; set; }
|
||||||
|
|
||||||
/// <summary>Новые права на удаление треков.</summary>
|
/// <summary>Права на удаление треков.</summary>
|
||||||
[JsonPropertyName("removePermission")]
|
[JsonPropertyName("removePermission")]
|
||||||
public EditPermission RemovePermission { get; set; }
|
public EditPermission RemovePermission { get; set; }
|
||||||
}
|
}
|
||||||