Files
MilkyShots/MilkStream.Client/Services/AssetService.cs
T

153 lines
8.1 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>
/// <param name="deletedOnly">If true, filters exclusively for soft-deleted assets (scoped by access level server-side).</param>
/// <param name="albumId">Optional album ID to filter assets belonging to that album.</param>
/// <param name="personId">Optional person ID to filter assets in albums owned by that person.</param>
/// <param name="startDate">Optional start date string (server-side <c>StartDate</c> filter on CreatedAt).</param>
/// <param name="endDate">Optional end date string (server-side <c>EndDate</c> filter on UpdatedAt).</param>
/// <returns>A list of asset previews, or null if the request failed.</returns>
public 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,
bool deletedOnly = false, Guid? albumId = null, Guid? personId = null,
string? startDate = null, string? endDate = null
) {
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 (deletedOnly)
url += "&deletedOnly=true";
if (folderId.HasValue)
url += $"&folderId={folderId.Value}";
if (uploadedBy.HasValue)
url += $"&uploadedBy={uploadedBy.Value}";
if (albumId.HasValue)
url += $"&albumId={albumId.Value}";
if (personId.HasValue)
url += $"&personId={personId.Value}";
if (!string.IsNullOrEmpty(startDate))
url += $"&startDate={Uri.EscapeDataString(startDate)}";
if (!string.IsNullOrEmpty(endDate))
url += $"&endDate={Uri.EscapeDataString(endDate)}";
if (!string.IsNullOrEmpty(search))
url += $"&search={Uri.EscapeDataString(search)}";
if (!includeCount)
url += "&includeCount=false";
return GetJsonAsync<List<AssetPreviewDto>>(url);
}
/// <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 Task<List<AssetGroupDto>?> GetFolderGroupsAsync(bool unlinkedOnly = true) {
var url = $"/api/asset/unlinked-groups?unlinkedOnly={unlinkedOnly.ToString().ToLowerInvariant()}";
return GetJsonAsync<List<AssetGroupDto>>(url);
}
/// <summary>
/// Browses a folder's filesystem directly, returning the database asset records for the files found on disk.
/// </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 Task<AssetBrowseResultDto?> FsBrowseAsync(Guid folderId, string path = "", int page = 0, int pageSize = 150, string? search = null, bool unlinkedOnly = true) {
var url = $"/api/asset/directory?folderId={folderId}&path={Uri.EscapeDataString(path)}&page={page}&pageSize={pageSize}&unlinkedOnly={unlinkedOnly.ToString().ToLowerInvariant()}";
if (!string.IsNullOrEmpty(search))
url += $"&search={Uri.EscapeDataString(search)}";
return GetJsonAsync<AssetBrowseResultDto>(url);
}
/// <summary>
/// Gets a page of assets marked broken (processing failed, not deleted).
/// </summary>
/// <param name="page">The zero-based page number.</param>
/// <param name="pageSize">The number of assets per page.</param>
/// <returns>A page of broken assets, or null if the request failed.</returns>
public Task<List<AssetPreviewDto>?> GetBrokenAssetsAsync(int page = 0, int pageSize = 50) =>
GetJsonAsync<List<AssetPreviewDto>>($"/api/asset/broken?page={page}&pageSize={pageSize}");
/// <summary>
/// Clears the broken (processing failed) state for the given assets so they are picked up again on the next job run.
/// </summary>
/// <param name="ids">The asset IDs to retry.</param>
/// <returns>True if the request succeeded.</returns>
public async Task<bool> RetryBrokenAssetsAsync(List<Guid> ids) {
var response = await Client.PostAsJsonAsync("/api/asset/broken/retry", ids);
return response.IsSuccessStatusCode;
}
/// <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;
}
/// <summary>
/// Restores soft-deleted assets by clearing their deletion timestamp.
/// Assets whose source files no longer exist on disk are skipped server-side and reported.
/// </summary>
/// <param name="ids">The asset IDs to restore.</param>
/// <returns>The restore outcome listing restored and skipped IDs, or null if the request failed.</returns>
public async Task<BulkRestoreResultDto?> RestoreAssetsAsync(List<Guid> ids) {
var payload = new BulkDto<AssetUpdateDto> { Ids = ids, Data = new AssetUpdateDto { IsDeleted = false } };
var response = await Client.PostAsJsonAsync("/api/asset", payload);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<BulkRestoreResultDto>();
}
}