Nightly Issues (#2618)

This commit is contained in:
Joe Milazzo 2024-01-18 08:35:54 -06:00 committed by GitHub
parent 0ff6d4a6fc
commit d145dca0e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 138 additions and 100 deletions

View File

@ -11,7 +11,7 @@
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.4" />
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@ -64,7 +64,7 @@
<PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.7" />
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" />
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
@ -81,7 +81,7 @@
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="2.4.0" />
<PackageReference Include="NetVips.Native" Version="8.15.0" />
<PackageReference Include="NetVips.Native" Version="8.15.1" />
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
@ -92,15 +92,15 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.35.0" />
<PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.17.0.82934">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.4" />
<PackageReference Include="System.Drawing.Common" Version="8.0.1" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" />

View File

@ -31,12 +31,7 @@ public class LicenseController(
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
{
var ret = await licenseService.HasActiveLicense(forceCheck);
if (ret)
{
await taskScheduler.ScheduleKavitaPlusTasks();
}
return Ok(ret);
return Ok(await licenseService.HasActiveLicense(forceCheck));
}
/// <summary>

View File

@ -190,23 +190,12 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId)
{
var seriesDetail = new SeriesDetailPlusDto();
if (!await licenseService.HasActiveLicense())
{
seriesDetail.Recommendations = null;
seriesDetail.Ratings = Enumerable.Empty<RatingDto>();
return Ok(seriesDetail);
return Ok(null);
}
seriesDetail = await metadataService.GetSeriesDetail(User.GetUserId(), seriesId);
// Temp solution, needs to be updated with new API
// seriesDetail.Ratings = await ratingService.GetRatings(seriesId);
// seriesDetail.Reviews = await reviewService.GetReviewsForSeries(User.GetUserId(), seriesId);
// seriesDetail.Recommendations =
// await recommendationService.GetRecommendationsForSeries(User.GetUserId(), seriesId);
return Ok(seriesDetail);
return Ok(await metadataService.GetSeriesDetail(User.GetUserId(), seriesId));
}
}

View File

@ -106,6 +106,7 @@ public class LibraryRepository : ILibraryRepository
return await _context.Library
.Include(l => l.AppUsers)
.Includes(includes)
.AsSplitQuery()
.ToListAsync();
}

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.Constants;
using API.Data.ManualMigrations;
using API.Data.Misc;
using API.Data.Scanner;
@ -31,6 +32,7 @@ using API.Services.Tasks;
using API.Services.Tasks.Scanner;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SQLite;
@ -152,14 +154,16 @@ public class SeriesRepository : ISeriesRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
private readonly UserManager<AppUser> _userManager;
private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled,
Services.Tasks.Scanner.Parser.Parser.RegexTimeout);
public SeriesRepository(DataContext context, IMapper mapper)
public SeriesRepository(DataContext context, IMapper mapper, UserManager<AppUser> userManager)
{
_context = context;
_mapper = mapper;
_userManager = userManager;
}
public void Add(Series series)
@ -462,14 +466,18 @@ public class SeriesRepository : ISeriesRepository
.SelectMany(v => v.Chapters)
.SelectMany(c => c.Files.Select(f => f.Id));
result.Files = await _context.MangaFile
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
.AsSplitQuery()
.Take(maxRecords)
.OrderBy(f => f.FilePath)
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
.ToListAsync();
// Need to check if an admin
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
if (await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
{
result.Files = await _context.MangaFile
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
.AsSplitQuery()
.Take(maxRecords)
.OrderBy(f => f.FilePath)
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
result.Chapters = await _context.Chapter
.Include(c => c.Files)

View File

@ -48,7 +48,7 @@ public class UnitOfWork : IUnitOfWork
_userManager = userManager;
}
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _userManager);
public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper);
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);

View File

@ -114,11 +114,19 @@ public class ExternalMetadataService : IExternalMetadataService
Reviews = result.Reviews
};
}
catch (Exception e)
catch (FlurlHttpException ex)
{
_logger.LogError(e, "An error happened during the request to Kavita+ API");
return null;
if (ex.StatusCode == 404)
{
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "An error happened during the request to Kavita+ API");
}
return null;
}
private async Task<RecommendationDto> ProcessRecommendations(Series series, AppUser user, IEnumerable<MediaRecommendationDto> recs)

View File

