using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Sakayaki.Models;
using Sakayaki.Services;
namespace Sakayaki.Pages;
public class GalleryModel(ThumbnailService thumbnailService, AppDbContext dbContext, IConfiguration configuration) : PageModel
{
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"
};
private readonly ThumbnailService _thumbnailService = thumbnailService;
private readonly AppDbContext _dbContext = dbContext;
private readonly string? _rootPath = configuration["Fanbox:RootPath"];
public sealed class FolderCard
{
public required FanboxFolder Folder { get; init; }
public string? CoverUrl { get; init; }
public string? CoverFileName { get; init; }
}
public IReadOnlyList<FolderCard> Folders { get; private set; } = Array.Empty<FolderCard>();
public string? Keyword { get; private set; }
public string? Author { get; private set; }
public int PageNumber { get; private set; } = 1;
public int TotalPages { get; private set; } = 1;
public int PageSize { get; } = 30;
/// <summary>
/// 加载作者目录列表与分页数据。
/// </summary>
public async Task OnGetAsync(string? author, string? keyword, int p = 1)
{
// 归一化检索关键词与页码。
Author = string.IsNullOrWhiteSpace(author) ? null : author.Trim();
Keyword = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
PageNumber = Math.Max(1, p);
if (string.IsNullOrWhiteSpace(Author))
{
Folders = Array.Empty<FolderCard>();
TotalPages = 1;
return;
}
var query = _dbContext.FanboxFolders.AsNoTracking()
.Where(x => x.Author == Author);
if (!string.IsNullOrWhiteSpace(Keyword))
{
// 关键词模糊匹配数据库字段。
var pattern = $"%{Keyword}%";
query = query.Where(x => x.Keywords != null && EF.Functions.Like(x.Keywords, pattern));
}
var totalCount = await query.CountAsync();
TotalPages = Math.Max(1, (int)Math.Ceiling(totalCount / (double)PageSize));
if (PageNumber > TotalPages)
{
PageNumber = TotalPages;
}
var folders = await query
.OrderByDescending(x => x.Date)
.ThenByDescending(x => x.CreatedAt)
.Skip((PageNumber - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
var cards = new List<FolderCard>(folders.Count);
foreach (var folder in folders)
{
string? coverFile = null;
string? coverUrl = null;
if (!string.IsNullOrWhiteSpace(_rootPath))
{
var folderPath = Path.Combine(_rootPath, folder.Author, folder.FolderName);
if (Directory.Exists(folderPath))
{
// 优先以 0.* 图片作为封面。
coverFile = Directory.EnumerateFiles(folderPath)
.Select(Path.GetFileName)
.FirstOrDefault(name =>
!string.IsNullOrWhiteSpace(name) &&
name.StartsWith("0.", StringComparison.OrdinalIgnoreCase) &&
ImageExtensions.Contains(Path.GetExtension(name)));
}
}
if (!string.IsNullOrWhiteSpace(coverFile))
{
coverUrl = Url.Page("Gallery", "Image", new { id = folder.Id, file = coverFile });
}
cards.Add(new FolderCard
{
Folder = folder,
CoverUrl = coverUrl,
CoverFileName = coverFile
});
}
Folders = cards;
}
/// <summary>
/// 为指定文件夹生成缺失缩略图并返回数量。
/// </summary>
public async Task<IActionResult> OnGetEnsureThumbnailsAsync(Guid id)
{
var folder = await _dbContext.FanboxFolders.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);
if (folder is null)
{
return NotFound();
}
var created = await _thumbnailService.EnsureFolderThumbnailsAsync(folder.Id, folder.Author, folder.FolderName, HttpContext.RequestAborted);
return new JsonResult(new { created });
}
/// <summary>
/// 返回指定图片的原图文件。
/// </summary>
public async Task<IActionResult> OnGetImageAsync(Guid id, string file)
{
if (string.IsNullOrWhiteSpace(_rootPath) || string.IsNullOrWhiteSpace(file))
return NotFound();
// 仅允许安全文件名与图片扩展名。
var safeFile = Path.GetFileName(file);
if (string.IsNullOrWhiteSpace(safeFile))
return NotFound();
if (!ImageExtensions.Contains(Path.GetExtension(safeFile)))
return NotFound();
var folder = await _dbContext.FanboxFolders.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);
if (folder is null)
return NotFound();
var filePath = Path.Combine(_rootPath, folder.Author, folder.FolderName, safeFile);
if (!System.IO.File.Exists(filePath))
return NotFound();
// 根据扩展名获取 Content-Type。
var provider = new FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(filePath, out var contentType))
contentType = "application/octet-stream";
return PhysicalFile(filePath, contentType);
}
}