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).
130 lines
5.9 KiB
C#
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;
|
|
}
|
|
}
|