Добавлены избранные плейлисты

This commit is contained in:
FrigaT
2026-04-14 14:14:19 +03:00
parent 8230951839
commit dcb2efbedb
17 changed files with 1118 additions and 5 deletions

View File

@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using PlaylistShared.Api.Entities;
using PlaylistShared.Api.Extensions;
using PlaylistShared.Api.Services;
using PlaylistShared.Shared;
using PlaylistShared.Shared.Shared;
namespace PlaylistShared.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class FavoritesController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly FavoritesService _favoritesService;
private readonly SharedPlaylistService _sharedPlaylistService;
public FavoritesController(
UserManager<ApplicationUser> userManager,
FavoritesService favoritesService,
SharedPlaylistService sharedPlaylistService)
{
_userManager = userManager;
_favoritesService = favoritesService;
_sharedPlaylistService = sharedPlaylistService;
}
/// <summary>Получить список избранных плейлистов текущего пользователя.</summary>
[HttpGet]
public async Task<ActionResult<ApiResponse<List<SharedPlaylistDto>>>> GetFavorites()
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var favorites = await _favoritesService.GetUserFavoritesAsync(userId);
return Ok(ApiResponse<List<SharedPlaylistDto>>.Ok(favorites));
}
/// <summary>Проверить, добавлен ли плейлист в избранное.</summary>
[HttpGet("{shareToken}/check")]
public async Task<ActionResult<ApiResponse<bool>>> CheckFavorite(string shareToken)
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var isFavorite = await _favoritesService.IsFavoriteAsync(userId, shareToken);
return Ok(ApiResponse<bool>.Ok(isFavorite));
}
/// <summary>Добавить плейлист в избранное.</summary>
[HttpPost("{shareToken}")]
public async Task<ActionResult<ApiResponse<object>>> AddFavorite(string shareToken)
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
if (playlist == null)
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
await _favoritesService.AddFavoriteAsync(userId, shareToken);
return Ok(ApiResponse<object>.Ok(new { message = "Плейлист добавлен в избранное" }));
}
/// <summary>Удалить плейлист из избранного.</summary>
[HttpDelete("{shareToken}")]
public async Task<ActionResult<ApiResponse<object>>> RemoveFavorite(string shareToken)
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
await _favoritesService.RemoveFavoriteAsync(userId, shareToken);
return Ok(ApiResponse<object>.Ok(new { message = "Плейлист удалён из избранного" }));
}
}

View File

