.NET 8 Update (#2496)

This commit is contained in:
Joe Milazzo 2023-12-13 19:16:54 -06:00 committed by GitHub
parent 6d4d2d4a7f
commit b838fd53e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 590 additions and 405 deletions

View File

@ -17,7 +17,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
- name: Install Swashbuckle CLI - name: Install Swashbuckle CLI
shell: powershell shell: powershell

View File

@ -33,7 +33,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
- name: Bump versions - name: Bump versions
uses: SiqiLu/dotnet-bump-version@2.0.0 uses: SiqiLu/dotnet-bump-version@2.0.0
@ -98,7 +98,7 @@ jobs:
- name: Compile dotnet app - name: Compile dotnet app
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
- name: Install Swashbuckle CLI - name: Install Swashbuckle CLI
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli

View File

@ -46,7 +46,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
- name: Bump versions - name: Bump versions
uses: majora2007/dotnet-bump-version@v0.0.10 uses: majora2007/dotnet-bump-version@v0.0.10
@ -131,7 +131,7 @@ jobs:
- name: Compile dotnet app - name: Compile dotnet app
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
- name: Install Swashbuckle CLI - name: Install Swashbuckle CLI
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli

View File

@ -119,7 +119,7 @@ jobs:
- name: Compile dotnet app - name: Compile dotnet app
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
- name: Install Swashbuckle CLI - name: Install Swashbuckle CLI
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
</PropertyGroup> </PropertyGroup>
@ -10,8 +10,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.10" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.11" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.10" /> <PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.11" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.13" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.69" /> <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.69" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.4" />
<PackageReference Include="xunit" Version="2.6.1" /> <PackageReference Include="xunit" Version="2.6.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<AnalysisMode>Default</AnalysisMode> <AnalysisMode>Default</AnalysisMode>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<TieredPGO>true</TieredPGO> <TieredPGO>true</TieredPGO>
@ -53,7 +53,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -72,38 +72,37 @@
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" /> <PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" /> <PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" /> <PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="2.3.1" /> <PackageReference Include="NetVips" Version="2.4.0" />
<PackageReference Include="NetVips.Native" Version="8.14.5" /> <PackageReference Include="NetVips.Native" Version="8.15.0" />
<PackageReference Include="NReco.Logging.File" Version="1.1.7" /> <PackageReference Include="NReco.Logging.File" Version="1.1.7" />
<PackageReference Include="Serilog" Version="3.1.0" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" /> <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" /> <PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" /> <PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.34.1" /> <PackageReference Include="SharpCompress" Version="0.34.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.2" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.12.0.78982"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.12" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.12" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.69" /> <PackageReference Include="System.IO.Abstractions" Version="20.0.4" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" /> <PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" /> <PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup> </ItemGroup>

View File

@ -40,15 +40,13 @@ public class LibraryController : BaseApiController
private readonly IEventHub _eventHub; private readonly IEventHub _eventHub;
private readonly ILibraryWatcher _libraryWatcher; private readonly ILibraryWatcher _libraryWatcher;
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
private readonly IStreamService _streamService;
private readonly IEasyCachingProvider _libraryCacheProvider; private readonly IEasyCachingProvider _libraryCacheProvider;
private const string CacheKey = "library_"; private const string CacheKey = "library_";
public LibraryController(IDirectoryService directoryService, public LibraryController(IDirectoryService directoryService,
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler, ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher, IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher,
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
IStreamService streamService)
{ {
_directoryService = directoryService; _directoryService = directoryService;
_logger = logger; _logger = logger;
@ -58,7 +56,6 @@ public class LibraryController : BaseApiController
_eventHub = eventHub; _eventHub = eventHub;
_libraryWatcher = libraryWatcher; _libraryWatcher = libraryWatcher;
_localizationService = localizationService; _localizationService = localizationService;
_streamService = streamService;
_libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library); _libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library);
} }
@ -157,7 +154,7 @@ public class LibraryController : BaseApiController
})); }));
} }
if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path))); if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path)!));
return Ok(_directoryService.ListDirectory(path)); return Ok(_directoryService.ListDirectory(path));
} }

View File

