Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post Release Shakeout (Hotfix Testing) #1200

Merged
merged 12 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 96 additions & 1 deletion API.Tests/Parser/DefaultParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, s
[Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
[InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!~1~2")]
[InlineData("/manga/Monster #8/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster #8~0~1")]
[InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster~0~1")]
public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string expectedParseInfo)
{
const string rootDirectory = "/manga/";
Expand All @@ -56,6 +56,27 @@ public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string in
Assert.Equal(tokens[2], actual.Chapters);
}

[Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")]
[InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")]
[InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")]
[InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")]
[InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")]
[InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")]
[InlineData("/manga/Btooom!/Specials/Just a special SP01.cbz", "Btooom!")]
public void ParseFromFallbackFolders_ShouldUseExistingSeriesName(string inputFile, string expectedParseInfo)
{
const string rootDirectory = "/manga/";
var fs = new MockFileSystem();
fs.AddDirectory(rootDirectory);
fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new DefaultParser(ds);
var actual = parser.Parse(inputFile, rootDirectory);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series);
}

#endregion


Expand Down Expand Up @@ -243,6 +264,80 @@ public void Parse_ParseInfo_Manga()
}
}

[Fact]
public void Parse_ParseInfo_Manga_WithSpecialsFolder()
{
const string rootPath = @"E:/Manga/";
var filesystem = new MockFileSystem();
filesystem.AddDirectory("E:/Manga");
filesystem.AddDirectory("E:/Foo 50");
filesystem.AddDirectory("E:/Foo 50/Specials");
filesystem.AddFile(@"E:/Manga/Foo 50/Foo 50 v1.cbz", new MockFileData(""));
filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData(""));

var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var parser = new DefaultParser(ds);

var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz";
// There is a bad parse for series like "Foo 50", so we have parsed chapter as 50
var expected = new ParserInfo
{
Series = "Foo 50", Volumes = "1",
Chapters = "50", Filename = "Foo 50 v1.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
};

var actual = parser.Parse(filepath, rootPath);

Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expected.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expected.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expected.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expected.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expected.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expected.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expected.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
Assert.Equal(expected.IsSpecial, actual.IsSpecial);
_testOutputHelper.WriteLine("IsSpecial ✓");

filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz";
expected = new ParserInfo
{
Series = "Foo 50", Volumes = "0", IsSpecial = true,
Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
};

actual = parser.Parse(filepath, rootPath);
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expected.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expected.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expected.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expected.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expected.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expected.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expected.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
Assert.Equal(expected.IsSpecial, actual.IsSpecial);
_testOutputHelper.WriteLine("IsSpecial ✓");

}

[Fact]
public void Parse_ParseInfo_Comic()
{
Expand Down
11 changes: 9 additions & 2 deletions API/Controllers/SeriesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,20 @@ public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)

if (series == null) return BadRequest("Series does not exist");

if (series.Name != updateSeries.Name && await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name, series.Format))
var seriesExists =
await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId,
series.Format);
if (series.Name != updateSeries.Name && seriesExists)
{
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
}

series.Name = updateSeries.Name.Trim();
series.SortName = updateSeries.SortName.Trim();
if (!string.IsNullOrEmpty(updateSeries.SortName.Trim()))
{
series.SortName = updateSeries.SortName.Trim();
}

series.LocalizedName = updateSeries.LocalizedName.Trim();

series.NameLocked = updateSeries.NameLocked;
Expand Down
21 changes: 8 additions & 13 deletions API/Data/Repositories/SeriesRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public interface ISeriesRepository
void Update(Series series);
void Remove(Series series);
void Remove(IEnumerable<Series> series);
Task<bool> DoesSeriesNameExistInLibrary(string name, MangaFormat format);
Task<bool> DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format);
/// <summary>
/// Adds user information like progress, ratings, etc
/// </summary>
Expand Down Expand Up @@ -135,17 +135,12 @@ public void Remove(IEnumerable<Series> series)
/// <param name="name">Name of series</param>
/// <param name="format">Format of series</param>
/// <returns></returns>
public async Task<bool> DoesSeriesNameExistInLibrary(string name, MangaFormat format)
public async Task<bool> DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format)
{
var libraries = _context.Series
.AsNoTracking()
.Where(x => x.Name.Equals(name) && x.Format == format)
.Select(s => s.LibraryId);

return await _context.Series
.AsNoTracking()
.Where(s => libraries.Contains(s.LibraryId) && s.Name.Equals(name) && s.Format == format)
.CountAsync() > 1;
.Where(s => s.LibraryId == libraryId && s.Name.Equals(name) && s.Format == format)
.AnyAsync();
}