@@ -82,6 +82,7 @@ public class PlaylistsController : ControllerBase
var dto = new SharePlaylistDto
{
YandexPlaylistUuid = playlist.PlaylistUuid,
YandexPlaylistKind = request.Kind,
YandexPlaylistOwnerUid = request.OwnerUid,
Title = playlist.Title,

View File

@@ -11,8 +11,9 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>();
public DbSet<UserSession> UserSessions => Set<UserSession>();
public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>();
public DbSet<UserSession> UserSessions => Set<UserSession>();
public DbSet<FavoritePlaylist> FavoritePlaylists => Set<FavoritePlaylist>();
protected override void OnModelCreating(ModelBuilder builder)
{
@@ -26,6 +27,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
.WithMany(u => u.OwnedPlaylists)
.HasForeignKey(e => e.CreatorUserId)
.OnDelete(DeleteBehavior.Restrict);
entity.Property(e => e.YandexPlaylistUuid).IsRequired().HasMaxLength(50);
entity.Property(e => e.YandexPlaylistKind).IsRequired().HasMaxLength(50);
entity.Property(e => e.YandexPlaylistOwnerUid).IsRequired().HasMaxLength(50);
entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
@@ -77,5 +79,19 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
.HasForeignKey(e => e.SessionId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<FavoritePlaylist>(entity =>
{
entity.HasKey(e => new { e.UserId, e.SharedPlaylistId });
entity.HasOne(e => e.User)
.WithMany(u => u.FavoritePlaylists)
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.SharedPlaylist)
.WithMany()
.HasForeignKey(e => e.SharedPlaylistId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(e => e.AddedAtUtc).IsRequired();
});
}
}

View File

@@ -0,0 +1,591 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PlaylistShared.Api.Data;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260414111229_AddUserFavorites")]
partial class AddUserFavorites
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("RefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("RefreshTokenExpiryUtc")
.HasColumnType("datetime2");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("YandexAccessToken")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexId")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexRefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("YandexTokenExpiryUtc")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.HasKey("UserId", "SharedPlaylistId");
b.HasIndex("SharedPlaylistId");
b.ToTable("FavoritePlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AddPermission")
.HasColumnType("int");
b.Property<string>("CoverUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid>("CreatorUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<int>("PlayPermission")
.HasColumnType("int");
b.Property<int>("RemovePermission")
.HasColumnType("int");
b.Property<string>("ShareToken")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<int>("ViewPermission")
.HasColumnType("int");
b.Property<string>("YandexPlaylistKind")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("YandexPlaylistOwnerUid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("YandexPlaylistUuid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("CreatorUserId");
b.HasIndex("ShareToken")
.IsUnique();
b.ToTable("SharedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("AddedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("AddedByUserId");
b.HasIndex("SessionId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackAdditionLogs");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.Property<string>("SessionId")
.HasMaxLength(449)
.HasColumnType("nvarchar(449)");
b.Property<Guid?>("AssociatedUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("ClientIpAddress")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("FirstSeenUtc")
.HasColumnType("datetime2");
b.Property<DateTime>("LastSeenUtc")
.HasColumnType("datetime2");
b.Property<string>("UserAgent")
.HasColumnType("nvarchar(max)");
b.HasKey("SessionId");
b.HasIndex("AssociatedUserId");
b.ToTable("UserSessions");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("RemovedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("RemovedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RemovedByUserId");
b.HasIndex("SessionId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackRemovalLogs");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
{
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany()
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
.WithMany("FavoritePlaylists")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SharedPlaylist");
b.Navigation("User");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
.WithMany("OwnedPlaylists")
.HasForeignKey("CreatorUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
.WithMany()
.HasForeignKey("AddedByUserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AddedByUser");
b.Navigation("Session");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
.WithMany()
.HasForeignKey("AssociatedUserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
.WithMany()
.HasForeignKey("RemovedByUserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
.WithMany("TrackRemovalLogs")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany()
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RemovedByUser");
b.Navigation("Session");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Navigation("FavoritePlaylists");
b.Navigation("OwnedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.Navigation("TrackAdditionLogs");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.Navigation("TrackAdditionLogs");
b.Navigation("TrackRemovalLogs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddUserFavorites : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "YandexPlaylistUuid",
table: "SharedPlaylists",
type: "nvarchar(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
migrationBuilder.CreateTable(
name: "FavoritePlaylists",
columns: table => new
{
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SharedPlaylistId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
AddedAtUtc = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FavoritePlaylists", x => new { x.UserId, x.SharedPlaylistId });
table.ForeignKey(
name: "FK_FavoritePlaylists_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_FavoritePlaylists_SharedPlaylists_SharedPlaylistId",
column: x => x.SharedPlaylistId,
principalTable: "SharedPlaylists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_FavoritePlaylists_SharedPlaylistId",
table: "FavoritePlaylists",
column: "SharedPlaylistId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "FavoritePlaylists");
migrationBuilder.DropColumn(
name: "YandexPlaylistUuid",
table: "SharedPlaylists");
}
}
}

View File

@@ -237,6 +237,24 @@ namespace PlaylistShared.Api.Data.Migrations
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.HasKey("UserId", "SharedPlaylistId");
b.HasIndex("SharedPlaylistId");
b.ToTable("FavoritePlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.Property<Guid>("Id")
@@ -292,6 +310,11 @@ namespace PlaylistShared.Api.Data.Migrations
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("YandexPlaylistUuid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("CreatorUserId");
@@ -449,6 +472,25 @@ namespace PlaylistShared.Api.Data.Migrations
.IsRequired();
});
modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b =>
{
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany()
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
.WithMany("FavoritePlaylists")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SharedPlaylist");
b.Navigation("User");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
@@ -524,6 +566,8 @@ namespace PlaylistShared.Api.Data.Migrations
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Navigation("FavoritePlaylists");
b.Navigation("OwnedPlaylists");
});

View File

@@ -25,4 +25,7 @@ public class ApplicationUser : IdentityUser<Guid>
/// <summary>Плейлисты, созданные пользователем.</summary>
public ICollection<SharedPlaylist> OwnedPlaylists { get; set; } = new List<SharedPlaylist>();
/// <summary>Избранные плейлисты.</summary>
public ICollection<FavoritePlaylist> FavoritePlaylists { get; set; } = new List<FavoritePlaylist>();
}

View File

@@ -0,0 +1,13 @@
namespace PlaylistShared.Api.Entities;
/// <summary>Избранный расшаренный плейлист пользователя.</summary>
public class FavoritePlaylist
{
public Guid UserId { get; set; }
public Guid SharedPlaylistId { get; set; }
public DateTime AddedAtUtc { get; set; }
// Навигационные свойства
public ApplicationUser User { get; set; } = null!;
public SharedPlaylist SharedPlaylist { get; set; } = null!;
}

View File

@@ -7,6 +7,7 @@ public class SharedPlaylist
{
public Guid Id { get; set; }
public Guid CreatorUserId { get; set; }
public string YandexPlaylistUuid { get; set; } = null!;
public string YandexPlaylistKind { get; set; } = null!;
public string YandexPlaylistOwnerUid { get; set; } = null!;
public string Title { get; set; } = null!;

View File

@@ -96,6 +96,7 @@ public class Program
builder.Services.AddScoped<SharedPlaylistService>();
builder.Services.AddScoped<TrackAdditionLogService>();
builder.Services.AddScoped<TrackRemovalLogService>();
builder.Services.AddScoped<FavoritesService>();
builder.Services.AddDataProtection();
builder.Services.AddHttpClient();

View File

@@ -0,0 +1,100 @@
using Microsoft.EntityFrameworkCore;
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities;
using PlaylistShared.Shared.Shared;
namespace PlaylistShared.Api.Services;
public class FavoritesService
{
private readonly ApplicationDbContext _db;
private readonly SharedPlaylistService _sharedPlaylistService;
public FavoritesService(ApplicationDbContext db, SharedPlaylistService sharedPlaylistService)
{
_db = db;
_sharedPlaylistService = sharedPlaylistService;
}
public async Task<bool> IsFavoriteAsync(Guid userId, string shareToken)
{
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
if (playlist == null) return false;
return await _db.FavoritePlaylists
.AnyAsync(f => f.UserId == userId && f.SharedPlaylistId == playlist.Id);
}
public async Task AddFavoriteAsync(Guid userId, string shareToken)
{
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
if (playlist == null)
throw new ArgumentException("Playlist not found");
var exists = await _db.FavoritePlaylists
.AnyAsync(f => f.UserId == userId && f.SharedPlaylistId == playlist.Id);
if (exists) return;
var favorite = new FavoritePlaylist
{
UserId = userId,
SharedPlaylistId = playlist.Id,
AddedAtUtc = DateTime.UtcNow
};
_db.FavoritePlaylists.Add(favorite);
await _db.SaveChangesAsync();
}
public async Task RemoveFavoriteAsync(Guid userId, string shareToken)
{
var playlist = await _sharedPlaylistService.GetEntityByTokenAsync(shareToken);
if (playlist == null) return;
var favorite = await _db.FavoritePlaylists
.FirstOrDefaultAsync(f => f.UserId == userId && f.SharedPlaylistId == playlist.Id);
if (favorite != null)
{
_db.FavoritePlaylists.Remove(favorite);
await _db.SaveChangesAsync();
}
}
public async Task<List<SharedPlaylistDto>> GetUserFavoritesAsync(Guid userId)
{
var favoritePlaylists = await _db.FavoritePlaylists
.Include(f => f.SharedPlaylist)
.ThenInclude(sp => sp.Creator)
.Where(f => f.UserId == userId)
.OrderByDescending(f => f.AddedAtUtc)
.Select(f => f.SharedPlaylist)
.ToListAsync();
// Маппинг в DTO (можно использовать AutoMapper, но для простоты сделаем вручную)
return favoritePlaylists.Select(sp => new SharedPlaylistDto
{
Id = sp.Id,
CreatorUserId = sp.CreatorUserId,
YandexPlaylistUuid = sp.YandexPlaylistUuid,
YandexPlaylistKind = sp.YandexPlaylistKind,
YandexPlaylistOwnerUid = sp.YandexPlaylistOwnerUid,
Title = sp.Title,
Description = sp.Description,
CoverUrl = sp.CoverUrl,
CreatedAt = sp.CreatedAt,
UpdatedAt = sp.UpdatedAt,
IsDeleted = sp.IsDeleted,
ShareToken = sp.ShareToken,
ViewPermission = sp.ViewPermission,
PlayPermission = sp.PlayPermission,
AddPermission = sp.AddPermission,
RemovePermission = sp.RemovePermission,
Creator = sp.Creator != null ? new Shared.Auth.ApplicationUserDto
{
Id = sp.Creator.Id,
UserName = sp.Creator.UserName,
Email = sp.Creator.Email,
YandexId = sp.Creator.YandexId,
DisplayName = sp.Creator.UserName
} : null
}).ToList();
}
}

View File

@@ -27,6 +27,7 @@ public class SharedPlaylistService
{
Id = Guid.NewGuid(),
CreatorUserId = creatorUserId,
YandexPlaylistUuid = dto.YandexPlaylistUuid,
YandexPlaylistKind = dto.YandexPlaylistKind,
YandexPlaylistOwnerUid = dto.YandexPlaylistOwnerUid,
Title = dto.Title,