Misc bunch of changes (#2815)
This commit is contained in:
parent
18792b7b56
commit
63c9bff32e
81 changed files with 4567 additions and 339 deletions
|
@ -1,5 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.DTOs.Progress;
|
||||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
@ -29,4 +30,15 @@ public class AdminController : BaseApiController
|
|||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
||||
return users.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the progress information for a particular user
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("update-chapter-progress")]
|
||||
public async Task<ActionResult<bool>> UpdateChapterProgress(UpdateUserProgressDto dto)
|
||||
{
|
||||
return Ok(await Task.FromResult(false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,12 @@ using System.Collections.Generic;
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Collection;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -23,14 +25,16 @@ public class CollectionController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICollectionTagService _collectionService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IExternalMetadataService _externalMetadataService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService,
|
||||
ILocalizationService localizationService)
|
||||
ILocalizationService localizationService, IExternalMetadataService externalMetadataService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_collectionService = collectionService;
|
||||
_localizationService = localizationService;
|
||||
_externalMetadataService = externalMetadataService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -168,4 +172,15 @@ public class CollectionController : BaseApiController
|
|||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For the authenticated user, if they have an active Kavita+ subscription and a MAL username on record,
|
||||
/// fetch their Mal interest stacks (including restacks)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("mal-stacks")]
|
||||
public async Task<ActionResult<IList<MalStackDto>>> GetMalStacksForUser()
|
||||
{
|
||||
return Ok(await _externalMetadataService.GetStacksForUser(User.GetUserId()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ using API.DTOs.CollectionTags;
|
|||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.OPDS;
|
||||
using API.DTOs.Progress;
|
||||
using API.DTOs.Search;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Progress;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
|
|
@ -7,8 +7,8 @@ using API.Constants;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Progress;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
@ -880,4 +880,21 @@ public class ReaderController : BaseApiController
|
|||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all progress events for a given chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("all-chapter-progress")]
|
||||
public async Task<ActionResult<IEnumerable<FullProgressDto>>> GetProgressForChapter(int chapterId)
|
||||
{
|
||||
if (User.IsInRole(PolicyConstants.AdminRole))
|
||||
{
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId));
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, User.GetUserId()));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,23 @@ public class ScrobblingController : BaseApiController
|
|||
return Ok(user.AniListAccessToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current user's MAL token & username
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("mal-token")]
|
||||
public async Task<ActionResult<MalUserInfoDto>> GetMalToken()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
return Ok(new MalUserInfoDto()
|
||||
{
|
||||
Username = user.MalUserName,
|
||||
AccessToken = user.MalAccessToken
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the current user's AniList token
|
||||
/// </summary>
|
||||
|
@ -76,6 +93,26 @@ public class ScrobblingController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the current user's MAL token (Client ID) and Username
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-mal-token")]
|
||||
public async Task<ActionResult> UpdateMalToken(MalUserInfoDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.MalAccessToken = dto.AccessToken;
|
||||
user.MalUserName = dto.Username;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current Scrobbling token for the given Provider has expired for the current user
|
||||
/// </summary>
|
||||
|
|
|
@ -457,6 +457,7 @@ public class SettingsController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup.
|
||||
/// </summary>
|
||||
|
|
|
@ -8,6 +8,7 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -22,14 +23,16 @@ public class StatsController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILicenseService _licenseService;
|
||||
|
||||
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork,
|
||||
UserManager<AppUser> userManager, ILocalizationService localizationService)
|
||||
UserManager<AppUser> userManager, ILocalizationService localizationService, ILicenseService licenseService)
|
||||
{
|
||||
_statService = statService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_userManager = userManager;
|
||||
_localizationService = localizationService;
|
||||
_licenseService = licenseService;
|
||||
}
|
||||
|
||||
[HttpGet("user/{userId}/read")]
|
||||
|
@ -181,6 +184,18 @@ public class StatsController : BaseApiController
|
|||
return Ok(_statService.GetWordsReadCountByYear(userId));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns for Kavita+ the number of Series that have been processed, errored, and not processed
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpGet("kavitaplus-metadata-breakdown")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public async Task<ActionResult<IEnumerable<StatCount<int>>>> GetKavitaPlusMetadataBreakdown()
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense())
|
||||
return BadRequest("This data is not available for non-Kavita+ servers");
|
||||
return Ok(await _statService.GetKavitaPlusMetadataBreakdown());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
19
API/DTOs/Collection/MalStackDto.cs
Normal file
19
API/DTOs/Collection/MalStackDto.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace API.DTOs.Collection;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Interest Stack from MAL
|
||||
/// </summary>
|
||||
public class MalStackDto
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public required long StackId { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public required string? Author { get; set; }
|
||||
public required int SeriesCount { get; set; }
|
||||
public required int RestackCount { get; set; }
|
||||
/// <summary>
|
||||
/// If an existing collection exists within Kavita
|
||||
/// </summary>
|
||||
/// <remarks>This is filled out from Kavita and not Kavita+</remarks>
|
||||
public int ExistingId { get; set; }
|
||||
}
|
19
API/DTOs/Progress/FullProgressDto.cs
Normal file
19
API/DTOs/Progress/FullProgressDto.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.Progress;
|
||||
|
||||
/// <summary>
|
||||
/// A full progress Record from the DB (not all data, only what's needed for API)
|
||||
/// </summary>
|
||||
public class FullProgressDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
public int PagesRead { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public int AppUserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
namespace API.DTOs.Progress;
|
||||
#nullable enable
|
||||
|
||||
public class ProgressDto
|
11
API/DTOs/Progress/UpdateUserProgressDto.cs
Normal file
11
API/DTOs/Progress/UpdateUserProgressDto.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.Progress;
|
||||
#nullable enable
|
||||
|
||||
public class UpdateUserProgressDto
|
||||
{
|
||||
public int PageNum { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
13
API/DTOs/Scrobbling/MalUserInfoDto.cs
Normal file
13
API/DTOs/Scrobbling/MalUserInfoDto.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
/// <summary>
|
||||
/// Information about a User's MAL connection
|
||||
/// </summary>
|
||||
public class MalUserInfoDto
|
||||
{
|
||||
public required string Username { get; set; }
|
||||
/// <summary>
|
||||
/// This is actually the Client Id
|
||||
/// </summary>
|
||||
public required string AccessToken { get; set; }
|
||||
}
|
17
API/DTOs/Statistics/KavitaPlusMetadataBreakdownDto.cs
Normal file
17
API/DTOs/Statistics/KavitaPlusMetadataBreakdownDto.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace API.DTOs.Statistics;
|
||||
|
||||
public class KavitaPlusMetadataBreakdownDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Total amount of Series
|
||||
/// </summary>
|
||||
public int TotalSeries { get; set; }
|
||||
/// <summary>
|
||||
/// Series on the Blacklist (errored or bad match)
|
||||
/// </summary>
|
||||
public int ErroredSeries { get; set; }
|
||||
/// <summary>
|
||||
/// Completed so far
|
||||
/// </summary>
|
||||
public int SeriesCompleted { get; set; }
|
||||
}
|
2904
API/Data/Migrations/20240321173812_UserMalToken.Designer.cs
generated
Normal file
2904
API/Data/Migrations/20240321173812_UserMalToken.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
38
API/Data/Migrations/20240321173812_UserMalToken.cs
Normal file
38
API/Data/Migrations/20240321173812_UserMalToken.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UserMalToken : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "MalAccessToken",
|
||||
table: "AspNetUsers",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "MalUserName",
|
||||
table: "AspNetUsers",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MalAccessToken",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MalUserName",
|
||||
table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -97,6 +97,12 @@ namespace API.Data.Migrations
|
|||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MalAccessToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MalUserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
|
|
@ -5,8 +5,10 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Progress;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
@ -36,6 +38,7 @@ public interface IAppUserProgressRepository
|
|||
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
|
||||
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
|
||||
Task UpdateAllProgressThatAreMoreThanChapterPages();
|
||||
Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0);
|
||||
}
|
||||
#nullable disable
|
||||
public class AppUserProgressRepository : IAppUserProgressRepository
|
||||
|
@ -233,6 +236,33 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
await _context.Database.ExecuteSqlRawAsync(batchSql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="userId">If 0, will pull all records</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
.WhereIf(userId > 0, p => p.AppUserId == userId)
|
||||
.Where(p => p.ChapterId == chapterId)
|
||||
.Include(p => p.AppUser)
|
||||
.Select(p => new FullProgressDto()
|
||||
{
|
||||
AppUserId = p.AppUserId,
|
||||
ChapterId = p.ChapterId,
|
||||
PagesRead = p.PagesRead,
|
||||
Id = p.Id,
|
||||
Created = p.Created,
|
||||
CreatedUtc = p.CreatedUtc,
|
||||
LastModified = p.LastModified,
|
||||
LastModifiedUtc = p.LastModifiedUtc,
|
||||
UserName = p.AppUser.UserName
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
|
||||
{
|
||||
|
|
|
@ -63,6 +63,15 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||
/// <remarks>Requires Kavita+ Subscription</remarks>
|
||||
public string? AniListAccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Username of the MAL user
|
||||
/// </summary>
|
||||
public string? MalUserName { get; set; }
|
||||
/// <summary>
|
||||
/// The Client ID for the user's MAL account. User should create a client on MAL for this.
|
||||
/// </summary>
|
||||
public string? MalAccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of Series the user doesn't want scrobbling for
|
||||
/// </summary>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Metadata;
|
||||
using API.Services.Plus;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Entities;
|
||||
|
@ -41,6 +43,21 @@ public class CollectionTag
|
|||
|
||||
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Is this Collection tag managed by another system, like Kavita+
|
||||
/// </summary>
|
||||
//public bool IsManaged { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The last time this Collection was Synchronized. Only applicable for Managed Tags.
|
||||
/// </summary>
|
||||
//public DateTime LastSynchronized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Who created this Collection (Kavita, or external services)
|
||||
/// </summary>
|
||||
//public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita;
|
||||
|
||||
/// <summary>
|
||||
/// Not Used due to not using concurrency update
|
||||
/// </summary>
|
||||
|
|
|
@ -10,6 +10,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.MediaErrors;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Progress;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Recommendation;
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Collection;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
@ -61,6 +62,8 @@ public interface IExternalMetadataService
|
|||
/// <param name="libraryType"></param>
|
||||
/// <returns></returns>
|
||||
Task GetNewSeriesData(int seriesId, LibraryType libraryType);
|
||||
|
||||
Task<IList<MalStackDto>> GetStacksForUser(int userId);
|
||||
}
|
||||
|
||||
public class ExternalMetadataService : IExternalMetadataService
|
||||
|
@ -70,7 +73,8 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
private readonly IMapper _mapper;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30);
|
||||
public static readonly ImmutableArray<LibraryType> NonEligibleLibraryTypes = ImmutableArray.Create<LibraryType>(LibraryType.Comic, LibraryType.Book, LibraryType.Image, LibraryType.ComicVine);
|
||||
public static readonly ImmutableArray<LibraryType> NonEligibleLibraryTypes = ImmutableArray.Create
|
||||
(LibraryType.Comic, LibraryType.Book, LibraryType.Image, LibraryType.ComicVine);
|
||||
private readonly SeriesDetailPlusDto _defaultReturn = new()
|
||||
{
|
||||
Recommendations = null,
|
||||
|
@ -137,12 +141,15 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
public async Task ForceKavitaPlusRefresh(int seriesId)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
// Remove from Blacklist if applicable
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeBySeriesIdAsync(seriesId);
|
||||
if (!IsPlusEligible(libraryType)) return;
|
||||
|
||||
// Remove from Blacklist if applicable
|
||||
await _unitOfWork.ExternalSeriesMetadataRepository.RemoveFromBlacklist(seriesId);
|
||||
|
||||
var metadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId);
|
||||
if (metadata == null) return;
|
||||
|
||||
metadata.ValidUntilUtc = DateTime.UtcNow.Subtract(_externalSeriesMetadataCache);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
@ -170,10 +177,50 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
// Prefetch SeriesDetail data
|
||||
await GetSeriesDetailPlus(seriesId, libraryType);
|
||||
|
||||
// TODO: Fetch Series Metadata
|
||||
// TODO: Fetch Series Metadata (Summary, etc)
|
||||
|
||||
}
|
||||
|
||||
public async Task<IList<MalStackDto>> GetStacksForUser(int userId)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return ArraySegment<MalStackDto>.Empty;
|
||||
|
||||
// See if this user has Mal account on record
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null || string.IsNullOrEmpty(user.MalUserName) || string.IsNullOrEmpty(user.MalAccessToken))
|
||||
{
|
||||
_logger.LogInformation("User is attempting to fetch MAL Stacks, but missing information on their account");
|
||||
return ArraySegment<MalStackDto>.Empty;
|
||||
}
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Fetching Kavita+ for MAL Stacks for user {UserName}", user.MalUserName);
|
||||
|
||||
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
|
||||
var result = await ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={user.MalUserName}")
|
||||
.WithHeader("Accept", "application/json")
|
||||
.WithHeader("User-Agent", "Kavita")
|
||||
.WithHeader("x-license-key", license)
|
||||
.WithHeader("x-installId", HashUtil.ServerToken())
|
||||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||
.WithHeader("Content-Type", "application/json")
|
||||
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
||||
.GetJsonAsync<IList<MalStackDto>>();
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return ArraySegment<MalStackDto>.Empty;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Fetching Kavita+ for MAL Stacks for user {UserName} failed", user.MalUserName);
|
||||
return ArraySegment<MalStackDto>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves Metadata about a Recommended External Series
|
||||
/// </summary>
|
||||
|
|
|
@ -94,6 +94,7 @@ public class ScrobblingService : IScrobblingService
|
|||
ScrobbleProvider.AniList
|
||||
};
|
||||
|
||||
|
||||
private const string UnknownSeriesErrorMessage = "Series cannot be matched for Scrobbling";
|
||||
private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling";
|
||||
|
||||
|
@ -332,15 +333,7 @@ public class ScrobblingService : IScrobblingService
|
|||
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId),
|
||||
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
};
|
||||
// NOTE: Not sure how to handle scrobbling specials or handling sending loose leaf volumes
|
||||
if (evt.VolumeNumber is Parser.SpecialVolumeNumber)
|
||||
{
|
||||
evt.VolumeNumber = 0;
|
||||
}
|
||||
if (evt.VolumeNumber is Parser.DefaultChapterNumber)
|
||||
{
|
||||
evt.VolumeNumber = 0;
|
||||
}
|
||||
|
||||
_unitOfWork.ScrobbleRepository.Attach(evt);
|
||||
await _unitOfWork.CommitAsync();
|
||||
_logger.LogDebug("Added Scrobbling Read update on {SeriesName} with Userid {UserId} ", series.Name, userId);
|
||||
|
@ -826,6 +819,20 @@ public class ScrobblingService : IScrobblingService
|
|||
try
|
||||
{
|
||||
var data = await createEvent(evt);
|
||||
// We need to handle the encoding and changing it to the old one until we can update the API layer to handle these
|
||||
// which could happen in v0.8.3
|
||||
if (data.VolumeNumber is Parser.SpecialVolumeNumber)
|
||||
{
|
||||
data.VolumeNumber = 0;
|
||||
}
|
||||
if (data.VolumeNumber is Parser.DefaultChapterNumber)
|
||||
{
|
||||
data.VolumeNumber = 0;
|
||||
}
|
||||
if (data.ChapterNumber is Parser.DefaultChapterNumber)
|
||||
{
|
||||
data.ChapterNumber = 0;
|
||||
}
|
||||
userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt);
|
||||
evt.IsProcessed = true;
|
||||
evt.ProcessDateUtc = DateTime.UtcNow;
|
||||
|
@ -870,6 +877,7 @@ public class ScrobblingService : IScrobblingService
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static bool DoesUserHaveProviderAndValid(ScrobbleEvent readEvent)
|
||||
{
|
||||
var userProviders = GetUserProviders(readEvent.AppUser);
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Comparators;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Progress;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
@ -33,6 +34,7 @@ public interface IStatisticService
|
|||
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
|
||||
Task UpdateServerStatistics();
|
||||
Task<long> TimeSpentReadingForUsersAsync(IList<int> userIds, IList<int> libraryIds);
|
||||
Task<KavitaPlusMetadataBreakdownDto> GetKavitaPlusMetadataBreakdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -531,6 +533,29 @@ public class StatisticService : IStatisticService
|
|||
p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages))));
|
||||
}
|
||||
|
||||
public async Task<KavitaPlusMetadataBreakdownDto> GetKavitaPlusMetadataBreakdown()
|
||||
{
|
||||
// We need to count number of Series that have an external series record
|
||||
// Then count how many series are blacklisted
|
||||
// Then get total count of series that are Kavita+ eligible
|
||||
var plusLibraries = await _context.Library
|
||||
.Where(l => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(l.Type))
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var countOfBlacklisted = await _context.SeriesBlacklist.CountAsync();
|
||||
var totalSeries = await _context.Series.Where(s => plusLibraries.Contains(s.LibraryId)).CountAsync();
|
||||
var seriesWithMetadata = await _context.ExternalSeriesMetadata.CountAsync();
|
||||
|
||||
return new KavitaPlusMetadataBreakdownDto()
|
||||
{
|
||||
TotalSeries = totalSeries,
|
||||
ErroredSeries = countOfBlacklisted,
|
||||
SeriesCompleted = seriesWithMetadata
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TopReadDto>> GetTopUsers(int days)
|
||||
{
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Entities.Enums;
|
|||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
using ExCSS;
|
||||
using Kavita.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -448,22 +449,42 @@ public class ParseScannedFiles
|
|||
var infos = scannedSeries[series].Where(info => info.Volumes == volume.Key).ToList();
|
||||
IList<ParserInfo> chapters;
|
||||
var specialTreatment = infos.TrueForAll(info => info.IsSpecial);
|
||||
var hasAnySpMarker = infos.Exists(info => info.SpecialIndex > 0);
|
||||
var counter = 0f;
|
||||
|
||||
if (specialTreatment)
|
||||
if (specialTreatment && hasAnySpMarker)
|
||||
{
|
||||
chapters = infos
|
||||
.OrderBy(info => info.SpecialIndex)
|
||||
.ToList();
|
||||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
chapter.IssueOrder = counter;
|
||||
counter++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
|
||||
chapters = infos
|
||||
.OrderByNatural(info => info.Chapters)
|
||||
.ToList();
|
||||
|
||||
|
||||
// If everything is a special but we don't have any SpecialIndex, then order naturally and use 0, 1, 2
|
||||
if (specialTreatment)
|
||||
{
|
||||
chapters = infos
|
||||
.OrderByNatural(info => info.Chapters)
|
||||
.ToList();
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
chapter.IssueOrder = counter;
|
||||
counter++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var counter = 0f;
|
||||
counter = 0f;
|
||||
var prevIssue = string.Empty;
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
|
|
|
@ -95,6 +95,11 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag
|
|||
// Patch in other information from ComicInfo
|
||||
UpdateFromComicInfo(ret);
|
||||
|
||||
if (ret.Volumes == Parser.LooseLeafVolume && ret.Chapters == Parser.DefaultChapter)
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
}
|
||||
|
||||
// v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number
|
||||
if (ret.IsSpecial)
|
||||
{
|
||||
|
|
|
@ -22,6 +22,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer
|
|||
|
||||
if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series)))
|
||||
{
|
||||
// NOTE: I'm not sure the comment is true. I've never seen this triggered
|
||||
// This is likely a light novel for which we can set series from parsed title
|
||||
info.Series = Parser.ParseSeries(info.Title);
|
||||
info.Volumes = Parser.ParseVolume(info.Title);
|
||||
|
@ -30,6 +31,12 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer
|
|||
{
|
||||
var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo);
|
||||
info.Merge(info2);
|
||||
if (type == LibraryType.LightNovel && hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series)
|
||||
.Equals(Parser.LooseLeafVolume))
|
||||
{
|
||||
// Override the Series name so it groups appropriately
|
||||
info.Series = info2.Series;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue