Compare commits
5 Commits
5717b1ec0c
...
4b3036364b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b3036364b | ||
|
|
fd3ed2f317 | ||
|
|
41e0fd0563 | ||
|
|
fbfc6990e6 | ||
|
|
40ea9166d2 |
@@ -2,6 +2,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
|
using PlaylistShared.Shared;
|
||||||
|
using PlaylistShared.Shared.Auth;
|
||||||
using PlaylistShared.Shared.DTO;
|
using PlaylistShared.Shared.DTO;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@@ -11,12 +13,14 @@ public class AccountController : ControllerBase
|
|||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
private readonly JwtService _jwtService;
|
private readonly JwtService _jwtService;
|
||||||
|
private readonly UserSessionService _userSessionService;
|
||||||
|
|
||||||
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, JwtService jwtService)
|
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, JwtService jwtService, UserSessionService userSessionService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
_jwtService = jwtService;
|
_jwtService = jwtService;
|
||||||
|
_userSessionService = userSessionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
@@ -54,6 +58,8 @@ public class AccountController : ControllerBase
|
|||||||
|
|
||||||
private async Task<ActionResult<ApiResponse<LoginResponse>>> GenerateTokenResponse(ApplicationUser user)
|
private async Task<ActionResult<ApiResponse<LoginResponse>>> GenerateTokenResponse(ApplicationUser user)
|
||||||
{
|
{
|
||||||
|
await _userSessionService.GetOrCreateCurrentSessionAsync(user.Id);
|
||||||
|
|
||||||
var (token, refreshToken, expiration) = await _jwtService.GenerateTokenAsync(user);
|
var (token, refreshToken, expiration) = await _jwtService.GenerateTokenAsync(user);
|
||||||
return Ok(ApiResponse<LoginResponse>.Ok(new LoginResponse
|
return Ok(ApiResponse<LoginResponse>.Ok(new LoginResponse
|
||||||
{
|
{
|
||||||
|
|||||||
84
PlaylistShared.Api/Controllers/AudioController.cs
Normal file
84
PlaylistShared.Api/Controllers/AudioController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
using PlaylistShared.Api.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AudioController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly YandexMusicService _yandexService;
|
||||||
|
private readonly JwtService _jwtService;
|
||||||
|
|
||||||
|
public AudioController(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
YandexMusicService yandexService,
|
||||||
|
JwtService jwtService)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_yandexService = yandexService;
|
||||||
|
_jwtService = jwtService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Потоковое воспроизведение трека из Яндекс.Музыки.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackId">ID трека (например, "21696942").</param>
|
||||||
|
[HttpGet("track/{trackId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? access_token = null)
|
||||||
|
{
|
||||||
|
var user = await GetUserFromToken(access_token);
|
||||||
|
if (user == null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId);
|
||||||
|
if (string.IsNullOrEmpty(streamUrl))
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, streamUrl);
|
||||||
|
|
||||||
|
// Пробрасываем Range-заголовок клиента к Яндекс.Музыке
|
||||||
|
if (Request.Headers.ContainsKey("Range"))
|
||||||
|
{
|
||||||
|
request.Headers.Add("Range", Request.Headers["Range"].ToString());
|
||||||
|
}
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
|
// Если Яндекс.Музыка поддерживает range, пробрасываем статус 206
|
||||||
|
Response.StatusCode = (int)response.StatusCode;
|
||||||
|
Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
|
||||||
|
|
||||||
|
if (response.Content.Headers.Contains("Content-Range"))
|
||||||
|
Response.Headers.Add("Content-Range", response.Content.Headers.ContentRange?.ToString());
|
||||||
|
if (response.Headers.Contains("Accept-Ranges"))
|
||||||
|
Response.Headers.Add("Accept-Ranges", response.Headers.AcceptRanges?.ToString());
|
||||||
|
if (response.Content.Headers.Contains("Content-Length"))
|
||||||
|
Response.Headers.Add("Content-Length", response.Content.Headers.ContentLength?.ToString());
|
||||||
|
|
||||||
|
await response.Content.CopyToAsync(Response.Body);
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ApplicationUser?> GetUserFromToken(string? token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var principal = _jwtService.ValidateToken(token);
|
||||||
|
if (principal == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await _userManager.FindByIdAsync(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,17 +14,20 @@ public class OpenIdController : ControllerBase
|
|||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly JwtService _jwtService;
|
private readonly JwtService _jwtService;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly UserSessionService _userSessionService;
|
||||||
|
|
||||||
public OpenIdController(
|
public OpenIdController(
|
||||||
SignInManager<ApplicationUser> signInManager,
|
SignInManager<ApplicationUser> signInManager,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
IConfiguration configuration)
|
IConfiguration configuration,
|
||||||
|
UserSessionService userSessionService)
|
||||||
{
|
{
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_jwtService = jwtService;
|
_jwtService = jwtService;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_userSessionService = userSessionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("login")]
|
[HttpGet("login")]
|
||||||
@@ -70,6 +73,7 @@ public class OpenIdController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||||
|
await _userSessionService.GetOrCreateCurrentSessionAsync(user.Id);
|
||||||
var (token, refreshToken, _) = await _jwtService.GenerateTokenAsync(user);
|
var (token, refreshToken, _) = await _jwtService.GenerateTokenAsync(user);
|
||||||
return Redirect($"{_configuration["Client:BaseUrl"]}/auth-callback?token={token}&refreshToken={refreshToken}");
|
return Redirect($"{_configuration["Client:BaseUrl"]}/auth-callback?token={token}&refreshToken={refreshToken}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
using PlaylistShared.Api.Extensions;
|
using PlaylistShared.Api.Extensions;
|
||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
using PlaylistShared.Shared.DTO;
|
using PlaylistShared.Shared;
|
||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using PlaylistShared.Shared.Models;
|
using PlaylistShared.Shared.Playlist;
|
||||||
|
using PlaylistShared.Shared.Shared;
|
||||||
using YandexMusic;
|
using YandexMusic;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Controllers;
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using PlaylistShared.Api.Entities;
|
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.DTO;
|
using PlaylistShared.Shared.DTO;
|
||||||
using PlaylistShared.Shared.Models;
|
using PlaylistShared.Shared.Shared;
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@@ -15,18 +16,24 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
private readonly SharedPlaylistService _sharedService;
|
private readonly SharedPlaylistService _sharedService;
|
||||||
private readonly YandexMusicService _yandexService;
|
private readonly YandexMusicService _yandexService;
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly TrackAdditionLogService _trackLogService;
|
private readonly UserSessionService _userSessionService;
|
||||||
|
private readonly TrackAdditionLogService _trackAdditionLogService;
|
||||||
|
private readonly TrackRemovalLogService _trackRemovalLogService;
|
||||||
|
|
||||||
public SharedPlaylistController(
|
public SharedPlaylistController(
|
||||||
SharedPlaylistService sharedService,
|
SharedPlaylistService sharedService,
|
||||||
YandexMusicService yandexService,
|
YandexMusicService yandexService,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
TrackAdditionLogService trackLogService)
|
TrackAdditionLogService trackAdditionLogService,
|
||||||
|
TrackRemovalLogService trackRemovalLogService,
|
||||||
|
UserSessionService userSessionService)
|
||||||
{
|
{
|
||||||
_sharedService = sharedService;
|
_sharedService = sharedService;
|
||||||
_yandexService = yandexService;
|
_yandexService = yandexService;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_trackLogService = trackLogService;
|
_trackAdditionLogService = trackAdditionLogService;
|
||||||
|
_trackRemovalLogService = trackRemovalLogService;
|
||||||
|
_userSessionService = userSessionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/sharedplaylist/{token}
|
// GET /api/sharedplaylist/{token}
|
||||||
@@ -112,6 +119,13 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
if (updatedPlaylist == null)
|
if (updatedPlaylist == null)
|
||||||
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при добавлении треков" }));
|
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при добавлении треков" }));
|
||||||
|
|
||||||
|
var session = await _userSessionService.GetOrCreateCurrentSessionAsync(currentUserId);
|
||||||
|
var sessionId = session.SessionId;
|
||||||
|
foreach (var trackId in request.TrackIds)
|
||||||
|
{
|
||||||
|
await _trackAdditionLogService.LogAdditionAsync(playlist.Id, trackId, currentUserId, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.Ok(new { message = "Треки добавлены" }));
|
return Ok(ApiResponse<object>.Ok(new { message = "Треки добавлены" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +138,12 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
if (playlist == null)
|
if (playlist == null)
|
||||||
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
||||||
|
|
||||||
|
var session = await _userSessionService.GetOrCreateCurrentSessionAsync(currentUserId);
|
||||||
|
var sessionId = session.SessionId;
|
||||||
|
|
||||||
foreach (var trackId in request.TrackIds)
|
foreach (var trackId in request.TrackIds)
|
||||||
{
|
{
|
||||||
if (!await _sharedService.CanRemoveTrackAsync(playlist, currentUserId, trackId))
|
if (!await _sharedService.CanRemoveTrackAsync(playlist, currentUserId, trackId, sessionId))
|
||||||
return StatusCode(403, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 403, Message = $"Недостаточно прав для удаления трека {trackId}" }));
|
return StatusCode(403, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 403, Message = $"Недостаточно прав для удаления трека {trackId}" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +156,10 @@ public class SharedPlaylistController : ControllerBase
|
|||||||
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при удалении треков" }));
|
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при удалении треков" }));
|
||||||
|
|
||||||
foreach (var trackId in request.TrackIds)
|
foreach (var trackId in request.TrackIds)
|
||||||
await _trackLogService.RemoveLogsForTrackAsync(playlist.Id, trackId);
|
{
|
||||||
|
await _trackRemovalLogService.LogRemovalAsync(playlist.Id, trackId, currentUserId, sessionId);
|
||||||
|
await _trackAdditionLogService.RemoveLogsForTrackAsync(playlist.Id, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" }));
|
return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
using PlaylistShared.Api.Extensions;
|
using PlaylistShared.Api.Extensions;
|
||||||
using PlaylistShared.Api.Services;
|
using PlaylistShared.Api.Services;
|
||||||
using PlaylistShared.Shared.DTO;
|
using PlaylistShared.Shared;
|
||||||
|
using PlaylistShared.Shared.Profile;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Controllers;
|
namespace PlaylistShared.Api.Controllers;
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
{
|
{
|
||||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
|
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
|
||||||
|
|
||||||
public DbSet<SharedPlaylistEntity> SharedPlaylists => Set<SharedPlaylistEntity>();
|
public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
|
||||||
public DbSet<TrackAdditionLogEntity> TrackAdditionLogs => Set<TrackAdditionLogEntity>();
|
public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>();
|
||||||
|
public DbSet<UserSession> UserSessions => Set<UserSession>();
|
||||||
|
public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
builder.Entity<SharedPlaylistEntity>(entity =>
|
builder.Entity<SharedPlaylist>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
entity.HasIndex(e => e.ShareToken).IsUnique();
|
entity.HasIndex(e => e.ShareToken).IsUnique();
|
||||||
@@ -29,7 +31,18 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
|
entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Entity<TrackAdditionLogEntity>(entity =>
|
builder.Entity<UserSession>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.SessionId);
|
||||||
|
entity.Property(e => e.SessionId).HasMaxLength(449);
|
||||||
|
entity.HasIndex(e => e.AssociatedUserId);
|
||||||
|
entity.HasOne(e => e.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.AssociatedUserId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<TrackAdditionLog>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId });
|
entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId });
|
||||||
@@ -41,6 +54,28 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(e => e.AddedByUserId)
|
.HasForeignKey(e => e.AddedByUserId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
entity.HasOne(e => e.Session)
|
||||||
|
.WithMany(s => s.TrackAdditionLogs)
|
||||||
|
.HasForeignKey(e => e.SessionId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<TrackRemovalLog>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId });
|
||||||
|
entity.HasOne(e => e.SharedPlaylist)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.SharedPlaylistId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
entity.HasOne(e => e.RemovedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.RemovedByUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
entity.HasOne(e => e.Session)
|
||||||
|
.WithMany(s => s.TrackRemovalLogs)
|
||||||
|
.HasForeignKey(e => e.SessionId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
426
PlaylistShared.Api/Data/Migrations/20260413221546_AddSessionCacheTable.Designer.cs
generated
Normal file
426
PlaylistShared.Api/Data/Migrations/20260413221546_AddSessionCacheTable.Designer.cs
generated
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
// <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("20260413221546_AddSessionCacheTable")]
|
||||||
|
partial class AddSessionCacheTable
|
||||||
|
{
|
||||||
|
/// <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.SharedPlaylistEntity", 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>("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.TrackAdditionLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("AddedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
|
b.ToTable("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.SharedPlaylistEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
|
.WithMany("OwnedPlaylists")
|
||||||
|
.HasForeignKey("CreatorUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylistEntity", "SharedPlaylist")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SharedPlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AddedByUser");
|
||||||
|
|
||||||
|
b.Navigation("SharedPlaylist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("OwnedPlaylists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSessionCacheTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TABLE [dbo].[SessionCache] (
|
||||||
|
[Id] NVARCHAR(449) NOT NULL,
|
||||||
|
[Value] VARBINARY(MAX) NOT NULL,
|
||||||
|
[ExpiresAtTime] DATETIMEOFFSET NOT NULL,
|
||||||
|
[SlidingExpirationInSeconds] BIGINT NULL,
|
||||||
|
[AbsoluteExpiration] DATETIMEOFFSET NULL,
|
||||||
|
CONSTRAINT [pk_SessionCache] PRIMARY KEY ([Id])
|
||||||
|
);
|
||||||
|
CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[SessionCache] ([ExpiresAtTime]);
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("DROP TABLE [dbo].[SessionCache]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
544
PlaylistShared.Api/Data/Migrations/20260413223451_AddUserSessionsAndLogging.Designer.cs
generated
Normal file
544
PlaylistShared.Api/Data/Migrations/20260413223451_AddUserSessionsAndLogging.Designer.cs
generated
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
// <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("20260413223451_AddUserSessionsAndLogging")]
|
||||||
|
partial class AddUserSessionsAndLogging
|
||||||
|
{
|
||||||
|
/// <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>("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,151 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PlaylistShared.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserSessionsAndLogging : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "AddedByUserId",
|
||||||
|
table: "TrackAdditionLogs",
|
||||||
|
type: "uniqueidentifier",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uniqueidentifier");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "SessionId",
|
||||||
|
table: "TrackAdditionLogs",
|
||||||
|
type: "nvarchar(449)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
SessionId = table.Column<string>(type: "nvarchar(449)", maxLength: 449, nullable: false),
|
||||||
|
ClientIpAddress = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UserAgent = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
FirstSeenUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
LastSeenUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
AssociatedUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserSessions", x => x.SessionId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserSessions_AspNetUsers_AssociatedUserId",
|
||||||
|
column: x => x.AssociatedUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TrackRemovalLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
SharedPlaylistId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
TrackId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
RemovedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
RemovedAtUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
SessionId = table.Column<string>(type: "nvarchar(449)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TrackRemovalLogs", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TrackRemovalLogs_AspNetUsers_RemovedByUserId",
|
||||||
|
column: x => x.RemovedByUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TrackRemovalLogs_SharedPlaylists_SharedPlaylistId",
|
||||||
|
column: x => x.SharedPlaylistId,
|
||||||
|
principalTable: "SharedPlaylists",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TrackRemovalLogs_UserSessions_SessionId",
|
||||||
|
column: x => x.SessionId,
|
||||||
|
principalTable: "UserSessions",
|
||||||
|
principalColumn: "SessionId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TrackAdditionLogs_SessionId",
|
||||||
|
table: "TrackAdditionLogs",
|
||||||
|
column: "SessionId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TrackRemovalLogs_RemovedByUserId",
|
||||||
|
table: "TrackRemovalLogs",
|
||||||
|
column: "RemovedByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TrackRemovalLogs_SessionId",
|
||||||
|
table: "TrackRemovalLogs",
|
||||||
|
column: "SessionId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TrackRemovalLogs_SharedPlaylistId_TrackId",
|
||||||
|
table: "TrackRemovalLogs",
|
||||||
|
columns: new[] { "SharedPlaylistId", "TrackId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserSessions_AssociatedUserId",
|
||||||
|
table: "UserSessions",
|
||||||
|
column: "AssociatedUserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_TrackAdditionLogs_UserSessions_SessionId",
|
||||||
|
table: "TrackAdditionLogs",
|
||||||
|
column: "SessionId",
|
||||||
|
principalTable: "UserSessions",
|
||||||
|
principalColumn: "SessionId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_TrackAdditionLogs_UserSessions_SessionId",
|
||||||
|
table: "TrackAdditionLogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TrackRemovalLogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_TrackAdditionLogs_SessionId",
|
||||||
|
table: "TrackAdditionLogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SessionId",
|
||||||
|
table: "TrackAdditionLogs");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "AddedByUserId",
|
||||||
|
table: "TrackAdditionLogs",
|
||||||
|
type: "uniqueidentifier",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uniqueidentifier",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -237,7 +237,7 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.ToTable("AspNetUsers", (string)null);
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -299,7 +299,7 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.ToTable("SharedPlaylists");
|
b.ToTable("SharedPlaylists");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -308,9 +308,13 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.Property<DateTime>("AddedAtUtc")
|
b.Property<DateTime>("AddedAtUtc")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<Guid>("AddedByUserId")
|
b.Property<Guid?>("AddedByUserId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(449)");
|
||||||
|
|
||||||
b.Property<Guid>("SharedPlaylistId")
|
b.Property<Guid>("SharedPlaylistId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
@@ -322,11 +326,75 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("AddedByUserId");
|
b.HasIndex("AddedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
b.HasIndex("SharedPlaylistId", "TrackId");
|
b.HasIndex("SharedPlaylistId", "TrackId");
|
||||||
|
|
||||||
b.ToTable("TrackAdditionLogs");
|
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 =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
|
||||||
@@ -378,7 +446,7 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
|
||||||
.WithMany("OwnedPlaylists")
|
.WithMany("OwnedPlaylists")
|
||||||
@@ -389,15 +457,20 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.Navigation("Creator");
|
b.Navigation("Creator");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("AddedByUserId")
|
.HasForeignKey("AddedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
|
||||||
|
.WithMany("TrackAdditionLogs")
|
||||||
|
.HasForeignKey("SessionId")
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylistEntity", "SharedPlaylist")
|
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
|
||||||
.WithMany("TrackAdditionLogs")
|
.WithMany("TrackAdditionLogs")
|
||||||
.HasForeignKey("SharedPlaylistId")
|
.HasForeignKey("SharedPlaylistId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -405,6 +478,44 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
|
|
||||||
b.Navigation("AddedByUser");
|
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");
|
b.Navigation("SharedPlaylist");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -413,10 +524,17 @@ namespace PlaylistShared.Api.Data.Migrations
|
|||||||
b.Navigation("OwnedPlaylists");
|
b.Navigation("OwnedPlaylists");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
|
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("TrackAdditionLogs");
|
b.Navigation("TrackAdditionLogs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("TrackAdditionLogs");
|
||||||
|
|
||||||
|
b.Navigation("TrackRemovalLogs");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ public class ApplicationUser : IdentityUser<Guid>
|
|||||||
public DateTime RefreshTokenExpiryUtc { get; set; }
|
public DateTime RefreshTokenExpiryUtc { get; set; }
|
||||||
|
|
||||||
/// <summary>Плейлисты, созданные пользователем.</summary>
|
/// <summary>Плейлисты, созданные пользователем.</summary>
|
||||||
public ICollection<SharedPlaylistEntity> OwnedPlaylists { get; set; } = new List<SharedPlaylistEntity>();
|
public ICollection<SharedPlaylist> OwnedPlaylists { get; set; } = new List<SharedPlaylist>();
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace PlaylistShared.Api.Entities;
|
namespace PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
/// <summary>Сущность шеринг-плейлиста (таблица в БД).</summary>
|
/// <summary>Сущность шеринг-плейлиста.</summary>
|
||||||
public class SharedPlaylistEntity
|
public class SharedPlaylist
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid CreatorUserId { get; set; }
|
public Guid CreatorUserId { get; set; }
|
||||||
@@ -22,5 +22,5 @@ public class SharedPlaylistEntity
|
|||||||
|
|
||||||
// Навигационные свойства
|
// Навигационные свойства
|
||||||
public ApplicationUser Creator { get; set; } = null!;
|
public ApplicationUser Creator { get; set; } = null!;
|
||||||
public ICollection<TrackAdditionLogEntity> TrackAdditionLogs { get; set; } = new List<TrackAdditionLogEntity>();
|
public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
|
||||||
}
|
}
|
||||||
16
PlaylistShared.Api/Entities/TrackAdditionLog.cs
Normal file
16
PlaylistShared.Api/Entities/TrackAdditionLog.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
|
/// <summary>Лог добавления трека.</summary>
|
||||||
|
public class TrackAdditionLog
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid SharedPlaylistId { get; set; }
|
||||||
|
public string TrackId { get; set; } = null!;
|
||||||
|
public Guid? AddedByUserId { get; set; }
|
||||||
|
public DateTime AddedAtUtc { get; set; }
|
||||||
|
public string SessionId { get; set; } = null!;
|
||||||
|
|
||||||
|
public SharedPlaylist SharedPlaylist { get; set; } = null!;
|
||||||
|
public ApplicationUser? AddedByUser { get; set; }
|
||||||
|
public UserSession Session { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace PlaylistShared.Api.Entities;
|
|
||||||
|
|
||||||
/// <summary>Лог добавления трека (таблица в БД).</summary>
|
|
||||||
public class TrackAdditionLogEntity
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public Guid SharedPlaylistId { get; set; }
|
|
||||||
public string TrackId { get; set; } = null!;
|
|
||||||
public Guid AddedByUserId { get; set; }
|
|
||||||
public DateTime AddedAtUtc { get; set; }
|
|
||||||
|
|
||||||
// Навигационные свойства
|
|
||||||
public SharedPlaylistEntity SharedPlaylist { get; set; } = null!;
|
|
||||||
public ApplicationUser AddedByUser { get; set; } = null!;
|
|
||||||
}
|
|
||||||
15
PlaylistShared.Api/Entities/TrackRemovalLog.cs
Normal file
15
PlaylistShared.Api/Entities/TrackRemovalLog.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
|
public class TrackRemovalLog
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid SharedPlaylistId { get; set; }
|
||||||
|
public string TrackId { get; set; } = null!;
|
||||||
|
public Guid? RemovedByUserId { get; set; }
|
||||||
|
public DateTime RemovedAtUtc { get; set; }
|
||||||
|
public string SessionId { get; set; } = null!;
|
||||||
|
|
||||||
|
public SharedPlaylist SharedPlaylist { get; set; } = null!;
|
||||||
|
public ApplicationUser? RemovedByUser { get; set; }
|
||||||
|
public UserSession Session { get; set; } = null!;
|
||||||
|
}
|
||||||
15
PlaylistShared.Api/Entities/UserSession.cs
Normal file
15
PlaylistShared.Api/Entities/UserSession.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
|
public class UserSession
|
||||||
|
{
|
||||||
|
public string SessionId { get; set; } = null!; // HttpContext.Session.Id
|
||||||
|
public string? ClientIpAddress { get; set; }
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
public DateTime FirstSeenUtc { get; set; }
|
||||||
|
public DateTime LastSeenUtc { get; set; }
|
||||||
|
public Guid? AssociatedUserId { get; set; } // если позже залогинился
|
||||||
|
|
||||||
|
public ApplicationUser? User { get; set; }
|
||||||
|
public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
|
||||||
|
public ICollection<TrackRemovalLog> TrackRemovalLogs { get; set; } = new List<TrackRemovalLog>();
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using PlaylistShared.Api.Entities;
|
using PlaylistShared.Api.Entities;
|
||||||
using PlaylistShared.Shared.Models;
|
using PlaylistShared.Shared.Auth;
|
||||||
|
using PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Mapping;
|
namespace PlaylistShared.Api.Mapping;
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ public class AppMappingProfile : Profile
|
|||||||
{
|
{
|
||||||
public AppMappingProfile()
|
public AppMappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<SharedPlaylistEntity, SharedPlaylistDto>()
|
CreateMap<SharedPlaylist, SharedPlaylistDto>()
|
||||||
.ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator));
|
.ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator));
|
||||||
CreateMap<ApplicationUser, ApplicationUserDto>();
|
CreateMap<ApplicationUser, ApplicationUserDto>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,12 @@
|
|||||||
<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.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="10.0.5" />
|
||||||
<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.5" />
|
<PackageReference Include="YandexMusic" Version="0.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -35,6 +35,22 @@ public class Program
|
|||||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
.AddDefaultTokenProviders();
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
// Session
|
||||||
|
builder.Services.AddDistributedSqlServerCache(options =>
|
||||||
|
{
|
||||||
|
options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||||
|
options.SchemaName = "dbo";
|
||||||
|
options.TableName = "SessionCache";
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSession(options =>
|
||||||
|
{
|
||||||
|
options.IdleTimeout = TimeSpan.FromDays(30);
|
||||||
|
options.Cookie.HttpOnly = true;
|
||||||
|
options.Cookie.IsEssential = true;
|
||||||
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||||
|
});
|
||||||
|
|
||||||
// JWT
|
// JWT
|
||||||
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new Exception("Jwt:Key missing");
|
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new Exception("Jwt:Key missing");
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(options =>
|
||||||
@@ -70,12 +86,15 @@ public class Program
|
|||||||
options.SignInScheme = IdentityConstants.ExternalScheme;
|
options.SignInScheme = IdentityConstants.ExternalScheme;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
builder.Services.AddAutoMapper(t => t.AddProfile<AppMappingProfile>());
|
builder.Services.AddAutoMapper(t => t.AddProfile<AppMappingProfile>());
|
||||||
|
builder.Services.AddScoped<JwtService>();
|
||||||
|
builder.Services.AddScoped<UserSessionService>();
|
||||||
builder.Services.AddScoped<YandexMusicService>();
|
builder.Services.AddScoped<YandexMusicService>();
|
||||||
builder.Services.AddScoped<SharedPlaylistService>();
|
builder.Services.AddScoped<SharedPlaylistService>();
|
||||||
builder.Services.AddScoped<TrackAdditionLogService>();
|
builder.Services.AddScoped<TrackAdditionLogService>();
|
||||||
builder.Services.AddScoped<JwtService>();
|
builder.Services.AddScoped<TrackRemovalLogService>();
|
||||||
builder.Services.AddDataProtection();
|
builder.Services.AddDataProtection();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
@@ -123,6 +142,7 @@ public class Program
|
|||||||
app.UseCors("Production");
|
app.UseCors("Production");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.UseSession();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
|||||||
@@ -46,4 +46,29 @@ public class JwtService
|
|||||||
|
|
||||||
return (tokenString, refreshToken, tokenDescriptor.Expires.Value);
|
return (tokenString, refreshToken, tokenDescriptor.Expires.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClaimsPrincipal? ValidateToken(string token)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = _configuration["Jwt:Issuer"],
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = _configuration["Jwt:Audience"],
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
}, out _);
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
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.DTO;
|
|
||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using PlaylistShared.Shared.Models;
|
using PlaylistShared.Shared.Playlist;
|
||||||
|
using PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
namespace PlaylistShared.Api.Services;
|
namespace PlaylistShared.Api.Services;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ public class SharedPlaylistService
|
|||||||
|
|
||||||
public async Task<SharedPlaylistDto> CreateAsync(Guid creatorUserId, SharePlaylistDto dto)
|
public async Task<SharedPlaylistDto> CreateAsync(Guid creatorUserId, SharePlaylistDto dto)
|
||||||
{
|
{
|
||||||
var entity = new SharedPlaylistEntity
|
var entity = new SharedPlaylist
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
CreatorUserId = creatorUserId,
|
CreatorUserId = creatorUserId,
|
||||||
@@ -51,7 +51,7 @@ public class SharedPlaylistService
|
|||||||
return entity == null ? null : _mapper.Map<SharedPlaylistDto>(entity);
|
return entity == null ? null : _mapper.Map<SharedPlaylistDto>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SharedPlaylistEntity?> GetEntityByTokenAsync(string token)
|
public async Task<SharedPlaylist?> GetEntityByTokenAsync(string token)
|
||||||
{
|
{
|
||||||
return await _db.SharedPlaylists
|
return await _db.SharedPlaylists
|
||||||
.Include(sp => sp.Creator)
|
.Include(sp => sp.Creator)
|
||||||
@@ -80,21 +80,21 @@ public class SharedPlaylistService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanViewAsync(SharedPlaylistEntity playlist, Guid? currentUserId)
|
public async Task<bool> CanViewAsync(SharedPlaylist playlist, Guid? currentUserId)
|
||||||
{
|
{
|
||||||
if (currentUserId == playlist.CreatorUserId) return true;
|
if (currentUserId == playlist.CreatorUserId) return true;
|
||||||
return playlist.ViewPermission == ViewPermission.Everyone ||
|
return playlist.ViewPermission == ViewPermission.Everyone ||
|
||||||
(playlist.ViewPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue);
|
(playlist.ViewPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanAddTrackAsync(SharedPlaylistEntity playlist, Guid? currentUserId)
|
public async Task<bool> CanAddTrackAsync(SharedPlaylist playlist, Guid? currentUserId)
|
||||||
{
|
{
|
||||||
if (currentUserId == playlist.CreatorUserId) return true;
|
if (currentUserId == playlist.CreatorUserId) return true;
|
||||||
return playlist.AddPermission == EditPermission.Everyone ||
|
return playlist.AddPermission == EditPermission.Everyone ||
|
||||||
(playlist.AddPermission == EditPermission.AuthorizedOnly && currentUserId.HasValue);
|
(playlist.AddPermission == EditPermission.AuthorizedOnly && currentUserId.HasValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanRemoveTrackAsync(SharedPlaylistEntity playlist, Guid? currentUserId, string trackId)
|
public async Task<bool> CanRemoveTrackAsync(SharedPlaylist playlist, Guid? currentUserId, string trackId, string sessionId)
|
||||||
{
|
{
|
||||||
if (currentUserId == playlist.CreatorUserId) return true;
|
if (currentUserId == playlist.CreatorUserId) return true;
|
||||||
return playlist.RemovePermission switch
|
return playlist.RemovePermission switch
|
||||||
@@ -102,7 +102,9 @@ public class SharedPlaylistService
|
|||||||
EditPermission.Everyone => true,
|
EditPermission.Everyone => true,
|
||||||
EditPermission.AuthorizedOnly => currentUserId.HasValue,
|
EditPermission.AuthorizedOnly => currentUserId.HasValue,
|
||||||
EditPermission.AddedByUserOnly when currentUserId.HasValue =>
|
EditPermission.AddedByUserOnly when currentUserId.HasValue =>
|
||||||
await _trackLogService.IsTrackAddedByUserAsync(playlist.Id, trackId, currentUserId.Value),
|
await _trackLogService.IsTrackAddedByCurrentUserOrSessionAsync(playlist.Id, trackId, currentUserId, sessionId),
|
||||||
|
EditPermission.AddedByUserOnly when !currentUserId.HasValue =>
|
||||||
|
await _trackLogService.IsTrackAddedByCurrentUserOrSessionAsync(playlist.Id, trackId, null, sessionId),
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,7 @@ public class SharedPlaylistService
|
|||||||
.TrimEnd('=');
|
.TrimEnd('=');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<SharedPlaylistEntity>> GetAllByUserAsync(Guid userId)
|
public async Task<List<SharedPlaylist>> GetAllByUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
return await _db.SharedPlaylists
|
return await _db.SharedPlaylists
|
||||||
.Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted)
|
.Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted)
|
||||||
|
|||||||
@@ -13,24 +13,26 @@ public class TrackAdditionLogService
|
|||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogAdditionAsync(Guid sharedPlaylistId, string trackId, Guid addedByUserId)
|
public async Task LogAdditionAsync(Guid sharedPlaylistId, string trackId, Guid? addedByUserId, string sessionId)
|
||||||
{
|
{
|
||||||
var log = new TrackAdditionLogEntity
|
var log = new TrackAdditionLog
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
SharedPlaylistId = sharedPlaylistId,
|
SharedPlaylistId = sharedPlaylistId,
|
||||||
TrackId = trackId,
|
TrackId = trackId,
|
||||||
AddedByUserId = addedByUserId,
|
AddedByUserId = addedByUserId,
|
||||||
AddedAtUtc = DateTime.UtcNow
|
AddedAtUtc = DateTime.UtcNow,
|
||||||
|
SessionId = sessionId
|
||||||
};
|
};
|
||||||
_db.TrackAdditionLogs.Add(log);
|
_db.TrackAdditionLogs.Add(log);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsTrackAddedByUserAsync(Guid sharedPlaylistId, string trackId, Guid userId)
|
public async Task<bool> IsTrackAddedByCurrentUserOrSessionAsync(Guid sharedPlaylistId, string trackId, Guid? userId, string sessionId)
|
||||||
{
|
{
|
||||||
return await _db.TrackAdditionLogs
|
return await _db.TrackAdditionLogs
|
||||||
.AnyAsync(l => l.SharedPlaylistId == sharedPlaylistId && l.TrackId == trackId && l.AddedByUserId == userId);
|
.AnyAsync(l => l.SharedPlaylistId == sharedPlaylistId && l.TrackId == trackId &&
|
||||||
|
(userId != null ? l.AddedByUserId == userId : l.SessionId == sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveLogsForTrackAsync(Guid sharedPlaylistId, string trackId)
|
public async Task RemoveLogsForTrackAsync(Guid sharedPlaylistId, string trackId)
|
||||||
|
|||||||
22
PlaylistShared.Api/Services/TrackRemovalLogService.cs
Normal file
22
PlaylistShared.Api/Services/TrackRemovalLogService.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
|
||||||
|
public class TrackRemovalLogService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
public TrackRemovalLogService(ApplicationDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task LogRemovalAsync(Guid sharedPlaylistId, string trackId, Guid? removedByUserId, string sessionId)
|
||||||
|
{
|
||||||
|
var log = new TrackRemovalLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
SharedPlaylistId = sharedPlaylistId,
|
||||||
|
TrackId = trackId,
|
||||||
|
RemovedByUserId = removedByUserId,
|
||||||
|
RemovedAtUtc = DateTime.UtcNow,
|
||||||
|
SessionId = sessionId
|
||||||
|
};
|
||||||
|
_db.TrackRemovalLogs.Add(log);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
PlaylistShared.Api/Services/UserSessionService.cs
Normal file
47
PlaylistShared.Api/Services/UserSessionService.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using PlaylistShared.Api.Data;
|
||||||
|
using PlaylistShared.Api.Entities;
|
||||||
|
|
||||||
|
public class UserSessionService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
public UserSessionService(ApplicationDbContext db, IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserSession> GetOrCreateCurrentSessionAsync(Guid? associatedUserId = null)
|
||||||
|
{
|
||||||
|
var httpContext = _httpContextAccessor.HttpContext
|
||||||
|
?? throw new InvalidOperationException("No HttpContext available");
|
||||||
|
|
||||||
|
var sessionId = httpContext.Session.Id;
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var session = await _db.UserSessions.FindAsync(sessionId);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
session = new UserSession
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
ClientIpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
|
||||||
|
UserAgent = httpContext.Request.Headers["User-Agent"].ToString(),
|
||||||
|
FirstSeenUtc = now,
|
||||||
|
LastSeenUtc = now,
|
||||||
|
AssociatedUserId = associatedUserId
|
||||||
|
};
|
||||||
|
_db.UserSessions.Add(session);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
session.LastSeenUtc = now;
|
||||||
|
if (session.AssociatedUserId == null && associatedUserId != null)
|
||||||
|
session.AssociatedUserId = associatedUserId;
|
||||||
|
_db.UserSessions.Update(session);
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,15 @@ public class YandexMusicService
|
|||||||
return await playlist.RemoveTracksAsync(tracks.ToArray());
|
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 EncryptToken(string token) => _dataProtector.Protect(token);
|
||||||
|
|
||||||
public string DecryptToken(string encryptedToken)
|
public string DecryptToken(string encryptedToken)
|
||||||
|
|||||||
300
PlaylistShared.Pwa/Components/AudioPlayer.razor
Normal file
300
PlaylistShared.Pwa/Components/AudioPlayer.razor
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
@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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
50
PlaylistShared.Pwa/Components/TrackCoverWithPlay.razor
Normal file
50
PlaylistShared.Pwa/Components/TrackCoverWithPlay.razor
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
@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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
<MudText Typo="Typo.h4" GutterBottom>🎵 PlaylistShared</MudText>
|
<MudText Typo="Typo.h4" GutterBottom>🎵 Playlist share</MudText>
|
||||||
<MudText Typo="Typo.body1">Делитесь плейлистами Яндекс.Музыки с друзьями и управляйте треками вместе!</MudText>
|
<MudText Typo="Typo.body1">Делитесь плейлистами Яндекс.Музыки с друзьями и управляйте треками вместе!</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<MudText Typo="Typo.h6" GutterBottom>1️⃣ Регистрация и вход</MudText>
|
<MudText Typo="Typo.h6" GutterBottom>1️⃣ Регистрация и вход</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">
|
<MudText Typo="Typo.body2" Class="mb-2">
|
||||||
• Нажмите <MudLink Href="/register" Style="font-weight:bold;">«Регистрация»</MudLink> и создайте аккаунт.<br />
|
• Нажмите <MudLink Href="/register" Style="font-weight:bold;">«Регистрация»</MudLink> и создайте аккаунт.<br />
|
||||||
• Или войдите через <MudLink Href="/login" Style="font-weight:bold;">вход</MudLink>, если уже зарегистрированы.
|
• Или <MudLink Href="/login" Style="font-weight:bold;">войдите</MudLink> в систему, если уже зарегистрированы.
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/login"
|
@page "/login"
|
||||||
|
@using PlaylistShared.Shared.Auth
|
||||||
@using PlaylistShared.Shared.DTO
|
@using PlaylistShared.Shared.DTO
|
||||||
@using PlaylistShared.Pwa.Services
|
@using PlaylistShared.Pwa.Services
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/my-playlists"
|
@page "/my-playlists"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using PlaylistShared.Shared.DTO
|
@using PlaylistShared.Shared.DTO
|
||||||
|
@using PlaylistShared.Shared.Playlist
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudTable Items="@FilteredPlaylists" Hover="true" Breakpoint="Breakpoint.Sm">
|
<MudTable Items="@FilteredPlaylists">
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>Название</MudTh>
|
<MudTh>Название</MudTh>
|
||||||
<MudTh>Треков</MudTh>
|
<MudTh>Треков</MudTh>
|
||||||
@@ -36,9 +37,9 @@
|
|||||||
<MudTh></MudTh>
|
<MudTh></MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd><MudText>@context.Title</MudText></MudTd>
|
<MudTd DataLabel="Название"><MudText>@context.Title</MudText></MudTd>
|
||||||
<MudTd>@context.TrackCount</MudTd>
|
<MudTd DataLabel="Треков">@context.TrackCount</MudTd>
|
||||||
<MudTd>
|
<MudTd DataLabel="Статус">
|
||||||
<!-- Явно указываем T="string" для MudChip -->
|
<!-- Явно указываем T="string" для MudChip -->
|
||||||
@if (context.IsShared)
|
@if (context.IsShared)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/register"
|
@page "/register"
|
||||||
|
@using PlaylistShared.Shared.Auth
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@inject AuthStateProvider AuthProvider
|
@inject AuthStateProvider AuthProvider
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@using PlaylistShared.Shared.DTO
|
@using PlaylistShared.Shared.DTO
|
||||||
@using PlaylistShared.Shared.Enums
|
@using PlaylistShared.Shared.Enums
|
||||||
@using PlaylistShared.Pwa.Services
|
@using PlaylistShared.Pwa.Services
|
||||||
@using PlaylistShared.Shared.Models
|
@using PlaylistShared.Shared.Shared
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@@ -112,10 +112,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Список треков -->
|
<!-- Список треков -->
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||||
<MudText Typo="Typo.h6" GutterBottom>Треки</MudText>
|
<MudText Typo="Typo.h6">Треки</MudText>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadTracks" Disabled="_tracksLoading" Size="Size.Small" />
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadTracks" Disabled="_tracksLoading" Size="Size.Medium" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (_tracksLoading)
|
@if (_tracksLoading)
|
||||||
{
|
{
|
||||||
<MudProgressCircular Indeterminate />
|
<MudProgressCircular Indeterminate />
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudTable Items="@_tracks" Hover="true" Breakpoint="Breakpoint.Sm">
|
<MudTable Items="@_tracks">
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>#</MudTh>
|
<MudTh>#</MudTh>
|
||||||
<MudTh>Обложка</MudTh>
|
<MudTh>Обложка</MudTh>
|
||||||
@@ -139,27 +140,28 @@
|
|||||||
}
|
}
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.Index</MudTd>
|
<MudTd DataLabel="#" Style="font-weight: normal;">@context.Index</MudTd>
|
||||||
<MudTd>
|
<MudTd DataLabel="Обложка">
|
||||||
@if (!string.IsNullOrEmpty(context.CoverUri))
|
@if (!string.IsNullOrEmpty(context.CoverUri))
|
||||||
{
|
{
|
||||||
<MudImage Src="@FormatCoverUrl(context.CoverUri, "50x50")" Height="50" Width="50" Class="rounded" />
|
<TrackCoverWithPlay CoverUrl="@context.CoverUri"
|
||||||
|
TrackId="@context.Id"
|
||||||
|
Width="50" Height="50"
|
||||||
|
IsPlaying="@(_currentTrackId == context.Id && _isPlaying)"
|
||||||
|
OnPlay="PlayTrack" />
|
||||||
}
|
}
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd>
|
<MudTd DataLabel="Название">
|
||||||
<MudLink Href="@($"https://music.yandex.ru/track/{context.Id}")"
|
<MudLink Href="@($"https://music.yandex.ru/track/{context.Id}")" Target="_blank" Underline="Underline.Hover">
|
||||||
Target="_blank"
|
|
||||||
Underline="Underline.Hover"
|
|
||||||
Style="cursor: pointer; display: inline-flex; align-items: center;">
|
|
||||||
@context.Title
|
@context.Title
|
||||||
<MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" Class="ml-1" />
|
<MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" Class="ml-1" />
|
||||||
</MudLink>
|
</MudLink>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd>@string.Join(", ", context.Artists)</MudTd>
|
<MudTd DataLabel="Исполнитель">@string.Join(", ", context.Artists)</MudTd>
|
||||||
<MudTd>@FormatDuration(context.DurationMs)</MudTd>
|
<MudTd DataLabel="Длительность">@FormatDuration(context.DurationMs)</MudTd>
|
||||||
@if (_canRemove)
|
@if (_canRemove)
|
||||||
{
|
{
|
||||||
<MudTd>
|
<MudTd DataLabel="">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => RemoveTrack(context)" />
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => RemoveTrack(context)" />
|
||||||
</MudTd>
|
</MudTd>
|
||||||
}
|
}
|
||||||
@@ -169,11 +171,22 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Фиксированный плеер внизу -->
|
||||||
|
<div class="fixed-player" style="display: @(_isPlayerVisible ? "block" : "none");">
|
||||||
|
<AudioPlayer @ref="_audioPlayer" OnTrackEnded="OnTrackEnded" />
|
||||||
|
</div>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public string Token { get; set; }
|
[Parameter] public string Token { get; set; }
|
||||||
|
|
||||||
|
private AudioPlayer? _audioPlayer;
|
||||||
|
private string? _currentTrackId { get; set; }
|
||||||
|
private bool _isPlaying = false;
|
||||||
|
private bool _isPlayerVisible = false;
|
||||||
|
|
||||||
private SharedPlaylistDto? _playlist;
|
private SharedPlaylistDto? _playlist;
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private bool _isAuthenticated;
|
private bool _isAuthenticated;
|
||||||
@@ -215,7 +228,7 @@
|
|||||||
_canRemove = _isCreator
|
_canRemove = _isCreator
|
||||||
|| _playlist.RemovePermission == EditPermission.Everyone
|
|| _playlist.RemovePermission == EditPermission.Everyone
|
||||||
|| (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated);
|
|| (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated);
|
||||||
|
|
||||||
if (_isCreator && _isAuthenticated)
|
if (_isCreator && _isAuthenticated)
|
||||||
{
|
{
|
||||||
_editPermissions = new UpdatePermissionsDto
|
_editPermissions = new UpdatePermissionsDto
|
||||||
@@ -402,4 +415,38 @@
|
|||||||
{
|
{
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PlayTrack(string trackId)
|
||||||
|
{
|
||||||
|
if (_audioPlayer == null) return;
|
||||||
|
|
||||||
|
if (_currentTrackId == trackId && _isPlaying)
|
||||||
|
{
|
||||||
|
await _audioPlayer.PauseAsync();
|
||||||
|
_isPlaying = false;
|
||||||
|
}
|
||||||
|
else if (_currentTrackId == trackId && !_isPlaying)
|
||||||
|
{
|
||||||
|
await _audioPlayer.PlayAsync();
|
||||||
|
_isPlaying = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_currentTrackId) && _isPlaying)
|
||||||
|
await _audioPlayer.StopAsync();
|
||||||
|
|
||||||
|
_currentTrackId = trackId;
|
||||||
|
await _audioPlayer.LoadAndPlayAsync(trackId);
|
||||||
|
_isPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isPlayerVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTrackEnded()
|
||||||
|
{
|
||||||
|
_currentTrackId = null;
|
||||||
|
_isPlaying = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,10 @@
|
|||||||
<ServiceWorker Include="wwwroot\\service-worker.js" PublishedContent="wwwroot\\service-worker.published.js" />
|
<ServiceWorker Include="wwwroot\\service-worker.js" PublishedContent="wwwroot\\service-worker.published.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="wwwroot\js\AudioPlayer.js">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ internal class Program
|
|||||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AuthStateProvider>());
|
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AuthStateProvider>());
|
||||||
builder.Services.AddScoped<ApiClient>();
|
builder.Services.AddScoped<ApiClient>();
|
||||||
|
|
||||||
/*
|
|
||||||
builder.Services.AddOidcAuthentication(options =>
|
|
||||||
{
|
|
||||||
// Configure your authentication provider options here.
|
|
||||||
// For more information, see https://aka.ms/blazor-standalone-auth
|
|
||||||
builder.Configuration.Bind("Local", options.ProviderOptions);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using PlaylistShared.Shared.DTO;
|
using PlaylistShared.Shared;
|
||||||
|
using PlaylistShared.Shared.Auth;
|
||||||
|
using PlaylistShared.Shared.DTO;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
namespace PlaylistShared.Pwa.Services;
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|||||||
@@ -12,4 +12,5 @@
|
|||||||
@using PlaylistShared.Pwa.Services
|
@using PlaylistShared.Pwa.Services
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using PlaylistShared.Shared.DTO
|
@using PlaylistShared.Shared
|
||||||
|
@using PlaylistShared.Pwa.Components
|
||||||
@@ -108,3 +108,42 @@ code {
|
|||||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-cover-container {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-cover-container:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; /* если плеер выше на мобильных */
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 172 KiB |
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PlaylistShared.Pwa</title>
|
<title>Playlist Share</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="preload" id="webassembly" />
|
<link rel="preload" id="webassembly" />
|
||||||
<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" />
|
||||||
@@ -16,6 +16,74 @@
|
|||||||
<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" />
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
background-color: #1a1a27 !important;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кастомный спиннер в стиле MudBlazor (тёмная тема) */
|
||||||
|
.loading-progress {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress circle {
|
||||||
|
fill: none;
|
||||||
|
stroke: #2a2833;
|
||||||
|
stroke-width: 4;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
animation: spin 1.5s linear 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 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 126;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dashoffset: 63;
|
||||||
|
transform: rotate(135deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 126;
|
||||||
|
transform: rotate(450deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #b2b0bf;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Убираем белые вспышки */
|
||||||
|
#app {
|
||||||
|
background-color: #1a1a27;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -28,8 +96,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
An unhandled error has occurred.
|
Произошла необработанная ошибка.
|
||||||
<a href="." class="reload">Reload</a>
|
<a href="." class="reload">Перезагрузить</a>
|
||||||
<span class="dismiss">🗙</span>
|
<span class="dismiss">🗙</span>
|
||||||
</div>
|
</div>
|
||||||
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||||
|
|||||||
54
PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js
Normal file
54
PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export function init(audioId, dotNetHelper) {
|
||||||
|
const audio = document.getElementById(audioId);
|
||||||
|
if (!audio) throw new Error(`Audio element with id ${audioId} not found`);
|
||||||
|
|
||||||
|
let durationReady = false;
|
||||||
|
let durationValue = 0;
|
||||||
|
|
||||||
|
const toNumber = (val) => {
|
||||||
|
const num = Number(val);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAndPlay = (src, token) => {
|
||||||
|
const url = new URL(src, window.location.href);
|
||||||
|
if (token) url.searchParams.set('access_token', token);
|
||||||
|
audio.src = url.toString();
|
||||||
|
audio.load();
|
||||||
|
durationReady = false;
|
||||||
|
durationValue = 0;
|
||||||
|
audio.play().catch(e => console.error('Play failed:', e));
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = () => audio.play();
|
||||||
|
const pause = () => audio.pause();
|
||||||
|
const stop = () => { audio.pause(); audio.currentTime = 0; };
|
||||||
|
const setVolume = (volume) => { audio.volume = toNumber(volume); };
|
||||||
|
const setCurrentTime = (time) => { audio.currentTime = toNumber(time); };
|
||||||
|
const getDuration = () => durationReady ? durationValue : 0;
|
||||||
|
const getCurrentTime = () => toNumber(audio.currentTime);
|
||||||
|
|
||||||
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
durationValue = toNumber(audio.duration);
|
||||||
|
durationReady = durationValue > 0;
|
||||||
|
if (dotNetHelper && durationReady) {
|
||||||
|
dotNetHelper.invokeMethodAsync('OnDurationReady', durationValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('timeupdate', () => {
|
||||||
|
if (dotNetHelper && durationReady) {
|
||||||
|
const current = toNumber(audio.currentTime);
|
||||||
|
dotNetHelper.invokeMethodAsync('OnTimeUpdate', current, durationValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('ended', () => {
|
||||||
|
if (dotNetHelper) {
|
||||||
|
dotNetHelper.invokeMethodAsync('OnAudioEnded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Возвращаем все методы, которые будут вызываться из C#
|
||||||
|
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime, getDuration, getCurrentTime };
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "PlaylistShared.Pwa",
|
"name": "Playlist Share",
|
||||||
"short_name": "PlaylistShared.Pwa",
|
"short_name": "PlaylistShare",
|
||||||
"id": "./",
|
"id": "./",
|
||||||
"start_url": "./",
|
"start_url": "./",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared;
|
||||||
|
|
||||||
/// <summary>Универсальный контейнер ответа API.</summary>
|
/// <summary>Универсальный контейнер ответа API.</summary>
|
||||||
/// <typeparam name="T">Тип данных ответа.</typeparam>
|
/// <typeparam name="T">Тип данных ответа.</typeparam>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.Models;
|
namespace PlaylistShared.Shared.Auth;
|
||||||
|
|
||||||
/// <summary>DTO пользователя (без конфиденциальных данных).</summary>
|
/// <summary>DTO пользователя (без конфиденциальных данных).</summary>
|
||||||
public class ApplicationUserDto
|
public class ApplicationUserDto
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Auth;
|
||||||
|
|
||||||
/// <summary>Запрос на вход по паролю.</summary>
|
/// <summary>Запрос на вход по паролю.</summary>
|
||||||
public class LoginRequest
|
public class LoginRequest
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Auth;
|
||||||
|
|
||||||
/// <summary>Ответ после успешного входа.</summary>
|
/// <summary>Ответ после успешного входа.</summary>
|
||||||
public class LoginResponse
|
public class LoginResponse
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Auth;
|
||||||
|
|
||||||
/// <summary>Запрос на регистрацию нового пользователя.</summary>
|
/// <summary>Запрос на регистрацию нового пользователя.</summary>
|
||||||
public class RegisterRequest
|
public class RegisterRequest
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared;
|
||||||
|
|
||||||
/// <summary>Стандартный ответ сервера при ошибке.</summary>
|
/// <summary>Стандартный ответ сервера при ошибке.</summary>
|
||||||
public class ErrorResponse
|
public class ErrorResponse
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Playlist;
|
||||||
|
|
||||||
/// <summary>Запрос на создание нового шеринг-плейлиста.</summary>
|
/// <summary>Запрос на создание нового шеринг-плейлиста.</summary>
|
||||||
public class SharePlaylistDto
|
public class SharePlaylistDto
|
||||||
7
PlaylistShared.Shared/Playlist/SharePlaylistRequest.cs
Normal file
7
PlaylistShared.Shared/Playlist/SharePlaylistRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PlaylistShared.Shared.Playlist;
|
||||||
|
|
||||||
|
public class SharePlaylistRequest
|
||||||
|
{
|
||||||
|
public string Kind { get; set; }
|
||||||
|
public string OwnerUid { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Playlist;
|
||||||
|
|
||||||
/// <summary>Информация о плейлисте из Яндекс.Музыки (для импорта).</summary>
|
/// <summary>Информация о плейлисте из Яндекс.Музыки (для импорта).</summary>
|
||||||
public class YandexPlaylistInfo
|
public class YandexPlaylistInfo
|
||||||
@@ -37,9 +37,3 @@ public class YandexPlaylistInfo
|
|||||||
[JsonPropertyName("shareToken")]
|
[JsonPropertyName("shareToken")]
|
||||||
public string? ShareToken { get; set; }
|
public string? ShareToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SharePlaylistRequest
|
|
||||||
{
|
|
||||||
public string Kind { get; set; }
|
|
||||||
public string OwnerUid { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Profile;
|
||||||
|
|
||||||
public class SetYandexTokenRequest
|
public class SetYandexTokenRequest
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Profile;
|
||||||
|
|
||||||
public class YandexTokenStatus
|
public class YandexTokenStatus
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
public class AddTrackByLinkRequest
|
public class AddTrackByLinkRequest
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
public class AddTracksRequest
|
public class AddTracksRequest
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Auth;
|
||||||
|
using PlaylistShared.Shared.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.Models;
|
namespace PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
/// <summary>DTO шеринг-плейлиста (без навигационных свойств).</summary>
|
/// <summary>DTO шеринг-плейлиста (без навигационных свойств).</summary>
|
||||||
public class SharedPlaylistDto
|
public class SharedPlaylistDto
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using PlaylistShared.Shared.Enums;
|
using PlaylistShared.Shared.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace PlaylistShared.Shared.DTO;
|
namespace PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
/// <summary>Запрос на обновление прав доступа шеринг-плейлиста.</summary>
|
/// <summary>Запрос на обновление прав доступа шеринг-плейлиста.</summary>
|
||||||
public class UpdatePermissionsDto
|
public class UpdatePermissionsDto
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace PlaylistShared.Shared.DTO;
|
using PlaylistShared.Shared.DTO;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Shared.Shared;
|
||||||
|
|
||||||
public class YandexPlaylistData
|
public class YandexPlaylistData
|
||||||
{
|
{
|
||||||
Reference in New Issue
Block a user