Merge pull request 'Async migration PR 5: Asset repository (final slice)' (#199) from feature/webapi/async-asset into develop
Reviewed-on: #199
This commit was merged in pull request #199.
This commit is contained in:
@@ -106,7 +106,7 @@ public class AlbumController(
|
||||
album.UpdatedAt = DateTime.UtcNow;
|
||||
album.PersonOwnerId = albumDto.RemovePerson ? null : albumDto.Person ?? album.PersonOwnerId;
|
||||
album.Visibility = albumDto.Visibility ?? album.Visibility;
|
||||
album.Assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : album.Assets;
|
||||
album.Assets = albumDto.Assets != null ? await assetRepository.FindBulkAsync(albumDto.Assets, cancellationToken) : album.Assets;
|
||||
album.CoverAssetId = albumDto.CoverAssetId ?? album.CoverAssetId;
|
||||
|
||||
// Save changes
|
||||
@@ -159,7 +159,7 @@ public class AlbumController(
|
||||
album.UpdatedAt = DateTime.UtcNow;
|
||||
album.PersonOwnerId = bulkDto.Data.Person ?? album.PersonOwnerId;
|
||||
album.Visibility = bulkDto.Data.Visibility ?? album.Visibility;
|
||||
album.Assets = bulkDto.Data.Assets != null ? assetRepository.FindBulk(bulkDto.Data.Assets).ToList() : album.Assets;
|
||||
album.Assets = bulkDto.Data.Assets != null ? await assetRepository.FindBulkAsync(bulkDto.Data.Assets, CancellationToken.None) : album.Assets;
|
||||
album.CoverAssetId = bulkDto.Data.CoverAssetId ?? album.CoverAssetId;
|
||||
}
|
||||
|
||||
@@ -168,8 +168,8 @@ public class AlbumController(
|
||||
var visibility = bulkDto.Data.Visibility;
|
||||
|
||||
if (cascade?.ToAssets == true && visibility.HasValue) {
|
||||
result.AssetsUpdated = assetRepository.BulkSetVisibilityByAlbumIds(
|
||||
albums.Select(a => a.Id).ToList(), visibility.Value, uid, accessLevel);
|
||||
result.AssetsUpdated = await assetRepository.BulkSetVisibilityByAlbumIdsAsync(
|
||||
albums.Select(a => a.Id).ToList(), visibility.Value, uid, accessLevel, CancellationToken.None);
|
||||
}
|
||||
|
||||
await albumRepository.SaveAsync(CancellationToken.None);
|
||||
@@ -190,7 +190,7 @@ public class AlbumController(
|
||||
public async Task<ActionResult> Create([FromBody] AlbumCreateDto albumDto, CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
var assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : new List<Asset>();
|
||||
var assets = albumDto.Assets != null ? await assetRepository.FindBulkAsync(albumDto.Assets, cancellationToken) : [];
|
||||
|
||||
var album = new Album {
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
@@ -34,9 +34,10 @@ public class AssetController(
|
||||
/// Gets an asset by its ID with full details, respecting access level.
|
||||
/// </summary>
|
||||
/// <param name="id">The asset ID.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The full asset details, or 404/401 based on permissions.</returns>
|
||||
[HttpGet("{id}")]
|
||||
public ActionResult<AssetDto> Get(Guid id) {
|
||||
public async Task<ActionResult<AssetDto>> Get(Guid id, CancellationToken cancellationToken) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
@@ -48,7 +49,7 @@ public class AssetController(
|
||||
"""
|
||||
);
|
||||
|
||||
var asset = assetRepository.FindVisible(id, uid, accessLevel);
|
||||
var asset = await assetRepository.FindVisibleAsync(id, uid, accessLevel, cancellationToken);
|
||||
|
||||
if (asset == null) {
|
||||
logger.LogWarning($"Request asset {id} not found or not visible!");
|
||||
@@ -78,9 +79,10 @@ public class AssetController(
|
||||
/// Searches assets with optional date range, type filter, random ordering, unlinked filter, folder filter, uploader filter, search term, and pagination.
|
||||
/// </summary>
|
||||
/// <param name="searchOptionsDto">Search options including date range, type filter, unlinked filter, folder filter, uploader filter, random ordering, search term, and pagination.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A list of asset previews.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<List<AssetPreviewDto>> GetAll([FromQuery] AssetSearchOptionsDto searchOptionsDto) {
|
||||
public async Task<ActionResult<List<AssetPreviewDto>>> GetAll([FromQuery] AssetSearchOptionsDto searchOptionsDto, CancellationToken cancellationToken) {
|
||||
var userData = authService.GetUserData(User);
|
||||
var uid = userData?.Id;
|
||||
EAccessLevel accessLevel = userData?.AccessLevel ?? EAccessLevel.User;
|
||||
@@ -97,12 +99,12 @@ public class AssetController(
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var assets = assetRepository.GetAssets(
|
||||
var (assets, total) = await assetRepository.GetAssetsAsync(
|
||||
searchOptionsDto.Type, from, to, searchOptionsDto.Random, searchOptionsDto.Seed,
|
||||
searchOptionsDto.Page, searchOptionsDto.PageSize, out int total,
|
||||
searchOptionsDto.Page, searchOptionsDto.PageSize,
|
||||
uid, accessLevel,
|
||||
searchOptionsDto.Unlinked, searchOptionsDto.FolderId, searchOptionsDto.UploadedBy, searchOptionsDto.Search,
|
||||
searchOptionsDto.IncludeCount
|
||||
searchOptionsDto.IncludeCount, cancellationToken
|
||||
);
|
||||
|
||||
logger.LogTrace(
|
||||
@@ -117,7 +119,7 @@ public class AssetController(
|
||||
"""
|
||||
);
|
||||
|
||||
logger.LogTrace($"Returning {assets.Count()} assets (total: {total})");
|
||||
logger.LogTrace($"Returning {assets.Count} assets (total: {total})");
|
||||
Response.Headers["X-Total-Count"] = total.ToString();
|
||||
return Ok(assets);
|
||||
}
|
||||
@@ -130,14 +132,14 @@ public class AssetController(
|
||||
/// <returns>A list of unlinked asset groups with folder ID, name, and asset count.</returns>
|
||||
[HttpGet("unlinked-groups")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public ActionResult<List<AssetGroupDto>> GetFolderGroups([FromQuery] bool unlinkedOnly = true) {
|
||||
public async Task<ActionResult<List<AssetGroupDto>>> GetFolderGroups([FromQuery] bool unlinkedOnly = true, CancellationToken cancellationToken = default) {
|
||||
var userData = authService.GetUserData(User);
|
||||
var uid = userData?.Id;
|
||||
EAccessLevel accessLevel = userData?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
logger.LogTrace("GetFolderGroups: uid={Uid}, accessLevel={AccessLevel}, unlinkedOnly={UnlinkedOnly}", uid, accessLevel, unlinkedOnly);
|
||||
|
||||
var groups = assetRepository.GetFolderGroups(uid, accessLevel, unlinkedOnly);
|
||||
var groups = await assetRepository.GetFolderGroupsAsync(uid, accessLevel, unlinkedOnly, cancellationToken);
|
||||
logger.LogTrace("GetFolderGroups: returned {Count} groups", groups.Count);
|
||||
return Ok(groups);
|
||||
}
|
||||
@@ -171,17 +173,18 @@ public class AssetController(
|
||||
/// </summary>
|
||||
/// <param name="page">The zero-based page number.</param>
|
||||
/// <param name="pageSize">The number of assets per page.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A page of broken assets; total count in the <c>X-Total-Count</c> header.</returns>
|
||||
[HttpGet("broken")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
public ActionResult<List<AssetPreviewDto>> GetBroken([FromQuery] int page = 0, [FromQuery] int pageSize = 50) {
|
||||
public async Task<ActionResult<List<AssetPreviewDto>>> GetBroken([FromQuery] int page = 0, [FromQuery] int pageSize = 50, CancellationToken cancellationToken = default) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel < EAccessLevel.Curator) { return Unauthorized(); }
|
||||
|
||||
if (page < 0 || pageSize < 1 || pageSize > PagedParametersDto.MaxPageSize)
|
||||
return BadRequest();
|
||||
|
||||
var assets = assetRepository.GetBrokenAssets(page, pageSize, out int total);
|
||||
var (assets, total) = await assetRepository.GetBrokenAssetsAsync(page, pageSize, cancellationToken);
|
||||
Response.Headers["X-Total-Count"] = total.ToString();
|
||||
return Ok(assets);
|
||||
}
|
||||
@@ -191,17 +194,18 @@ public class AssetController(
|
||||
/// for re-processing on the next job run.
|
||||
/// </summary>
|
||||
/// <param name="ids">The list of asset IDs to retry.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPost("broken/retry")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
public IStatusCodeActionResult RetryBroken([FromBody] List<Guid> ids) {
|
||||
public async Task<IStatusCodeActionResult> RetryBroken([FromBody] List<Guid> ids, CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel < EAccessLevel.Curator) { return Unauthorized(); }
|
||||
|
||||
if (ids.Count == 0) return BadRequest();
|
||||
|
||||
assetRepository.ClearProcessError(ids);
|
||||
assetRepository.Save();
|
||||
await assetRepository.ClearProcessErrorAsync(ids, cancellationToken);
|
||||
await assetRepository.SaveAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -217,7 +221,7 @@ public class AssetController(
|
||||
public async Task<IStatusCodeActionResult> Update([FromRoute] Guid id, [FromBody] AssetUpdateDto dto, CancellationToken cancellationToken) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
var asset = assetRepository.FindWithAlbums(id);
|
||||
var asset = await assetRepository.FindWithAlbumsAsync(id, cancellationToken);
|
||||
|
||||
if (asset == null) {
|
||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||
@@ -246,7 +250,7 @@ public class AssetController(
|
||||
logger.LogTrace($"Updating asset {id}\n{log}");
|
||||
|
||||
assetRepository.Update(asset);
|
||||
assetRepository.Save();
|
||||
await assetRepository.SaveAsync(cancellationToken);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
@@ -262,7 +266,7 @@ public class AssetController(
|
||||
public async Task<IStatusCodeActionResult> BulkUpdate([FromBody] BulkDto<AssetUpdateDto> bulkDto, CancellationToken cancellationToken) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
var assets = assetRepository.FindBulk(bulkDto.Ids);
|
||||
var assets = await assetRepository.FindBulkAsync(bulkDto.Ids, cancellationToken);
|
||||
|
||||
var enumerable = assets as List<Asset> ?? assets.ToList();
|
||||
|
||||
@@ -294,7 +298,7 @@ public class AssetController(
|
||||
);
|
||||
|
||||
assetRepository.UpdateBulk(enumerable);
|
||||
assetRepository.Save();
|
||||
await assetRepository.SaveAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -309,7 +313,7 @@ public class AssetController(
|
||||
public async Task<IStatusCodeActionResult> Delete([FromRoute] Guid id, CancellationToken cancellationToken) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
var asset = assetRepository.FindWithAlbums(id);
|
||||
var asset = await assetRepository.FindWithAlbumsAsync(id, cancellationToken);
|
||||
|
||||
if (asset == null) {
|
||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||
@@ -324,7 +328,7 @@ public class AssetController(
|
||||
|
||||
asset.DeletedAt = DateTime.UtcNow;
|
||||
assetRepository.Update(asset);
|
||||
assetRepository.Save();
|
||||
await assetRepository.SaveAsync(cancellationToken);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
@@ -340,7 +344,7 @@ public class AssetController(
|
||||
public async Task<IStatusCodeActionResult> BulkDelete([FromBody] List<Guid> ids, CancellationToken cancellationToken) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
var assets = assetRepository.FindBulk(ids);
|
||||
var assets = await assetRepository.FindBulkAsync(ids, cancellationToken);
|
||||
|
||||
var enumerable = assets as List<Asset> ?? assets.ToList();
|
||||
|
||||
@@ -363,7 +367,7 @@ public class AssetController(
|
||||
enumerable.ForEach(x => x.DeletedAt = DateTime.UtcNow);
|
||||
|
||||
assetRepository.UpdateBulk(enumerable);
|
||||
assetRepository.Save();
|
||||
await assetRepository.SaveAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -408,8 +412,8 @@ public class AssetController(
|
||||
IngestedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
assetRepository.Insert(asset);
|
||||
assetRepository.Save();
|
||||
await assetRepository.InsertAsync(asset, cancellationToken);
|
||||
await assetRepository.SaveAsync(cancellationToken);
|
||||
|
||||
return Ok(asset.Id);
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace Lactose.Controllers;
|
||||
|
||||
count = Math.Clamp(count, 1, 50);
|
||||
|
||||
var paths = assetRepository.GetRandomPathsByFolder(id, count);
|
||||
var paths = await assetRepository.GetRandomPathsByFolderAsync(id, count, cancellationToken);
|
||||
|
||||
return Ok(new FolderSamplePathsDto {
|
||||
FolderId = id,
|
||||
|
||||
@@ -43,7 +43,7 @@ public class MediaController(
|
||||
[EnableRateLimiting("media_original")]
|
||||
public async Task<ActionResult> GetImage(Guid id, CancellationToken cancellationToken) {
|
||||
var user = GetUserFromRequest();
|
||||
var asset = assetRepository.Find(id);
|
||||
var asset = await assetRepository.FindAsync(id, cancellationToken);
|
||||
|
||||
if (asset == null) return NotFound();
|
||||
|
||||
@@ -115,7 +115,7 @@ public class MediaController(
|
||||
[EnableRateLimiting("media_thumb")]
|
||||
public async Task<ActionResult> GetThumb(Guid id, CancellationToken cancellationToken) {
|
||||
var user = GetUserFromRequest();
|
||||
var asset = assetRepository.Find(id);
|
||||
var asset = await assetRepository.FindAsync(id, cancellationToken);
|
||||
|
||||
if (asset == null) return NotFound();
|
||||
|
||||
@@ -142,7 +142,7 @@ public class MediaController(
|
||||
[EnableRateLimiting("media_preview")]
|
||||
public async Task<ActionResult> GetPreview(Guid id, CancellationToken cancellationToken) {
|
||||
var user = GetUserFromRequest();
|
||||
var asset = assetRepository.Find(id);
|
||||
var asset = await assetRepository.FindAsync(id, cancellationToken);
|
||||
|
||||
if (asset == null) return NotFound();
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ public class PersonController(
|
||||
if (cascade.ToAlbums)
|
||||
result.AlbumsUpdated = await albumRepository.BulkSetVisibilityByPersonIdsAsync(personBulkDto.Ids, visibility.Value, CancellationToken.None);
|
||||
if (cascade.ToAssets)
|
||||
result.AssetsUpdated = assetRepository.BulkSetVisibilityByPersonIds(personBulkDto.Ids, visibility.Value);
|
||||
result.AssetsUpdated = await assetRepository.BulkSetVisibilityByPersonIdsAsync(personBulkDto.Ids, visibility.Value, CancellationToken.None);
|
||||
}
|
||||
|
||||
await personRepository.SaveAsync(CancellationToken.None);
|
||||
|
||||
@@ -169,7 +169,7 @@ public sealed class ConvertAnimatedJob : Job {
|
||||
}
|
||||
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
await AssetRepository.SaveAsync(token);
|
||||
|
||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(resultMsg);
|
||||
@@ -218,7 +218,7 @@ public sealed class ConvertAnimatedJob : Job {
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsNeedingConversion();
|
||||
totalAssets = await AssetRepository.CountAssetsNeedingConversionAsync(token);
|
||||
Logger.LogInformation("Found {Count} animated assets needing conversion.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
@@ -230,7 +230,7 @@ public sealed class ConvertAnimatedJob : Job {
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsNeedingConversion(batchSize, offset);
|
||||
var batch = await AssetRepository.GetAssetsNeedingConversionAsync(batchSize, offset, token);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
@@ -55,11 +55,11 @@ public sealed class MetadataJob : Job {
|
||||
if (ParentJob == null) {
|
||||
await MasterJob(token);
|
||||
} else {
|
||||
SlaveJob(token);
|
||||
await SlaveJob(token);
|
||||
}
|
||||
}
|
||||
|
||||
void SlaveJob(CancellationToken token) {
|
||||
async Task SlaveJob(CancellationToken token) {
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
@@ -103,7 +103,7 @@ public sealed class MetadataJob : Job {
|
||||
}
|
||||
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
await AssetRepository.SaveAsync(token);
|
||||
|
||||
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(msg);
|
||||
@@ -130,7 +130,7 @@ public sealed class MetadataJob : Job {
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsMissingMetadata();
|
||||
totalAssets = await AssetRepository.CountAssetsMissingMetadataAsync(token);
|
||||
Logger.LogInformation("Found {Count} image assets missing metadata.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
@@ -142,7 +142,7 @@ public sealed class MetadataJob : Job {
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsMissingMetadata(batchSize, offset);
|
||||
var batch = await AssetRepository.GetAssetsMissingMetadataAsync(batchSize, offset, token);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
@@ -58,11 +58,11 @@ public sealed class PHashJob : Job {
|
||||
if (ParentJob == null) {
|
||||
await MasterJob(token);
|
||||
} else {
|
||||
SlaveJob(token);
|
||||
await SlaveJob(token);
|
||||
}
|
||||
}
|
||||
|
||||
void SlaveJob(CancellationToken token) {
|
||||
async Task SlaveJob(CancellationToken token) {
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
@@ -114,7 +114,7 @@ public sealed class PHashJob : Job {
|
||||
}
|
||||
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
await AssetRepository.SaveAsync(token);
|
||||
|
||||
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(msg);
|
||||
@@ -141,7 +141,7 @@ public sealed class PHashJob : Job {
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsMissingPHash();
|
||||
totalAssets = await AssetRepository.CountAssetsMissingPHashAsync(token);
|
||||
Logger.LogInformation("Found {Count} assets missing pHash.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
@@ -153,7 +153,7 @@ public sealed class PHashJob : Job {
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsMissingPHash(batchSize, offset);
|
||||
var batch = await AssetRepository.GetAssetsMissingPHashAsync(batchSize, offset, token);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
@@ -82,11 +82,11 @@ public class PreviewJob : Job {
|
||||
if (ParentJob == null) {
|
||||
await MasterJob(token);
|
||||
} else {
|
||||
SlaveJob(token);
|
||||
await SlaveJob(token);
|
||||
}
|
||||
}
|
||||
|
||||
void SlaveJob(CancellationToken token) {
|
||||
async Task SlaveJob(CancellationToken token) {
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
@@ -166,7 +166,7 @@ public class PreviewJob : Job {
|
||||
}
|
||||
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
await AssetRepository.SaveAsync(token);
|
||||
|
||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(resultMsg);
|
||||
@@ -216,7 +216,7 @@ public class PreviewJob : Job {
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsMissingOrWrongPreview(previewSize, ExpectedFormat);
|
||||
totalAssets = await AssetRepository.CountAssetsMissingOrWrongPreviewAsync(previewSize, ExpectedFormat, token);
|
||||
Logger.LogInformation("Found {Count} assets needing previews.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
@@ -228,7 +228,7 @@ public class PreviewJob : Job {
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsMissingOrWrongPreview(previewSize, ExpectedFormat, batchSize, offset);
|
||||
var batch = await AssetRepository.GetAssetsMissingOrWrongPreviewAsync(previewSize, ExpectedFormat, batchSize, offset, token);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
@@ -82,11 +82,11 @@ public class ThumbnailJob : Job {
|
||||
if (ParentJob == null) {
|
||||
await MasterJob(token);
|
||||
} else {
|
||||
SlaveJob(token);
|
||||
await SlaveJob(token);
|
||||
}
|
||||
}
|
||||
|
||||
void SlaveJob(CancellationToken token) {
|
||||
async Task SlaveJob(CancellationToken token) {
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
@@ -166,7 +166,7 @@ public class ThumbnailJob : Job {
|
||||
}
|
||||
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
await AssetRepository.SaveAsync(token);
|
||||
|
||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(resultMsg);
|
||||
@@ -216,7 +216,7 @@ public class ThumbnailJob : Job {
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsMissingOrWrongThumbnail(thumbnailSize, ExpectedFormat);
|
||||
totalAssets = await AssetRepository.CountAssetsMissingOrWrongThumbnailAsync(thumbnailSize, ExpectedFormat, token);
|
||||
Logger.LogInformation("Found {Count} assets needing thumbnails.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
@@ -228,7 +228,7 @@ public class ThumbnailJob : Job {
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsMissingOrWrongThumbnail(thumbnailSize, ExpectedFormat, batchSize, offset);
|
||||
var batch = await AssetRepository.GetAssetsMissingOrWrongThumbnailAsync(thumbnailSize, ExpectedFormat, batchSize, offset, token);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
@@ -13,12 +13,13 @@ namespace Lactose.Repositories;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository> logger) : IAssetRepository {
|
||||
|
||||
/// <inheritdoc />
|
||||
public Asset? Find(Guid id) => context.Assets.Find(id);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Asset? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel) {
|
||||
public async Task<Asset?> FindAsync(Guid id, CancellationToken cancellationToken) =>
|
||||
await context.Assets.FindAsync([id], cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Asset?> FindVisibleAsync(Guid id, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken) {
|
||||
IQueryable<Asset> query = context.Assets
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
@@ -26,50 +27,51 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
.Where(a => a.Id == id);
|
||||
|
||||
return accessLevel switch {
|
||||
>= EAccessLevel.Admin => query.FirstOrDefault(),
|
||||
EAccessLevel.Curator => query.FirstOrDefault(a =>
|
||||
a.DeletedAt == null || a.UploadedBy == userId),
|
||||
>= EAccessLevel.Admin => query.FirstOrDefaultAsync(cancellationToken),
|
||||
EAccessLevel.Curator => query.FirstOrDefaultAsync(a =>
|
||||
a.DeletedAt == null || a.UploadedBy == userId, cancellationToken),
|
||||
EAccessLevel.Maintainer when userId.HasValue => query
|
||||
.Include(a => a.Albums)
|
||||
.FirstOrDefault(a => a.DeletedAt == null && (
|
||||
.FirstOrDefaultAsync(a => a.DeletedAt == null && (
|
||||
a.Visibility == EVisibility.Public ||
|
||||
a.Visibility == EVisibility.Protected ||
|
||||
a.UploadedBy == userId ||
|
||||
a.Albums!.Any(al => al.PersonOwnerId.HasValue &&
|
||||
context.PersonMaintainers.Any(pm =>
|
||||
pm.UserId == userId.Value && pm.PersonId == al.PersonOwnerId.Value))
|
||||
)),
|
||||
_ => query.FirstOrDefault(a =>
|
||||
), cancellationToken),
|
||||
_ => query.FirstOrDefaultAsync(a =>
|
||||
a.DeletedAt == null && (
|
||||
a.Visibility == EVisibility.Public ||
|
||||
(a.Visibility == EVisibility.Protected && userId.HasValue) ||
|
||||
(a.Visibility == EVisibility.Private && userId.HasValue && a.UploadedBy == userId)
|
||||
))
|
||||
), cancellationToken)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Asset? FindWithAlbums(Guid id) => context.Assets
|
||||
public Task<Asset?> FindWithAlbumsAsync(Guid id, CancellationToken cancellationToken) => context.Assets
|
||||
.AsSplitQuery()
|
||||
.Include(a => a.Albums)
|
||||
.FirstOrDefault(a => a.Id == id);
|
||||
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to) =>
|
||||
context.Assets.AsNoTracking().Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
|
||||
public Task<List<Asset>> FindByDateRangeAsync(DateTime from, DateTime to, CancellationToken cancellationToken) =>
|
||||
context.Assets.AsNoTracking().Where(x => x.CreatedAt >= from && x.UpdatedAt <= to).ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to, int pageNumber, int pageSize) =>
|
||||
public Task<List<Asset>> FindByDateRangeAsync(DateTime from, DateTime to, int pageNumber, int pageSize, CancellationToken cancellationToken) =>
|
||||
context.Assets.AsNoTracking().Where(x => x.CreatedAt >= from && x.UpdatedAt <= to)
|
||||
.Skip(pageNumber * pageSize)
|
||||
.Take(pageSize);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save() => context.SaveChanges();
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Insert(Asset asset) {
|
||||
if(!context.Assets.Any(x => x.OriginalPath == asset.OriginalPath))
|
||||
public Task SaveAsync(CancellationToken cancellationToken) => context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InsertAsync(Asset asset, CancellationToken cancellationToken) {
|
||||
if(!await context.Assets.AnyAsync(x => x.OriginalPath == asset.OriginalPath, cancellationToken))
|
||||
context.Assets.Add(asset);
|
||||
else
|
||||
throw new DuplicateNameException("The required asset already exists in the database.");
|
||||
@@ -90,10 +92,11 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> FindByUploader(Guid uploaderId) => context.Assets.AsNoTracking().Where(a => a.UploadedBy == uploaderId);
|
||||
public Task<List<Asset>> FindByUploaderAsync(Guid uploaderId, CancellationToken cancellationToken) =>
|
||||
context.Assets.AsNoTracking().Where(a => a.UploadedBy == uploaderId).ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int BulkSetVisibilityByAlbumIds(IEnumerable<Guid> albumIds, EVisibility visibility, Guid? userId, EAccessLevel accessLevel) {
|
||||
public Task<int> BulkSetVisibilityByAlbumIdsAsync(IEnumerable<Guid> albumIds, EVisibility visibility, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken) {
|
||||
// Scope the update to assets the caller is permitted to modify (R2). Curators/admins
|
||||
// see all non-deleted assets; maintainers additionally respect their uploads and
|
||||
// the albums of people they maintain.
|
||||
@@ -112,130 +115,140 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
_ => query.Where(a => false)
|
||||
};
|
||||
|
||||
return query.ExecuteUpdate(s => s
|
||||
return query.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(a => a.Visibility, visibility)
|
||||
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow));
|
||||
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility) =>
|
||||
public Task<int> BulkSetVisibilityByPersonIdsAsync(IEnumerable<Guid> personIds, EVisibility visibility, CancellationToken cancellationToken) =>
|
||||
context.Assets
|
||||
.Where(a => a.DeletedAt == null && a.Albums!.Any(al =>
|
||||
al.PersonOwnerId.HasValue && personIds.Contains(al.PersonOwnerId.Value)))
|
||||
.ExecuteUpdate(s => s
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(a => a.Visibility, visibility)
|
||||
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow));
|
||||
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids) => context.Assets
|
||||
public Task<List<Asset>> FindBulkAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken) => context.Assets
|
||||
.AsSplitQuery()
|
||||
.Include(a => a.Albums)
|
||||
.Where(a => ids.Contains(a.Id));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Asset? FindByPath(string path) => context.Assets.AsNoTracking().FirstOrDefault(a => a.OriginalPath == path);
|
||||
.Where(a => ids.Contains(a.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CountAssetsMissingPHash() {
|
||||
public Task<Asset?> FindByPathAsync(string path, CancellationToken cancellationToken) =>
|
||||
context.Assets.AsNoTracking().FirstOrDefaultAsync(a => a.OriginalPath == path, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CountAssetsMissingPHashAsync(CancellationToken cancellationToken) {
|
||||
var empty = new BitArray(64);
|
||||
return context.Assets.Count(a => a.Hash == empty && a.DeletedAt == null && a.ProcessFailedAt == null);
|
||||
return context.Assets.CountAsync(a => a.Hash == empty && a.DeletedAt == null && a.ProcessFailedAt == null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetAssetsMissingPHash(int limit, int offset) {
|
||||
public Task<List<Asset>> GetAssetsMissingPHashAsync(int limit, int offset, CancellationToken cancellationToken) {
|
||||
var empty = new BitArray(64);
|
||||
return context.Assets.Where(a => a.Hash == empty && a.DeletedAt == null && a.ProcessFailedAt == null)
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(limit);
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<List<Asset>> GetDuplicates() => context.Assets
|
||||
public Task<List<List<Asset>>> GetDuplicatesAsync(CancellationToken cancellationToken) => context.Assets
|
||||
.GroupBy(a => new { a.Hash })
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.ToList());
|
||||
|
||||
.Select(g => g.ToList())
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetWithinHammingDistance(ulong phash, int distance) {
|
||||
if(distance < 0 || distance > 63)
|
||||
throw new ArgumentOutOfRangeException(nameof(distance), "Distance must be between 0 and 64.");
|
||||
|
||||
return context.Assets
|
||||
public async Task<List<Asset>> GetWithinHammingDistanceAsync(ulong phash, float distance, CancellationToken cancellationToken) {
|
||||
if(distance < 0 || distance > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(distance), "Distance must be between 0 and 1.");
|
||||
|
||||
int intDistance = (int)(distance * 64);
|
||||
if(intDistance < 0 || intDistance > 63)
|
||||
throw new ArgumentOutOfRangeException(nameof(distance), "Distance must be between 0 and 1.");
|
||||
|
||||
return await context.Assets
|
||||
.OrderBy(a => a.HammingDistance(phash))
|
||||
.Where(a => a.Hash.HammingDistance(phash) <= distance);
|
||||
.Where(a => a.Hash.HammingDistance(phash) <= intDistance)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetWithinHammingDistance(ulong phash, float distance)
|
||||
=> GetWithinHammingDistance(phash, (int)(distance * 64));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetAssetsMissingThumbnail(out int count) {
|
||||
count = context.Assets.Count(a => (a.ThumbnailPath == null || a.ThumbnailPath == "") && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif");
|
||||
return context.Assets.Where(a => (a.ThumbnailPath == null || a.ThumbnailPath == "") && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif");
|
||||
public async Task<(List<Asset> Assets, int Total)> GetAssetsMissingThumbnailAsync(CancellationToken cancellationToken) {
|
||||
var total = await context.Assets.CountAsync(a => (a.ThumbnailPath == null || a.ThumbnailPath == "") && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif", cancellationToken);
|
||||
var assets = await context.Assets.Where(a => (a.ThumbnailPath == null || a.ThumbnailPath == "") && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif").ToListAsync(cancellationToken);
|
||||
return (assets, total);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CountAssetsMissingOrWrongThumbnail(int thumbnailSize, string expectedFormat) =>
|
||||
context.Assets.Count(a => (a.ThumbnailPath == null || a.ThumbnailPath == "" || a.ThumbnailSize != thumbnailSize || a.ThumbnailFormat != expectedFormat) && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif");
|
||||
public Task<int> CountAssetsMissingOrWrongThumbnailAsync(int thumbnailSize, string expectedFormat, CancellationToken cancellationToken) =>
|
||||
context.Assets.CountAsync(a => (a.ThumbnailPath == null || a.ThumbnailPath == "" || a.ThumbnailSize != thumbnailSize || a.ThumbnailFormat != expectedFormat) && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetAssetsMissingOrWrongThumbnail(int thumbnailSize, string expectedFormat, int limit, int offset) =>
|
||||
public Task<List<Asset>> GetAssetsMissingOrWrongThumbnailAsync(int thumbnailSize, string expectedFormat, int limit, int offset, CancellationToken cancellationToken) =>
|
||||
context.Assets
|
||||
.Where(a => (a.ThumbnailPath == null || a.ThumbnailPath == "" || a.ThumbnailSize != thumbnailSize || a.ThumbnailFormat != expectedFormat) && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif")
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(limit);
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CountAssetsMissingOrWrongPreview(int previewSize, string expectedFormat) =>
|
||||
context.Assets.Count(a => (a.PreviewPath == null || a.PreviewPath == "" || a.PreviewSize != previewSize || a.PreviewFormat != expectedFormat) && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif");
|
||||
public Task<int> CountAssetsMissingOrWrongPreviewAsync(int previewSize, string expectedFormat, CancellationToken cancellationToken) =>
|
||||
context.Assets.CountAsync(a => (a.PreviewPath == null || a.PreviewPath == "" || a.PreviewSize != previewSize || a.PreviewFormat != expectedFormat) && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetAssetsMissingOrWrongPreview(int previewSize, string expectedFormat, int limit, int offset) =>
|
||||
public Task<List<Asset>> GetAssetsMissingOrWrongPreviewAsync(int previewSize, string expectedFormat, int limit, int offset, CancellationToken cancellationToken) =>
|
||||
context.Assets
|
||||
.Where(a => (a.PreviewPath == null || a.PreviewPath == "" || a.PreviewSize != previewSize || a.PreviewFormat != expectedFormat) && a.DeletedAt == null && a.ProcessFailedAt == null && a.MimeType != "image/gif")
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(limit);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CountAssetsMissingMetadata() =>
|
||||
context.Assets.Count(a => a.Type == EAssetType.Image && a.ResolutionWidth == 0 && a.DeletedAt == null && a.ProcessFailedAt == null);
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetAssetsMissingMetadata(int limit, int offset) =>
|
||||
public Task<int> CountAssetsMissingMetadataAsync(CancellationToken cancellationToken) =>
|
||||
context.Assets.CountAsync(a => a.Type == EAssetType.Image && a.ResolutionWidth == 0 && a.DeletedAt == null && a.ProcessFailedAt == null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<Asset>> GetAssetsMissingMetadataAsync(int limit, int offset, CancellationToken cancellationToken) =>
|
||||
context.Assets.Where(a => a.Type == EAssetType.Image && a.ResolutionWidth == 0 && a.DeletedAt == null && a.ProcessFailedAt == null)
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(limit);
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CountAssetsNeedingConversion() =>
|
||||
context.Assets.Count(a => a.MimeType == "image/gif" && a.DeletedAt == null && a.ProcessFailedAt == null && (
|
||||
public Task<int> CountAssetsNeedingConversionAsync(CancellationToken cancellationToken) =>
|
||||
context.Assets.CountAsync(a => a.MimeType == "image/gif" && a.DeletedAt == null && a.ProcessFailedAt == null && (
|
||||
a.ConvertedPath == null || a.ConvertedPath == "" ||
|
||||
a.ThumbnailPath == null || a.ThumbnailPath == ""));
|
||||
a.ThumbnailPath == null || a.ThumbnailPath == ""), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Asset> GetAssetsNeedingConversion(int limit, int offset) =>
|
||||
public Task<List<Asset>> GetAssetsNeedingConversionAsync(int limit, int offset, CancellationToken cancellationToken) =>
|
||||
context.Assets
|
||||
.Where(a => a.MimeType == "image/gif" && a.DeletedAt == null && a.ProcessFailedAt == null && (
|
||||
a.ConvertedPath == null || a.ConvertedPath == "" ||
|
||||
a.ThumbnailPath == null || a.ThumbnailPath == ""))
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(limit);
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CountBrokenAssets() =>
|
||||
context.Assets.Count(a => a.ProcessFailedAt != null && a.DeletedAt == null);
|
||||
public Task<int> CountBrokenAssetsAsync(CancellationToken cancellationToken) =>
|
||||
context.Assets.CountAsync(a => a.ProcessFailedAt != null && a.DeletedAt == null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<AssetPreviewDto> GetBrokenAssets(int page, int pageSize, out int total) {
|
||||
total = CountBrokenAssets();
|
||||
public async Task<(List<AssetPreviewDto> Assets, int Total)> GetBrokenAssetsAsync(int page, int pageSize, CancellationToken cancellationToken) {
|
||||
var total = await CountBrokenAssetsAsync(cancellationToken);
|
||||
|
||||
var assets = context.Assets
|
||||
var assets = await context.Assets
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(a => a.Albums!).ThenInclude(al => al.PersonOwner)
|
||||
@@ -243,14 +256,14 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
.OrderBy(a => a.ProcessFailedAt)
|
||||
.Skip(page * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return assets.Select(a => a.ToAssetPreviewDto(EAccessLevel.Admin, null)).ToList();
|
||||
return (assets.Select(a => a.ToAssetPreviewDto(EAccessLevel.Admin, null)).ToList(), total);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearProcessError(IEnumerable<Guid> ids) {
|
||||
var assets = context.Assets.Where(a => ids.Contains(a.Id)).ToList();
|
||||
public async Task ClearProcessErrorAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken) {
|
||||
var assets = await context.Assets.Where(a => ids.Contains(a.Id)).ToListAsync(cancellationToken);
|
||||
foreach (var asset in assets) {
|
||||
asset.ProcessFailedAt = null;
|
||||
asset.ProcessErrorMessage = null;
|
||||
@@ -258,7 +271,7 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<AssetPreviewDto> GetAssets(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, out int total, Guid? userId = null, EAccessLevel accessLevel = EAccessLevel.User, bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null, bool includeCount = true) {
|
||||
public async Task<(List<AssetPreviewDto> Assets, int Total)> GetAssetsAsync(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, Guid? userId, EAccessLevel accessLevel, bool unlinked, Guid? folderId, Guid? uploadedBy, string? search, bool includeCount, CancellationToken cancellationToken) {
|
||||
var query = context.Assets.AsNoTracking().AsQueryable();
|
||||
if (type.HasValue)
|
||||
query = query.Where(a => a.Type == type.Value);
|
||||
@@ -294,7 +307,7 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
))
|
||||
};
|
||||
|
||||
total = includeCount ? query.Count() : 0;
|
||||
var total = includeCount ? await query.CountAsync(cancellationToken) : 0;
|
||||
|
||||
// Deterministic page of IDs first: random order is a seeded md5 shuffle on the server,
|
||||
// so the full ID set is never materialized in memory.
|
||||
@@ -304,16 +317,16 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
false => query.OrderByDescending(a => a.CreatedAt)
|
||||
};
|
||||
|
||||
var pageIds = ordered
|
||||
var pageIds = await ordered
|
||||
.Skip(pageNumber * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(a => a.Id)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (pageIds.Count == 0)
|
||||
return [];
|
||||
return ([], total);
|
||||
|
||||
var assets = query
|
||||
var assets = await query
|
||||
.Where(a => pageIds.Contains(a.Id))
|
||||
.Select(a => new AssetPreviewDto {
|
||||
Id = a.Id,
|
||||
@@ -326,14 +339,14 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? a.Visibility : null,
|
||||
DeletedAt = accessLevel >= EAccessLevel.Admin || a.UploadedBy == userId ? a.DeletedAt : null
|
||||
})
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Album and cosplayer names per asset via a single grouped query
|
||||
var albumLinks = (from a in context.Assets
|
||||
var albumLinks = await (from a in context.Assets
|
||||
from al in a.Albums!
|
||||
where pageIds.Contains(a.Id)
|
||||
select new { AssetId = a.Id, al.Id, al.Title, al.PersonOwnerId, PersonName = al.PersonOwner!.Name })
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var linksByAsset = albumLinks
|
||||
.GroupBy(x => x.AssetId)
|
||||
@@ -348,23 +361,23 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
}
|
||||
|
||||
var orderMap = pageIds.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i);
|
||||
return [.. assets.OrderBy(a => orderMap.GetValueOrDefault(a.Id))];
|
||||
return ([.. assets.OrderBy(a => orderMap.GetValueOrDefault(a.Id))], total);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => context.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<string> GetRandomPathsByFolder(Guid folderId, int count) =>
|
||||
public Task<List<string>> GetRandomPathsByFolderAsync(Guid folderId, int count, CancellationToken cancellationToken) =>
|
||||
context.Assets.AsNoTracking()
|
||||
.Where(a => a.FolderId == folderId && a.DeletedAt == null)
|
||||
.OrderBy(_ => Guid.NewGuid())
|
||||
.Take(count)
|
||||
.Select(a => a.OriginalPath)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<Butter.Dtos.Asset.AssetGroupDto> GetFolderGroups(Guid? userId, EAccessLevel accessLevel, bool unlinkedOnly = true) {
|
||||
public async Task<List<Butter.Dtos.Asset.AssetGroupDto>> GetFolderGroupsAsync(Guid? userId, EAccessLevel accessLevel, bool unlinkedOnly, CancellationToken cancellationToken) {
|
||||
logger.LogTrace("GetFolderGroups: userId={UserId}, accessLevel={AccessLevel}, unlinkedOnly={UnlinkedOnly}", userId, accessLevel, unlinkedOnly);
|
||||
|
||||
var query = context.Assets.AsNoTracking()
|
||||
@@ -375,7 +388,7 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
|
||||
if (accessLevel < EAccessLevel.Curator) {
|
||||
if (accessLevel == EAccessLevel.Maintainer && userId.HasValue) {
|
||||
var count = query.Count(a => a.UploadedBy == userId);
|
||||
var count = await query.CountAsync(a => a.UploadedBy == userId, cancellationToken);
|
||||
return [new Butter.Dtos.Asset.AssetGroupDto {
|
||||
FolderId = null,
|
||||
Name = "My Uploads",
|
||||
@@ -385,17 +398,17 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
return [];
|
||||
}
|
||||
|
||||
var groups = query
|
||||
var groups = await query
|
||||
.GroupBy(a => a.FolderId)
|
||||
.Select(g => new {
|
||||
FolderId = g.Key,
|
||||
AssetCount = g.Count()
|
||||
})
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var folderNames = context.Folders
|
||||
var folderNames = await context.Folders
|
||||
.Where(f => groups.Select(g => g.FolderId).Contains(f.Id))
|
||||
.ToDictionary(f => f.Id, f => f.BasePath);
|
||||
.ToDictionaryAsync(f => f.Id, f => f.BasePath, cancellationToken);
|
||||
|
||||
var result = groups
|
||||
.Where(g => g.AssetCount > 0)
|
||||
@@ -410,4 +423,4 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ public interface IAssetRepository : IDisposable {
|
||||
/// Finds an asset by its ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the asset.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
||||
public Asset? Find(Guid id);
|
||||
Task<Asset?> FindAsync(Guid id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds an asset by its ID, respecting the requesting user's access level.
|
||||
@@ -22,29 +23,33 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="id">The ID of the asset.</param>
|
||||
/// <param name="userId">The requesting user's ID.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The asset if found and visible; otherwise null.</returns>
|
||||
public Asset? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel);
|
||||
Task<Asset?> FindVisibleAsync(Guid id, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds an asset by its ID with its album navigation properties loaded.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the asset.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>Null if not found, otherwise the requested asset with albums.</returns>
|
||||
public Asset? FindWithAlbums(Guid id);
|
||||
Task<Asset?> FindWithAlbumsAsync(Guid id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a set of assets by their IDs.
|
||||
/// </summary>
|
||||
/// <param name="ids">The IDs of the assets.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A collection of assets with the specified IDs.</returns>
|
||||
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids);
|
||||
Task<List<Asset>> FindBulkAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all assets uploaded by the user.
|
||||
/// </summary>
|
||||
/// <param name="uploaderId">The ID of the uploader.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A collection of assets uploaded by the specified user.</returns>
|
||||
public IEnumerable<Asset> FindByUploader(Guid uploaderId);
|
||||
Task<List<Asset>> FindByUploaderAsync(Guid uploaderId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk-updates the visibility of non-deleted assets in the specified albums via a single SQL UPDATE,
|
||||
@@ -54,8 +59,9 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="visibility">The target visibility level.</param>
|
||||
/// <param name="userId">The requesting user's ID.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The number of assets updated.</returns>
|
||||
public int BulkSetVisibilityByAlbumIds(IEnumerable<Guid> albumIds, EVisibility visibility, Guid? userId, EAccessLevel accessLevel);
|
||||
Task<int> BulkSetVisibilityByAlbumIdsAsync(IEnumerable<Guid> albumIds, EVisibility visibility, Guid? userId, EAccessLevel accessLevel, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk-updates the visibility of non-deleted assets in any album owned by the specified people
|
||||
@@ -63,16 +69,18 @@ public interface IAssetRepository : IDisposable {
|
||||
/// </summary>
|
||||
/// <param name="personIds">The IDs of the people whose album assets to update.</param>
|
||||
/// <param name="visibility">The target visibility level.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The number of assets updated.</returns>
|
||||
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility);
|
||||
Task<int> BulkSetVisibilityByPersonIdsAsync(IEnumerable<Guid> personIds, EVisibility visibility, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all assets created or updated between the given dates.
|
||||
/// </summary>
|
||||
/// <param name="from">The start date of the range.</param>
|
||||
/// <param name="to">The end date of the range.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A collection of assets created or updated within the specified date range.</returns>
|
||||
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to);
|
||||
Task<List<Asset>> FindByDateRangeAsync(DateTime from, DateTime to, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all assets created or updated between the given dates with pagination.
|
||||
@@ -81,73 +89,88 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="to">The end date of the range.</param>
|
||||
/// <param name="pageNumber">The page number for pagination.</param>
|
||||
/// <param name="pageSize">The number of items per page.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A collection of assets created or updated within the specified date range.</returns>
|
||||
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to, int pageNumber, int pageSize);
|
||||
Task<List<Asset>> FindByDateRangeAsync(DateTime from, DateTime to, int pageNumber, int pageSize, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves all the changes to the model.
|
||||
/// </summary>
|
||||
void Save();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task SaveAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new asset.
|
||||
/// Inserts a new asset. Throws when an asset with the same original path already exists.
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset to insert.</param>
|
||||
public void Insert(Asset asset);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task InsertAsync(Asset asset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a given asset.
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset to update.</param>
|
||||
public void Update(Asset asset);
|
||||
void Update(Asset asset);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a list of assets.
|
||||
/// </summary>
|
||||
/// <param name="assets">The list of assets to update.</param>
|
||||
public void UpdateBulk(IEnumerable<Asset> assets);
|
||||
void UpdateBulk(IEnumerable<Asset> assets);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an asset by its file path.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The file path of the asset.</param>
|
||||
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
||||
Asset? FindByPath(string filePath);
|
||||
/// Gets an asset by its file path.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The file path of the asset.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
||||
Task<Asset?> FindByPathAsync(string filePath, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Counts all assets that are missing a perceptual hash (pHash).
|
||||
/// </summary>
|
||||
int CountAssetsMissingPHash();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<int> CountAssetsMissingPHashAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a page of assets that are missing a perceptual hash (pHash).
|
||||
/// </summary>
|
||||
/// <param name="limit">Maximum number of assets to return.</param>
|
||||
/// <param name="offset">Number of assets to skip.</param>
|
||||
IEnumerable<Asset> GetAssetsMissingPHash(int limit, int offset);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<List<Asset>> GetAssetsMissingPHashAsync(int limit, int offset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Groups assets sharing an identical perceptual hash (pHash).
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>Groups of duplicate assets (only groups with more than one asset).</returns>
|
||||
Task<List<List<Asset>>> GetDuplicatesAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds assets within a normalized Hamming distance (0 to 1).
|
||||
/// </summary>
|
||||
/// <param name="phash">The perceptual hash to compare against.</param>
|
||||
/// <param name="distance">The maximum distance as a fraction (0 to 1).</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>Assets whose hash is within the specified distance.</returns>
|
||||
public IEnumerable<Asset> GetWithinHammingDistance(ulong phash, float distance);
|
||||
|
||||
Task<List<Asset>> GetWithinHammingDistanceAsync(ulong phash, float distance, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all assets that are missing a thumbnail.
|
||||
/// Finds all assets that are missing a thumbnail, together with the total count.
|
||||
/// </summary>
|
||||
/// <param name="totalAssets">The total number of assets missing thumbnails.</param>
|
||||
/// <returns>Assets that do not have a thumbnail path set.</returns>
|
||||
IEnumerable<Asset> GetAssetsMissingThumbnail(out int totalAssets);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>Assets that do not have a thumbnail path set, and the total count of such assets.</returns>
|
||||
Task<(List<Asset> Assets, int Total)> GetAssetsMissingThumbnailAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Counts all assets that need thumbnails — either missing entirely, with wrong dimensions, or wrong format.
|
||||
/// </summary>
|
||||
/// <param name="thumbnailSize">The expected longest-side size in pixels.</param>
|
||||
/// <param name="expectedFormat">The expected thumbnail format (e.g. "webp").</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The total count of assets needing thumbnails.</returns>
|
||||
int CountAssetsMissingOrWrongThumbnail(int thumbnailSize, string expectedFormat);
|
||||
Task<int> CountAssetsMissingOrWrongThumbnailAsync(int thumbnailSize, string expectedFormat, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a page of assets that need thumbnails — either missing entirely, with wrong dimensions, or wrong format.
|
||||
@@ -156,16 +179,18 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="expectedFormat">The expected thumbnail format (e.g. "webp").</param>
|
||||
/// <param name="limit">Maximum number of assets to return.</param>
|
||||
/// <param name="offset">Number of assets to skip.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A page of assets that need thumbnails regenerated.</returns>
|
||||
IEnumerable<Asset> GetAssetsMissingOrWrongThumbnail(int thumbnailSize, string expectedFormat, int limit, int offset);
|
||||
Task<List<Asset>> GetAssetsMissingOrWrongThumbnailAsync(int thumbnailSize, string expectedFormat, int limit, int offset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Counts all assets that need previews — either missing entirely, with wrong dimensions, or wrong format.
|
||||
/// </summary>
|
||||
/// <param name="previewSize">The expected longest-side size in pixels.</param>
|
||||
/// <param name="expectedFormat">The expected preview format (e.g. "webp").</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The total count of assets needing previews.</returns>
|
||||
int CountAssetsMissingOrWrongPreview(int previewSize, string expectedFormat);
|
||||
Task<int> CountAssetsMissingOrWrongPreviewAsync(int previewSize, string expectedFormat, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a page of assets that need previews — either missing entirely, with wrong dimensions, or wrong format.
|
||||
@@ -174,62 +199,71 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="expectedFormat">The expected preview format (e.g. "webp").</param>
|
||||
/// <param name="limit">Maximum number of assets to return.</param>
|
||||
/// <param name="offset">Number of assets to skip.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A page of assets that need previews regenerated.</returns>
|
||||
IEnumerable<Asset> GetAssetsMissingOrWrongPreview(int previewSize, string expectedFormat, int limit, int offset);
|
||||
Task<List<Asset>> GetAssetsMissingOrWrongPreviewAsync(int previewSize, string expectedFormat, int limit, int offset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Counts image assets that are missing resolution metadata.
|
||||
/// </summary>
|
||||
int CountAssetsMissingMetadata();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<int> CountAssetsMissingMetadataAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a page of image assets missing resolution metadata.
|
||||
/// </summary>
|
||||
/// <param name="limit">Maximum number of assets to return.</param>
|
||||
/// <param name="offset">Number of assets to skip.</param>
|
||||
IEnumerable<Asset> GetAssetsMissingMetadata(int limit, int offset);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<List<Asset>> GetAssetsMissingMetadataAsync(int limit, int offset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Counts animated assets (GIF) that need a converted video and/or first-frame thumbnail.
|
||||
/// Excludes deleted and broken assets.
|
||||
/// </summary>
|
||||
int CountAssetsNeedingConversion();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<int> CountAssetsNeedingConversionAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a page of animated assets (GIF) that need a converted video and/or first-frame thumbnail.
|
||||
/// </summary>
|
||||
/// <param name="limit">Maximum number of assets to return.</param>
|
||||
/// <param name="offset">Number of assets to skip.</param>
|
||||
IEnumerable<Asset> GetAssetsNeedingConversion(int limit, int offset);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<List<Asset>> GetAssetsNeedingConversionAsync(int limit, int offset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Counts assets currently marked broken (processing failed, not deleted).
|
||||
/// </summary>
|
||||
int CountBrokenAssets();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<int> CountBrokenAssetsAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a page of assets marked broken (processing failed, not deleted), most recently broken first.
|
||||
/// Finds a page of assets marked broken (processing failed, not deleted), most recently broken first,
|
||||
/// together with the total count.
|
||||
/// </summary>
|
||||
/// <param name="page">The zero-based page number.</param>
|
||||
/// <param name="pageSize">The number of assets per page.</param>
|
||||
/// <param name="total">The total count of broken assets.</param>
|
||||
/// <returns>A page of broken assets as preview DTOs.</returns>
|
||||
IEnumerable<AssetPreviewDto> GetBrokenAssets(int page, int pageSize, out int total);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A page of broken assets as preview DTOs and the total count.</returns>
|
||||
Task<(List<AssetPreviewDto> Assets, int Total)> GetBrokenAssetsAsync(int page, int pageSize, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the broken (processing failed) state for the given assets so they are picked up for
|
||||
/// re-processing on the next job run. Caller must invoke <see cref="Save"/> afterwards.
|
||||
/// re-processing on the next job run. Caller must invoke <see cref="SaveAsync"/> afterwards.
|
||||
/// </summary>
|
||||
/// <param name="ids">The IDs of the assets to retry.</param>
|
||||
void ClearProcessError(IEnumerable<Guid> ids);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task ClearProcessErrorAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random sample of asset paths from the specified folder.
|
||||
/// </summary>
|
||||
/// <param name="folderId">The folder ID.</param>
|
||||
/// <param name="count">The number of random paths to return.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A list of asset original paths.</returns>
|
||||
List<string> GetRandomPathsByFolder(Guid folderId, int count);
|
||||
Task<List<string>> GetRandomPathsByFolderAsync(Guid folderId, int count, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Queries assets with optional type filter, date range, random ordering, and visibility scoping.
|
||||
@@ -241,16 +275,16 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="seed">An optional seed for deterministic random ordering across paginated requests.</param>
|
||||
/// <param name="pageNumber">The zero-based page number.</param>
|
||||
/// <param name="pageSize">The number of items per page.</param>
|
||||
/// <param name="total">The total count of matching assets (after visibility filtering).</param>
|
||||
/// <param name="userId">The requesting user's ID for visibility filtering.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level for visibility filtering.</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 ILike matching against OriginalFilename.</param>
|
||||
/// <param name="includeCount">If true (default), computes the total matching count. When false, <paramref name="total"/> is set to zero and the count query is skipped.</param>
|
||||
/// <returns>A paginated collection of asset previews matching the filters.</returns>
|
||||
IEnumerable<AssetPreviewDto> GetAssets(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, out int total, Guid? userId = null, EAccessLevel accessLevel = EAccessLevel.User, bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null, bool includeCount = true);
|
||||
/// <param name="includeCount">If true (default), computes the total matching count. When false, the returned total is zero and the count query is skipped.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A paginated collection of asset previews matching the filters, and the total matching count.</returns>
|
||||
Task<(List<AssetPreviewDto> Assets, int Total)> GetAssetsAsync(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, Guid? userId, EAccessLevel accessLevel, bool unlinked, Guid? folderId, Guid? uploadedBy, string? search, bool includeCount, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Returns groups of unlinked assets (not assigned to any album), grouped by folder for drill-down browsing.
|
||||
@@ -260,6 +294,7 @@ public interface IAssetRepository : IDisposable {
|
||||
/// <param name="userId">The requesting user's ID.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <param name="unlinkedOnly">If true (default), only counts assets not assigned to any album.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A list of unlinked asset groups with folder ID, name, and asset count.</returns>
|
||||
List<Butter.Dtos.Asset.AssetGroupDto> GetFolderGroups(Guid? userId, EAccessLevel accessLevel, bool unlinkedOnly = true);
|
||||
Task<List<Butter.Dtos.Asset.AssetGroupDto>> GetFolderGroupsAsync(Guid? userId, EAccessLevel accessLevel, bool unlinkedOnly, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user