Expand Down Expand Up @@ -624,13 +619,13 @@ public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, U
LastReadingProgress = _context.AppUserProgresses
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
.Max(p => p.LastModified),
// BUG: This is only taking into account chapters that have progress on them, not all chapters in said series
LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created),
//LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created)
// This is only taking into account chapters that have progress on them, not all chapters in said series
//LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created),
LastChapterCreated = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created)
});
if (cutoffOnDate)
{
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint);
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterCreated >= cutoffProgressPoint);
}

// I think I need another Join statement. The problem is the chapters are still limited to progress
Expand Down
2 changes: 1 addition & 1 deletion API/Entities/Metadata/SeriesMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class SeriesMetadata : IHasConcurrencyToken
{
public int Id { get; set; }

public string Summary { get; set; }
public string Summary { get; set; } = string.Empty;

public ICollection<CollectionTag> CollectionTags { get; set; }

Expand Down
26 changes: 15 additions & 11 deletions API/Parser/DefaultParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,22 @@ public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryTy
}
}

var series = Parser.ParseSeries(folder);

if ((string.IsNullOrEmpty(series) && i == fallbackFolders.Count - 1))
{
ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic);
break;
}

if (!string.IsNullOrEmpty(series))
// Generally users group in series folders. Let's try to parse series from the top folder
if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1)
{
ret.Series = series;
break;
var series = Parser.ParseSeries(folder);

if (string.IsNullOrEmpty(series))
{
ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic);
break;
}

if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) || !folder.Contains(ret.Series)))
{
ret.Series = series;
break;
}
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions API/Services/SeriesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSerie
series.Metadata.PublicationStatusLocked = true;
}

// This shouldn't be needed post v0.5.3 release
if (string.IsNullOrEmpty(series.Metadata.Summary))
{
series.Metadata.Summary = string.Empty;
}

if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary))
{
updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty;
}

if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim())
{
series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim();
Expand Down
6 changes: 5 additions & 1 deletion API/Services/Tasks/ScannerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,17 @@ private async Task UpdateSeries(Series series, Dictionary<ParsedSeries, List<Par
series.Format = parsedInfos[0].Format;
}
series.OriginalName ??= parsedInfos[0].Series;
if (string.IsNullOrEmpty(series.SortName))
{
series.SortName = series.Name;
}
if (!series.SortNameLocked)
{
series.SortName = series.Name;
if (!string.IsNullOrEmpty(parsedInfos[0].SeriesSort))
{
series.SortName = parsedInfos[0].SeriesSort;
}
series.SortName = series.Name;
}

await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name));
Expand Down
7 changes: 1 addition & 6 deletions API/Services/Tasks/VersionUpdaterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,7 @@ private UpdateNotificationDto CreateDto(GithubReleaseMetadata update)
{
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty));
var currentVersion = BuildInfo.Version.ToString();

if (updateVersion.Revision == -1)
{
currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal));
}
var currentVersion = BuildInfo.Version.ToString(4);

