CBL Import Rework (#1862)

* Fixed a typo in a log

* Invalid XML files now "validate" correctly by sending back a failure.

* Cleaned up messaging on backend and frontend to provide some linking on series name when collision, handle corrupt xml files, etc.

* When reading list conflict occurs, show the reading list name that's conflicting. Started refactoring the code to allow multiple files to be imported at once.

* Started adding new CBL elements for some enhancements I have planned with maintainers.

* Default to empty string for IpAddress to allow to fallback into existing experience

* Tweaked the layout of reading list page (not complete), moved some not used much controls to page extras and reordered the buttons for reading list

* Edit Reading Lists now allows selection of cover image from existing items

* Fixed a bug where cover chooser base64 to image would fail to write webp files.

* Refactored the validate step to now handle multiple files in one go.

* Clean up code

* Don't show CBL name if there were xml errors that prevented showing it

* Don't allow user to go prev step after they perform the import.

* Cleaned up the heading code for accordions

* Fixed a bug with import keeping failed items

* Sort the failures to the bottom of result windows

* CBL import is pretty solid. Need one pass from Robbie on Reading List Page
This commit is contained in:
Joe Milazzo 2023-03-07 15:18:26 -06:00 committed by GitHub
parent c846b36047
commit b55d9e3994
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 609 additions and 249 deletions

View file

@ -1,4 +1,6 @@
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using API.DTOs.ReadingLists.CBL;
using API.Extensions;
@ -32,10 +34,43 @@ public class CblController : BaseApiController
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file)
{
var userId = User.GetUserId();
var cbl = await SaveAndLoadCblFile(userId, file);
var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
return Ok(importSummary);
try
{
var cbl = await SaveAndLoadCblFile(file);
var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
importSummary.FileName = file.FileName;
return Ok(importSummary);
}
catch (ArgumentNullException)
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
new CblBookResult()
{
Reason = CblImportReason.InvalidFile
}
}
});
}
catch (InvalidOperationException)
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
new CblBookResult()
{
Reason = CblImportReason.InvalidFile
}
}
});
}
}
@ -48,13 +83,47 @@ public class CblController : BaseApiController
[HttpPost("import")]
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
{
var userId = User.GetUserId();
var cbl = await SaveAndLoadCblFile(userId, file);
try
{
var userId = User.GetUserId();
var cbl = await SaveAndLoadCblFile(file);
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun);
importSummary.FileName = file.FileName;
return Ok(importSummary);
} catch (ArgumentNullException)
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
new CblBookResult()
{
Reason = CblImportReason.InvalidFile
}
}
});
}
catch (InvalidOperationException)
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
new CblBookResult()
{
Reason = CblImportReason.InvalidFile
}
}
});
}
return Ok(await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun));
}
private async Task<CblReadingList> SaveAndLoadCblFile(int userId, IFormFile file)
private async Task<CblReadingList> SaveAndLoadCblFile(IFormFile file)
{
var filename = Path.GetRandomFileName();
var outputFile = Path.Join(_directoryService.TempDirectory, filename);

View file

@ -506,45 +506,6 @@ public class ReaderController : BaseApiController
return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId));
}
/// <summary>
/// Marks every chapter that is sorted below the passed number as Read. This will not mark any specials as read.
/// </summary>
/// <remarks>This is built for Tachiyomi and is not expected to be called by any other place</remarks>
/// <returns></returns>
[Obsolete("Deprecated. Use 'Tachiyomi/mark-chapter-until-as-read'")]
[HttpPost("mark-chapter-until-as-read")]
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
if (user == null) return Unauthorized();
user.Progresses ??= new List<AppUserProgress>();
// Tachiyomi sends chapter 0.0f when there's no chapters read.
// Due to the encoding for volumes this marks all chapters in volume 0 (loose chapters) as read so we ignore it
if (chapterNumber == 0.0f) return true;
if (chapterNumber < 1.0f)
{
// This is a hack to track volume number. We need to map it back by x100
var volumeNumber = int.Parse($"{chapterNumber * 100f}");
await _readerService.MarkVolumesUntilAsRead(user, seriesId, volumeNumber);
}
else
{
await _readerService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber);
}
_unitOfWork.UserRepository.Update(user);
if (!_unitOfWork.HasChanges()) return Ok(true);
if (await _unitOfWork.CommitAsync()) return Ok(true);
await _unitOfWork.RollbackAsync();
return Ok(false);
}
/// <summary>
/// Returns a list of bookmarked pages for a given Chapter
/// </summary>

View file

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.ReadingLists;
using API.Extensions;
using API.Helpers;
@ -421,6 +423,18 @@ public class ReadingListController : BaseApiController
return Ok("Nothing to do");
}
/// <summary>
/// Returns a list of characters associated with the reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("characters")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
public ActionResult<IEnumerable<PersonDto>> GetCharactersForList(int readingListId)
{
return Ok(_unitOfWork.ReadingListRepository.GetReadingListCharactersAsync(readingListId));
}
/// <summary>

View file

@ -80,7 +80,7 @@ public class SettingsController : BaseApiController
{
_logger.LogInformation("{UserName} is resetting IP Addresses Setting", User.GetUsername());
var ipAddresses = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses);
ipAddresses.Value = Configuration.DefaultIPAddresses;
ipAddresses.Value = Configuration.DefaultIpAddresses;
_unitOfWork.SettingsRepository.Update(ipAddresses);
if (!await _unitOfWork.CommitAsync())