Shakeout Part 2 (#630)

* When flattening directories, ensure the order or directories being enumerated follows a natural sort. Some users are discovering directories in a different order than other machines.

* Added a case for volume parsing and fixed a poorly designed negative lookahead.

Added a sentence case pipe for formatting things.

Added time for all dates.

* Some more sentence case

* Register user now has a white input

* Fixed an issue with Manga up/down reading mode where top of the page was going forwards, when it should have gone backwards

* Reworked some code to ensure that scanseries doesn't show errors where in fact there was just nothing to update.

* Last updated should be working as intended for new library flow.

* Code smell
This commit is contained in:
Joseph Milazzo 2021-10-03 15:40:22 -07:00 committed by GitHub
parent fcfc9e5159
commit 3907414ae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 88 additions and 37 deletions

View File

@ -10,5 +10,12 @@ namespace API.Tests.Parser
{ {
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
} }
[Theory]
[InlineData("Harrison, Kim - Dates from Hell - Hollows Vol 2.5.epub", "2.5")]
public void ParseVolumeTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
}
} }
} }

View File

@ -239,6 +239,7 @@ namespace API.Tests.Parser
[InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")] [InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")]
[InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")] [InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")]
[InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")] [InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")]
[InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename)); Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
@ -425,6 +426,14 @@ namespace API.Tests.Parser
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });
filepath = @"E:\Manga\Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub";
expected.Add(filepath, new ParserInfo
{
Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "",
Chapters = "0", Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub,
FullFilePath = filepath, IsSpecial = false
});
// If an image is cover exclusively, ignore it // If an image is cover exclusively, ignore it
filepath = @"E:\Manga\Seraph of the End\cover.png"; filepath = @"E:\Manga\Seraph of the End\cover.png";
expected.Add(filepath, null); expected.Add(filepath, null);

View File

@ -76,7 +76,8 @@ namespace API.Extensions
directoryIndex++; directoryIndex++;
} }
foreach (var subDirectory in directory.EnumerateDirectories()) var sort = new NaturalSortComparer();
foreach (var subDirectory in directory.EnumerateDirectories().OrderBy(d => d.FullName, sort))
{ {
FlattenDirectory(root, subDirectory, ref directoryIndex); FlattenDirectory(root, subDirectory, ref directoryIndex);
} }

View File

@ -470,7 +470,7 @@ namespace API.Parser
RegexTimeout), RegexTimeout),
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
new Regex( new Regex(
@"^(?!Vol)(?<Series>.+?)\s(?<!vol\. )(?<Chapter>\d+(?:.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", @"^(?!Vol)(?<Series>.+?)(?<!Vol)\.?\s(?<Chapter>\d+(?:.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
MatchOptions, MatchOptions,
RegexTimeout), RegexTimeout),
// Tower Of God S01 014 (CBT) (digital).cbz // Tower Of God S01 014 (CBT) (digital).cbz

View File

@ -72,9 +72,18 @@ namespace API.Services.Tasks
(await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)).Any(m => File.Exists(m.FilePath)); (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)).Any(m => File.Exists(m.FilePath));
if (!anyFilesExist) if (!anyFilesExist)
{
try
{ {
_unitOfWork.SeriesRepository.Remove(series); _unitOfWork.SeriesRepository.Remove(series);
await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds, token); await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "There was an error during ScanSeries to delete the series");
await _unitOfWork.RollbackAsync();
}
} }
else else
{ {
@ -97,7 +106,7 @@ namespace API.Services.Tasks
} }
} }
_logger.LogInformation("{SeriesName} has bad naming convention, forcing rescan at a higher directory.", series.OriginalName); _logger.LogInformation("{SeriesName} has bad naming convention, forcing rescan at a higher directory", series.OriginalName);
scanner = new ParseScannedFiles(_bookService, _logger); scanner = new ParseScannedFiles(_bookService, _logger);
parsedSeries = scanner.ScanLibrariesForSeries(library.Type, dirs.Keys, out var totalFiles2, out var scanElapsedTime2); parsedSeries = scanner.ScanLibrariesForSeries(library.Type, dirs.Keys, out var totalFiles2, out var scanElapsedTime2);
totalFiles += totalFiles2; totalFiles += totalFiles2;
@ -109,8 +118,19 @@ namespace API.Services.Tasks
// At this point, parsedSeries will have at least one key and we can perform the update. If it still doesn't, just return and don't do anything // At this point, parsedSeries will have at least one key and we can perform the update. If it still doesn't, just return and don't do anything
if (parsedSeries.Count == 0) return; if (parsedSeries.Count == 0) return;
try
{
UpdateSeries(series, parsedSeries); UpdateSeries(series, parsedSeries);
await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds, token); await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "There was an error during ScanSeries to update the series");
await _unitOfWork.RollbackAsync();
}
// Tell UI that this series is done
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId, series.Name),
cancellationToken: token);
} }
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series) private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
@ -124,10 +144,11 @@ namespace API.Services.Tasks
} }
private async Task CommitAndSend(int libraryId, int seriesId, int totalFiles, private async Task CommitAndSend(int libraryId, int seriesId, int totalFiles,
Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series, int[] chapterIds, CancellationToken token) Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series, int[] chapterIds)
{ {
if (await _unitOfWork.CommitAsync()) if (_unitOfWork.HasChanges())
{ {
await _unitOfWork.CommitAsync();
_logger.LogInformation( _logger.LogInformation(
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}", "Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
@ -135,15 +156,6 @@ namespace API.Services.Tasks
await CleanupDbEntities(); await CleanupDbEntities();
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, false)); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, false));
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
// Tell UI that this series is done
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId, series.Name),
cancellationToken: token);
}
else
{
_logger.LogCritical(
"There was a critical error that resulted in a failed scan. Please check logs and rescan");
await _unitOfWork.RollbackAsync();
} }
} }

