Files
MilkyShots/MilkStream.Client/Services/AlbumService.cs
T
REDCODE 52e026d7e1 perf(ui): page cosplayer albums via album endpoint and skip count queries
CosplayerDetail now fetches album pages from GET /api/album?personOwnerId=
instead of re-fetching the entire PersonDetailedDto per scroll page. The home
browse page opts out of the asset count query (includeCount=false).
2026-08-11 13:11:13 +02:00

183 lines
7.7 KiB
C#

using Butter.Dtos;
using Butter.Dtos.Album;
using Butter.Dtos.Asset;
using Butter.Types;
using Microsoft.Extensions.Options;
namespace MilkStream.Client.Services;
/// <summary>
/// Provides access to album CRUD and search operations via the API.
/// </summary>
public sealed class AlbumService(
IOptions<ServiceOptions> options,
IHttpClientFactory httpClientFactory,
LoginService loginService,
ILogger<AlbumService> logger
) : AuthServiceBase(options, httpClientFactory, loginService, logger) {
/// <summary>
/// Gets a paginated list of album previews with optional search, sort, and filter options.
/// </summary>
/// <param name="page">The zero-based page number.</param>
/// <param name="pageSize">The number of items per page.</param>
/// <param name="search">Optional search term to filter by title.</param>
/// <param name="sortBy">Optional field to sort by (e.g. "name", "created", "updated", "assets", "person").</param>
/// <param name="sortAsc">Whether to sort ascending. Default is <c>false</c> (descending).</param>
/// <param name="unassigned">When <c>true</c>, only return albums with no person assigned.</param>
/// <param name="personOwnerId">Optional person ID to filter albums by their owner.</param>
/// <returns>A list of album previews, or null if the request failed.</returns>
public async Task<List<AlbumPreviewDto>?> GetAlbumsAsync(int page = 0, int pageSize = PagedParametersDto.MaxPageSize, string? search = null, string? sortBy = null, bool sortAsc = false, bool unassigned = false, Guid? personOwnerId = null) {
var url = $"/api/album?page={page}&pageSize={pageSize}";
if (!string.IsNullOrEmpty(search))
url += $"&search={Uri.EscapeDataString(search)}";
if (!string.IsNullOrEmpty(sortBy))
url += $"&sortBy={Uri.EscapeDataString(sortBy)}";
url += $"&sortAsc={sortAsc.ToString().ToLowerInvariant()}";
if (unassigned)
url += "&unassigned=true";
if (personOwnerId.HasValue)
url += $"&personOwnerId={personOwnerId.Value}";
var response = await Client.GetAsync(url);
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<List<AlbumPreviewDto>>();
return null;
}
/// <summary>
/// Gets a single album by ID with its filtered assets.
/// </summary>
/// <param name="id">The album ID.</param>
/// <returns>The full album details, or null if not found.</returns>
public async Task<AlbumFullDto?> GetAlbumAsync(Guid id) {
var response = await Client.GetAsync($"/api/album/{id}");
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<AlbumFullDto>();
return null;
}
/// <summary>
/// Gets all albums that have no person assigned.
/// </summary>
/// <returns>A list of unassigned album previews, or null if the request failed.</returns>
public async Task<List<AlbumPreviewDto>?> GetUnassignedAsync(int pageSize = PagedParametersDto.MaxPageSize) {
var response = await Client.GetAsync($"/api/album?unassigned=true&pageSize={pageSize}");
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<List<AlbumPreviewDto>>();
return null;
}
/// <summary>
/// Assigns a person to an album.
/// </summary>
/// <param name="albumId">The album ID.</param>
/// <param name="personId">The person ID to assign.</param>
public async Task AssignPersonAsync(Guid albumId, Guid personId) {
var dto = new AlbumUpdateDto { Person = personId };
var response = await Client.PostAsJsonAsync($"/api/album/{albumId}", dto);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Removes the person assignment from an album.
/// </summary>
/// <param name="albumId">The album ID.</param>
public async Task UnlinkPersonAsync(Guid albumId) {
var dto = new AlbumUpdateDto { RemovePerson = true };
var response = await Client.PostAsJsonAsync($"/api/album/{albumId}", dto);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Creates a new album.
/// </summary>
/// <param name="dto">The album creation data.</param>
public async Task CreateAlbumAsync(AlbumCreateDto dto) {
var response = await Client.PutAsJsonAsync("/api/album", dto);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Updates an existing album.
/// </summary>
/// <param name="id">The album ID.</param>
/// <param name="dto">The album update data.</param>
public async Task UpdateAlbumAsync(Guid id, AlbumUpdateDto dto) {
var response = await Client.PostAsJsonAsync($"/api/album/{id}", dto);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Deletes an album by ID.
/// </summary>
/// <param name="id">The album ID.</param>
public async Task DeleteAlbumAsync(Guid id) {
var response = await Client.DeleteAsync($"/api/album/{id}");
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Bulk deletes multiple albums (soft delete).
/// </summary>
/// <param name="ids">The list of album IDs to delete.</param>
public async Task BulkDeleteAlbumsAsync(List<Guid> ids) {
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/album") {
Content = JsonContent.Create(ids)
};
var response = await Client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Bulk updates multiple albums (e.g. visibility).
/// </summary>
/// <param name="ids">The album IDs to update.</param>
/// <param name="dto">The shared update values.</param>
public async Task<bool> BulkUpdateAlbumsAsync(List<Guid> ids, AlbumUpdateDto dto) {
var payload = new BulkDto<AlbumUpdateDto> { Ids = ids, Data = dto };
var response = await Client.PostAsJsonAsync("/api/album", payload);
return response.IsSuccessStatusCode;
}
/// <summary>
/// Merges multiple source albums into a destination album.
/// All assets from source albums are moved to the destination; sources are hard-deleted.
/// </summary>
/// <param name="dto">The merge request with destination and source IDs.</param>
public async Task MergeAlbumsAsync(AlbumMergeDto dto) {
var response = await Client.PostAsJsonAsync("/api/album/merge", dto);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// For each album, collects non-deleted, non-private asset IDs and bulk-updates their visibility.
/// Skipped assets: deleted or already Private (their visibility is never raised).
/// </summary>
/// <param name="albumIds">The album IDs whose assets to cascade to.</param>
/// <param name="visibility">The target visibility level.</param>
public async Task CascadeVisibilityToAssetsAsync(List<Guid> albumIds, EVisibility visibility) {
var allAssetIds = new List<Guid>();
foreach (var albumId in albumIds) {
var album = await GetAlbumAsync(albumId);
if (album?.AssetPreviews != null) {
allAssetIds.AddRange(
album.AssetPreviews
.Where(a => a.DeletedAt == null && a.Visibility.HasValue && a.Visibility.Value != EVisibility.Private)
.Select(a => a.Id)
);
}
}
if (allAssetIds.Count > 0) {
var payload = new BulkDto<AssetUpdateDto> { Ids = allAssetIds, Data = new AssetUpdateDto { Visibility = visibility } };
var response = await Client.PostAsJsonAsync("/api/asset", payload);
response.EnsureSuccessStatusCode();
}
}
}