@ -99,7 +99,7 @@ public class OpdsController : BaseApiController
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix(); var (_, prefix) = await GetPrefix();
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix); var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix);
SetFeedId(feed, "root"); SetFeedId(feed, "root");
@ -141,10 +141,37 @@ public class OpdsController : BaseApiController
}); });
break; break;
case DashboardStreamType.RecentlyUpdated: case DashboardStreamType.RecentlyUpdated:
// TODO: See if we can implement this and use (count) on series name for number of updates feed.Entries.Add(new FeedEntry()
{
Id = "recentlyUpdated",
Title = await _localizationService.Translate(userId, "recently-updated"),
Content = new FeedEntryContent()
{
Text = await _localizationService.Translate(userId, "browse-recently-updated")
},
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-updated"),
}
});
break; break;
case DashboardStreamType.MoreInGenre: case DashboardStreamType.MoreInGenre:
// TODO: See if we can implement this var randomGenre = await _unitOfWork.GenreRepository.GetRandomGenre();
if (randomGenre == null) break;
feed.Entries.Add(new FeedEntry()
{
Id = "moreInGenre",
Title = await _localizationService.Translate(userId, "more-in-genre", randomGenre.Title),
Content = new FeedEntryContent()
{
Text = await _localizationService.Translate(userId, "browse-more-in-genre", randomGenre.Title)
},
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/more-in-genre?genreId={randomGenre.Id}"),
}
});
break; break;
case DashboardStreamType.SmartFilter: case DashboardStreamType.SmartFilter:
@ -632,6 +659,61 @@ public class OpdsController : BaseApiController
return CreateXmlResult(SerializeXml(feed)); return CreateXmlResult(SerializeXml(feed));
} }
[HttpGet("{apiKey}/more-in-genre")]
[Produces("application/xml")]
public async Task<IActionResult> GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var genre = await _unitOfWork.GenreRepository.GetGenreById(genreId);
var seriesDtos = await _unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(pageNumber));
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id));
var feed = CreateFeed(await _localizationService.Translate(userId, "more-in-genre", genre.Title), $"{prefix}{apiKey}/more-in-genre", apiKey, prefix);
SetFeedId(feed, "more-in-genre");
AddPagination(feed, seriesDtos, $"{prefix}{apiKey}/more-in-genre");
foreach (var seriesDto in seriesDtos)
{
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
}
return CreateXmlResult(SerializeXml(feed));
}
[HttpGet("{apiKey}/recently-updated")]
[Produces("application/xml")]
public async Task<IActionResult> GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var seriesDtos = (await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, PageSize)).ToList();
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId));
var feed = CreateFeed(await _localizationService.Translate(userId, "recently-updated"), $"{prefix}{apiKey}/recently-updated", apiKey, prefix);
SetFeedId(feed, "recently-updated");
//AddPagination(feed, seriesDtos, $"{prefix}{apiKey}/recently-updated");
foreach (var groupedSeries in seriesDtos)
{
var seriesDto = new SeriesDto()
{
Name = $"{groupedSeries.SeriesName} ({groupedSeries.Count})",
Id = groupedSeries.SeriesId,
Format = groupedSeries.Format,
LibraryId = groupedSeries.LibraryId,
};
var metadata = seriesMetadatas.First(s => s.SeriesId == seriesDto.Id);
feed.Entries.Add(CreateSeries(seriesDto, metadata, apiKey, prefix, baseUrl));
}
return CreateXmlResult(SerializeXml(feed));
}
[HttpGet("{apiKey}/on-deck")] [HttpGet("{apiKey}/on-deck")]
[Produces("application/xml")] [Produces("application/xml")]
public async Task<IActionResult> GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1) public async Task<IActionResult> GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1)
@ -1161,7 +1243,7 @@ public class OpdsController : BaseApiController
var userId = await GetUser(apiKey); var userId = await GetUser(apiKey);
var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId); var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId);
// TODO: Type could be wrong // NOTE: Type could be wrong, there is nothing I can do in the spec
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg", var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg",
$"{prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}"); $"{prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
link.TotalPages = mangaFile.Pages; link.TotalPages = mangaFile.Pages;

View File

