Добавлены права на воспроизведение.

This commit is contained in:
FrigaT
2026-04-14 13:11:34 +03:00
parent 4b3036364b
commit 164cf455fd
16 changed files with 724 additions and 136 deletions

View File

@@ -13,15 +13,18 @@ public class AudioController : ControllerBase
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly YandexMusicService _yandexService; private readonly YandexMusicService _yandexService;
private readonly SharedPlaylistService _sharedService;
private readonly JwtService _jwtService; private readonly JwtService _jwtService;
public AudioController( public AudioController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
YandexMusicService yandexService, YandexMusicService yandexService,
SharedPlaylistService sharedService,
JwtService jwtService) JwtService jwtService)
{ {
_userManager = userManager; _userManager = userManager;
_yandexService = yandexService; _yandexService = yandexService;
_sharedService = sharedService;
_jwtService = jwtService; _jwtService = jwtService;
} }
@@ -29,17 +32,18 @@ public class AudioController : ControllerBase
/// Потоковое воспроизведение трека из Яндекс.Музыки. /// Потоковое воспроизведение трека из Яндекс.Музыки.
/// </summary> /// </summary>
/// <param name="trackId">ID трека (например, "21696942").</param> /// <param name="trackId">ID трека (например, "21696942").</param>
/// <param name="access_token">gwt пользователя</param>
/// <param name="shared_id">ID расшаренного плейлиста</param>
[HttpGet("track/{trackId}")] [HttpGet("track/{trackId}")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? access_token = null) public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? access_token = null, [FromQuery] string? shared_id = null)
{ {
var user = await GetUserFromToken(access_token); var user = await GetUserFromToken(access_token);
if (user == null) if (user == null) user = await GetUserFromSharedPlaylistId(shared_id);
return Unauthorized(); if (user == null) return Unauthorized();
var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId); var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId);
if (string.IsNullOrEmpty(streamUrl)) if (string.IsNullOrEmpty(streamUrl)) return NotFound();
return NotFound();
var httpClient = new HttpClient(); var httpClient = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, streamUrl); var request = new HttpRequestMessage(HttpMethod.Get, streamUrl);
@@ -56,11 +60,11 @@ public class AudioController : ControllerBase
Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
if (response.Content.Headers.Contains("Content-Range")) if (response.Content.Headers.Contains("Content-Range"))
Response.Headers.Add("Content-Range", response.Content.Headers.ContentRange?.ToString()); Response.Headers.Append("Content-Range", response.Content.Headers.ContentRange?.ToString());
if (response.Headers.Contains("Accept-Ranges")) if (response.Headers.Contains("Accept-Ranges"))
Response.Headers.Add("Accept-Ranges", response.Headers.AcceptRanges?.ToString()); Response.Headers.Append("Accept-Ranges", response.Headers.AcceptRanges?.ToString());
if (response.Content.Headers.Contains("Content-Length")) if (response.Content.Headers.Contains("Content-Length"))
Response.Headers.Add("Content-Length", response.Content.Headers.ContentLength?.ToString()); Response.Headers.Append("Content-Length", response.Content.Headers.ContentLength?.ToString());
await response.Content.CopyToAsync(Response.Body); await response.Content.CopyToAsync(Response.Body);
return new EmptyResult(); return new EmptyResult();
@@ -68,17 +72,28 @@ public class AudioController : ControllerBase
private async Task<ApplicationUser?> GetUserFromToken(string? token) private async Task<ApplicationUser?> GetUserFromToken(string? token)
{ {
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token)) return null;
return null;
var principal = _jwtService.ValidateToken(token); var principal = _jwtService.ValidateToken(token);
if (principal == null) if (principal == null) return null;
return null;
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) if (string.IsNullOrEmpty(userId)) return null;
return null;
return await _userManager.FindByIdAsync(userId); return await _userManager.FindByIdAsync(userId);
} }
private async Task<ApplicationUser?> GetUserFromSharedPlaylistId(string? sharedId)
{
if (string.IsNullOrEmpty(sharedId)) return null;
var playlist = await _sharedService.GetEntityByTokenAsync(sharedId);
if (playlist == null) return null;
if (!await _sharedService.CanPlayEveryoneAsync(playlist)) return null;
return await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString());
}
} }