@ -151,9 +151,8 @@ public class TaskScheduler : ITaskScheduler
{
// KavitaPlus based (needs license check)
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
if (!await _licenseService.HasActiveSubscription(license))
if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license))
{
return;
}
RecurringJob.AddOrUpdate(CheckScrobblingTokens, () => _scrobblingService.CheckExternalAccessTokens(), Cron.Daily, RecurringJobOptions);

View File

@ -122,10 +122,16 @@ public class ProcessSeries : IProcessSeries
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception finding existing series for {SeriesName} with Localized name of {LocalizedName} for library {LibraryId}. This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan", firstInfo.Series, firstInfo.LocalizedSeries, library.Id);
var series2 = await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(firstInfo.LocalizedSeries, string.Empty, library.Id, firstInfo.Format, false);
var details = $"Series 1: {firstInfo.Series} Series 2: {series2.Name}" + "\n" +
$"Localized: {firstInfo.LocalizedSeries} Localized: {series2.LocalizedName}" + "\n" +
$"Filename: {_directoryService.FileSystem.FileInfo.New(firstInfo.FullFilePath).Directory} Filename: {series2.FolderPath}";
_logger.LogError(ex, "Scanner found a Series {SeriesName} which matched another Series {LocalizedName} in a different folder parallel to Library {LibraryName} root folder. This is not allowed. Please correct",
firstInfo.Series, firstInfo.LocalizedSeries, library.Name);
await _eventHub.SendMessageAsync(MessageFactory.Error,
MessageFactory.ErrorEvent($"There was an exception finding existing series for {firstInfo.Series} with Localized name of {firstInfo.LocalizedSeries} for library {library.Id}",
"This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan."));
MessageFactory.ErrorEvent($"Scanner found a Series {firstInfo.Series} which matched another Series {firstInfo.LocalizedSeries} in a different folder parallel to Library {library.Name} root folder. This is not allowed. Please correct",
details));
return;
}

View File

@ -349,16 +349,26 @@ public class Startup
opts.IncludeQueryInRequestPath = true;
});
var allowIframing = Configuration.AllowIFraming;
app.Use(async (context, next) =>
{
context.Response.Headers[HeaderNames.Vary] =
new[] { "Accept-Encoding" };
// Don't let the site be iframed outside the same origin (clickjacking)
context.Response.Headers.XFrameOptions = Configuration.XFrameOptions;
// Setup CSP to ensure we load assets only from these origins
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';");
if (!allowIframing)
{
// Don't let the site be iframed outside the same origin (clickjacking)
context.Response.Headers.XFrameOptions = "SAMEORIGIN";
// Setup CSP to ensure we load assets only from these origins
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';");
}
else
{
logger.LogCritical("appsetting.json has allow iframing on! This may allow for clickjacking on the server. User beware");
}
await next();
});

View File

@ -3,6 +3,5 @@
"Port": 5000,
"IpAddresses": "",
"BaseUrl": "/test/",
"Cache": 90,
"XFrameOrigins": "SAMEORIGIN"
}
"Cache": 90
}

View File

@ -13,7 +13,6 @@ public static class Configuration
public const string DefaultBaseUrl = "/";
public const int DefaultHttpPort = 5000;
public const int DefaultTimeOutSecs = 90;
public const string DefaultXFrameOptions = "SAMEORIGIN";
public const long DefaultCacheMemory = 75;
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
@ -49,7 +48,7 @@ public static class Configuration
set => SetCacheSize(GetAppSettingFilename(), value);
}
public static string XFrameOptions => GetXFrameOptions(GetAppSettingFilename());
public static bool AllowIFraming => GetAllowIFraming(GetAppSettingFilename());
private static string GetAppSettingFilename()
{
@ -293,26 +292,21 @@ public static class Configuration
#endregion
#region XFrameOrigins
private static string GetXFrameOptions(string filePath)
#region AllowIFraming
private static bool GetAllowIFraming(string filePath)
{
if (OsInfo.IsDocker)
{
return DefaultBaseUrl;
}
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<AppSettings>(json);
return !string.IsNullOrEmpty(jsonObj.XFrameOrigins) ? jsonObj.XFrameOrigins : DefaultXFrameOptions;
return jsonObj.AllowIFraming;
}
catch (Exception ex)
{
Console.WriteLine("Error reading app settings: " + ex.Message);
}
return DefaultXFrameOptions;
return false;
}
#endregion
@ -328,6 +322,6 @@ public static class Configuration
// ReSharper disable once MemberHidesStaticFromOuterClass
public long Cache { get; set; } = DefaultCacheMemory;
// ReSharper disable once MemberHidesStaticFromOuterClass
public string XFrameOrigins { get; set; } = DefaultXFrameOptions;
public bool AllowIFraming { get; set; } = false;
}
}