@ -19,13 +19,11 @@ public class StreamController : BaseApiController
{ {
private readonly IStreamService _streamService; private readonly IStreamService _streamService;
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<StreamController> _logger;
public StreamController(IStreamService streamService, IUnitOfWork unitOfWork, ILogger<StreamController> logger) public StreamController(IStreamService streamService, IUnitOfWork unitOfWork)
{ {
_streamService = streamService; _streamService = streamService;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_logger = logger;
} }
/// <summary> /// <summary>

View File

@ -53,7 +53,7 @@ public class ThemeController : BaseApiController
{ {
await _themeService.UpdateDefault(dto.ThemeId); await _themeService.UpdateDefault(dto.ThemeId);
} }
catch (KavitaException ex) catch (KavitaException)
{ {
return BadRequest(await _localizationService.Translate(User.GetUserId(), "theme-doesnt-exist")); return BadRequest(await _localizationService.Translate(User.GetUserId(), "theme-doesnt-exist"));
} }

View File

@ -1,4 +1,5 @@
namespace API.DTOs.Account; namespace API.DTOs.Account;
#nullable enable
public class LoginDto public class LoginDto
{ {

View File

@ -3,6 +3,7 @@ using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Filtering; namespace API.DTOs.Filtering;
#nullable enable
public class FilterDto public class FilterDto
{ {

View File

@ -1,4 +1,6 @@
namespace API.DTOs.Filtering; namespace API.DTOs.Filtering;
#nullable enable
/// <summary> /// <summary>
/// Represents a range between two int/float/double /// Represents a range between two int/float/double
/// </summary> /// </summary>

View File

@ -1,9 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace API.DTOs.Filtering.v2; namespace API.DTOs.Filtering.v2;
#nullable enable
/// <summary> /// <summary>
/// Metadata filtering for v2 API only /// Metadata filtering for v2 API only

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs; namespace API.DTOs;
#nullable enable
public class LibraryDto public class LibraryDto
{ {

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using API.DTOs.Account; using API.DTOs.Account;
namespace API.DTOs; namespace API.DTOs;
#nullable enable
/// <summary> /// <summary>
/// Represents a member of a Kavita server. /// Represents a member of a Kavita server.

View File

@ -2,6 +2,7 @@
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Metadata; namespace API.DTOs.Metadata;
#nullable enable
/// <summary> /// <summary>
/// Exclusively metadata about a given chapter /// Exclusively metadata about a given chapter

View File

@ -1,6 +1,7 @@
using API.Services.Plus; using API.Services.Plus;
namespace API.DTOs; namespace API.DTOs;
#nullable enable
public class RatingDto public class RatingDto
{ {

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace API.DTOs.Reader; namespace API.DTOs.Reader;
#nullable enable
public class BookmarkDto public class BookmarkDto
{ {

View File

@ -2,6 +2,7 @@
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Reader; namespace API.DTOs.Reader;
#nullable enable
public class BookmarkInfoDto public class BookmarkInfoDto
{ {

View File

@ -2,6 +2,7 @@
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Reader; namespace API.DTOs.Reader;
#nullable enable
/// <summary> /// <summary>
/// Information about the Chapter for the Reader to render /// Information about the Chapter for the Reader to render

View File

@ -1,5 +1,7 @@
namespace API.DTOs.Reader; namespace API.DTOs.Reader;
#nullable enable
public class PersonalToCDto public class PersonalToCDto
{ {
public required int ChapterId { get; set; } public required int ChapterId { get; set; }

View File

@ -2,6 +2,7 @@
using API.DTOs.Scrobbling; using API.DTOs.Scrobbling;
namespace API.DTOs.Recommendation; namespace API.DTOs.Recommendation;
#nullable enable
public class ExternalSeriesDetailDto public class ExternalSeriesDetailDto
{ {

View File

@ -1,4 +1,5 @@
namespace API.DTOs.Recommendation; namespace API.DTOs.Recommendation;
#nullable enable
public class SeriesStaffDto public class SeriesStaffDto
{ {

View File

@ -1,4 +1,5 @@
namespace API.DTOs.Scrobbling; namespace API.DTOs.Scrobbling;
#nullable enable
/// <summary> /// <summary>
/// Response from Kavita+ Scrobble API /// Response from Kavita+ Scrobble API

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace API.DTOs.SeriesDetail; namespace API.DTOs.SeriesDetail;
#nullable enable
public class UpdateUserReviewDto public class UpdateUserReviewDto
{ {

View File

@ -1,6 +1,7 @@
using API.Services.Plus; using API.Services.Plus;
namespace API.DTOs.SeriesDetail; namespace API.DTOs.SeriesDetail;
#nullable enable
/// <summary> /// <summary>
/// Represents a User Review for a given Series /// Represents a User Review for a given Series

View File

@ -2,6 +2,7 @@
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Statistics; namespace API.DTOs.Statistics;
#nullable enable
public class FileExtensionDto public class FileExtensionDto
{ {

View File

@ -1,4 +1,5 @@
namespace API.DTOs.Statistics; namespace API.DTOs.Statistics;
#nullable enable
public class TopReadDto public class TopReadDto
{ {

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using API.Entities.Enums; using API.Entities.Enums;
namespace API.DTOs.Stats; namespace API.DTOs.Stats;
#nullable enable
/// <summary> /// <summary>
/// Represents information about a Kavita Installation /// Represents information about a Kavita Installation

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace API.DTOs; namespace API.DTOs;
#nullable enable
public class UpdateRbsDto public class UpdateRbsDto
{ {

View File

@ -2,6 +2,7 @@
using API.DTOs.Account; using API.DTOs.Account;
namespace API.DTOs; namespace API.DTOs;
#nullable enable
public class UserDto public class UserDto
{ {

View File

@ -9,6 +9,7 @@ using Kavita.Common.Extensions;
using Nager.ArticleNumber; using Nager.ArticleNumber;
namespace API.Data.Metadata; namespace API.Data.Metadata;
#nullable enable
/// <summary> /// <summary>
/// A representation of a ComicInfo.xml file /// A representation of a ComicInfo.xml file

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Metadata; using API.DTOs.Metadata;
@ -22,6 +23,8 @@ public interface IGenreRepository
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId); Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId);
Task<int> GetCountAsync(); Task<int> GetCountAsync();
Task<GenreTagDto> GetRandomGenre();
Task<GenreTagDto> GetGenreById(int id);
} }
public class GenreRepository : IGenreRepository public class GenreRepository : IGenreRepository
@ -92,6 +95,27 @@ public class GenreRepository : IGenreRepository
return await _context.Genre.CountAsync(); return await _context.Genre.CountAsync();
} }
public async Task<GenreTagDto> GetRandomGenre()
{
var genreCount = await GetCountAsync();
if (genreCount == 0) return null;
var randomIndex = new Random().Next(0, genreCount);
return await _context.Genre
.Skip(randomIndex)
.Take(1)
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}
public async Task<GenreTagDto> GetGenreById(int id)
{
return await _context.Genre
.Where(g => g.Id == id)
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}
public async Task<IList<Genre>> GetAllGenresAsync() public async Task<IList<Genre>> GetAllGenresAsync()
{ {
return await _context.Genre.ToListAsync(); return await _context.Genre.ToListAsync();

View File

@ -17,7 +17,7 @@ public static class FileTypeGroupExtensions
case FileTypeGroup.Pdf: case FileTypeGroup.Pdf:
return Parser.PdfFileExtension; return Parser.PdfFileExtension;
case FileTypeGroup.Images: case FileTypeGroup.Images:
return Parser.ImageFileExtensions;; return Parser.ImageFileExtensions;
default: default:
throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null);
} }

View File

@ -21,8 +21,8 @@ public static class HttpExtensions
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}; };
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options)); response.Headers.Append("Pagination", JsonSerializer.Serialize(paginationHeader, options));
response.Headers.Add("Access-Control-Expose-Headers", "Pagination"); response.Headers.Append("Access-Control-Expose-Headers", "Pagination");
} }
/// <summary> /// <summary>
@ -33,7 +33,7 @@ public static class HttpExtensions
public static void AddCacheHeader(this HttpResponse response, byte[] content) public static void AddCacheHeader(this HttpResponse response, byte[] content)
{ {
if (content is not {Length: > 0}) return; if (content is not {Length: > 0}) return;
response.Headers.Add(HeaderNames.ETag, string.Concat(SHA256.HashData(content).Select(x => x.ToString("X2")))); response.Headers.Append(HeaderNames.ETag, string.Concat(SHA256.HashData(content).Select(x => x.ToString("X2"))));
response.Headers.CacheControl = $"private,max-age=100"; response.Headers.CacheControl = $"private,max-age=100";
} }
@ -47,7 +47,7 @@ public static class HttpExtensions
{ {
if (filename is not {Length: > 0}) return; if (filename is not {Length: > 0}) return;
var hashContent = filename + File.GetLastWriteTimeUtc(filename); var hashContent = filename + File.GetLastWriteTimeUtc(filename);
response.Headers.Add("ETag", string.Concat(SHA256.HashData(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")))); response.Headers.Append("ETag", string.Concat(SHA256.HashData(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
if (maxAge != 10) if (maxAge != 10)
{ {
response.Headers.CacheControl = $"max-age={maxAge}"; response.Headers.CacheControl = $"max-age={maxAge}";

View File

@ -22,7 +22,7 @@ public static class SeriesFilter
switch (comparison) switch (comparison)
{ {
case FilterComparison.Equal: case FilterComparison.Equal:
return queryable.Where(s => s.Metadata.Language.Equals(languages.First())); return queryable.Where(s => s.Metadata.Language.Equals(languages[0]));
case FilterComparison.Contains: case FilterComparison.Contains:
return queryable.Where(s => languages.Contains(s.Metadata.Language)); return queryable.Where(s => languages.Contains(s.Metadata.Language));
case FilterComparison.MustContains: case FilterComparison.MustContains:
@ -30,9 +30,9 @@ public static class SeriesFilter
case FilterComparison.NotContains: case FilterComparison.NotContains:
return queryable.Where(s => !languages.Contains(s.Metadata.Language)); return queryable.Where(s => !languages.Contains(s.Metadata.Language));
case FilterComparison.NotEqual: case FilterComparison.NotEqual:
return queryable.Where(s => !s.Metadata.Language.Equals(languages.First())); return queryable.Where(s => !s.Metadata.Language.Equals(languages[0]));
case FilterComparison.Matches: case FilterComparison.Matches:
return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages.First()}%")); return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages[0]}%"));
case FilterComparison.GreaterThan: case FilterComparison.GreaterThan:
case FilterComparison.GreaterThanEqual: case FilterComparison.GreaterThanEqual:
case FilterComparison.LessThan: case FilterComparison.LessThan:

View File

@ -6,8 +6,9 @@ namespace API.Extensions;
public static class StringExtensions public static class StringExtensions
{ {
private static readonly Regex SentenceCaseRegex = new Regex(@"(^[a-z])|\.\s+(.)", private static readonly Regex SentenceCaseRegex = new(@"(^[a-z])|\.\s+(.)",
RegexOptions.ExplicitCapture | RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); RegexOptions.ExplicitCapture | RegexOptions.Compiled,
Services.Tasks.Scanner.Parser.Parser.RegexTimeout);
public static string SentenceCase(this string value) public static string SentenceCase(this string value)
{ {
@ -21,17 +22,16 @@ public static class StringExtensions
/// <returns></returns> /// <returns></returns>
public static string ToNormalized(this string? value) public static string ToNormalized(this string? value)
{ {
if (string.IsNullOrEmpty(value)) return string.Empty; return string.IsNullOrEmpty(value) ? string.Empty : Services.Tasks.Scanner.Parser.Parser.Normalize(value);
return Services.Tasks.Scanner.Parser.Parser.Normalize(value);
} }
public static float AsFloat(this string value) public static float AsFloat(this string? value, float defaultValue = 0.0f)
{ {
return float.Parse(value, CultureInfo.InvariantCulture); return string.IsNullOrEmpty(value) ? defaultValue : float.Parse(value, CultureInfo.InvariantCulture);
} }
public static double AsDouble(this string value) public static double AsDouble(this string? value, double defaultValue = 0.0f)
{ {
return double.Parse(value, CultureInfo.InvariantCulture); return string.IsNullOrEmpty(value) ? defaultValue : double.Parse(value, CultureInfo.InvariantCulture);
} }
} }

View File

@ -5,6 +5,7 @@ using API.Entities;
using Kavita.Common; using Kavita.Common;
namespace API.Helpers.Builders; namespace API.Helpers.Builders;
#nullable enable
public class AppUserBuilder : IEntityBuilder<AppUser> public class AppUserBuilder : IEntityBuilder<AppUser>
{ {

View File

@ -6,6 +6,7 @@ using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
namespace API.Helpers.Builders; namespace API.Helpers.Builders;
#nullable enable
public class ChapterBuilder : IEntityBuilder<Chapter> public class ChapterBuilder : IEntityBuilder<Chapter>
{ {

View File

@ -1,6 +1,7 @@
using API.Entities.Scrobble; using API.Entities.Scrobble;
namespace API.Helpers.Builders; namespace API.Helpers.Builders;
#nullable enable
public class ScrobbleHoldBuilder : IEntityBuilder<ScrobbleHold> public class ScrobbleHoldBuilder : IEntityBuilder<ScrobbleHold>
{ {

View File

@ -40,7 +40,7 @@ public static class ParserInfoHelpers
} }
} }
if (series.Format == MangaFormat.Unknown && format != MangaFormat.Unknown) if (series.Format == MangaFormat.Unknown)
{ {
return true; return true;
} }

View File

@ -130,8 +130,9 @@ public static class SmartFilterHelper
private static SortOptions DecodeSortOptions(string encodedSortOptions) private static SortOptions DecodeSortOptions(string encodedSortOptions)
{ {
var parts = Uri.UnescapeDataString(encodedSortOptions).Split(InnerStatementSeparator); var parts = Uri.UnescapeDataString(encodedSortOptions).Split(InnerStatementSeparator);
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith(SortFieldKey));
var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith(IsAscendingKey)); var sortFieldPart = Array.Find(parts, part => part.StartsWith(SortFieldKey));
var isAscendingPart = Array.Find(parts, part => part.StartsWith(IsAscendingKey));
var isAscending = isAscendingPart?.Trim().Replace(IsAscendingKey, string.Empty).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; var isAscending = isAscendingPart?.Trim().Replace(IsAscendingKey, string.Empty).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
if (sortFieldPart == null) if (sortFieldPart == null)

View File

@ -151,6 +151,10 @@
"browse-libraries": "Browse by Libraries", "browse-libraries": "Browse by Libraries",
"collections": "All Collections", "collections": "All Collections",
"browse-collections": "Browse by Collections", "browse-collections": "Browse by Collections",
"more-in-genre": "More in Genre {0}",
"browse-more-in-genre": "Browse more in {0}",
"recently-updated": "Recently Updated",
"browse-recently-updated": "Browse Recently Updated",
"smart-filters": "Smart Filters", "smart-filters": "Smart Filters",
"external-sources": "External Sources", "external-sources": "External Sources",
"browse-external-sources": "Browse External Sources", "browse-external-sources": "Browse External Sources",

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
namespace API.Middleware.RateLimit; namespace API.Middleware.RateLimit;
#nullable enable
public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy<string> public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy<string>
{ {

View File

@ -25,6 +25,7 @@ using Serilog.Events;
using Serilog.Sinks.AspNetCore.SignalR.Extensions; using Serilog.Sinks.AspNetCore.SignalR.Extensions;
namespace API; namespace API;
#nullable enable
public class Program public class Program
{ {

View File

@ -199,7 +199,6 @@ public class BookService : IBookService
} }
if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue; if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue;
//var bookFile = book.Content.AllFiles.Local[key];
var content = await bookFile.ReadContentAsBytesAsync(); var content = await bookFile.ReadContentAsBytesAsync();
importBuilder.Append(Encoding.UTF8.GetString(content)); importBuilder.Append(Encoding.UTF8.GetString(content));
} }
@ -555,7 +554,6 @@ public class BookService : IBookService
// If this is a single book and not a collection, set publication status to Completed // If this is a single book and not a collection, set publication status to Completed
if (string.IsNullOrEmpty(info.Volume) && Parser.ParseVolume(filePath).Equals(Parser.DefaultVolume)) if (string.IsNullOrEmpty(info.Volume) && Parser.ParseVolume(filePath).Equals(Parser.DefaultVolume))
{ {
//info.Number = "1";
info.Count = 1; info.Count = 1;
} }
@ -938,8 +936,9 @@ public class BookService : IBookService
/// <param name="mappings"></param> /// <param name="mappings"></param>
/// <param name="key"></param> /// <param name="key"></param>
/// <returns></returns> /// <returns></returns>
private static string CoalesceKey(EpubBookRef book, IReadOnlyDictionary<string, int> mappings, string key) private static string? CoalesceKey(EpubBookRef book, IReadOnlyDictionary<string, int> mappings, string? key)
{ {
if (string.IsNullOrEmpty(key)) return key;
if (mappings.ContainsKey(CleanContentKeys(key))) return key; if (mappings.ContainsKey(CleanContentKeys(key))) return key;
// Fallback to searching for key (bad epub metadata) // Fallback to searching for key (bad epub metadata)
@ -949,7 +948,7 @@ public class BookService : IBookService
key = correctedKey; key = correctedKey;
} }
var stepsBack = CountParentDirectory(book.Content.NavigationHtmlFile?.FilePath); // FileName -> FilePath var stepsBack = CountParentDirectory(book.Content.NavigationHtmlFile?.FilePath);
if (mappings.TryGetValue(key, out _)) if (mappings.TryGetValue(key, out _))
{ {
return key; return key;

View File

@ -644,10 +644,10 @@ public class DirectoryService : IDirectoryService
/// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns /// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns
/// </summary> /// </summary>
/// <param name="folderPath"></param> /// <param name="folderPath"></param>
/// <param name="supportedExtensions"></param> /// <param name="fileTypes"></param>
/// <param name="matcher"></param> /// <param name="matcher"></param>
/// <returns></returns> /// <returns></returns>
public IList<string> ScanFiles(string folderPath, string supportedExtensions, GlobMatcher? matcher = null) public IList<string> ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null)
{ {
_logger.LogDebug("[ScanFiles] called on {Path}", folderPath); _logger.LogDebug("[ScanFiles] called on {Path}", folderPath);
var files = new List<string>(); var files = new List<string>();
@ -668,19 +668,19 @@ public class DirectoryService : IDirectoryService
foreach (var directory in directories) foreach (var directory in directories)
{ {
files.AddRange(ScanFiles(directory, supportedExtensions, matcher)); files.AddRange(ScanFiles(directory, fileTypes, matcher));
} }
// Get the matcher from either ignore or global (default setup) // Get the matcher from either ignore or global (default setup)
if (matcher == null) if (matcher == null)
{ {
files.AddRange(GetFilesWithCertainExtensions(folderPath, supportedExtensions)); files.AddRange(GetFilesWithCertainExtensions(folderPath, fileTypes));
} }
else else
{ {
var foundFiles = GetFilesWithCertainExtensions(folderPath, var foundFiles = GetFilesWithCertainExtensions(folderPath,
supportedExtensions) fileTypes)
.Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name)); .Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name));
files.AddRange(foundFiles); files.AddRange(foundFiles);
} }

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services; namespace API.Services;
#nullable enable
public interface IEmailService public interface IEmailService
{ {

View File

@ -14,6 +14,7 @@ using Hangfire;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services; namespace API.Services;
#nullable enable
public interface IMetadataService public interface IMetadataService
{ {

View File

@ -31,7 +31,7 @@ internal class ExternalMetadataIdsDto
public interface IExternalMetadataService public interface IExternalMetadataService
{ {
Task<ExternalSeriesDetailDto> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); Task<ExternalSeriesDetailDto?> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId);
} }
public class ExternalMetadataService : IExternalMetadataService public class ExternalMetadataService : IExternalMetadataService

View File

@ -22,6 +22,7 @@ using Kavita.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services; namespace API.Services;
#nullable enable
public interface IReaderService public interface IReaderService
{ {

View File

@ -21,6 +21,7 @@ using Kavita.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services; namespace API.Services;
#nullable enable
public interface IReadingListService public interface IReadingListService
{ {

View File

@ -399,7 +399,7 @@ public class ParseScannedFiles
/// World of Acceleration v02.cbz having Series "Accel World" and Localized Series of "World of Acceleration" /// World of Acceleration v02.cbz having Series "Accel World" and Localized Series of "World of Acceleration"
/// </example> /// </example>
/// <param name="infos">A collection of ParserInfos</param> /// <param name="infos">A collection of ParserInfos</param>
private void MergeLocalizedSeriesWithSeries(IReadOnlyCollection<ParserInfo> infos) private void MergeLocalizedSeriesWithSeries(IReadOnlyCollection<ParserInfo?> infos)
{ {
var hasLocalizedSeries = infos.Any(i => !string.IsNullOrEmpty(i.LocalizedSeries)); var hasLocalizedSeries = infos.Any(i => !string.IsNullOrEmpty(i.LocalizedSeries));
if (!hasLocalizedSeries) return; if (!hasLocalizedSeries) return;

View File

@ -34,7 +34,7 @@ public class DefaultParser : IDefaultParser
public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga)
{ {
var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
// TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. (we can probably remove this and have users use kavitaignore) // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this.
if (type != LibraryType.Image && Parser.IsCoverImage(_directoryService.FileSystem.Path.GetFileName(filePath))) return null; if (type != LibraryType.Image && Parser.IsCoverImage(_directoryService.FileSystem.Path.GetFileName(filePath))) return null;
var ret = new ParserInfo() var ret = new ParserInfo()

View File

@ -501,7 +501,6 @@ public class ScannerService : IScannerService
// { // {
// await task(); // await task();
// } // }
// TODO: We might be able to do Task.WhenAll
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended)); MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended));

View File

@ -37,7 +37,7 @@ public class StatsService : IStatsService
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly DataContext _context; private readonly DataContext _context;
private readonly IStatisticService _statisticService; private readonly IStatisticService _statisticService;
private const string ApiUrl = "https://stats.kavitareader.com"; // "" private const string ApiUrl = "https://stats.kavitareader.com";
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService) public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService)
{ {

View File

@ -13,7 +13,6 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services.Tasks; namespace API.Services.Tasks;
#nullable enable #nullable enable
internal class GithubReleaseMetadata internal class GithubReleaseMetadata
@ -76,7 +75,7 @@ public class VersionUpdaterService : IVersionUpdaterService
/// Fetches the latest release from Github /// Fetches the latest release from Github
/// </summary> /// </summary>
/// <returns>Latest update</returns> /// <returns>Latest update</returns>
public async Task<UpdateNotificationDto> CheckForUpdate() public async Task<UpdateNotificationDto?> CheckForUpdate()
{ {
var update = await GetGithubRelease(); var update = await GetGithubRelease();
return CreateDto(update); return CreateDto(update);

View File

@ -17,6 +17,7 @@ using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegiste
namespace API.Services; namespace API.Services;
#nullable enable
public interface ITokenService public interface ITokenService
{ {

View File

@ -21,13 +21,11 @@ public class EventHub : IEventHub
{ {
private readonly IHubContext<MessageHub> _messageHub; private readonly IHubContext<MessageHub> _messageHub;
private readonly IPresenceTracker _presenceTracker; private readonly IPresenceTracker _presenceTracker;
private readonly IUnitOfWork _unitOfWork;
public EventHub(IHubContext<MessageHub> messageHub, IPresenceTracker presenceTracker, IUnitOfWork unitOfWork) public EventHub(IHubContext<MessageHub> messageHub, IPresenceTracker presenceTracker)
{ {
_messageHub = messageHub; _messageHub = messageHub;
_presenceTracker = presenceTracker; _presenceTracker = presenceTracker;
_unitOfWork = unitOfWork;
// TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array) // TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array)
} }

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace API.SignalR; namespace API.SignalR;
#nullable enable
public interface ILogHub : Serilog.Sinks.AspNetCore.SignalR.Interfaces.IHub public interface ILogHub : Serilog.Sinks.AspNetCore.SignalR.Interfaces.IHub
{ {

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace API.SignalR; namespace API.SignalR;
#nullable enable
/// <summary> /// <summary>
/// Generic hub for sending messages to UI /// Generic hub for sending messages to UI

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using API.Data; using API.Data;
namespace API.SignalR.Presence; namespace API.SignalR.Presence;
#nullable enable
public interface IPresenceTracker public interface IPresenceTracker
{ {
@ -22,7 +23,6 @@ internal class ConnectionDetail
public bool IsAdmin { get; set; } public bool IsAdmin { get; set; }
} }
// TODO: This can respond to UserRoleUpdate events to handle online users
/// <summary> /// <summary>
/// This is a singleton service for tracking what users have a SignalR connection and their difference connectionIds /// This is a singleton service for tracking what users have a SignalR connection and their difference connectionIds
/// </summary> /// </summary>

View File

@ -1,6 +1,7 @@
using System; using System;
namespace API.SignalR; namespace API.SignalR;
#nullable enable
/// <summary> /// <summary>
/// Payload for SignalR messages to Frontend /// Payload for SignalR messages to Frontend

View File

@ -232,7 +232,6 @@ public class Startup
Task.Run(async () => Task.Run(async () =>
{ {
// Apply all migrations on startup // Apply all migrations on startup
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
var dataContext = serviceProvider.GetRequiredService<DataContext>(); var dataContext = serviceProvider.GetRequiredService<DataContext>();

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Company>kavitareader.com</Company> <Company>kavitareader.com</Company>
<Product>Kavita</Product> <Product>Kavita</Product>
<AssemblyVersion>0.7.11.2</AssemblyVersion> <AssemblyVersion>0.7.11.2</AssemblyVersion>
@ -12,9 +12,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.12.0.78982"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -55,7 +55,7 @@ install methods and platforms.
**Note: Kavita is under heavy development and is being updated all the time, so the tag for bleeding edge builds is `:nightly`. The `:latest` tag will be the latest stable release.** **Note: Kavita is under heavy development and is being updated all the time, so the tag for bleeding edge builds is `:nightly`. The `:latest` tag will be the latest stable release.**
## Feature Requests ## Feature Requests
Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects?type=classic) first for a list of planned features before you submit an idea. Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/), [Feature Discord Channel](https://discord.com/channels/821879810934439936/1164375153493422122) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects?type=classic) first for a list of planned features before you submit an idea.
## Notice ## Notice
Kavita is being actively developed and should be considered beta software until the 1.0 release. Kavita is being actively developed and should be considered beta software until the 1.0 release.
@ -107,7 +107,7 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="" width="32"> JetBrains](http:
Thank you to [Weblate](https://hosted.weblate.org/engage/kavita/) who hosts our localization infrastructure pro-bono. If you want to see Kavita in your language, please help us localize. Thank you to [Weblate](https://hosted.weblate.org/engage/kavita/) who hosts our localization infrastructure pro-bono. If you want to see Kavita in your language, please help us localize.
<a href="https://hosted.weblate.org/engage/kavita/"> <a href="https://hosted.weblate.org/engage/kavita/">
<img src="https://hosted.weblate.org/widgets/kavita/-/horizontal-blue.svg" alt="Translation status" /> <img src="https://hosted.weblate.org/widget/kavita/horizontal-auto.svg" alt="Translation status" />
</a> </a>
## PikaPods ## PikaPods

540
UI/Web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,26 +13,26 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.4", "@angular/animations": "^17.0.6",
"@angular/cdk": "^17.0.1", "@angular/cdk": "^17.0.4",
"@angular/common": "^17.0.4", "@angular/common": "^17.0.6",
"@angular/compiler": "^17.0.4", "@angular/compiler": "^17.0.6",
"@angular/core": "^17.0.4", "@angular/core": "^17.0.6",
"@angular/forms": "^17.0.4", "@angular/forms": "^17.0.6",
"@angular/localize": "^17.0.4", "@angular/localize": "^17.0.6",
"@angular/platform-browser": "^17.0.4", "@angular/platform-browser": "^17.0.6",
"@angular/platform-browser-dynamic": "^17.0.4", "@angular/platform-browser-dynamic": "^17.0.6",
"@angular/router": "^17.0.4", "@angular/router": "^17.0.6",
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.4.2",
"@iharbeck/ngx-virtual-scroller": "^17.0.0", "@iharbeck/ngx-virtual-scroller": "^17.0.0",
"@iplab/ngx-file-upload": "^17.0.0", "@iplab/ngx-file-upload": "^17.0.0",
"@microsoft/signalr": "^7.0.12", "@microsoft/signalr": "^8.0.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ngneat/transloco": "^6.0.0", "@ngneat/transloco": "^6.0.4",
"@ngneat/transloco-locale": "^5.1.1", "@ngneat/transloco-locale": "^5.1.1",
"@ngneat/transloco-persist-lang": "^5.0.0", "@ngneat/transloco-persist-lang": "^5.0.0",
"@ngneat/transloco-persist-translations": "^5.0.0", "@ngneat/transloco-persist-translations": "^5.0.0",
"@ngneat/transloco-preload-langs": "^5.0.0", "@ngneat/transloco-preload-langs": "^5.0.1",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.5.0", "@swimlane/ngx-charts": "^20.5.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
@ -58,17 +58,17 @@
"zone.js": "^0.14.2" "zone.js": "^0.14.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.3", "@angular-devkit/build-angular": "^17.0.7",
"@angular-eslint/builder": "^17.1.0", "@angular-eslint/builder": "^17.1.1",
"@angular-eslint/eslint-plugin": "^17.1.0", "@angular-eslint/eslint-plugin": "^17.1.1",
"@angular-eslint/eslint-plugin-template": "^17.1.0", "@angular-eslint/eslint-plugin-template": "^17.1.1",
"@angular-eslint/schematics": "^17.1.0", "@angular-eslint/schematics": "^17.1.1",
"@angular-eslint/template-parser": "^17.1.0", "@angular-eslint/template-parser": "^17.1.1",
"@angular/cli": "^17.0.3", "@angular/cli": "^17.0.7",
"@angular/compiler-cli": "^17.0.4", "@angular/compiler-cli": "^17.0.6",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/luxon": "^3.3.5", "@types/luxon": "^3.3.7",
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0", "@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0", "@typescript-eslint/parser": "^6.13.0",

View File

@ -141,6 +141,7 @@ export class MessageHubService {
accessTokenFactory: () => user.token accessTokenFactory: () => user.token
}) })
.withAutomaticReconnect() .withAutomaticReconnect()
.withStatefulReconnect()
.build(); .build();
this.hubConnection this.hubConnection

View File

@ -21,7 +21,6 @@ Build()
slnFile=Kavita.sln slnFile=Kavita.sln
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID

View File

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "7.0.0", "version": "8.0.0",
"rollForward": "latestMajor", "rollForward": "latestMajor",
"allowPrerelease": false "allowPrerelease": false
} }

View File

@ -53,9 +53,6 @@ Package()
echo "Copying LICENSE" echo "Copying LICENSE"
cp ../LICENSE "$lOutputFolder"/LICENSE.txt cp ../LICENSE "$lOutputFolder"/LICENSE.txt
echo "Show API structure"
find
echo "Copying appsettings.json" echo "Copying appsettings.json"
cp ./config/appsettings.Development.json $lOutputFolder/config/appsettings.json cp ./config/appsettings.Development.json $lOutputFolder/config/appsettings.json

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.11.1" "version": "0.7.11.2"
}, },
"servers": [ "servers": [
{ {
@ -3917,6 +3917,76 @@
} }
} }
}, },
"/api/Opds/{apiKey}/more-in-genre": {
"get": {
"tags": [
"Opds"
],
"parameters": [
{
"name": "apiKey",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "genreId",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "pageNumber",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 1
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/Opds/{apiKey}/recently-updated": {
"get": {
"tags": [
"Opds"
],
"parameters": [
{
"name": "apiKey",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "pageNumber",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 1
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/Opds/{apiKey}/on-deck": { "/api/Opds/{apiKey}/on-deck": {
"get": { "get": {
"tags": [ "tags": [