Files
MilkyShots/MilkStream.Client/Services/AssetService.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

130 lines
5.9 KiB
C#

using Butter.Dtos;
using Butter.Dtos.Asset;
using Butter.Types;
using Microsoft.Extensions.Options;
namespace MilkStream.Client.Services;
/// <summary>
/// Provides access to asset listing, searching, and bulk operations via the API.
/// </summary>
public sealed class AssetService(
IOptions<ServiceOptions> options,
IHttpClientFactory httpClientFactory,
LoginService loginService,
ILogger<AssetService> logger
) : AuthServiceBase(options, httpClientFactory, loginService, logger) {
/// <summary>
/// Gets a page of assets with optional type filter, random ordering, unlinked filter, folder filter, uploader filter, and search term.
/// </summary>
/// <param name="type">Optional asset type filter.</param>
/// <param name="random">Whether to order randomly.</param>
/// <param name="seed">An optional seed for deterministic random ordering across paginated requests.</param>
/// <param name="page">The zero-based page number.</param>
/// <param name="pageSize">The number of items per page.</param>
/// <param name="unlinked">If true, filters for assets not assigned to any album.</param>
/// <param name="folderId">Optional folder ID to filter assets by their scan folder.</param>
/// <param name="uploadedBy">Optional uploader user ID to filter assets by their uploader.</param>
/// <param name="search">Optional search term for filename matching.</param>
/// <param name="includeCount">If true, the API also computes the total matching count. Skipping it avoids a full count query.</param>
/// <returns>A list of asset previews, or null if the request failed.</returns>
public async Task<List<AssetPreviewDto>?> GetAssetsAsync(
EAssetType? type = null, bool random = false, Guid? seed = null, int page = 0, int pageSize = PagedParametersDto.MaxPageSize,
bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null, bool includeCount = true
) {
var url = $"/api/asset?page={page}&pageSize={pageSize}&random={random}";
if (type.HasValue)
url += $"&type={(int)type.Value}";
if (seed.HasValue)
url += $"&seed={seed.Value}";
if (unlinked)
url += "&unlinked=true";
if (folderId.HasValue)
url += $"&folderId={folderId.Value}";
if (uploadedBy.HasValue)
url += $"&uploadedBy={uploadedBy.Value}";
if (!string.IsNullOrEmpty(search))
url += $"&search={Uri.EscapeDataString(search)}";
if (!includeCount)
url += "&includeCount=false";
var response = await Client.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
try {
return await response.Content.ReadFromJsonAsync<List<AssetPreviewDto>>() ?? [];
} catch {
return [];
}
}
/// <summary>
/// Gets groups of unlinked assets for drill-down browsing in the asset picker.
/// </summary>
/// <returns>A list of unlinked asset groups with folder ID, name, and count, or null if the request failed.</returns>
public async Task<List<AssetGroupDto>?> GetFolderGroupsAsync(bool unlinkedOnly = true) {
var url = $"/api/asset/unlinked-groups?unlinkedOnly={unlinkedOnly.ToString().ToLowerInvariant()}";
var response = await Client.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
try {
return await response.Content.ReadFromJsonAsync<List<AssetGroupDto>>() ?? [];
} catch {
return [];
}
}
/// <summary>
/// Browses unlinked assets at a specific directory level within a folder, returning immediate subdirectories
/// and a page of assets at that level.
/// </summary>
/// <param name="folderId">The root folder ID to browse within.</param>
/// <param name="path">The current path relative to the folder root.</param>
/// <param name="page">The zero-based page number.</param>
/// <param name="pageSize">The number of assets per page.</param>
/// <param name="search">Optional search term for filename matching.</param>
/// <param name="unlinkedOnly">If true (default), only includes assets not assigned to any album.</param>
/// <returns>A browse result, or null if the request failed.</returns>
public async Task<AssetBrowseResultDto?> BrowseAsync(Guid folderId, string path = "", int page = 0, int pageSize = 150, string? search = null, bool unlinkedOnly = true) {
var url = $"/api/asset/browse?folderId={folderId}&path={Uri.EscapeDataString(path)}&page={page}&pageSize={pageSize}&unlinkedOnly={unlinkedOnly.ToString().ToLowerInvariant()}";
if (!string.IsNullOrEmpty(search))
url += $"&search={Uri.EscapeDataString(search)}";
var response = await Client.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
try {
return await response.Content.ReadFromJsonAsync<AssetBrowseResultDto>();
} catch {
return null;
}
}
/// <summary>
/// Bulk updates selected assets (e.g. visibility, soft-delete).
/// </summary>
/// <param name="ids">The asset IDs to update.</param>
/// <param name="dto">The shared update values.</param>
public async Task<bool> BulkUpdateAssetsAsync(List<Guid> ids, AssetUpdateDto dto) {
var payload = new BulkDto<AssetUpdateDto> { Ids = ids, Data = dto };
var response = await Client.PostAsJsonAsync("/api/asset", payload);
return response.IsSuccessStatusCode;
}
/// <summary>
/// Bulk soft-deletes selected assets.
/// </summary>
/// <param name="ids">The asset IDs to delete.</param>
public async Task<bool> BulkDeleteAssetsAsync(List<Guid> ids) {
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/asset") {
Content = JsonContent.Create(ids)
};
var response = await Client.SendAsync(request);
return response.IsSuccessStatusCode;
}
}