View File

@ -14,7 +14,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.16.0.82469">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.17.0.82934">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -0,0 +1,24 @@
const crypto = require('crypto');
const fs = require('fs');
const glob = require('glob');
const jsonFilesDir = 'dist/browser/assets/langs/'; // Adjust the path to your JSON files
const outputDir = 'dist/browser/assets/langs'; // Directory to store minified files
function generateChecksum(str, algorithm, encoding) {
return crypto
.createHash(algorithm || 'md5')
.update(str, 'utf8')
.digest(encoding || 'hex');
}
const result = {};
glob.sync(`${jsonFilesDir}**/*.json`).forEach(path => {
const [_, lang] = path.split('dist\\browser\\assets\\langs\\');
const content = fs.readFileSync(path, { encoding: 'utf-8' });
result[lang.replace('.json', '')] = generateChecksum(content);
});
fs.writeFileSync('./i18n-cache-busting.json', JSON.stringify(result));
fs.writeFileSync(`dist/browser/i18n-cache-busting.json`, JSON.stringify(result));

View File

@ -0,0 +1 @@
{"zh_Hant":"05191aaae25a26a8597559e8318f97db","zh_Hans":"ca663a190b259b41ac365b6b5537558e","uk":"ccf59f571821ab842882378395ccf48c","tr":"5d6427179210cc370400b816c9d1116d","th":"1e27a1e1cadb2b9f92d85952bffaab95","sk":"24de417448b577b4899e917b70a43263","ru":"c547f0995c167817dd2408e4e9279de2","pt_BR":"44c7cd3da6baad38887fb03ac4ec5581","pt":"af4162a48f01c5260d6436e7e000c5ef","pl":"c6488fdb9a1ecfe5cde6bd1c264902aa","nl":"3ff322f7b24442bd6bceb5c692146d4f","nb_NO":"99914b932bd37a50b983c5e7c90ae93b","ms":"9fdfcc11a2e8a58a4baa691b93d93ff7","ko":"447e24f9f60e1b9f36bc0b087d059dbd","ja":"2f46a5ad1364a71255dd76c0094a9264","it":"4ef0a0ef56bab4650eda37e0dd841982","id":"0beab79883a28035c393768fb8c8ecbd","hi":"d850bb49ec6b5a5ccf9986823f095ab8","fr":"b41b7065960b431a9833140e9014189e","es":"151b6c17ef7382da9f0f22b87a346b7d","en":"d07463979db4cc7ab6e0089889cfc730","de":"f8e3ec31790044be07e222703ed0575a","cs":"0f0c433b9fd641977e89a42e92e4b884"}

View File

@ -3,10 +3,11 @@
"version": "0.7.12.1",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"start": "npm run cache-langs && ng serve",
"build": "npm run cache-langs && ng build",
"minify-langs": "node minify-json.js",
"prod": "ng build --configuration production && npm run minify-langs",
"cache-langs": "node hash-localization.js",
"prod": "ng build --configuration production && npm run minify-langs && npm run cache-langs",
"explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json",
"lint": "ng lint",
"e2e": "ng e2e"

View File

@ -93,6 +93,8 @@ export class AppComponent implements OnInit {
// Bootstrap anything that's needed
this.themeService.getThemes().subscribe();
this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe();
// On load, make an initial call for valid license
this.accountService.hasValidLicense().subscribe();
}
}
}

View File

@ -277,7 +277,6 @@ export class CardItemComponent implements OnInit {
});
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
console.log('Card Item download obv called for entity: ', this.entity);
return this.downloadService.mapToEntityType(events, this.entity);
}));
}

View File

@ -61,7 +61,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
activeEvents: number = 0;
debugMode: boolean = true;
debugMode: boolean = false;
protected readonly EVENTS = EVENTS;

View File

