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 @@
- - 0 || chapters.length > 0)">
+
-
{{t('storyline-tab')}}
@@ -181,8 +181,8 @@
- - 0">
- {{libraryType === LibraryType.Book ? t('books-tab') : t('volumes-tab')}}
+
-
+ {{UseBookLogic ? t('books-tab') : t('volumes-tab')}}
@@ -202,7 +202,7 @@
- - 0">
+
-
{{utilityService.formatChapterName(libraryType) + 's'}}
diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss
index d652cf937..183a32759 100644
--- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss
+++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss
@@ -48,6 +48,7 @@
.image-container {
max-height: 400px;
+ max-width: 280px;
}
.info-container {
@@ -63,4 +64,4 @@
border-bottom-right-radius: 6px !important;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
-}
\ No newline at end of file
+}
diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts
index 63a334501..ca022e22a 100644
--- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts
+++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts
@@ -137,7 +137,7 @@ interface StoryLineItem {
isChapter: boolean;
}
-const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book];
+const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.LightNovel];
@Component({
selector: 'app-series-detail',
@@ -337,6 +337,21 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
}
+ get ShowStorylineTab() {
+ return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel) && (this.volumes.length > 0 || this.chapters.length > 0);
+ }
+
+ get ShowVolumeTab() {
+ return this.volumes.length > 0;
+ }
+ get ShowChaptersTab() {
+ return this.chapters.length > 0;
+ }
+
+ get UseBookLogic() {
+ return this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel;
+ }
+
get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
const navbar = this.document.querySelector('.navbar') as HTMLElement;
@@ -614,6 +629,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.loadPlusMetadata(this.seriesId, this.libraryType);
}
+ if (this.libraryType === LibraryType.LightNovel) {
+ this.renderMode = PageLayoutMode.List;
+ this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
+ this.cdRef.markForCheck();
+ }
+
+
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
@@ -694,12 +716,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
*/
updateSelectedTab() {
// Book libraries only have Volumes or Specials enabled
- if (this.libraryType === LibraryType.Book) {
+ if (this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel) {
if (this.volumes.length === 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Volumes;
}
+ this.cdRef.markForCheck();
return;
}
@@ -708,6 +731,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
} else {
this.activeTabId = TabID.Storyline;
}
+ this.cdRef.markForCheck();
}
diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
index fe6d5f27c..437c6a016 100644
--- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
+++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
@@ -3,14 +3,14 @@
= Breakpoint.Desktop ? 1000 : 250">
-
+
-
+
@@ -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 @@