View File

@@ -87,8 +87,9 @@ public class PlaylistsController : ControllerBase
Title = playlist.Title, Title = playlist.Title,
Description = playlist.Description, Description = playlist.Description,
ViewPermission = ViewPermission.Everyone, ViewPermission = ViewPermission.Everyone,
PlayPermission = ViewPermission.Everyone,
AddPermission = EditPermission.AuthorizedOnly, AddPermission = EditPermission.AuthorizedOnly,
RemovePermission = EditPermission.AddedByUserOnly RemovePermission = EditPermission.AddedByUserOnly,
}; };
var result = await _sharedService.CreateAsync(userId, dto); var result = await _sharedService.CreateAsync(userId, dto);

View File

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

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddSharedPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PlayPermission",
table: "SharedPlaylists",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PlayPermission",
table: "SharedPlaylists");
}
}
}

View File

@@ -261,6 +261,9 @@ namespace PlaylistShared.Api.Data.Migrations
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.HasColumnType("bit"); .HasColumnType("bit");
b.Property<int>("PlayPermission")
.HasColumnType("int");
b.Property<int>("RemovePermission") b.Property<int>("RemovePermission")
.HasColumnType("int"); .HasColumnType("int");

View File

@@ -17,6 +17,7 @@ public class SharedPlaylist
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public string ShareToken { get; set; } = null!; public string ShareToken { get; set; } = null!;
public ViewPermission ViewPermission { get; set; } public ViewPermission ViewPermission { get; set; }
public ViewPermission PlayPermission { get; set; }
public EditPermission AddPermission { get; set; } public EditPermission AddPermission { get; set; }
public EditPermission RemovePermission { get; set; } public EditPermission RemovePermission { get; set; }

View File