@ -692,16 +692,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
}
// loadRecommendations() {
// this.seriesService.getRecommendationsForSeries(this.seriesId).subscribe(rec => {
// rec.ownedSeries.map(r => {
// this.seriesService.getMetadata(r.id).subscribe(m => r.summary = m.summary);
// });
// this.combinedRecs = [...rec.ownedSeries, ...rec.externalSeries];
// this.hasRecommendations = this.combinedRecs.length > 0;
// this.cdRef.markForCheck();
// });
// }
loadPlusMetadata(seriesId: number) {
this.metadataService.getSeriesMetadataFromPlus(seriesId).subscribe(data => {

View File

@ -9,8 +9,8 @@
<i class="fa-solid fa-circle-check ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('token-valid')"></i>
<span class="visually-hidden">{{t('token-valid')}}</span>
} @else {
<i class="fa-solid fa-circle ms-1 confirm-icon error" aria-hidden="true" [ngbTooltip]="t('token-not-valid')"></i>
<span class="visually-hidden">{{t('token-not-valid')}}</span>
<i class="fa-solid fa-circle ms-1 confirm-icon error" aria-hidden="true" [ngbTooltip]="t('token-expired')"></i>
<span class="visually-hidden">{{t('token-expired')}}</span>
}
</h4>

View File

@ -436,8 +436,10 @@
}
@defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) {
<app-user-scrobble-history></app-user-scrobble-history>
<app-user-holds></app-user-holds>
@if(hasActiveLicense) {
<app-user-scrobble-history></app-user-scrobble-history>
<app-user-holds></app-user-holds>
}
}
</ng-template>
</li>

View File

@ -81,6 +81,19 @@ enum FragmentID {
})
export class UserPreferencesComponent implements OnInit, OnDestroy {
private readonly destroyRef = inject(DestroyRef);
private readonly accountService = inject(AccountService);
private readonly toastr = inject(ToastrService);
private readonly bookService = inject(BookService);
private readonly titleService = inject(Title);
private readonly route = inject(ActivatedRoute);
private readonly settingsService = inject(SettingsService);
private readonly router = inject(Router);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly localizationService = inject(LocalizationService);
protected readonly AccordionPanelID = AccordionPanelID;
protected readonly FragmentID = FragmentID;
readingDirectionsTranslated = readingDirections.map(this.translatePrefOptions);
scalingOptionsTranslated = scalingOptions.map(this.translatePrefOptions);
pageSplitOptionsTranslated = pageSplitOptions.map(this.translatePrefOptions);
@ -115,19 +128,9 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
opdsEnabled: boolean = false;
opdsUrl: string = '';
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
hasActiveLicense = false;
private readonly destroyRef = inject(DestroyRef);
private readonly accountService = inject(AccountService);
private readonly toastr = inject(ToastrService);
private readonly bookService = inject(BookService);
private readonly titleService = inject(Title);
private readonly route = inject(ActivatedRoute);
private readonly settingsService = inject(SettingsService);
private readonly router = inject(Router);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly localizationService = inject(LocalizationService);
protected readonly AccordionPanelID = AccordionPanelID;
protected readonly FragmentID = FragmentID;
constructor() {
@ -144,9 +147,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
});
this.accountService.hasValidLicense().subscribe(res => {
this.accountService.hasValidLicense$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(res => {
if (res) {
this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling});
this.hasActiveLicense = true;
this.cdRef.markForCheck();
}
@ -159,7 +165,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
}
this.cdRef.markForCheck();
});
})
});
this.settingsService.getOpdsEnabled().subscribe(res => {

View File

@ -284,6 +284,7 @@
"scrobbling-providers": {
"title": "Scrobbling Providers",
"requires": "This feature requires an active {{product}} license",
"token-valid": "Token Valid",
"token-expired": "Token Expired",
"no-token-set": "No Token Set",
"token-set": "Token Set",

View File

@ -1,7 +1,7 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {Translation, TranslocoLoader} from "@ngneat/transloco";
import cacheBusting from 'i18n-cache-busting.json'; // allowSyntheticDefaultImports must be true
@Injectable({ providedIn: 'root' })
export class HttpLoader implements TranslocoLoader {
@ -9,6 +9,7 @@ export class HttpLoader implements TranslocoLoader {
getTranslation(langPath: string) {
const tokens = langPath.split('/');
return this.http.get<Translation>(`assets/langs/${tokens[tokens.length - 1]}.json`);
const langCode = tokens[tokens.length - 1];
return this.http.get<Translation>(`assets/langs/${langCode}.json?v=${(cacheBusting as { [key: string]: string })[langCode]}`);
}
}

View File

@ -19,6 +19,7 @@
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"allowSyntheticDefaultImports": true,
"lib": [
"ES2022",
"dom"
@ -31,9 +32,10 @@
"strictTemplates": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"extendedDiagnostics": {
"nullishCoalescingNotNullable": "warning",
}
}
}

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.12.4"
"version": "0.7.12.5"
},
"servers": [
{