View File

@ -1,6 +1,6 @@
<form [formGroup]="resetPasswordForm"> <form [formGroup]="resetPasswordForm">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Reset {{member.username | titlecase}}'s Password</h4> <h4 class="modal-title" id="modal-basic-title">Reset {{member.username | sentenceCase}}'s Password</h4>
<button type="button" class="close" aria-label="Close" (click)="close()"> <button type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>

View File

@ -3,7 +3,7 @@
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills"> <ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills">
<li *ngFor="let tab of tabs" [ngbNavItem]="tab"> <li *ngFor="let tab of tabs" [ngbNavItem]="tab">
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | titlecase }}</a> <a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container *ngIf="tab.fragment === ''"> <ng-container *ngIf="tab.fragment === ''">
<app-manage-settings></app-manage-settings> <app-manage-settings></app-manage-settings>

View File

@ -4,7 +4,7 @@
<div class="col-4"><button class="btn btn-primary float-right" (click)="addLibrary()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden">&nbsp;Add Library</span></button></div> <div class="col-4"><button class="btn btn-primary float-right" (click)="addLibrary()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden">&nbsp;Add Library</span></button></div>
</div> </div>
<ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary"> <ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary">
<li *ngFor="let library of libraries; let idx = index;" class="list-group-item"> <li *ngFor="let library of libraries; let idx = index; trackby: trackbyLibrary" class="list-group-item">
<div> <div>
<h4> <h4>
<span id="library-name--{{idx}}">{{library.name}}</span>&nbsp; <span id="library-name--{{idx}}">{{library.name}}</span>&nbsp;
@ -13,8 +13,8 @@
</div> </div>
<div class="float-right"> <div class="float-right">
<button class="btn btn-secondary mr-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" attr.aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button> <button class="btn btn-secondary mr-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" attr.aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button>
<button class="btn btn-danger mr-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | titlecase}}"></i></button> <button class="btn btn-danger mr-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | sentenceCase}}"></i></button>
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{library.name | titlecase}}"></i></button> <button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{library.name | sentenceCase}}"></i></button>
</div> </div>
</h4> </h4>
</div> </div>

View File

@ -25,6 +25,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
*/ */
deletionInProgress: boolean = false; deletionInProgress: boolean = false;
scanInProgress: {[key: number]: boolean} = {}; scanInProgress: {[key: number]: boolean} = {};
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();

View File

@ -19,7 +19,7 @@
<div>Last Active: <div>Last Active:
<span *ngIf="member.lastActive == '0001-01-01T00:00:00'; else activeDate">Never</span> <span *ngIf="member.lastActive == '0001-01-01T00:00:00'; else activeDate">Never</span>
<ng-template #activeDate> <ng-template #activeDate>
{{member.lastActive | date: 'MM/dd/yyyy'}} {{member.lastActive | date: 'short'}}
</ng-template> </ng-template>
</div> </div>
<div *ngIf="!member.isAdmin">Sharing: {{formatLibraries(member)}}</div> <div *ngIf="!member.isAdmin">Sharing: {{formatLibraries(member)}}</div>

View File

@ -19,7 +19,7 @@
</div> </div>
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col" *ngIf="utilityService.isVolume(data)"> <div class="col" *ngIf="utilityService.isVolume(data)">
Added: {{(data.created | date: 'MM/dd/yyyy') || '-'}} Added: {{(data.created | date: 'short') || '-'}}
</div> </div>
<div class="col"> <div class="col">
Pages: {{data.pages}} Pages: {{data.pages}}

View File

