From 8b2649302c81e06f85b0500f26b1af5d5ede3e46 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Thu, 8 Feb 2024 12:35:05 -0600 Subject: [PATCH] Light Novel Library Type (#2682) --- .github/workflows/build-and-test.yml | 38 ----- .github/workflows/release-workflow.yml | 14 +- API/API.csproj | 2 +- API/Entities/Enums/LibraryType.cs | 5 + API/Helpers/Builders/LibraryBuilder.cs | 2 +- API/Helpers/LibraryTypeHelper.cs | 2 +- API/Services/Plus/ExternalMetadataService.cs | 3 +- API/Services/Plus/ScrobblingService.cs | 9 ++ API/Services/ReaderService.cs | 1 + API/Services/SeriesService.cs | 13 +- API/Services/StatisticService.cs | 3 +- API/Services/Tasks/ScannerService.cs | 4 +- API/Services/Tasks/VersionUpdaterService.cs | 2 - Kavita.Common/Kavita.Common.csproj | 2 +- UI/Web/src/app/_models/library/library.ts | 3 +- .../entity-title/entity-title.component.html | 3 + .../series-detail.component.html | 10 +- .../series-detail.component.scss | 3 +- .../series-detail/series-detail.component.ts | 28 +++- .../series-metadata-detail.component.html | 140 +++++++++++------- .../app/shared/_services/utility.service.ts | 1 + .../side-nav/side-nav.component.ts | 1 + .../library-settings-modal.component.html | 13 +- .../library-settings-modal.component.ts | 46 ++++-- UI/Web/src/assets/langs/en.json | 2 + openapi.json | 35 +++-- 26 files changed, 237 insertions(+), 148 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d9fab953f..98ce4c439 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,48 +26,10 @@ jobs: - name: Install dependencies run: dotnet restore - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '17' - - uses: actions/upload-artifact@v3 with: name: csproj path: Kavita.Common/Kavita.Common.csproj - - name: Cache SonarCloud packages - uses: actions/cache@v3 - with: - path: ~\sonar\cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - - name: Cache SonarCloud scanner - id: cache-sonar-scanner - uses: actions/cache@v3 - with: - path: .\.sonar\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner - - - name: Install SonarCloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell - run: | - New-Item -Path .\.sonar\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner - - - name: Sonar Scan - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell - run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" - dotnet build --configuration Release - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - - name: Test run: dotnet test --no-restore --verbosity normal diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index dca370460..ca1314e8b 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -59,6 +59,13 @@ jobs: id: parse-body run: | body="${{ steps.findPr.outputs.body }}" + body=${body//\'/} + body=${body//'%'/'%25'} + body=${body//$'\n'/'%0A'} + body=${body//$'\r'/'%0D'} + body=${body//$'`'/'%60'} + body=${body//$'>'/'%3E'} + if [[ ${#body} -gt 1870 ]] ; then body=${body:0:1870} body="${body}...and much more. @@ -66,16 +73,9 @@ jobs: Read full changelog: https://github.com/Kareadita/Kavita/releases/latest" fi - body=${body//\'/} - body=${body//'%'/'%25'} - body=${body//$'\n'/'%0A'} - body=${body//$'\r'/'%0D'} - body=${body//$'`'/'%60'} - body=${body//$'>'/'%3E'} echo $body echo "BODY=$body" >> $GITHUB_OUTPUT - - name: Check Out Repo uses: actions/checkout@v3 with: diff --git a/API/API.csproj b/API/API.csproj index ccf85f39b..aee5fa856 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -69,7 +69,7 @@ - + diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index 038ce7172..46fc56105 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -24,4 +24,9 @@ public enum LibraryType /// [Description("Image")] Image = 3, + /// + /// Allows Books to Scrobble with AniList for Kavita+ + /// + [Description("Light Novel")] + LightNovel = 4, } diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index ce392e78b..1cfd529a1 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -20,7 +20,7 @@ public class LibraryBuilder : IEntityBuilder Series = new List(), Folders = new List(), AppUsers = new List(), - AllowScrobbling = type is LibraryType.Book or LibraryType.Manga + AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga }; } diff --git a/API/Helpers/LibraryTypeHelper.cs b/API/Helpers/LibraryTypeHelper.cs index dac841e69..b65c31512 100644 --- a/API/Helpers/LibraryTypeHelper.cs +++ b/API/Helpers/LibraryTypeHelper.cs @@ -13,7 +13,7 @@ public static class LibraryTypeHelper { LibraryType.Manga => MediaFormat.Manga, LibraryType.Comic => MediaFormat.Comic, - LibraryType.Book => MediaFormat.LightNovel, + LibraryType.LightNovel => MediaFormat.LightNovel, }; } } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index b0d81097e..55eb6b862 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -69,7 +69,7 @@ public class ExternalMetadataService : IExternalMetadataService private readonly IMapper _mapper; private readonly ILicenseService _licenseService; private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); - public static readonly ImmutableArray NonEligibleLibraryTypes = ImmutableArray.Create(LibraryType.Comic); + public static readonly ImmutableArray NonEligibleLibraryTypes = ImmutableArray.Create(LibraryType.Comic, LibraryType.Book); private readonly SeriesDetailPlusDto _defaultReturn = new() { Recommendations = null, @@ -420,6 +420,7 @@ public class ExternalMetadataService : IExternalMetadataService LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga, LibraryType.Comic => MediaFormat.Comic, LibraryType.Book => MediaFormat.Book, + LibraryType.LightNovel => MediaFormat.LightNovel, _ => MediaFormat.Unknown }; } diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index 92f54feb5..93d75c246 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -80,6 +80,9 @@ public class ScrobblingService : IScrobblingService private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) private static readonly IList BookProviders = new List() + { + }; + private static readonly IList LightNovelProviders = new List() { ScrobbleProvider.AniList }; @@ -877,6 +880,12 @@ public class ScrobblingService : IScrobblingService return true; } + if (readEvent.Series.Library.Type == LibraryType.LightNovel && + LightNovelProviders.Intersect(userProviders).Any()) + { + return true; + } + return false; } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 693c7667b..19548e0a6 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -776,6 +776,7 @@ public class ReaderService : IReaderService } return "Issue" + (includeSpace ? " " : string.Empty); case LibraryType.Book: + case LibraryType.LightNovel: return "Book" + (includeSpace ? " " : string.Empty); default: throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null); diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index af6f5c906..973b6ee2c 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -489,7 +489,7 @@ public class SeriesService : ISeriesService // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. var processedVolumes = new List(); - if (libraryType == LibraryType.Book) + if (libraryType is LibraryType.Book or LibraryType.LightNovel) { var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); foreach (var volume in volumes) @@ -533,7 +533,7 @@ public class SeriesService : ISeriesService // Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes) IEnumerable retChapters; - if (libraryType == LibraryType.Book) + if (libraryType is LibraryType.Book or LibraryType.LightNovel) { retChapters = Array.Empty(); } else @@ -576,7 +576,7 @@ public class SeriesService : ISeriesService public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") { - if (libraryType == LibraryType.Book) + if (libraryType is LibraryType.Book or LibraryType.LightNovel) { if (string.IsNullOrEmpty(firstChapter.TitleName)) { @@ -587,6 +587,7 @@ public class SeriesService : ISeriesService } else if (volume.Name != "0") { + // If the titleName has Volume inside it, let's just send that back? volume.Name += $" - {firstChapter.TitleName}"; } // else @@ -614,6 +615,7 @@ public class SeriesService : ISeriesService return libraryType switch { LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle), + LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterTitle), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle), _ => await _localizationService.Translate(userId, "chapter-num", ' ') @@ -636,6 +638,7 @@ public class SeriesService : ISeriesService return (libraryType switch { LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty), + LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", string.Empty), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty), _ => await _localizationService.Translate(userId, "chapter-num", ' ') @@ -723,7 +726,8 @@ public class SeriesService : ISeriesService { throw new UnauthorizedAccessException("user-no-access-library-from-series"); } - if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) + if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || + (series.Library.Type is LibraryType.Book or LibraryType.LightNovel)) { return _emptyExpectedChapter; } @@ -803,6 +807,7 @@ public class SeriesService : ISeriesService LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), LibraryType.Book => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), + LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), _ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber) }; } diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index ad465af7b..6e39e76a3 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -595,7 +595,8 @@ public class StatisticService : IStatisticService { UserId = userId, Username = users.First(u => u.Id == userId).UserName, - BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0, + BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0 + + (user[userId].TryGetValue(LibraryType.LightNovel, out var bookTime2) ? bookTime2 : 0), ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0, MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0, }) diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 48cec71ff..c934deb10 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -152,7 +152,9 @@ public class ScannerService : IScannerService _logger.LogCritical("[ScannerService] Multiple series map to this folder. Library scan will be used for ScanFolder"); } } - if (series != null && series.Library.Type != LibraryType.Book) + + // TODO: Figure out why we have the library type restriction here + if (series != null && (series.Library.Type != LibraryType.Book || series.Library.Type != LibraryType.LightNovel)) { if (TaskScheduler.HasScanTaskRunningForSeries(series.Id)) { diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 2591fdbba..200851d10 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; using API.DTOs.Update; using API.SignalR; using Flurl.Http; -using HtmlAgilityPack; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using MarkdownDeep; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 7b8910070..61a52ce6f 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net8.0 kavitareader.com Kavita - 0.7.14.0 + 0.8.0.0 en true diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 76e463bb4..32ab99eab 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -4,7 +4,8 @@ export enum LibraryType { Manga = 0, Comic = 1, Book = 2, - Images = 3 + Images = 3, + LightNovel = 4 } export interface Library { diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.html b/UI/Web/src/app/cards/entity-title/entity-title.component.html index 6e1745d30..62730076c 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.html +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.html @@ -27,5 +27,8 @@ {{volumeTitle}} + + {{volumeTitle}} + diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index 032d6ef23..b8caa93fe 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -139,7 +139,7 @@ -
+
@@ -153,7 +153,7 @@
- + - + @@ -24,14 +24,17 @@ + {{item.title}} + {{item.title}} + @@ -40,7 +43,7 @@ - + @@ -53,6 +56,16 @@ + + + + + + + + + + @@ -62,70 +75,95 @@
+ + + + + + + + + + + - - - - - - - - - - + + + + + - - - - - - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- + diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 8c2382cb5..b76dc2956 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -64,6 +64,7 @@ export class UtilityService { formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) { switch(libraryType) { case LibraryType.Book: + case LibraryType.LightNovel: return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : ''); case LibraryType.Comic: if (includeHash) { diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts index 717ed2480..87fc22ec4 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts @@ -185,6 +185,7 @@ export class SideNavComponent implements OnInit { getLibraryTypeIcon(format: LibraryType) { switch (format) { case LibraryType.Book: + case LibraryType.LightNovel: return 'fa-book'; case LibraryType.Comic: case LibraryType.Manga: diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 492e36aca..22884717e 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -2,7 +2,7 @@