@@ -23,7 +23,8 @@ public class Program
// DbContext // DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options => builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));// Identity options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options => builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
{ {
options.User.RequireUniqueEmail = true; options.User.RequireUniqueEmail = true;
@@ -101,17 +102,9 @@ public class Program
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("Development", policy =>
{
policy.WithOrigins("http://localhost:5053", "https://localhost:7225", "http://localhost:5181", "https://api.playlistshare.frigat.duckdns.org", "https://playlistshare.frigat.duckdns.org")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
options.AddPolicy("Production", policy => options.AddPolicy("Production", policy =>
{ {
policy.WithOrigins("https://api.playlistshare.frigat.duckdns.org", "https://playlistshare.frigat.duckdns.org") policy.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>())
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
.AllowCredentials(); .AllowCredentials();
@@ -131,17 +124,14 @@ public class Program
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();
if (app.Environment.IsDevelopment())
{
app.UseCors("Development");
}
else
{
app.UseHttpsRedirection();
app.UseCors("Production"); app.UseCors("Production");
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
} }
app.UseSession(); app.UseSession();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@@ -34,6 +34,7 @@ public class SharedPlaylistService
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow,
ShareToken = GenerateToken(), ShareToken = GenerateToken(),
PlayPermission = dto.PlayPermission,
ViewPermission = dto.ViewPermission, ViewPermission = dto.ViewPermission,
AddPermission = dto.AddPermission, AddPermission = dto.AddPermission,
RemovePermission = dto.RemovePermission RemovePermission = dto.RemovePermission
@@ -63,6 +64,7 @@ public class SharedPlaylistService
var entity = await _db.SharedPlaylists.FindAsync(playlistId); var entity = await _db.SharedPlaylists.FindAsync(playlistId);
if (entity == null) return null; if (entity == null) return null;
entity.ViewPermission = dto.ViewPermission; entity.ViewPermission = dto.ViewPermission;
entity.PlayPermission = dto.PlayPermission;
entity.AddPermission = dto.AddPermission; entity.AddPermission = dto.AddPermission;
entity.RemovePermission = dto.RemovePermission; entity.RemovePermission = dto.RemovePermission;
entity.UpdatedAt = DateTime.UtcNow; entity.UpdatedAt = DateTime.UtcNow;
@@ -80,6 +82,18 @@ public class SharedPlaylistService
return true; return true;
} }
public async Task<bool> CanPlayAsync(SharedPlaylist playlist, Guid? currentUserId)
{
if (currentUserId == playlist.CreatorUserId) return true;
return playlist.PlayPermission == ViewPermission.Everyone ||
(playlist.PlayPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue);
}
public async Task<bool> CanPlayEveryoneAsync(SharedPlaylist playlist)
{
return playlist.PlayPermission == ViewPermission.Everyone;
}
public async Task<bool> CanViewAsync(SharedPlaylist playlist, Guid? currentUserId) public async Task<bool> CanViewAsync(SharedPlaylist playlist, Guid? currentUserId)
{ {
if (currentUserId == playlist.CreatorUserId) return true; if (currentUserId == playlist.CreatorUserId) return true;

View File

@@ -7,6 +7,12 @@
"Issuer": "PlaylistShared.Api", "Issuer": "PlaylistShared.Api",
"Audience": "PlaylistShared.Client" "Audience": "PlaylistShared.Client"
}, },
"Cors": {
"Origins": [
"https://api.playlistshare.frigat.duckdns.org",
"https://playlistshare.frigat.duckdns.org"
]
},
"Yandex": { "Yandex": {
"ClientId": "0916685f8a3641ca8fc382dbccf77236", "ClientId": "0916685f8a3641ca8fc382dbccf77236",
"ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698" "ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698"

View File

@@ -65,6 +65,9 @@
/// <summary>Требовать ли авторизацию для воспроизведения (по умолчанию true).</summary> /// <summary>Требовать ли авторизацию для воспроизведения (по умолчанию true).</summary>
[Parameter] public bool RequireAuth { get; set; } = true; [Parameter] public bool RequireAuth { get; set; } = true;
/// <summary>ID расшаренного плейлиста.</summary>
[Parameter] public string SharedPlaylistId { get; set; } = string.Empty;
/// <summary>Событие при завершении трека.</summary> /// <summary>Событие при завершении трека.</summary>
[Parameter] public EventCallback OnTrackEnded { get; set; } [Parameter] public EventCallback OnTrackEnded { get; set; }
@@ -139,16 +142,16 @@
var tokens = await TokenStorage.GetTokensAsync(); var tokens = await TokenStorage.GetTokensAsync();
var accessToken = tokens.token; var accessToken = tokens.token;
if (string.IsNullOrEmpty(accessToken)) if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId))
{ {
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error); Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
return; return;
} }
var streamUrl = new Uri(Http.BaseAddress, $"/api/audio/track/{trackId}").ToString(); var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
await EnsureAudioModuleAsync(); await EnsureAudioModuleAsync();
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken); await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, SharedPlaylistId);
_isPlaying = true; _isPlaying = true;
StartProgressTimer(); StartProgressTimer();
StateHasChanged(); StateHasChanged();

View File

@@ -50,6 +50,12 @@
<MudSelectItem Value="ViewPermission.AuthorizedOnly">Только авторизованные</MudSelectItem> <MudSelectItem Value="ViewPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="4">
<MudSelect T="ViewPermission" Label="Воспроизведение" @bind-Value="_editPermissions.PlayPermission" Variant="Variant.Outlined" FullWidth="true">
<MudSelectItem Value="ViewPermission.Everyone">Все</MudSelectItem>
<MudSelectItem Value="ViewPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="4"> <MudItem xs="12" sm="4">
<MudSelect T="EditPermission" Label="Добавление треков" @bind-Value="_editPermissions.AddPermission" Variant="Variant.Outlined" FullWidth="true"> <MudSelect T="EditPermission" Label="Добавление треков" @bind-Value="_editPermissions.AddPermission" Variant="Variant.Outlined" FullWidth="true">
<MudSelectItem Value="EditPermission.Everyone">Все</MudSelectItem> <MudSelectItem Value="EditPermission.Everyone">Все</MudSelectItem>
@@ -143,6 +149,8 @@
<MudTd DataLabel="#" Style="font-weight: normal;">@context.Index</MudTd> <MudTd DataLabel="#" Style="font-weight: normal;">@context.Index</MudTd>
<MudTd DataLabel="Обложка"> <MudTd DataLabel="Обложка">
@if (!string.IsNullOrEmpty(context.CoverUri)) @if (!string.IsNullOrEmpty(context.CoverUri))
{
@if (@_canPlay)
{ {
<TrackCoverWithPlay CoverUrl="@context.CoverUri" <TrackCoverWithPlay CoverUrl="@context.CoverUri"
TrackId="@context.Id" TrackId="@context.Id"
@@ -150,6 +158,12 @@
IsPlaying="@(_currentTrackId == context.Id && _isPlaying)" IsPlaying="@(_currentTrackId == context.Id && _isPlaying)"
OnPlay="PlayTrack" /> OnPlay="PlayTrack" />
} }
else
{
<MudImage Src="@FormatCoverUrl(context.CoverUri, "50x50")" Height="50" Width="50" Class="rounded" Style="display: block;" />
}
}
</MudTd> </MudTd>
<MudTd DataLabel="Название"> <MudTd DataLabel="Название">
<MudLink Href="@($"https://music.yandex.ru/track/{context.Id}")" Target="_blank" Underline="Underline.Hover"> <MudLink Href="@($"https://music.yandex.ru/track/{context.Id}")" Target="_blank" Underline="Underline.Hover">
@@ -174,7 +188,7 @@
<!-- Фиксированный плеер внизу --> <!-- Фиксированный плеер внизу -->
<div class="fixed-player" style="display: @(_isPlayerVisible ? "block" : "none");"> <div class="fixed-player" style="display: @(_isPlayerVisible ? "block" : "none");">
<AudioPlayer @ref="_audioPlayer" OnTrackEnded="OnTrackEnded" /> <AudioPlayer @ref="_audioPlayer" OnTrackEnded="OnTrackEnded" RequireAuth="false" SharedPlaylistId="@Token"/>
</div> </div>
</MudContainer> </MudContainer>
@@ -191,6 +205,7 @@
private bool _loading = true; private bool _loading = true;
private bool _isAuthenticated; private bool _isAuthenticated;
private bool _isCreator; private bool _isCreator;
private bool _canPlay;
private bool _canAdd; private bool _canAdd;
private bool _canRemove; private bool _canRemove;
private UpdatePermissionsDto _editPermissions = new(); private UpdatePermissionsDto _editPermissions = new();
@@ -211,14 +226,17 @@
await LoadPlaylist(); await LoadPlaylist();
} }
private async Task LoadPlaylist() private async Task ConfigurePermissions()
{ {
try if (_playlist is null)
{ {
var response = await Http.GetFromJsonAsync<ApiResponse<SharedPlaylistDto>>($"/api/sharedplaylist/{Token}"); _isCreator = false;
if (response?.Success == true) _canAdd = false;
_canRemove = false;
_canPlay = false;
}
else
{ {
_playlist = response.Data;
_isCreator = _playlist.CreatorUserId.ToString() == _currentUserId; _isCreator = _playlist.CreatorUserId.ToString() == _currentUserId;
_canAdd = _isCreator _canAdd = _isCreator
@@ -229,16 +247,35 @@
|| _playlist.RemovePermission == EditPermission.Everyone || _playlist.RemovePermission == EditPermission.Everyone
|| (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated); || (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated);
_canPlay = _isCreator
|| _playlist.PlayPermission == ViewPermission.Everyone
|| (_playlist.PlayPermission == ViewPermission.AuthorizedOnly && _isAuthenticated);
if (_isCreator && _isAuthenticated) if (_isCreator && _isAuthenticated)
{ {
_editPermissions = new UpdatePermissionsDto _editPermissions = new UpdatePermissionsDto
{ {
ViewPermission = _playlist.ViewPermission, ViewPermission = _playlist.ViewPermission,
AddPermission = _playlist.AddPermission, AddPermission = _playlist.AddPermission,
RemovePermission = _playlist.RemovePermission RemovePermission = _playlist.RemovePermission,
PlayPermission = _playlist.PlayPermission,
}; };
} }
}
}
private async Task LoadPlaylist()
{
try
{
var response = await Http.GetFromJsonAsync<ApiResponse<SharedPlaylistDto>>($"/api/sharedplaylist/{Token}");
if (response?.Success == true)
{
_playlist = response.Data;
await ConfigurePermissions();
await LoadTracks(); await LoadTracks();
} }
else else
@@ -371,11 +408,8 @@
if (result?.Success == true) if (result?.Success == true)
{ {
_playlist = result.Data; _playlist = result.Data;
Snackbar.Add("Права доступа обновлены", Severity.Success);
_canAdd = _isCreator || _playlist.AddPermission == EditPermission.Everyone || await ConfigurePermissions();
(_playlist.AddPermission == EditPermission.AuthorizedOnly && _isAuthenticated);
_canRemove = _isCreator || _playlist.RemovePermission == EditPermission.Everyone ||
(_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated);
} }
else else
{ {

View File

@@ -16,74 +16,6 @@
<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>

View File

@@ -10,9 +10,10 @@
return isNaN(num) ? 0 : num; return isNaN(num) ? 0 : num;
}; };
const loadAndPlay = (src, token) => { const loadAndPlay = (src, token, sharedPlaylistId) => {
const url = new URL(src, window.location.href); const url = new URL(src, window.location.href);
if (token) url.searchParams.set('access_token', token); if (token) url.searchParams.set('access_token', token);
if (sharedPlaylistId) url.searchParams.set('shared_id', sharedPlaylistId);
audio.src = url.toString(); audio.src = url.toString();
audio.load(); audio.load();
durationReady = false; durationReady = false;

View File

@@ -38,6 +38,10 @@ public class SharePlaylistDto
[JsonPropertyName("viewPermission")] [JsonPropertyName("viewPermission")]
public ViewPermission ViewPermission { get; set; } public ViewPermission ViewPermission { get; set; }
/// <summary>Права на воспроизведение.</summary>
[JsonPropertyName("playPermission")]
public ViewPermission PlayPermission { get; set; }
/// <summary>Права на добавление треков.</summary> /// <summary>Права на добавление треков.</summary>
[JsonPropertyName("addPermission")] [JsonPropertyName("addPermission")]
public EditPermission AddPermission { get; set; } public EditPermission AddPermission { get; set; }

View File

@@ -55,6 +55,10 @@ public class SharedPlaylistDto
[JsonPropertyName("viewPermission")] [JsonPropertyName("viewPermission")]
public ViewPermission ViewPermission { get; set; } public ViewPermission ViewPermission { get; set; }
/// <summary>Права на воспроизведение.</summary>
[JsonPropertyName("playPermission")]
public ViewPermission PlayPermission { get; set; }
/// <summary>Права на добавление треков.</summary> /// <summary>Права на добавление треков.</summary>
[JsonPropertyName("addPermission")] [JsonPropertyName("addPermission")]
public EditPermission AddPermission { get; set; } public EditPermission AddPermission { get; set; }

View File

@@ -6,15 +6,19 @@ namespace PlaylistShared.Shared.Shared;
/// <summary>Запрос на обновление прав доступа шеринг-плейлиста.</summary> /// <summary>Запрос на обновление прав доступа шеринг-плейлиста.</summary>
public class UpdatePermissionsDto public class UpdatePermissionsDto
{ {
/// <summary>Новые права на просмотр.</summary> /// <summary>Права на просмотр.</summary>
[JsonPropertyName("viewPermission")] [JsonPropertyName("viewPermission")]
public ViewPermission ViewPermission { get; set; } public ViewPermission ViewPermission { get; set; }
/// <summary>Новые права на добавление треков.</summary> /// <summary>Права на воспроизведение треков.</summary>
[JsonPropertyName("playPermission")]
public ViewPermission PlayPermission { get; set; }
/// <summary>Права на добавление треков.</summary>
[JsonPropertyName("addPermission")] [JsonPropertyName("addPermission")]
public EditPermission AddPermission { get; set; } public EditPermission AddPermission { get; set; }
/// <summary>Новые права на удаление треков.</summary> /// <summary>Права на удаление треков.</summary>
[JsonPropertyName("removePermission")] [JsonPropertyName("removePermission")]
public EditPermission RemovePermission { get; set; } public EditPermission RemovePermission { get; set; }
} }