@ -95,7 +95,7 @@
<ng-template ngbNavContent> <ng-template ngbNavContent>
<h4>Information</h4> <h4>Information</h4>
<div class="row no-gutters mb-2"> <div class="row no-gutters mb-2">
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | titlecase}}</div> <div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
<div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div> <div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div>
</div> </div>
<h4>Volumes</h4> <h4>Volumes</h4>
@ -110,10 +110,10 @@
<div> <div>
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
Created: {{volume.created | date: 'MM/dd/yyyy'}} Created: {{volume.created | date: 'short'}}
</div> </div>
<div class="col"> <div class="col">
Last Modified: {{volume.lastModified | date: 'MM/dd/yyyy'}} Last Modified: {{volume.lastModified | date: 'short'}}
</div> </div>
</div> </div>
<div class="row no-gutters"> <div class="row no-gutters">

View File

@ -38,6 +38,6 @@
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables> <app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
</span> </span>
</div> </div>
<a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!supressLibraryLink && libraryName">{{libraryName}}</a> <a class="card-title library" [routerLink]="['/library', libraryId]" routerLinkActive="router-link-active" *ngIf="!supressLibraryLink && libraryName">{{libraryName | sentenceCase}}</a>
</div> </div>
</div> </div>

View File

@ -31,8 +31,8 @@
<app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages - 1" [urlProvider]="getPageUrl" (loadNextChapter)="loadNextChapter()" (loadPrevChapter)="loadPrevChapter()"></app-infinite-scroller> <app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages - 1" [urlProvider]="getPageUrl" (loadNextChapter)="loadNextChapter()" (loadPrevChapter)="loadPrevChapter()"></app-infinite-scroller>
</div> </div>
<ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD"> <ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD">
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'top'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div> <div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div>
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'bottom'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div> <div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div>
</ng-container> </ng-container>
</div> </div>

View File

@ -63,7 +63,7 @@
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown> <div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
<button class="btn btn-outline-secondary primary-text" ngbDropdownToggle> <button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>
{{user.username | titlecase}} {{user.username | sentenceCase}}
</button> </button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<a ngbDropdownItem routerLink="/preferences/">User Settings</a> <a ngbDropdownItem routerLink="/preferences/">User Settings</a>

View File

@ -11,3 +11,7 @@
background-color: #343c59; background-color: #343c59;
box-shadow: 0 0 0 0.2rem rgb(68 79 117 / 50%); box-shadow: 0 0 0 0.2rem rgb(68 79 117 / 50%);
} }
input {
background-color: #fff !important;
}

View File

@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sentenceCase'
})
export class SentenceCasePipe implements PipeTransform {
transform(value: string | null): string {
if (value === null || value === undefined) return '';
return value.charAt(0).toUpperCase() + value.substr(1);
}
}

View File

@ -15,6 +15,7 @@ import { SeriesFormatComponent } from './series-format/series-format.component';
import { UpdateNotificationModalComponent } from './update-notification/update-notification-modal.component'; import { UpdateNotificationModalComponent } from './update-notification/update-notification-modal.component';
import { CircularLoaderComponent } from './circular-loader/circular-loader.component'; import { CircularLoaderComponent } from './circular-loader/circular-loader.component';
import { NgCircleProgressModule } from 'ng-circle-progress'; import { NgCircleProgressModule } from 'ng-circle-progress';
import { SentenceCasePipe } from './sentence-case.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -29,6 +30,7 @@ import { NgCircleProgressModule } from 'ng-circle-progress';
SeriesFormatComponent, SeriesFormatComponent,
UpdateNotificationModalComponent, UpdateNotificationModalComponent,
CircularLoaderComponent, CircularLoaderComponent,
SentenceCasePipe,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -40,6 +42,7 @@ import { NgCircleProgressModule } from 'ng-circle-progress';
exports: [ exports: [
RegisterMemberComponent, RegisterMemberComponent,
SafeHtmlPipe, SafeHtmlPipe,
SentenceCasePipe,
ReadMoreComponent, ReadMoreComponent,
DrawerComponent, DrawerComponent,
TagBadgeComponent, TagBadgeComponent,

View File

@ -15,7 +15,7 @@
<div class="col align-self-center card p-3 m-3" style="width: 12rem;"> <div class="col align-self-center card p-3 m-3" style="width: 12rem;">
<span tabindex="0" (click)="select(member)" a11y-click="13,32"> <span tabindex="0" (click)="select(member)" a11y-click="13,32">
<div class="logo-container"> <div class="logo-container">
<h3 class="card-title text-center">{{member | titlecase}}</h3> <h3 class="card-title text-center">{{member | sentenceCase}}</h3>
</div> </div>
</span> </span>

View File

@ -55,10 +55,6 @@
.card-text { .card-text {
font-family: "EBGaramond", "Helvetica Neue", sans-serif; font-family: "EBGaramond", "Helvetica Neue", sans-serif;
input {
background-color: #fff;
}
} }
.alt { .alt {
@ -81,3 +77,7 @@
display: inline-block; display: inline-block;
color: #343c59; color: #343c59;
} }
input {
background-color: #fff !important;
}