return new UpdateNotificationDto()
{
Expand Down
16 changes: 0 additions & 16 deletions UI/Web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions UI/Web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
"@fortawesome/fontawesome-free": "^6.0.0",
"@microsoft/signalr": "^6.0.2",
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
"@ngx-lite/nav-drawer": "^0.4.7",
"@ngx-lite/util": "0.0.1",
"@popperjs/core": "^2.11.2",
"@types/file-saver": "^2.0.5",
"bootstrap": "^5.1.2",
Expand Down
5 changes: 5 additions & 0 deletions UI/Web/src/app/_interceptors/error.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export class ErrorInterceptor implements HttpInterceptor {
}

private handleAuthError(error: any) {

// Special hack for register url, to not care about auth
if (location.href.includes('/registration/confirm-email?token=')) {
return;
}
// NOTE: Signin has error.error or error.statusText available.
// if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334
this.accountService.logout();
Expand Down
11 changes: 6 additions & 5 deletions UI/Web/src/app/announcements/changelog/changelog.component.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<div class="changelog">
<p class="pb-2">If you do not see an <span class="badge bg-secondary">Installed</span> tag, you are on a nightly release. Only major versions will show as available.</p>
<ng-container *ngFor="let update of updates; let indx = index;">
<div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body">
<h4 class="card-title">{{update.updateTitle}}&nbsp;
<span class="badge bg-secondary" *ngIf="update.updateVersion === installedVersion">Installed</span>
<span class="badge bg-secondary" *ngIf="update.updateVersion > installedVersion">Available</span>
<span class="badge bg-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span>
<span class="badge bg-secondary" *ngIf="update.updateVersion > update.currentVersion">Available</span>
</h4>
<h6 class="card-subtitle mb-2 text-muted">Published: {{update.publishDate | date: 'short'}}</h6>
<h6 class="card-subtitle mb-1 mt-1 text-muted">Published: {{update.publishDate | date: 'short'}}</h6>


<pre class="card-text update-body">
<app-read-more [text]="update.updateBody" [maxLength]="500"></app-read-more>
</pre>
<a *ngIf="!update.isDocker && update.updateVersion === installedVersion" href="{{update.updateUrl}}" class="btn disabled btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank">Installed</a>
<a *ngIf="!update.isDocker && update.updateVersion !== installedVersion" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank">Download</a>
<a *ngIf="!update.isDocker && update.updateVersion === update.currentVersion" href="{{update.updateUrl}}" class="btn disabled btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank">Installed</a>
<a *ngIf="!update.isDocker && update.updateVersion !== update.currentVersion" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank">Download</a>
</div>
</div>
</ng-container>
Expand Down
15 changes: 3 additions & 12 deletions UI/Web/src/app/announcements/changelog/changelog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,14 @@ export class ChangelogComponent implements OnInit {

updates: Array<UpdateVersionEvent> = [];
isLoading: boolean = true;
installedVersion: string = '';

constructor(private serverService: ServerService) { }

ngOnInit(): void {

this.serverService.getServerInfo().subscribe(info => {
this.installedVersion = info.kavitaVersion;
this.serverService.getChangelog().subscribe(updates => {
this.updates = updates;
this.isLoading = false;

if (this.updates.filter(u => u.updateVersion === this.installedVersion).length === 0) {
// User is on a nightly version. Tell them the last stable is installed
this.installedVersion = this.updates[0].updateVersion;
}
});
this.serverService.getChangelog().subscribe(updates => {
this.updates = updates;
this.isLoading = false;
});


Expand Down
2 changes: 1 addition & 1 deletion UI/Web/src/app/nav-header/nav-header.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<nav class="navbar navbar-expand-md navbar-dark fixed-top" *ngIf="navService?.navbarVisible$ | async">
<div class="container-fluid">
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
<a class="side-nav-toggle" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
<a class="side-nav-toggle" *ngIf="navService?.sideNavVisibility$ | async" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
<a class="navbar-brand dark-exempt" routerLink="/library" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="d-none d-md-inline"> Kavita</span></a>
<ul class="navbar-nav col me-auto">

Expand Down
Loading