mirror of
https://github.com/Ikatono/ComiServ.git
synced 2025-10-28 20:45:35 -05:00
updated controllers and some services to use async/await
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ComiServ.Entities;
|
using ComiServ.Entities;
|
||||||
|
|
||||||
namespace ComiServ
|
namespace ComiServ;
|
||||||
|
|
||||||
|
public class ComicsContext : DbContext
|
||||||
{
|
{
|
||||||
public class ComicsContext : DbContext
|
|
||||||
{
|
|
||||||
//TODO is this the best place for this to live?
|
//TODO is this the best place for this to live?
|
||||||
public const int HANDLE_LENGTH = 12;
|
public const int HANDLE_LENGTH = 12;
|
||||||
//relies on low probability of repeat handles in a short period of time
|
//relies on low probability of repeat handles in a short period of time
|
||||||
@@ -77,5 +77,4 @@ namespace ComiServ
|
|||||||
return null;
|
return null;
|
||||||
return handle.ToUpper();
|
return handle.ToUpper();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ using ComiServ.Extensions;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using ComiServ.Services;
|
using ComiServ.Services;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Data;
|
||||||
using SQLitePCL;
|
using SQLitePCL;
|
||||||
|
|
||||||
namespace ComiServ.Controllers
|
namespace ComiServ.Controllers;
|
||||||
{
|
|
||||||
[Route(ROUTE)]
|
[Route(ROUTE)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class ComicController(ComicsContext context, ILogger<ComicController> logger, IConfigService config, IComicAnalyzer analyzer, IPictureConverter converter, IAuthenticationService _auth)
|
public class ComicController(ComicsContext context, ILogger<ComicController> logger, IConfigService config, IComicAnalyzer analyzer, IPictureConverter converter, IAuthenticationService _auth)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
public const string ROUTE = "/api/v1/comics";
|
public const string ROUTE = "/api/v1/comics";
|
||||||
private readonly ComicsContext _context = context;
|
private readonly ComicsContext _context = context;
|
||||||
private readonly ILogger<ComicController> _logger = logger;
|
private readonly ILogger<ComicController> _logger = logger;
|
||||||
@@ -31,7 +32,7 @@ namespace ComiServ.Controllers
|
|||||||
//TODO search parameters
|
//TODO search parameters
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<Paginated<ComicData>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Paginated<ComicData>>(StatusCodes.Status200OK)]
|
||||||
public IActionResult SearchComics(
|
public async Task<IActionResult> SearchComics(
|
||||||
[FromQuery(Name = "TitleSearch")]
|
[FromQuery(Name = "TitleSearch")]
|
||||||
string? titleSearch,
|
string? titleSearch,
|
||||||
[FromQuery(Name = "DescriptionSearch")]
|
[FromQuery(Name = "DescriptionSearch")]
|
||||||
@@ -143,16 +144,16 @@ namespace ComiServ.Controllers
|
|||||||
results = results.Where(c => EF.Functions.Like(c.Description, $"%{descSearch}%"));
|
results = results.Where(c => EF.Functions.Like(c.Description, $"%{descSearch}%"));
|
||||||
}
|
}
|
||||||
int offset = page * pageSize;
|
int offset = page * pageSize;
|
||||||
return Ok(new Paginated<ComicData>(pageSize, page, results
|
return Ok(await Paginated<ComicData>.CreateAsync(pageSize, page, results
|
||||||
.OrderBy(c => c.Id)
|
.OrderBy(c => c.Id)
|
||||||
.Select(c => new ComicData(c))));
|
.Select(c => new ComicData(c))));
|
||||||
}
|
}
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public IActionResult DeleteComicsThatDontExist()
|
public async Task<IActionResult> DeleteComicsThatDontExist()
|
||||||
{
|
{
|
||||||
var search = _context.Comics.Where(c => !c.Exists);
|
var search = _context.Comics.Where(c => !c.Exists);
|
||||||
var nonExtant = search.ToList();
|
var nonExtant = await search.ToListAsync();
|
||||||
search.ExecuteDelete();
|
search.ExecuteDelete();
|
||||||
_context.SaveChanges();
|
_context.SaveChanges();
|
||||||
return Ok(search.Select(c => new ComicData(c)));
|
return Ok(search.Select(c => new ComicData(c)));
|
||||||
@@ -161,16 +162,16 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType<ComicData>(StatusCodes.Status200OK)]
|
[ProducesResponseType<ComicData>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult GetSingleComicInfo(string handle)
|
public async Task<IActionResult> GetSingleComicInfo(string handle)
|
||||||
{
|
{
|
||||||
//_logger.LogInformation("GetSingleComicInfo: {handle}", handle);
|
//_logger.LogInformation("GetSingleComicInfo: {handle}", handle);
|
||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = _context.Comics
|
var comic = await _context.Comics
|
||||||
.Include("ComicAuthors.Author")
|
.Include("ComicAuthors.Author")
|
||||||
.Include("ComicTags.Tag")
|
.Include("ComicTags.Tag")
|
||||||
.SingleOrDefault(c => c.Handle == handle);
|
.SingleOrDefaultAsync(c => c.Handle == handle);
|
||||||
if (comic is Comic actualComic)
|
if (comic is Comic actualComic)
|
||||||
return Ok(new ComicData(comic));
|
return Ok(new ComicData(comic));
|
||||||
else
|
else
|
||||||
@@ -182,11 +183,11 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult UpdateComicMetadata(string handle, [FromBody] ComicMetadataUpdateRequest metadata)
|
public async Task<IActionResult> UpdateComicMetadata(string handle, [FromBody] ComicMetadataUpdateRequest metadata)
|
||||||
{
|
{
|
||||||
if (handle.Length != ComicsContext.HANDLE_LENGTH)
|
if (handle.Length != ComicsContext.HANDLE_LENGTH)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = _context.Comics.SingleOrDefault(c => c.Handle == handle);
|
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
|
||||||
if (comic is Comic actualComic)
|
if (comic is Comic actualComic)
|
||||||
{
|
{
|
||||||
if (metadata.Title != null)
|
if (metadata.Title != null)
|
||||||
@@ -196,7 +197,7 @@ namespace ComiServ.Controllers
|
|||||||
//make sure all authors exist, without changing Id of pre-existing authors
|
//make sure all authors exist, without changing Id of pre-existing authors
|
||||||
_context.InsertOrIgnore(authors.Select(author => new Author() { Name = author }), ignorePrimaryKey: true);
|
_context.InsertOrIgnore(authors.Select(author => new Author() { Name = author }), ignorePrimaryKey: true);
|
||||||
//get the Id of needed authors
|
//get the Id of needed authors
|
||||||
var authorEntities = _context.Authors.Where(a => authors.Contains(a.Name)).ToList();
|
var authorEntities = await _context.Authors.Where(a => authors.Contains(a.Name)).ToListAsync();
|
||||||
//delete existing author mappings
|
//delete existing author mappings
|
||||||
_context.ComicAuthors.RemoveRange(_context.ComicAuthors.Where(ca => ca.Comic.Id == comic.Id));
|
_context.ComicAuthors.RemoveRange(_context.ComicAuthors.Where(ca => ca.Comic.Id == comic.Id));
|
||||||
//add all author mappings
|
//add all author mappings
|
||||||
@@ -207,13 +208,13 @@ namespace ComiServ.Controllers
|
|||||||
//make sure all tags exist, without changing Id of pre-existing tags
|
//make sure all tags exist, without changing Id of pre-existing tags
|
||||||
_context.InsertOrIgnore(tags.Select(t => new Tag() { Name = t }), ignorePrimaryKey: true);
|
_context.InsertOrIgnore(tags.Select(t => new Tag() { Name = t }), ignorePrimaryKey: true);
|
||||||
//get the needed tags
|
//get the needed tags
|
||||||
var tagEntities = _context.Tags.Where(t => tags.Contains(t.Name)).ToList();
|
var tagEntities = await _context.Tags.Where(t => tags.Contains(t.Name)).ToListAsync();
|
||||||
//delete existing tag mappings
|
//delete existing tag mappings
|
||||||
_context.ComicTags.RemoveRange(_context.ComicTags.Where(ta => ta.Comic.Id == comic.Id));
|
_context.ComicTags.RemoveRange(_context.ComicTags.Where(ta => ta.Comic.Id == comic.Id));
|
||||||
//add all tag mappings
|
//add all tag mappings
|
||||||
_context.ComicTags.AddRange(tagEntities.Select(t => new ComicTag { Comic = comic, Tag = t }));
|
_context.ComicTags.AddRange(tagEntities.Select(t => new ComicTag { Comic = comic, Tag = t }));
|
||||||
}
|
}
|
||||||
_context.SaveChanges();
|
await _context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -223,14 +224,14 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
public IActionResult MarkComicAsRead(
|
public async Task<IActionResult> MarkComicAsRead(
|
||||||
ComicsContext context,
|
ComicsContext context,
|
||||||
string handle)
|
string handle)
|
||||||
{
|
{
|
||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = context.Comics.SingleOrDefault(c => c.Handle == validated);
|
var comic = await context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
if (_auth.User is null)
|
if (_auth.User is null)
|
||||||
@@ -242,32 +243,32 @@ namespace ComiServ.Controllers
|
|||||||
ComicId = comic.Id
|
ComicId = comic.Id
|
||||||
};
|
};
|
||||||
context.InsertOrIgnore(comicRead, ignorePrimaryKey: false);
|
context.InsertOrIgnore(comicRead, ignorePrimaryKey: false);
|
||||||
context.SaveChanges();
|
await context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
[HttpPatch("{handle}/markunread")]
|
[HttpPatch("{handle}/markunread")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
public IActionResult MarkComicAsUnread(
|
public async Task<IActionResult> MarkComicAsUnread(
|
||||||
ComicsContext context,
|
ComicsContext context,
|
||||||
string handle)
|
string handle)
|
||||||
{
|
{
|
||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = context.Comics.SingleOrDefault(c => c.Handle == validated);
|
var comic = await context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
if (_auth.User is null)
|
if (_auth.User is null)
|
||||||
//user shouldn't have passed authentication if username doesn't match
|
//user shouldn't have passed authentication if username doesn't match
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
var comicRead = context.ComicsRead.SingleOrDefault(cr =>
|
var comicRead = await context.ComicsRead.SingleOrDefaultAsync(cr =>
|
||||||
cr.ComicId == comic.Id && cr.UserId == _auth.User.Id);
|
cr.ComicId == comic.Id && cr.UserId == _auth.User.Id);
|
||||||
if (comicRead is null)
|
if (comicRead is null)
|
||||||
return Ok();
|
return Ok();
|
||||||
context.ComicsRead.Remove(comicRead);
|
context.ComicsRead.Remove(comicRead);
|
||||||
context.SaveChanges();
|
await context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
[HttpDelete("{handle}")]
|
[HttpDelete("{handle}")]
|
||||||
@@ -276,7 +277,7 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult DeleteComic(
|
public async Task<IActionResult> DeleteComic(
|
||||||
string handle,
|
string handle,
|
||||||
[FromBody]
|
[FromBody]
|
||||||
ComicDeleteRequest req)
|
ComicDeleteRequest req)
|
||||||
@@ -288,14 +289,14 @@ namespace ComiServ.Controllers
|
|||||||
}
|
}
|
||||||
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
|
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
var comic = _context.Comics.SingleOrDefault(c => c.Handle == handle);
|
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
comic.Exists = _analyzer.ComicFileExists(string.Join(config.Config.LibraryRoot, comic.Filepath));
|
comic.Exists = _analyzer.ComicFileExists(string.Join(config.Config.LibraryRoot, comic.Filepath));
|
||||||
if (comic.Exists && !req.DeleteIfFileExists)
|
if (comic.Exists && !req.DeleteIfFileExists)
|
||||||
return BadRequest(RequestError.ComicFileExists);
|
return BadRequest(RequestError.ComicFileExists);
|
||||||
_context.Comics.Remove(comic);
|
_context.Comics.Remove(comic);
|
||||||
_context.SaveChanges();
|
await _context.SaveChangesAsync();
|
||||||
_analyzer.DeleteComicFile(string.Join(config.Config.LibraryRoot, comic.Filepath));
|
_analyzer.DeleteComicFile(string.Join(config.Config.LibraryRoot, comic.Filepath));
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
@@ -303,34 +304,34 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType<byte[]>(StatusCodes.Status200OK)]
|
[ProducesResponseType<byte[]>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult GetComicFile(string handle)
|
public async Task<IActionResult> GetComicFile(string handle)
|
||||||
{
|
{
|
||||||
//_logger.LogInformation(nameof(GetComicFile) + ": {handle}", handle);
|
//_logger.LogInformation(nameof(GetComicFile) + ": {handle}", handle);
|
||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = _context.Comics.SingleOrDefault(c => c.Handle == handle);
|
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
var data = System.IO.File.ReadAllBytes(Path.Join(_config.LibraryRoot, comic.Filepath));
|
var data = await System.IO.File.ReadAllBytesAsync(Path.Join(_config.LibraryRoot, comic.Filepath));
|
||||||
return File(data, "application/octet-stream", new FileInfo(comic.Filepath).Name);
|
return File(data, "application/octet-stream", new FileInfo(comic.Filepath).Name);
|
||||||
}
|
}
|
||||||
[HttpGet("{handle}/cover")]
|
[HttpGet("{handle}/cover")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult GetComicCover(string handle)
|
public async Task<IActionResult> GetComicCover(string handle)
|
||||||
{
|
{
|
||||||
//_logger.LogInformation(nameof(GetComicCover) + ": {handle}", handle);
|
//_logger.LogInformation(nameof(GetComicCover) + ": {handle}", handle);
|
||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = _context.Comics
|
var comic = await _context.Comics
|
||||||
.SingleOrDefault(c => c.Handle == validated);
|
.SingleOrDefaultAsync(c => c.Handle == validated);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
var cover = _context.Covers
|
var cover = await _context.Covers
|
||||||
.SingleOrDefault(cov => cov.FileXxhash64 == comic.FileXxhash64);
|
.SingleOrDefaultAsync(cov => cov.FileXxhash64 == comic.FileXxhash64);
|
||||||
if (cover is null)
|
if (cover is null)
|
||||||
return NotFound(RequestError.CoverNotFound);
|
return NotFound(RequestError.CoverNotFound);
|
||||||
var mime = IComicAnalyzer.GetImageMime(cover.Filename);
|
var mime = IComicAnalyzer.GetImageMime(cover.Filename);
|
||||||
@@ -342,7 +343,7 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult GetComicPage(string handle, int page,
|
public async Task<IActionResult> GetComicPage(string handle, int page,
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(0)]
|
[DefaultValue(0)]
|
||||||
int? maxWidth,
|
int? maxWidth,
|
||||||
@@ -357,10 +358,10 @@ namespace ComiServ.Controllers
|
|||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = _context.Comics.SingleOrDefault(c => c.Handle == validated);
|
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
var comicPage = _analyzer.GetComicPage(Path.Join(_config.LibraryRoot, comic.Filepath), page);
|
var comicPage = await _analyzer.GetComicPageAsync(Path.Join(_config.LibraryRoot, comic.Filepath), page);
|
||||||
if (comicPage is null)
|
if (comicPage is null)
|
||||||
//TODO rethink error code
|
//TODO rethink error code
|
||||||
return NotFound(RequestError.PageNotFound);
|
return NotFound(RequestError.PageNotFound);
|
||||||
@@ -382,7 +383,8 @@ namespace ComiServ.Controllers
|
|||||||
null => comicPage.Mime,
|
null => comicPage.Mime,
|
||||||
};
|
};
|
||||||
//TODO using the stream directly throws but I think it should be valid, need to debug
|
//TODO using the stream directly throws but I think it should be valid, need to debug
|
||||||
var arr = _converter.ResizeIfBigger(stream, limit, format).ReadAllBytes();
|
using var resizedStream = await _converter.ResizeIfBigger(stream, limit, format);
|
||||||
|
var arr = await resizedStream.ReadAllBytesAsync();
|
||||||
return File(arr, mime);
|
return File(arr, mime);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -392,7 +394,7 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
|
||||||
public IActionResult GetComicThumbnail(
|
public async Task<IActionResult> GetComicThumbnail(
|
||||||
string handle,
|
string handle,
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(false)]
|
[DefaultValue(false)]
|
||||||
@@ -403,7 +405,7 @@ namespace ComiServ.Controllers
|
|||||||
var validated = ComicsContext.CleanValidateHandle(handle);
|
var validated = ComicsContext.CleanValidateHandle(handle);
|
||||||
if (validated is null)
|
if (validated is null)
|
||||||
return BadRequest(RequestError.InvalidHandle);
|
return BadRequest(RequestError.InvalidHandle);
|
||||||
var comic = _context.Comics.SingleOrDefault(c => c.Handle == validated);
|
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
|
||||||
if (comic is null)
|
if (comic is null)
|
||||||
return NotFound(RequestError.ComicNotFound);
|
return NotFound(RequestError.ComicNotFound);
|
||||||
if (comic.ThumbnailWebp is byte[] img)
|
if (comic.ThumbnailWebp is byte[] img)
|
||||||
@@ -412,7 +414,7 @@ namespace ComiServ.Controllers
|
|||||||
}
|
}
|
||||||
if (fallbackToCover)
|
if (fallbackToCover)
|
||||||
{
|
{
|
||||||
var cover = _context.Covers.SingleOrDefault(c => c.FileXxhash64 == comic.FileXxhash64);
|
var cover = await _context.Covers.SingleOrDefaultAsync(c => c.FileXxhash64 == comic.FileXxhash64);
|
||||||
if (cover is not null)
|
if (cover is not null)
|
||||||
{
|
{
|
||||||
//TODO should this convert to a thumbnail on the fly?
|
//TODO should this convert to a thumbnail on the fly?
|
||||||
@@ -424,23 +426,23 @@ namespace ComiServ.Controllers
|
|||||||
}
|
}
|
||||||
[HttpPost("cleandb")]
|
[HttpPost("cleandb")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public IActionResult CleanUnusedTagAuthors()
|
public async Task<IActionResult> CleanUnusedTagAuthors()
|
||||||
{
|
{
|
||||||
_context.Authors
|
await _context.Authors
|
||||||
.Include(a => a.ComicAuthors)
|
.Include(a => a.ComicAuthors)
|
||||||
.Where(a => a.ComicAuthors.Count == 0)
|
.Where(a => a.ComicAuthors.Count == 0)
|
||||||
.ExecuteDelete();
|
.ExecuteDeleteAsync();
|
||||||
_context.Tags
|
await _context.Tags
|
||||||
.Include(a => a.ComicTags)
|
.Include(a => a.ComicTags)
|
||||||
.Where(a => a.ComicTags.Count == 0)
|
.Where(a => a.ComicTags.Count == 0)
|
||||||
.ExecuteDelete();
|
.ExecuteDeleteAsync();
|
||||||
//ExecuteDelete doesn't wait for SaveChanges
|
//ExecuteDelete doesn't wait for SaveChanges
|
||||||
//_context.SaveChanges();
|
//_context.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
[HttpGet("duplicates")]
|
[HttpGet("duplicates")]
|
||||||
[ProducesResponseType<Paginated<ComicDuplicateList>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Paginated<ComicDuplicateList>>(StatusCodes.Status200OK)]
|
||||||
public IActionResult GetDuplicateFiles(
|
public async Task<IActionResult> GetDuplicateFiles(
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(0)]
|
[DefaultValue(0)]
|
||||||
int page,
|
int page,
|
||||||
@@ -454,7 +456,7 @@ namespace ComiServ.Controllers
|
|||||||
.GroupBy(c => c.FileXxhash64)
|
.GroupBy(c => c.FileXxhash64)
|
||||||
.Where(g => g.Count() > 1)
|
.Where(g => g.Count() > 1)
|
||||||
.OrderBy(g => g.Key);
|
.OrderBy(g => g.Key);
|
||||||
var ret = new Paginated<ComicDuplicateList>(pageSize, page,
|
var ret = await Paginated<ComicDuplicateList>.CreateAsync(pageSize, page,
|
||||||
groups.Select(g =>
|
groups.Select(g =>
|
||||||
new ComicDuplicateList(g.Key, g.Select(g => g))
|
new ComicDuplicateList(g.Key, g.Select(g => g))
|
||||||
));
|
));
|
||||||
@@ -464,12 +466,11 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType<LibraryResponse>(StatusCodes.Status200OK)]
|
[ProducesResponseType<LibraryResponse>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult GetLibraryStats()
|
public async Task<IActionResult> GetLibraryStats()
|
||||||
{
|
{
|
||||||
return Ok(new LibraryResponse(
|
return Ok(new LibraryResponse(
|
||||||
_context.Comics.Count(),
|
await _context.Comics.CountAsync(),
|
||||||
_context.Comics.Select(c => c.FileXxhash64).Distinct().Count()
|
await _context.Comics.Select(c => c.FileXxhash64).Distinct().CountAsync()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ using ComiServ.Background;
|
|||||||
using ComiServ.Models;
|
using ComiServ.Models;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace ComiServ.Controllers
|
namespace ComiServ.Controllers;
|
||||||
{
|
|
||||||
[Route(ROUTE)]
|
[Route(ROUTE)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class MiscController(ComicsContext context, ILogger<MiscController> logger, IConfigService config, IAuthenticationService auth)
|
public class MiscController(ComicsContext context, ILogger<MiscController> logger, IConfigService config, IAuthenticationService auth)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
public const string ROUTE = "/api/v1/";
|
public const string ROUTE = "/api/v1/";
|
||||||
ComicsContext _context = context;
|
ComicsContext _context = context;
|
||||||
ILogger<MiscController> _logger = logger;
|
ILogger<MiscController> _logger = logger;
|
||||||
@@ -22,7 +22,7 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType<Paginated<AuthorResponse>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Paginated<AuthorResponse>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult GetAuthors(
|
public async Task<IActionResult> GetAuthors(
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(0)]
|
[DefaultValue(0)]
|
||||||
int page,
|
int page,
|
||||||
@@ -38,16 +38,16 @@ namespace ComiServ.Controllers
|
|||||||
}
|
}
|
||||||
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
|
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
var items = context.Authors
|
var items = _context.Authors
|
||||||
.OrderBy(a => a.ComicAuthors.Count())
|
.OrderBy(a => a.ComicAuthors.Count())
|
||||||
.Select(a => new AuthorResponse(a.Name, a.ComicAuthors.Count()));
|
.Select(a => new AuthorResponse(a.Name, a.ComicAuthors.Count()));
|
||||||
return Ok(new Paginated<AuthorResponse>(pageSize, page, items));
|
return Ok(await Paginated<AuthorResponse>.CreateAsync(pageSize, page, items));
|
||||||
}
|
}
|
||||||
[HttpGet("tags")]
|
[HttpGet("tags")]
|
||||||
[ProducesResponseType<Paginated<TagResponse>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Paginated<TagResponse>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult GetTags(
|
public async Task<IActionResult> GetTags(
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(0)]
|
[DefaultValue(0)]
|
||||||
int page,
|
int page,
|
||||||
@@ -63,10 +63,9 @@ namespace ComiServ.Controllers
|
|||||||
}
|
}
|
||||||
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
|
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
var items = context.Tags
|
var items = _context.Tags
|
||||||
.OrderBy(t => t.ComicTags.Count())
|
.OrderBy(t => t.ComicTags.Count())
|
||||||
.Select(t => new TagResponse(t.Name, t.ComicTags.Count()));
|
.Select(t => new TagResponse(t.Name, t.ComicTags.Count()));
|
||||||
return Ok(new Paginated<TagResponse>(pageSize, page, items));
|
return Ok(await Paginated<TagResponse>.CreateAsync(pageSize, page, items));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Security.Policy;
|
using System.Security.Policy;
|
||||||
|
|
||||||
namespace ComiServ.Controllers
|
namespace ComiServ.Controllers;
|
||||||
{
|
|
||||||
[Route(ROUTE)]
|
[Route(ROUTE)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class TaskController(
|
public class TaskController(
|
||||||
ComicsContext context
|
ComicsContext context
|
||||||
,ITaskManager manager
|
,ITaskManager manager
|
||||||
,IComicScanner scanner
|
,IComicScanner scanner
|
||||||
,ILogger<TaskController> logger
|
,ILogger<TaskController> logger
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
public const string ROUTE = "/api/v1/tasks";
|
public const string ROUTE = "/api/v1/tasks";
|
||||||
private readonly ComicsContext _context = context;
|
private readonly ComicsContext _context = context;
|
||||||
private readonly ITaskManager _manager = manager;
|
private readonly ITaskManager _manager = manager;
|
||||||
@@ -26,7 +26,7 @@ namespace ComiServ.Controllers
|
|||||||
private readonly CancellationTokenSource cancellationToken = new();
|
private readonly CancellationTokenSource cancellationToken = new();
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<Truncated<string>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Truncated<string>>(StatusCodes.Status200OK)]
|
||||||
public IActionResult GetTasks(
|
public Task<IActionResult> GetTasks(
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(20)]
|
[DefaultValue(20)]
|
||||||
int limit
|
int limit
|
||||||
@@ -57,5 +57,4 @@ namespace ComiServ.Controllers
|
|||||||
manager.CancelAll();
|
manager.CancelAll();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace ComiServ.Controllers
|
namespace ComiServ.Controllers;
|
||||||
{
|
|
||||||
[Route(ROUTE)]
|
[Route(ROUTE)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class UserController
|
public class UserController
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
public const string ROUTE = "/api/v1/users";
|
public const string ROUTE = "/api/v1/users";
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<Paginated<UserDescription>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Paginated<UserDescription>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult GetUsers(IAuthenticationService auth,
|
public async Task<IActionResult> GetUsers(IAuthenticationService auth,
|
||||||
ComicsContext context,
|
ComicsContext context,
|
||||||
[FromQuery]
|
[FromQuery]
|
||||||
[DefaultValue(null)]
|
[DefaultValue(null)]
|
||||||
@@ -46,7 +46,7 @@ namespace ComiServ.Controllers
|
|||||||
users = users.Where(u => u.UserTypeId == t);
|
users = users.Where(u => u.UserTypeId == t);
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
users = users.Where(u => EF.Functions.Like(u.Username, $"%{search}%"));
|
users = users.Where(u => EF.Functions.Like(u.Username, $"%{search}%"));
|
||||||
return Ok(new Paginated<UserDescription>(pageSize, page, users
|
return Ok(await Paginated<UserDescription>.CreateAsync(pageSize, page, users
|
||||||
.Include(u => u.UserType)
|
.Include(u => u.UserType)
|
||||||
.Select(u => new UserDescription(u.Username, u.UserType.Name))));
|
.Select(u => new UserDescription(u.Username, u.UserType.Name))));
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult AddUser(IAuthenticationService auth,
|
public async Task<IActionResult> AddUser(IAuthenticationService auth,
|
||||||
ComicsContext context,
|
ComicsContext context,
|
||||||
[FromBody]
|
[FromBody]
|
||||||
UserCreateRequest req)
|
UserCreateRequest req)
|
||||||
@@ -76,7 +76,7 @@ namespace ComiServ.Controllers
|
|||||||
UserTypeId = req.UserType
|
UserTypeId = req.UserType
|
||||||
};
|
};
|
||||||
context.Users.Add(newUser);
|
context.Users.Add(newUser);
|
||||||
context.SaveChanges();
|
await context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
[HttpDelete("delete")]
|
[HttpDelete("delete")]
|
||||||
@@ -84,7 +84,7 @@ namespace ComiServ.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult DeleteUser(IAuthenticationService auth,
|
public async Task<IActionResult> DeleteUser(IAuthenticationService auth,
|
||||||
ComicsContext context,
|
ComicsContext context,
|
||||||
[FromBody]
|
[FromBody]
|
||||||
string username)
|
string username)
|
||||||
@@ -97,17 +97,18 @@ namespace ComiServ.Controllers
|
|||||||
if (auth.User.UserTypeId != UserTypeEnum.Administrator)
|
if (auth.User.UserTypeId != UserTypeEnum.Administrator)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
username = username.Trim();
|
username = username.Trim();
|
||||||
var user = context.Users.SingleOrDefault(u => EF.Functions.Like(u.Username, $"{username}"));
|
var user = await context.Users.SingleOrDefaultAsync(u => EF.Functions.Like(u.Username, $"{username}"));
|
||||||
if (user is null)
|
if (user is null)
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
context.Users.Remove(user);
|
context.Users.Remove(user);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
[HttpPost("modify")]
|
[HttpPost("modify")]
|
||||||
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public IActionResult ModifyUser(IAuthenticationService auth,
|
public async Task<IActionResult> ModifyUser(IAuthenticationService auth,
|
||||||
ComicsContext context,
|
ComicsContext context,
|
||||||
[FromBody]
|
[FromBody]
|
||||||
UserModifyRequest req)
|
UserModifyRequest req)
|
||||||
@@ -131,8 +132,8 @@ namespace ComiServ.Controllers
|
|||||||
{
|
{
|
||||||
return Forbid();
|
return Forbid();
|
||||||
}
|
}
|
||||||
var user = context.Users
|
var user = await context.Users
|
||||||
.SingleOrDefault(u => EF.Functions.Like(u.Username, req.Username));
|
.SingleOrDefaultAsync(u => EF.Functions.Like(u.Username, req.Username));
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
return BadRequest(RequestError.UserNotFound);
|
return BadRequest(RequestError.UserNotFound);
|
||||||
@@ -145,8 +146,7 @@ namespace ComiServ.Controllers
|
|||||||
{
|
{
|
||||||
user.UserTypeId = nut;
|
user.UserTypeId = nut;
|
||||||
}
|
}
|
||||||
context.SaveChanges();
|
await context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
//using System.ComponentModel.DataAnnotations.Schema;
|
//using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[Index(nameof(Name), IsUnique = true)]
|
||||||
|
public class Author
|
||||||
{
|
{
|
||||||
[Index(nameof(Name), IsUnique = true)]
|
|
||||||
public class Author
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string Name { get; set; } = null!;
|
public string Name { get; set; } = null!;
|
||||||
public ICollection<ComicAuthor> ComicAuthors { get; set; } = null!;
|
public ICollection<ComicAuthor> ComicAuthors { get; set; } = null!;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[Index(nameof(Handle), IsUnique = true)]
|
||||||
|
[Index(nameof(Filepath), IsUnique = true)]
|
||||||
|
public class Comic
|
||||||
{
|
{
|
||||||
[Index(nameof(Handle), IsUnique = true)]
|
|
||||||
[Index(nameof(Filepath), IsUnique = true)]
|
|
||||||
public class Comic
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public bool Exists { get; set; }
|
public bool Exists { get; set; }
|
||||||
//id exposed through the API
|
//id exposed through the API
|
||||||
@@ -31,5 +31,4 @@ namespace ComiServ.Entities
|
|||||||
public ICollection<ComicAuthor> ComicAuthors { get; set; } = [];
|
public ICollection<ComicAuthor> ComicAuthors { get; set; } = [];
|
||||||
[InverseProperty("Comic")]
|
[InverseProperty("Comic")]
|
||||||
public ICollection<ComicRead> ReadBy { get; set; } = [];
|
public ICollection<ComicRead> ReadBy { get; set; } = [];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[PrimaryKey("ComicId", "AuthorId")]
|
||||||
|
[Index("ComicId")]
|
||||||
|
[Index("AuthorId")]
|
||||||
|
public class ComicAuthor
|
||||||
{
|
{
|
||||||
[PrimaryKey("ComicId", "AuthorId")]
|
|
||||||
[Index("ComicId")]
|
|
||||||
[Index("AuthorId")]
|
|
||||||
public class ComicAuthor
|
|
||||||
{
|
|
||||||
[ForeignKey(nameof(Comic))]
|
[ForeignKey(nameof(Comic))]
|
||||||
public int ComicId { get; set; }
|
public int ComicId { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
@@ -17,5 +17,4 @@ namespace ComiServ.Entities
|
|||||||
public int AuthorId { get; set; }
|
public int AuthorId { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public Author Author { get; set; } = null!;
|
public Author Author { get; set; } = null!;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[PrimaryKey(nameof(UserId), nameof(ComicId))]
|
||||||
|
[Index(nameof(UserId))]
|
||||||
|
[Index(nameof(ComicId))]
|
||||||
|
public class ComicRead
|
||||||
{
|
{
|
||||||
[PrimaryKey(nameof(UserId), nameof(ComicId))]
|
|
||||||
[Index(nameof(UserId))]
|
|
||||||
[Index(nameof(ComicId))]
|
|
||||||
public class ComicRead
|
|
||||||
{
|
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
public int ComicId { get; set; }
|
public int ComicId { get; set; }
|
||||||
public Comic Comic { get; set; }
|
public Comic Comic { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[PrimaryKey("ComicId", "TagId")]
|
||||||
|
[Index("ComicId")]
|
||||||
|
[Index("TagId")]
|
||||||
|
public class ComicTag
|
||||||
{
|
{
|
||||||
[PrimaryKey("ComicId", "TagId")]
|
|
||||||
[Index("ComicId")]
|
|
||||||
[Index("TagId")]
|
|
||||||
public class ComicTag
|
|
||||||
{
|
|
||||||
public int ComicId { get; set; }
|
public int ComicId { get; set; }
|
||||||
public Comic Comic { get; set; } = null!;
|
public Comic Comic { get; set; } = null!;
|
||||||
public int TagId { get; set; }
|
public int TagId { get; set; }
|
||||||
public Tag Tag { get; set; } = null!;
|
public Tag Tag { get; set; } = null!;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[PrimaryKey("FileXxhash64")]
|
||||||
|
public class Cover
|
||||||
{
|
{
|
||||||
[PrimaryKey("FileXxhash64")]
|
|
||||||
public class Cover
|
|
||||||
{
|
|
||||||
public long FileXxhash64 { get; set; }
|
public long FileXxhash64 { get; set; }
|
||||||
public string Filename { get; set; } = null!;
|
public string Filename { get; set; } = null!;
|
||||||
public byte[] CoverFile { get; set; } = null!;
|
public byte[] CoverFile { get; set; } = null!;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This was originally made to remove Entity types that were being added to the Swagger schema.
|
||||||
|
/// I found that there was a bug in `ProducesResponseTypeAttribute` that caused it, and this is
|
||||||
|
/// no longer necessary. I changed Apply to a nop but am keeping this around as an example and
|
||||||
|
/// in case I actually need something like this in the future.
|
||||||
|
/// </summary>
|
||||||
|
public class EntitySwaggerFilter : ISchemaFilter
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// This was originally made to remove Entity types that were being added to the Swagger schema.
|
|
||||||
/// I found that there was a bug in `ProducesResponseTypeAttribute` that caused it, and this is
|
|
||||||
/// no longer necessary. I changed Apply to a nop but am keeping this around as an example and
|
|
||||||
/// in case I actually need something like this in the future.
|
|
||||||
/// </summary>
|
|
||||||
public class EntitySwaggerFilter : ISchemaFilter
|
|
||||||
{
|
|
||||||
public readonly static string[] FILTER = [
|
public readonly static string[] FILTER = [
|
||||||
nameof(Author),
|
nameof(Author),
|
||||||
nameof(Comic),
|
nameof(Comic),
|
||||||
@@ -25,13 +25,12 @@ namespace ComiServ.Entities
|
|||||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
foreach (var item in context.SchemaRepository.Schemas.Keys)
|
//foreach (var item in context.SchemaRepository.Schemas.Keys)
|
||||||
{
|
//{
|
||||||
if (FILTER.Contains(item))
|
// if (FILTER.Contains(item))
|
||||||
{
|
// {
|
||||||
context.SchemaRepository.Schemas.Remove(item);
|
// context.SchemaRepository.Schemas.Remove(item);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[Index(nameof(Name), IsUnique = true)]
|
||||||
|
public class Tag
|
||||||
{
|
{
|
||||||
[Index(nameof(Name), IsUnique = true)]
|
|
||||||
public class Tag
|
|
||||||
{
|
|
||||||
//[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
//[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string Name { get; set; } = null!;
|
public string Name { get; set; } = null!;
|
||||||
public ICollection<ComicTag> ComicTags { get; set; } = null!;
|
public ICollection<ComicTag> ComicTags { get; set; } = null!;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[PrimaryKey(nameof(Id))]
|
||||||
|
[Index(nameof(Username), IsUnique = true)]
|
||||||
|
public class User
|
||||||
{
|
{
|
||||||
[PrimaryKey(nameof(Id))]
|
|
||||||
[Index(nameof(Username), IsUnique = true)]
|
|
||||||
public class User
|
|
||||||
{
|
|
||||||
public const int HashLengthBytes = 512 / 8;
|
public const int HashLengthBytes = 512 / 8;
|
||||||
public const int SaltLengthBytes = HashLengthBytes;
|
public const int SaltLengthBytes = HashLengthBytes;
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
@@ -34,5 +34,4 @@ namespace ComiServ.Entities
|
|||||||
var salted = salt.Append((byte)':').Concat(password).ToArray();
|
var salted = salt.Append((byte)':').Concat(password).ToArray();
|
||||||
return SHA512.HashData(salted);
|
return SHA512.HashData(salted);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ComiServ.Entities
|
namespace ComiServ.Entities;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum UserTypeEnum
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
||||||
public enum UserTypeEnum
|
|
||||||
{
|
|
||||||
//important that this is 0 as a safety precaution,
|
//important that this is 0 as a safety precaution,
|
||||||
//in case it's accidentally left as default
|
//in case it's accidentally left as default
|
||||||
Invalid = 0,
|
Invalid = 0,
|
||||||
@@ -17,12 +17,11 @@ namespace ComiServ.Entities
|
|||||||
Restricted = 3,
|
Restricted = 3,
|
||||||
//refuses to authenticate but maintains records
|
//refuses to authenticate but maintains records
|
||||||
Disabled = 4,
|
Disabled = 4,
|
||||||
}
|
}
|
||||||
public class UserType
|
public class UserType
|
||||||
{
|
{
|
||||||
public UserTypeEnum Id { get; set; }
|
public UserTypeEnum Id { get; set; }
|
||||||
[MaxLength(26)]
|
[MaxLength(26)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public ICollection<User> Users { get; set; }
|
public ICollection<User> Users { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
//https://stackoverflow.com/a/42467710/25956209
|
//https://stackoverflow.com/a/42467710/25956209
|
||||||
//https://archive.ph/RvjOy
|
//https://archive.ph/RvjOy
|
||||||
namespace ComiServ.Extensions
|
namespace ComiServ.Extensions;
|
||||||
|
|
||||||
|
public static class DatabaseExtensions
|
||||||
{
|
{
|
||||||
public static class DatabaseExtensions
|
|
||||||
{
|
|
||||||
//with a compound primary key, `ignorePrimaryKey` will ignore all of them
|
//with a compound primary key, `ignorePrimaryKey` will ignore all of them
|
||||||
public static int InsertOrIgnore<T>(this DbContext context, T item, bool ignorePrimaryKey = false)
|
public static int InsertOrIgnore<T>(this DbContext context, T item, bool ignorePrimaryKey = false)
|
||||||
{
|
{
|
||||||
@@ -58,5 +58,4 @@ namespace ComiServ.Extensions
|
|||||||
var formattable = FormattableStringFactory.Create(query, args);
|
var formattable = FormattableStringFactory.Create(query, args);
|
||||||
return context.Database.ExecuteSql(formattable);
|
return context.Database.ExecuteSql(formattable);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace ComiServ.Extensions
|
namespace ComiServ.Extensions;
|
||||||
|
|
||||||
|
public static class StreamExtensions
|
||||||
{
|
{
|
||||||
public static class StreamExtensions
|
|
||||||
{
|
|
||||||
//https://stackoverflow.com/questions/1080442/how-do-i-convert-a-stream-into-a-byte-in-c
|
//https://stackoverflow.com/questions/1080442/how-do-i-convert-a-stream-into-a-byte-in-c
|
||||||
//https://archive.ph/QUKys
|
//https://archive.ph/QUKys
|
||||||
public static byte[] ReadAllBytes(this Stream instream)
|
public static byte[] ReadAllBytes(this Stream instream)
|
||||||
@@ -9,11 +9,17 @@
|
|||||||
if (instream is MemoryStream)
|
if (instream is MemoryStream)
|
||||||
return ((MemoryStream)instream).ToArray();
|
return ((MemoryStream)instream).ToArray();
|
||||||
|
|
||||||
using (var memoryStream = new MemoryStream())
|
using var memoryStream = new MemoryStream();
|
||||||
{
|
|
||||||
instream.CopyTo(memoryStream);
|
instream.CopyTo(memoryStream);
|
||||||
return memoryStream.ToArray();
|
return memoryStream.ToArray();
|
||||||
}
|
}
|
||||||
}
|
public static async Task<byte[]> ReadAllBytesAsync(this Stream instream)
|
||||||
|
{
|
||||||
|
if (instream is MemoryStream)
|
||||||
|
return ((MemoryStream)instream).ToArray();
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await instream.CopyToAsync(memoryStream);
|
||||||
|
return memoryStream.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace ComiServ.Logging
|
namespace ComiServ.Logging;
|
||||||
{
|
|
||||||
public static class Events
|
public static class Events
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
{
|
|
||||||
public record class AuthorResponse(string Name, int WorkCount)
|
public record class AuthorResponse(string Name, int WorkCount)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
{
|
|
||||||
//handle is taken from URL
|
//handle is taken from URL
|
||||||
public record class ComicDeleteRequest
|
public record class ComicDeleteRequest
|
||||||
(
|
(
|
||||||
bool DeleteIfFileExists
|
bool DeleteIfFileExists
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using ComiServ.Entities;
|
using ComiServ.Entities;
|
||||||
|
|
||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public class ComicDuplicateList
|
||||||
{
|
{
|
||||||
public class ComicDuplicateList
|
|
||||||
{
|
|
||||||
public long Hash { get; set; }
|
public long Hash { get; set; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public List<ComicData> Comics { get; set; }
|
public List<ComicData> Comics { get; set; }
|
||||||
@@ -19,5 +19,4 @@ namespace ComiServ.Models
|
|||||||
Comics = comics.ToList();
|
Comics = comics.ToList();
|
||||||
Count = Comics.Count;
|
Count = Comics.Count;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public class ComicMetadataUpdateRequest
|
||||||
{
|
{
|
||||||
public class ComicMetadataUpdateRequest
|
|
||||||
{
|
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public List<string>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
public List<string>? Authors { get; set; }
|
public List<string>? Authors { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public record class LibraryResponse(int ComicCount, int UniqueFiles)
|
||||||
{
|
{
|
||||||
public record class LibraryResponse(int ComicCount, int UniqueFiles)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
namespace ComiServ.Models
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ComiServ.Models;
|
||||||
|
public class Paginated<T>
|
||||||
{
|
{
|
||||||
public class Paginated<T>
|
|
||||||
{
|
|
||||||
public int Max { get; }
|
public int Max { get; }
|
||||||
public int Page { get;}
|
public int Page { get; }
|
||||||
public bool Last { get; }
|
public bool Last { get; }
|
||||||
public int Count { get; }
|
public int Count { get; }
|
||||||
public List<T> Items { get; }
|
public List<T> Items { get; }
|
||||||
public Paginated(int max, int page, IEnumerable<T> iter)
|
public Paginated(int max, int page, IEnumerable<T> iter)
|
||||||
{
|
{
|
||||||
Max = max;
|
|
||||||
Page = page;
|
|
||||||
if (max <= 0)
|
if (max <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
|
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
|
||||||
}
|
}
|
||||||
|
Max = max;
|
||||||
|
Page = page;
|
||||||
Items = iter.Skip(max * page).Take(max + 1).ToList();
|
Items = iter.Skip(max * page).Take(max + 1).ToList();
|
||||||
if (Items.Count > max)
|
if (Items.Count > max)
|
||||||
{
|
{
|
||||||
@@ -31,5 +34,62 @@
|
|||||||
}
|
}
|
||||||
Count = Items.Count;
|
Count = Items.Count;
|
||||||
}
|
}
|
||||||
|
private Paginated(int max, int page, bool last, List<T> items)
|
||||||
|
{
|
||||||
|
Max = max;
|
||||||
|
Page = page;
|
||||||
|
Last = last;
|
||||||
|
Items = items;
|
||||||
|
Count = Items.Count;
|
||||||
|
}
|
||||||
|
public static async Task<Paginated<T>> CreateAsync(int max, int page, IQueryable<T> iter)
|
||||||
|
{
|
||||||
|
if (max <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
||||||
|
}
|
||||||
|
if (page < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
|
||||||
|
}
|
||||||
|
var items = await iter.Skip(max * page).Take(max + 1).ToListAsync();
|
||||||
|
bool last = true;
|
||||||
|
if (items.Count > max)
|
||||||
|
{
|
||||||
|
last = false;
|
||||||
|
items.RemoveAt(max);
|
||||||
|
}
|
||||||
|
return new(max, page, last, items);
|
||||||
|
}
|
||||||
|
public static async Task<Paginated<T>> CreateAsync(int max, int page, IAsyncEnumerable<T> iter)
|
||||||
|
{
|
||||||
|
if (max <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
||||||
|
}
|
||||||
|
if (page < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
|
||||||
|
}
|
||||||
|
List<T> items = [];
|
||||||
|
var skip = max * page;
|
||||||
|
await foreach (T item in iter)
|
||||||
|
{
|
||||||
|
if (skip > 0)
|
||||||
|
{
|
||||||
|
skip--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items.Add(item);
|
||||||
|
if (items.Count >= max + 1)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var last = true;
|
||||||
|
if (items.Count > max)
|
||||||
|
{
|
||||||
|
last = false;
|
||||||
|
items.RemoveAt(max);
|
||||||
|
}
|
||||||
|
return new(max, page, last, items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public class RequestError : IEnumerable<string>
|
||||||
{
|
{
|
||||||
public class RequestError : IEnumerable<string>
|
|
||||||
{
|
|
||||||
public static RequestError InvalidHandle => new("Invalid handle");
|
public static RequestError InvalidHandle => new("Invalid handle");
|
||||||
public static RequestError ComicNotFound => new("Comic not found");
|
public static RequestError ComicNotFound => new("Comic not found");
|
||||||
public static RequestError CoverNotFound => new("Cover not found");
|
public static RequestError CoverNotFound => new("Cover not found");
|
||||||
@@ -48,5 +48,4 @@ namespace ComiServ.Models
|
|||||||
{
|
{
|
||||||
return GetEnumerator();
|
return GetEnumerator();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
{
|
|
||||||
public record class TagResponse(string Name, int WorkCount)
|
public record class TagResponse(string Name, int WorkCount)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
using System.Reflection.PortableExecutable;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
|
||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public class Truncated<T>
|
||||||
{
|
{
|
||||||
public class Truncated<T>
|
|
||||||
{
|
|
||||||
public int Max { get; }
|
public int Max { get; }
|
||||||
public int Count { get; }
|
public int Count { get; }
|
||||||
public bool Complete { get; }
|
public bool Complete { get; }
|
||||||
public List<T> Items { get; }
|
public List<T> Items { get; }
|
||||||
public Truncated(int max, IEnumerable<T> items)
|
public Truncated(int max, IEnumerable<T> iter)
|
||||||
{
|
{
|
||||||
if (max <= 0)
|
if (max <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
||||||
}
|
}
|
||||||
Max = max;
|
Max = max;
|
||||||
Items = items.Take(max+1).ToList();
|
Items = iter.Take(max+1).ToList();
|
||||||
if (Items.Count <= max)
|
if (Items.Count <= max)
|
||||||
{
|
{
|
||||||
Complete = true;
|
Complete = true;
|
||||||
@@ -27,5 +29,47 @@ namespace ComiServ.Models
|
|||||||
}
|
}
|
||||||
Count = Items.Count;
|
Count = Items.Count;
|
||||||
}
|
}
|
||||||
|
private Truncated(int max, bool complete, List<T> items)
|
||||||
|
{
|
||||||
|
Max = max;
|
||||||
|
Complete = complete;
|
||||||
|
Count = items.Count;
|
||||||
|
Items = items;
|
||||||
|
}
|
||||||
|
public static async Task<Truncated<T>> CreateAsync(int max, IQueryable<T> iter)
|
||||||
|
{
|
||||||
|
if (max <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
||||||
|
}
|
||||||
|
var items = await iter.Take(max+1).ToListAsync();
|
||||||
|
var complete = true;
|
||||||
|
if (items.Count < max)
|
||||||
|
{
|
||||||
|
items.RemoveAt(max);
|
||||||
|
complete = false;
|
||||||
|
}
|
||||||
|
return new(max, complete, items);
|
||||||
|
}
|
||||||
|
public static async Task<Truncated<T>> CreateAsync(int max, IAsyncEnumerable<T> iter)
|
||||||
|
{
|
||||||
|
if (max <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
|
||||||
|
}
|
||||||
|
List<T> items = [];
|
||||||
|
await foreach (T item in iter)
|
||||||
|
{
|
||||||
|
items.Add(item);
|
||||||
|
if (items.Count > max)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var complete = true;
|
||||||
|
if (items.Count <= max)
|
||||||
|
{
|
||||||
|
items.RemoveAt(max);
|
||||||
|
complete = false;
|
||||||
|
}
|
||||||
|
return new Truncated<T>(max, complete, items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using ComiServ.Entities;
|
using ComiServ.Entities;
|
||||||
|
|
||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public class UserCreateRequest
|
||||||
{
|
{
|
||||||
public class UserCreateRequest
|
|
||||||
{
|
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public UserTypeEnum UserType { get; set; }
|
public UserTypeEnum UserType { get; set; }
|
||||||
//NOT HASHED do not persist this object
|
//NOT HASHED do not persist this object
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public record class UserDescription(string Username, string Usertype)
|
||||||
{
|
{
|
||||||
public record class UserDescription(string Username, string Usertype);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
using ComiServ.Entities;
|
using ComiServ.Entities;
|
||||||
|
|
||||||
namespace ComiServ.Models
|
namespace ComiServ.Models;
|
||||||
|
|
||||||
|
public class UserModifyRequest
|
||||||
{
|
{
|
||||||
public class UserModifyRequest
|
|
||||||
{
|
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string? NewUsername { get; set; }
|
public string? NewUsername { get; set; }
|
||||||
public UserTypeEnum? NewUserType { get; set; }
|
public UserTypeEnum? NewUserType { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
using ComiServ.Entities;
|
using ComiServ.Entities;
|
||||||
|
|
||||||
namespace ComiServ.Services
|
namespace ComiServ.Services;
|
||||||
|
|
||||||
|
public interface IAuthenticationService
|
||||||
{
|
{
|
||||||
public interface IAuthenticationService
|
|
||||||
{
|
|
||||||
public bool Tested { get; }
|
public bool Tested { get; }
|
||||||
public User? User { get; }
|
public User? User { get; }
|
||||||
public void Authenticate(User user);
|
public void Authenticate(User user);
|
||||||
public void FailAuth();
|
public void FailAuth();
|
||||||
}
|
}
|
||||||
//acts as a per-request container of authentication info
|
//acts as a per-request container of authentication info
|
||||||
public class AuthenticationService : IAuthenticationService
|
public class AuthenticationService : IAuthenticationService
|
||||||
{
|
{
|
||||||
public bool Tested { get; private set; } = false;
|
public bool Tested { get; private set; } = false;
|
||||||
|
|
||||||
public User? User { get; private set; }
|
public User? User { get; private set; }
|
||||||
@@ -29,5 +29,4 @@ namespace ComiServ.Services
|
|||||||
User = null;
|
User = null;
|
||||||
Tested = true;
|
Tested = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ using System.IO.Compression;
|
|||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace ComiServ.Background
|
namespace ComiServ.Background;
|
||||||
{
|
|
||||||
public record class ComicAnalysis
|
public record class ComicAnalysis
|
||||||
(
|
(
|
||||||
long FileSizeBytes,
|
long FileSizeBytes,
|
||||||
int PageCount,
|
int PageCount,
|
||||||
Int64 Xxhash
|
Int64 Xxhash
|
||||||
);
|
);
|
||||||
public record class ComicPage
|
public record class ComicPage
|
||||||
(
|
(
|
||||||
string Filename,
|
string Filename,
|
||||||
string Mime,
|
string Mime,
|
||||||
byte[] Data
|
byte[] Data
|
||||||
);
|
);
|
||||||
public interface IComicAnalyzer
|
public interface IComicAnalyzer
|
||||||
{
|
{
|
||||||
public static readonly IReadOnlyList<string> ZIP_EXTS = [".cbz", ".zip"];
|
public static readonly IReadOnlyList<string> ZIP_EXTS = [".cbz", ".zip"];
|
||||||
public static readonly IReadOnlyList<string> RAR_EXTS = [".cbr", ".rar"];
|
public static readonly IReadOnlyList<string> RAR_EXTS = [".cbr", ".rar"];
|
||||||
public static readonly IReadOnlyList<string> ZIP7_EXTS = [".cb7", ".7z"];
|
public static readonly IReadOnlyList<string> ZIP7_EXTS = [".cb7", ".7z"];
|
||||||
@@ -34,6 +34,7 @@ namespace ComiServ.Background
|
|||||||
public Task<ComicAnalysis?> AnalyzeComicAsync(string filename);
|
public Task<ComicAnalysis?> AnalyzeComicAsync(string filename);
|
||||||
//returns null if out of range, throws for file error
|
//returns null if out of range, throws for file error
|
||||||
public ComicPage? GetComicPage(string filepath, int page);
|
public ComicPage? GetComicPage(string filepath, int page);
|
||||||
|
public Task<ComicPage?> GetComicPageAsync(string filepath, int page);
|
||||||
//based purely on filename, doesn't try to open file
|
//based purely on filename, doesn't try to open file
|
||||||
//returns null for ALL UNRECOGNIZED OR NON-IMAGES
|
//returns null for ALL UNRECOGNIZED OR NON-IMAGES
|
||||||
public static string? GetImageMime(string filename)
|
public static string? GetImageMime(string filename)
|
||||||
@@ -45,11 +46,11 @@ namespace ComiServ.Background
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//async methods actually just block
|
//async methods actually just block
|
||||||
public class SynchronousComicAnalyzer(ILogger<IComicAnalyzer>? logger)
|
public class SynchronousComicAnalyzer(ILogger<IComicAnalyzer>? logger)
|
||||||
: IComicAnalyzer
|
: IComicAnalyzer
|
||||||
{
|
{
|
||||||
private readonly ILogger<IComicAnalyzer>? _logger = logger;
|
private readonly ILogger<IComicAnalyzer>? _logger = logger;
|
||||||
public bool ComicFileExists(string filename)
|
public bool ComicFileExists(string filename)
|
||||||
{
|
{
|
||||||
@@ -146,6 +147,12 @@ namespace ComiServ.Background
|
|||||||
return GetPage7Zip(filepath, page);
|
return GetPage7Zip(filepath, page);
|
||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
public async Task<ComicPage?> GetComicPageAsync(string filepath, int page)
|
||||||
|
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
{
|
||||||
|
return GetComicPage(filepath, page);
|
||||||
|
}
|
||||||
protected ComicPage? GetPageZip(string filepath, int page)
|
protected ComicPage? GetPageZip(string filepath, int page)
|
||||||
{
|
{
|
||||||
Debug.Assert(page >= 1, "Page number must be positive");
|
Debug.Assert(page >= 1, "Page number must be positive");
|
||||||
@@ -224,5 +231,4 @@ namespace ComiServ.Background
|
|||||||
Data: pageData
|
Data: pageData
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Configuration.Ini;
|
using Microsoft.Extensions.Configuration.Ini;
|
||||||
using Microsoft.OpenApi.Writers;
|
using Microsoft.OpenApi.Writers;
|
||||||
|
|
||||||
namespace ComiServ.Background
|
namespace ComiServ.Background;
|
||||||
{
|
|
||||||
public record class ComicScanItem
|
public record class ComicScanItem
|
||||||
(
|
(
|
||||||
string Filepath,
|
string Filepath,
|
||||||
long FileSizeBytes,
|
long FileSizeBytes,
|
||||||
Int64 Xxhash,
|
Int64 Xxhash,
|
||||||
int PageCount
|
int PageCount
|
||||||
);
|
);
|
||||||
public interface IComicScanner : IDisposable
|
public interface IComicScanner : IDisposable
|
||||||
{
|
{
|
||||||
//TODO should be configurable
|
//TODO should be configurable
|
||||||
public static readonly IReadOnlyList<string> COMIC_EXTENSIONS = [
|
public static readonly IReadOnlyList<string> COMIC_EXTENSIONS = [
|
||||||
"cbz", "zip",
|
"cbz", "zip",
|
||||||
@@ -29,11 +29,11 @@ namespace ComiServ.Background
|
|||||||
public void TriggerLibraryScan();
|
public void TriggerLibraryScan();
|
||||||
public void ScheduleRepeatedLibraryScans(TimeSpan period);
|
public void ScheduleRepeatedLibraryScans(TimeSpan period);
|
||||||
public IDictionary<string, ComicScanItem> PerfomLibraryScan(CancellationToken? token = null);
|
public IDictionary<string, ComicScanItem> PerfomLibraryScan(CancellationToken? token = null);
|
||||||
}
|
}
|
||||||
public class ComicScanner(
|
public class ComicScanner(
|
||||||
IServiceProvider provider
|
IServiceProvider provider
|
||||||
) : IComicScanner
|
) : IComicScanner
|
||||||
{
|
{
|
||||||
//private readonly ComicsContext _context = context;
|
//private readonly ComicsContext _context = context;
|
||||||
private readonly ITaskManager _manager = provider.GetRequiredService<ITaskManager>();
|
private readonly ITaskManager _manager = provider.GetRequiredService<ITaskManager>();
|
||||||
private readonly Configuration _config = provider.GetRequiredService<IConfigService>().Config;
|
private readonly Configuration _config = provider.GetRequiredService<IConfigService>().Config;
|
||||||
@@ -65,14 +65,14 @@ namespace ComiServ.Background
|
|||||||
}
|
}
|
||||||
public void TriggerLibraryScan()
|
public void TriggerLibraryScan()
|
||||||
{
|
{
|
||||||
TaskItem ti = new(
|
SyncTaskItem ti = new(
|
||||||
TaskTypes.Scan,
|
TaskTypes.Scan,
|
||||||
"Library Scan",
|
"Library Scan",
|
||||||
token =>
|
async token =>
|
||||||
{
|
{
|
||||||
var items = PerfomLibraryScan(token);
|
var items = PerfomLibraryScan(token);
|
||||||
token?.ThrowIfCancellationRequested();
|
token?.ThrowIfCancellationRequested();
|
||||||
UpdateDatabaseWithScanResults(items);
|
await UpdateDatabaseWithScanResults(items);
|
||||||
},
|
},
|
||||||
null);
|
null);
|
||||||
_manager.StartTask(ti);
|
_manager.StartTask(ti);
|
||||||
@@ -83,19 +83,19 @@ namespace ComiServ.Background
|
|||||||
RepeatedLibraryScanTokenSource?.Cancel();
|
RepeatedLibraryScanTokenSource?.Cancel();
|
||||||
RepeatedLibraryScanTokenSource?.Dispose();
|
RepeatedLibraryScanTokenSource?.Dispose();
|
||||||
RepeatedLibraryScanTokenSource = new();
|
RepeatedLibraryScanTokenSource = new();
|
||||||
TaskItem ti = new(
|
AsyncTaskItem ti = new(
|
||||||
TaskTypes.Scan,
|
TaskTypes.Scan,
|
||||||
"Scheduled Library Scan",
|
"Scheduled Library Scan",
|
||||||
token =>
|
async token =>
|
||||||
{
|
{
|
||||||
var items = PerfomLibraryScan(token);
|
var items = PerfomLibraryScan(token);
|
||||||
token?.ThrowIfCancellationRequested();
|
token?.ThrowIfCancellationRequested();
|
||||||
UpdateDatabaseWithScanResults(items);
|
await UpdateDatabaseWithScanResults(items);
|
||||||
},
|
},
|
||||||
RepeatedLibraryScanTokenSource.Token);
|
RepeatedLibraryScanTokenSource.Token);
|
||||||
_manager.ScheduleTask(ti, interval);
|
_manager.ScheduleTask(ti, interval);
|
||||||
}
|
}
|
||||||
public void UpdateDatabaseWithScanResults(IDictionary<string, ComicScanItem> items)
|
public async Task UpdateDatabaseWithScanResults(IDictionary<string, ComicScanItem> items)
|
||||||
{
|
{
|
||||||
using var scope = _provider.CreateScope();
|
using var scope = _provider.CreateScope();
|
||||||
var services = scope.ServiceProvider;
|
var services = scope.ServiceProvider;
|
||||||
@@ -135,30 +135,46 @@ namespace ComiServ.Background
|
|||||||
FileXxhash64 = p.Value.Xxhash,
|
FileXxhash64 = p.Value.Xxhash,
|
||||||
PageCount = p.Value.PageCount
|
PageCount = p.Value.PageCount
|
||||||
}).ToList();
|
}).ToList();
|
||||||
newComics.ForEach(c => _manager.StartTask(new(
|
//newComics.ForEach(c => _manager.StartTask(new(
|
||||||
|
// TaskTypes.GetCover,
|
||||||
|
// $"Get Cover: {c.Title}",
|
||||||
|
// token => InsertCover(Path.Join(_config.LibraryRoot, c.Filepath), c.FileXxhash64)
|
||||||
|
// )));
|
||||||
|
foreach (var comic in newComics)
|
||||||
|
{
|
||||||
|
_manager.StartTask((AsyncTaskItem)new(
|
||||||
TaskTypes.GetCover,
|
TaskTypes.GetCover,
|
||||||
$"Get Cover: {c.Title}",
|
$"Get Cover: {comic.Title}",
|
||||||
token => InsertCover(Path.Join(_config.LibraryRoot, c.Filepath), c.FileXxhash64)
|
token => InsertCover(Path.Join(_config.LibraryRoot, comic.Filepath), comic.FileXxhash64)
|
||||||
)));
|
));
|
||||||
newComics.ForEach(c => _manager.StartTask(new(
|
|
||||||
TaskTypes.MakeThumbnail,
|
|
||||||
$"Make Thumbnail: {c.Title}",
|
|
||||||
token => InsertThumbnail(c.Handle, Path.Join(_config.LibraryRoot, c.Filepath), 1)
|
|
||||||
)));
|
|
||||||
context.Comics.AddRange(newComics);
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
}
|
||||||
protected void InsertCover(string filepath, long hash)
|
//newComics.ForEach(c => _manager.StartTask(new(
|
||||||
|
// TaskTypes.MakeThumbnail,
|
||||||
|
// $"Make Thumbnail: {c.Title}",
|
||||||
|
// token => InsertThumbnail(c.Handle, Path.Join(_config.LibraryRoot, c.Filepath), 1)
|
||||||
|
// )));
|
||||||
|
foreach (var comic in newComics)
|
||||||
|
{
|
||||||
|
_manager.StartTask((AsyncTaskItem)new(
|
||||||
|
TaskTypes.MakeThumbnail,
|
||||||
|
$"Make Thumbnail: {comic.Title}",
|
||||||
|
token => InsertThumbnail(comic.Handle, Path.Join(_config.LibraryRoot, comic.Filepath), 1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
context.Comics.AddRange(newComics);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
protected async Task InsertCover(string filepath, long hash)
|
||||||
{
|
{
|
||||||
using var scope = _provider.CreateScope();
|
using var scope = _provider.CreateScope();
|
||||||
var services = scope.ServiceProvider;
|
var services = scope.ServiceProvider;
|
||||||
using var context = services.GetRequiredService<ComicsContext>();
|
using var context = services.GetRequiredService<ComicsContext>();
|
||||||
var existing = context.Covers.SingleOrDefault(c => c.FileXxhash64 == hash);
|
var existing = await context.Covers.SingleOrDefaultAsync(c => c.FileXxhash64 == hash);
|
||||||
//assuming no hash overlap
|
//assuming no hash overlap
|
||||||
//if you already have a cover, assume it's correct
|
//if you already have a cover, assume it's correct
|
||||||
if (existing is not null)
|
if (existing is not null)
|
||||||
return;
|
return;
|
||||||
var page = _analyzer.GetComicPage(filepath, 1);
|
var page = await _analyzer.GetComicPageAsync(filepath, 1);
|
||||||
if (page is null)
|
if (page is null)
|
||||||
return;
|
return;
|
||||||
Cover cover = new()
|
Cover cover = new()
|
||||||
@@ -169,12 +185,12 @@ namespace ComiServ.Background
|
|||||||
};
|
};
|
||||||
context.InsertOrIgnore(cover, true);
|
context.InsertOrIgnore(cover, true);
|
||||||
}
|
}
|
||||||
protected void InsertThumbnail(string handle, string filepath, int page = 1)
|
protected async Task InsertThumbnail(string handle, string filepath, int page = 1)
|
||||||
{
|
{
|
||||||
using var scope = _provider.CreateScope();
|
using var scope = _provider.CreateScope();
|
||||||
var services = scope.ServiceProvider;
|
var services = scope.ServiceProvider;
|
||||||
using var context = services.GetRequiredService<ComicsContext>();
|
using var context = services.GetRequiredService<ComicsContext>();
|
||||||
var comic = context.Comics.SingleOrDefault(c => c.Handle == handle);
|
var comic = await context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
|
||||||
if (comic?.ThumbnailWebp is null)
|
if (comic?.ThumbnailWebp is null)
|
||||||
return;
|
return;
|
||||||
var comicPage = _analyzer.GetComicPage(filepath, page);
|
var comicPage = _analyzer.GetComicPage(filepath, page);
|
||||||
@@ -182,12 +198,12 @@ namespace ComiServ.Background
|
|||||||
return;
|
return;
|
||||||
var converter = services.GetRequiredService<IPictureConverter>();
|
var converter = services.GetRequiredService<IPictureConverter>();
|
||||||
using var inStream = new MemoryStream(comicPage.Data);
|
using var inStream = new MemoryStream(comicPage.Data);
|
||||||
var outStream = converter.MakeThumbnail(inStream);
|
var outStream = await converter.MakeThumbnail(inStream);
|
||||||
comic.ThumbnailWebp = outStream.ReadAllBytes();
|
comic.ThumbnailWebp = outStream.ReadAllBytes();
|
||||||
}
|
}
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
RepeatedLibraryScanTokenSource?.Dispose();
|
RepeatedLibraryScanTokenSource?.Dispose();
|
||||||
}
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ComiServ.Services
|
namespace ComiServ.Services;
|
||||||
|
|
||||||
|
public class Configuration
|
||||||
{
|
{
|
||||||
public class Configuration
|
|
||||||
{
|
|
||||||
public string LibraryRoot { get; set; }
|
public string LibraryRoot { get; set; }
|
||||||
public string DatabaseFile { get; set; }
|
public string DatabaseFile { get; set; }
|
||||||
public double AutoScanPeriodHours { get; set; }
|
public double AutoScanPeriodHours { get; set; }
|
||||||
@@ -11,13 +11,13 @@ namespace ComiServ.Services
|
|||||||
=> MemberwiseClone() as Configuration
|
=> MemberwiseClone() as Configuration
|
||||||
//this really shouldn't be possible
|
//this really shouldn't be possible
|
||||||
?? throw new Exception("Failed to clone configuration");
|
?? throw new Exception("Failed to clone configuration");
|
||||||
}
|
}
|
||||||
public interface IConfigService
|
public interface IConfigService
|
||||||
{
|
{
|
||||||
public Configuration Config { get; }
|
public Configuration Config { get; }
|
||||||
}
|
}
|
||||||
public class JsonConfigService : IConfigService
|
public class JsonConfigService : IConfigService
|
||||||
{
|
{
|
||||||
public Configuration _Config;
|
public Configuration _Config;
|
||||||
//protect original
|
//protect original
|
||||||
public Configuration Config => _Config.Copy();
|
public Configuration Config => _Config.Copy();
|
||||||
@@ -27,5 +27,4 @@ namespace ComiServ.Services
|
|||||||
_Config = JsonSerializer.Deserialize<Configuration>(fileStream)
|
_Config = JsonSerializer.Deserialize<Configuration>(fileStream)
|
||||||
?? throw new ArgumentException("Failed to parse config file");
|
?? throw new ArgumentException("Failed to parse config file");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,27 +11,27 @@ using SixLabors.ImageSharp.Formats.Bmp;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
|
|
||||||
namespace ComiServ.Background
|
namespace ComiServ.Background;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum PictureFormats
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
||||||
public enum PictureFormats
|
|
||||||
{
|
|
||||||
Webp,
|
Webp,
|
||||||
Jpg,
|
Jpg,
|
||||||
Png,
|
Png,
|
||||||
Gif,
|
Gif,
|
||||||
Bmp,
|
Bmp,
|
||||||
}
|
}
|
||||||
//never closes stream!
|
//never closes stream!
|
||||||
public interface IPictureConverter
|
public interface IPictureConverter
|
||||||
{
|
{
|
||||||
public static System.Drawing.Size ThumbnailResolution => new(200, 320);
|
public static System.Drawing.Size ThumbnailResolution => new(200, 320);
|
||||||
public static PictureFormats ThumbnailFormat => PictureFormats.Webp;
|
public static PictureFormats ThumbnailFormat => PictureFormats.Webp;
|
||||||
//keeps aspect ratio, crops to horizontally to center, vertically to top
|
//keeps aspect ratio, crops to horizontally to center, vertically to top
|
||||||
//uses System.Drawing.Size so interface isn't dependant on ImageSharp
|
//uses System.Drawing.Size so interface isn't dependant on ImageSharp
|
||||||
public Stream Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null);
|
public Task<Stream> Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null);
|
||||||
public Stream ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null);
|
public Task<Stream> ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null);
|
||||||
public Stream MakeThumbnail(Stream image);
|
public Task<Stream> MakeThumbnail(Stream image);
|
||||||
public static string GetMime(PictureFormats format)
|
public static string GetMime(PictureFormats format)
|
||||||
{
|
{
|
||||||
switch (format)
|
switch (format)
|
||||||
@@ -50,10 +50,10 @@ namespace ComiServ.Background
|
|||||||
throw new ArgumentException("Cannot handle this format", nameof(format));
|
throw new ArgumentException("Cannot handle this format", nameof(format));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class ResharperPictureConverter(bool webpLossless = false)
|
public class ResharperPictureConverter(bool webpLossless = false)
|
||||||
: IPictureConverter
|
: IPictureConverter
|
||||||
{
|
{
|
||||||
public static IImageFormat ConvertFormatEnum(PictureFormats format)
|
public static IImageFormat ConvertFormatEnum(PictureFormats format)
|
||||||
{
|
{
|
||||||
switch (format)
|
switch (format)
|
||||||
@@ -73,7 +73,7 @@ namespace ComiServ.Background
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool WebpLossless { get; } = webpLossless;
|
public bool WebpLossless { get; } = webpLossless;
|
||||||
public Stream Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null)
|
public async Task<Stream> Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null)
|
||||||
{
|
{
|
||||||
using var img = Image.Load(image);
|
using var img = Image.Load(image);
|
||||||
IImageFormat format;
|
IImageFormat format;
|
||||||
@@ -110,15 +110,15 @@ namespace ComiServ.Background
|
|||||||
{
|
{
|
||||||
FileFormat = WebpLossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy
|
FileFormat = WebpLossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy
|
||||||
};
|
};
|
||||||
img.Save(outStream, enc);
|
await img.SaveAsync(outStream, enc);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
img.Save(outStream, format);
|
await img.SaveAsync(outStream, format);
|
||||||
}
|
}
|
||||||
return outStream;
|
return outStream;
|
||||||
}
|
}
|
||||||
public Stream ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null)
|
public async Task<Stream> ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null)
|
||||||
{
|
{
|
||||||
using Image img = Image.Load(image);
|
using Image img = Image.Load(image);
|
||||||
IImageFormat format;
|
IImageFormat format;
|
||||||
@@ -146,17 +146,16 @@ namespace ComiServ.Background
|
|||||||
{
|
{
|
||||||
FileFormat = WebpLossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy
|
FileFormat = WebpLossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy
|
||||||
};
|
};
|
||||||
img.Save(outStream, enc);
|
await img.SaveAsync(outStream, enc);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
img.Save(outStream, format);
|
await img.SaveAsync(outStream, format);
|
||||||
}
|
}
|
||||||
return outStream;
|
return outStream;
|
||||||
}
|
}
|
||||||
public Stream MakeThumbnail(Stream image)
|
public async Task<Stream> MakeThumbnail(Stream image)
|
||||||
{
|
{
|
||||||
return Resize(image, IPictureConverter.ThumbnailResolution, IPictureConverter.ThumbnailFormat);
|
return await Resize(image, IPictureConverter.ThumbnailResolution, IPictureConverter.ThumbnailFormat);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,68 @@
|
|||||||
using System.Collections.Concurrent;
|
using NuGet.Common;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace ComiServ.Background
|
namespace ComiServ.Services;
|
||||||
|
|
||||||
|
public enum TaskTypes
|
||||||
{
|
{
|
||||||
public enum TaskTypes
|
|
||||||
{
|
|
||||||
Scan,
|
Scan,
|
||||||
GetCover,
|
GetCover,
|
||||||
MakeThumbnail,
|
MakeThumbnail,
|
||||||
}
|
}
|
||||||
//task needs to use the token parameter rather than its own token, because it gets merged with the master token
|
public abstract class BaseTaskItem
|
||||||
public class TaskItem(TaskTypes type, string name, Action<CancellationToken?> action, CancellationToken? token = null)
|
{
|
||||||
|
public readonly TaskTypes Type;
|
||||||
|
public readonly string Name;
|
||||||
|
public readonly CancellationToken Token;
|
||||||
|
protected BaseTaskItem(TaskTypes type, string name, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
public readonly TaskTypes Type = type;
|
Type = type;
|
||||||
public readonly string Name = name;
|
Name = name;
|
||||||
public readonly Action<CancellationToken?> Action = action;
|
Token = token ?? CancellationToken.None;
|
||||||
public readonly CancellationToken Token = token ?? CancellationToken.None;
|
|
||||||
}
|
}
|
||||||
public interface ITaskManager : IDisposable
|
}
|
||||||
|
//task needs to use the token parameter rather than its own token, because it gets merged with the master token
|
||||||
|
public class SyncTaskItem
|
||||||
|
: BaseTaskItem
|
||||||
|
{
|
||||||
|
public readonly Action<CancellationToken?> Action;
|
||||||
|
public SyncTaskItem(TaskTypes type, string name, Action<CancellationToken?> action, CancellationToken? token = null)
|
||||||
|
: base(type, name, token)
|
||||||
{
|
{
|
||||||
public void StartTask(TaskItem taskItem);
|
Action = action;
|
||||||
public void ScheduleTask(TaskItem taskItem, TimeSpan interval);
|
}
|
||||||
|
}
|
||||||
|
public class AsyncTaskItem
|
||||||
|
: BaseTaskItem
|
||||||
|
{
|
||||||
|
public readonly Func<CancellationToken?, Task?> AsyncAction;
|
||||||
|
public AsyncTaskItem(TaskTypes type, string name, Func<CancellationToken?, Task?> asyncAction, CancellationToken? token = null)
|
||||||
|
: base(type, name, token)
|
||||||
|
{
|
||||||
|
AsyncAction = asyncAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public interface ITaskManager : IDisposable
|
||||||
|
{
|
||||||
|
public void StartTask(SyncTaskItem taskItem);
|
||||||
|
public void StartTask(AsyncTaskItem taskItem);
|
||||||
|
public void ScheduleTask(BaseTaskItem taskItem, TimeSpan interval);
|
||||||
public string[] GetTasks(int limit);
|
public string[] GetTasks(int limit);
|
||||||
public void CancelAll();
|
public void CancelAll();
|
||||||
}
|
}
|
||||||
public class TaskManager(ILogger<ITaskManager>? logger)
|
public class TaskManager(ILogger<ITaskManager>? logger)
|
||||||
: ITaskManager
|
: ITaskManager
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<Task, TaskItem> ActiveTasks = [];
|
private readonly ConcurrentDictionary<Task, BaseTaskItem> ActiveTasks = [];
|
||||||
private CancellationTokenSource MasterToken { get; set; } = new();
|
private CancellationTokenSource MasterToken { get; set; } = new();
|
||||||
private readonly ILogger<ITaskManager>? _logger = logger;
|
private readonly ILogger<ITaskManager>? _logger = logger;
|
||||||
private readonly ConcurrentDictionary<System.Timers.Timer,TaskItem> Scheduled = [];
|
private readonly ConcurrentDictionary<System.Timers.Timer, BaseTaskItem> Scheduled = [];
|
||||||
public void StartTask(TaskItem taskItem)
|
public void StartTask(SyncTaskItem taskItem)
|
||||||
{
|
{
|
||||||
_logger?.LogTrace($"Start Task: {taskItem.Name}");
|
//_logger?.LogTrace($"Start Task: {taskItem.Name}");
|
||||||
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
|
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
|
||||||
var newTask = Task.Run(() => taskItem.Action(tokenSource.Token),
|
var newTask = Task.Run(() => taskItem.Action(tokenSource.Token),
|
||||||
tokenSource.Token);
|
tokenSource.Token);
|
||||||
@@ -44,7 +74,20 @@ namespace ComiServ.Background
|
|||||||
//TODO should master token actually cancel followup?
|
//TODO should master token actually cancel followup?
|
||||||
newTask.ContinueWith(ManageFinishedTasks, MasterToken.Token);
|
newTask.ContinueWith(ManageFinishedTasks, MasterToken.Token);
|
||||||
}
|
}
|
||||||
public void ScheduleTask(TaskItem taskItem, TimeSpan interval)
|
public void StartTask(AsyncTaskItem taskItem)
|
||||||
|
{
|
||||||
|
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
|
||||||
|
var newTask = Task.Run(() => taskItem.AsyncAction(tokenSource.Token),
|
||||||
|
tokenSource.Token);
|
||||||
|
if (!ActiveTasks.TryAdd(newTask, taskItem))
|
||||||
|
{
|
||||||
|
//TODO better exception
|
||||||
|
throw new Exception("failed to add task");
|
||||||
|
}
|
||||||
|
//TODO should master token actually cancel followup?
|
||||||
|
newTask.ContinueWith(ManageFinishedTasks, MasterToken.Token);
|
||||||
|
}
|
||||||
|
public void ScheduleTask(SyncTaskItem taskItem, TimeSpan interval)
|
||||||
{
|
{
|
||||||
//var timer = new Timer((_) => StartTask(taskItem), null, dueTime, period ?? Timeout.InfiniteTimeSpan);
|
//var timer = new Timer((_) => StartTask(taskItem), null, dueTime, period ?? Timeout.InfiniteTimeSpan);
|
||||||
var timer = new System.Timers.Timer(interval);
|
var timer = new System.Timers.Timer(interval);
|
||||||
@@ -58,6 +101,27 @@ namespace ComiServ.Background
|
|||||||
timer.Elapsed += (_, _) => taskItem.Action(token.Token);
|
timer.Elapsed += (_, _) => taskItem.Action(token.Token);
|
||||||
timer.Start();
|
timer.Start();
|
||||||
}
|
}
|
||||||
|
public void ScheduleTask(BaseTaskItem taskItem, TimeSpan interval)
|
||||||
|
{
|
||||||
|
var timer = new System.Timers.Timer(interval);
|
||||||
|
var token = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
|
||||||
|
Scheduled.TryAdd(timer, taskItem);
|
||||||
|
token.Token.Register(() =>
|
||||||
|
{
|
||||||
|
timer.Stop();
|
||||||
|
Scheduled.TryRemove(timer, out var _);
|
||||||
|
});
|
||||||
|
if (taskItem is AsyncTaskItem ati)
|
||||||
|
timer.Elapsed += async (_, _) =>
|
||||||
|
{
|
||||||
|
var task = ati.AsyncAction(token.Token);
|
||||||
|
if (task != null)
|
||||||
|
await task;
|
||||||
|
};
|
||||||
|
else if (taskItem is SyncTaskItem sti)
|
||||||
|
timer.Elapsed += (_, _) => sti.Action(token.Token);
|
||||||
|
timer.Start();
|
||||||
|
}
|
||||||
public string[] GetTasks(int limit)
|
public string[] GetTasks(int limit)
|
||||||
{
|
{
|
||||||
return ActiveTasks.Select(p => p.Value.Name).Take(limit).ToArray();
|
return ActiveTasks.Select(p => p.Value.Name).Take(limit).ToArray();
|
||||||
@@ -87,7 +151,7 @@ namespace ComiServ.Background
|
|||||||
bool taskRemoved = ActiveTasks.TryRemove(pair.Key, out _);
|
bool taskRemoved = ActiveTasks.TryRemove(pair.Key, out _);
|
||||||
if (taskRemoved)
|
if (taskRemoved)
|
||||||
{
|
{
|
||||||
_logger?.LogTrace($"Removed Task: {pair.Value.Name}");
|
_logger?.LogTrace("Removed Task: {TaskName}", pair.Value.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +160,6 @@ namespace ComiServ.Background
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
MasterToken?.Dispose();
|
MasterToken?.Dispose();
|
||||||
}
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user