Misc Bugfixes (#2216)
* Folder watching will now appropriately ignore changes that occur in blacklisted folders. * Fixed up recently updated from dashboard not opening a pre-sorted page. There were issues with how encoding and decoding was done plus missing code. * Fixed up all streams from Dashboard opening to correctly filtered pages. * All search linking now works. * Rating tooltip and stars are bigger on mobile. * A bit of cleanup * Added day breakdown to user stats page. * Removed Token checks before we write events to the history table for scrobbling. Refactored so series holds will prevent writing events for reviews, ratings, etc. * Fixed a potential bug where series name could be taken from a chapter that isn't the first ordered (very unlikely) for epubs. Fixed a bug where Volume 1.5 could be selected for series-level metadata over Volume 1. * Optimized the license check code so that users without any license entered would still take advantage of the cache layer. * Sped up an API that checks if the library allows scrobbling * Cleaned up the mobile CSS a bit for filters.
This commit is contained in:
parent
ef3e76e3e5
commit
c84a3294e9
30 changed files with 324 additions and 246 deletions
|
|
@ -143,8 +143,7 @@ public class ScrobblingController : BaseApiController
|
|||
[HttpGet("library-allows-scrobbling")]
|
||||
public async Task<ActionResult<bool>> LibraryAllowsScrobbling(int seriesId)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library);
|
||||
return Ok(series != null && series.Library.AllowScrobbling);
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -125,14 +125,21 @@ public class StatsController : BaseApiController
|
|||
}
|
||||
|
||||
[HttpGet("day-breakdown")]
|
||||
[Authorize("RequireAdminRole")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public ActionResult<IEnumerable<StatCount<DayOfWeek>>> GetDayBreakdown()
|
||||
public async Task<ActionResult<IEnumerable<StatCount<DayOfWeek>>>> GetDayBreakdown(int userId = 0)
|
||||
{
|
||||
return Ok(_statService.GetDayBreakdown());
|
||||
if (userId == 0)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!isAdmin) return BadRequest();
|
||||
}
|
||||
|
||||
return Ok(_statService.GetDayBreakdown(userId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("user/reading-history")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public async Task<ActionResult<IEnumerable<ReadHistoryEvent>>> GetReadingHistory(int userId)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
|
@ -180,7 +180,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserBookmark");
|
||||
b.ToTable("AppUserBookmark", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||
|
|
@ -201,7 +201,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserOnDeckRemoval");
|
||||
b.ToTable("AppUserOnDeckRemoval", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
|
|
@ -315,7 +315,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("ThemeId");
|
||||
|
||||
b.ToTable("AppUserPreferences");
|
||||
b.ToTable("AppUserPreferences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||
|
|
@ -365,7 +365,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserProgresses");
|
||||
b.ToTable("AppUserProgresses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||
|
|
@ -398,7 +398,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserRating");
|
||||
b.ToTable("AppUserRating", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||
|
|
@ -466,7 +466,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserTableOfContent");
|
||||
b.ToTable("AppUserTableOfContent", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
|
|
@ -576,7 +576,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("Chapter");
|
||||
b.ToTable("Chapter", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||
|
|
@ -611,7 +611,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("Id", "Promoted")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CollectionTag");
|
||||
b.ToTable("CollectionTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Device", b =>
|
||||
|
|
@ -657,7 +657,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("Device");
|
||||
b.ToTable("Device", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||
|
|
@ -679,7 +679,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("FolderPath");
|
||||
b.ToTable("FolderPath", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Genre", b =>
|
||||
|
|
@ -699,7 +699,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("NormalizedTitle")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Genre");
|
||||
b.ToTable("Genre", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Library", b =>
|
||||
|
|
@ -757,7 +757,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Library");
|
||||
b.ToTable("Library", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||
|
|
@ -806,7 +806,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("MangaFile");
|
||||
b.ToTable("MangaFile", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MediaError", b =>
|
||||
|
|
@ -841,7 +841,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MediaError");
|
||||
b.ToTable("MediaError", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||
|
|
@ -942,7 +942,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("Id", "SeriesId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SeriesMetadata");
|
||||
b.ToTable("SeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
|
|
@ -966,7 +966,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("TargetSeriesId");
|
||||
|
||||
b.ToTable("SeriesRelation");
|
||||
b.ToTable("SeriesRelation", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
|
|
@ -986,7 +986,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Person");
|
||||
b.ToTable("Person", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
|
|
@ -1049,7 +1049,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("ReadingList");
|
||||
b.ToTable("ReadingList", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
||||
|
|
@ -1083,7 +1083,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("ReadingListItem");
|
||||
b.ToTable("ReadingListItem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b =>
|
||||
|
|
@ -1128,7 +1128,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ScrobbleError");
|
||||
b.ToTable("ScrobbleError", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b =>
|
||||
|
|
@ -1188,8 +1188,8 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
b.Property<float?>("VolumeNumber")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
|
@ -1199,7 +1199,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ScrobbleEvent");
|
||||
b.ToTable("ScrobbleEvent", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b =>
|
||||
|
|
@ -1232,7 +1232,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ScrobbleHold");
|
||||
b.ToTable("ScrobbleHold", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
|
|
@ -1328,7 +1328,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Series");
|
||||
b.ToTable("Series", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
|
|
@ -1345,7 +1345,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSetting");
|
||||
b.ToTable("ServerSetting", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerStatistics", b =>
|
||||
|
|
@ -1383,7 +1383,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ServerStatistics");
|
||||
b.ToTable("ServerStatistics", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SiteTheme", b =>
|
||||
|
|
@ -1421,7 +1421,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SiteTheme");
|
||||
b.ToTable("SiteTheme", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Tag", b =>
|
||||
|
|
@ -1441,7 +1441,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("NormalizedTitle")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tag");
|
||||
b.ToTable("Tag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
|
|
@ -1477,8 +1477,8 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("INTEGER");
|
||||
b.Property<float>("Number")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
|
@ -1493,7 +1493,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("Volume");
|
||||
b.ToTable("Volume", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
|
|
@ -1508,7 +1508,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("LibrariesId");
|
||||
|
||||
b.ToTable("AppUserLibrary");
|
||||
b.ToTable("AppUserLibrary", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterGenre", b =>
|
||||
|
|
@ -1523,7 +1523,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("GenresId");
|
||||
|
||||
b.ToTable("ChapterGenre");
|
||||
b.ToTable("ChapterGenre", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterPerson", b =>
|
||||
|
|
@ -1538,7 +1538,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("PeopleId");
|
||||
|
||||
b.ToTable("ChapterPerson");
|
||||
b.ToTable("ChapterPerson", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
|
|
@ -1553,7 +1553,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("ChapterTag");
|
||||
b.ToTable("ChapterTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
|
|
@ -1568,7 +1568,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("CollectionTagSeriesMetadata");
|
||||
b.ToTable("CollectionTagSeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
||||
|
|
@ -1583,7 +1583,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("GenreSeriesMetadata");
|
||||
b.ToTable("GenreSeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
|
|
@ -1682,7 +1682,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("PersonSeriesMetadata");
|
||||
b.ToTable("PersonSeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
|
|
@ -1697,7 +1697,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("SeriesMetadataTag");
|
||||
b.ToTable("SeriesMetadataTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public interface ILibraryRepository
|
|||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<IDictionary<int, LibraryType>> GetLibraryTypesForIdsAsync(IEnumerable<int> libraryIds);
|
||||
Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
|
|
@ -374,4 +375,11 @@ public class LibraryRepository : ILibraryRepository
|
|||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> GetAllowsScrobblingBySeriesId(int seriesId)
|
||||
{
|
||||
return await _context.Series.Where(s => s.Id == seriesId)
|
||||
.Select(s => s.Library.AllowScrobbling)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,15 +128,18 @@ public class LicenseService : ILicenseService
|
|||
/// <remarks>Expected to be called at startup and on reoccurring basis</remarks>
|
||||
public async Task ValidateLicenseStatus()
|
||||
{
|
||||
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
|
||||
try
|
||||
{
|
||||
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
if (string.IsNullOrEmpty(license.Value)) return;
|
||||
if (string.IsNullOrEmpty(license.Value)) {
|
||||
await provider.SetAsync(CacheKey, false, _licenseCacheTimeout);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Validating Kavita+ License");
|
||||
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
|
||||
await provider.FlushAsync();
|
||||
|
||||
await provider.FlushAsync();
|
||||
var isValid = await IsLicenseValid(license.Value);
|
||||
await provider.SetAsync(CacheKey, isValid, _licenseCacheTimeout);
|
||||
|
||||
|
|
@ -145,6 +148,7 @@ public class LicenseService : ILicenseService
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an error talking with Kavita+ API for license validation. Rescheduling check in 30 mins");
|
||||
await provider.SetAsync(CacheKey, false, _licenseCacheTimeout);
|
||||
BackgroundJob.Schedule(() => ValidateLicenseStatus(), TimeSpan.FromMinutes(30));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ public class ScrobblingService : IScrobblingService
|
|||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// An automated job that will run against all user's tokens and validate if they are still active
|
||||
/// </summary>
|
||||
/// <remarks>This service can validate without license check as the task which calls will be guarded</remarks>
|
||||
/// <returns></returns>
|
||||
|
|
@ -115,6 +115,7 @@ public class ScrobblingService : IScrobblingService
|
|||
foreach (var user in users)
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.AniListAccessToken) || !_tokenService.HasTokenExpired(user.AniListAccessToken)) continue;
|
||||
_logger.LogInformation("User {UserName}'s AniList token has expired! They need to regenerate it for scrobbling to work", user.UserName);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.ScrobblingKeyExpired,
|
||||
MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), user.Id);
|
||||
}
|
||||
|
|
@ -184,17 +185,13 @@ public class ScrobblingService : IScrobblingService
|
|||
public async Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "unable-to-register-k+"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
||||
_logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name);
|
||||
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
|
||||
|
||||
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
|
||||
ScrobbleEventType.Review);
|
||||
|
|
@ -229,17 +226,12 @@ public class ScrobblingService : IScrobblingService
|
|||
public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
||||
_logger.LogInformation("Processing Scrobbling rating event for {UserId} on {SeriesName}", userId, series.Name);
|
||||
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
|
||||
|
||||
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
|
||||
ScrobbleEventType.ScoreUpdated);
|
||||
|
|
@ -273,22 +265,12 @@ public class ScrobblingService : IScrobblingService
|
|||
public async Task ScrobbleReadingUpdate(int userId, int seriesId)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
|
||||
{
|
||||
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, userId);
|
||||
return;
|
||||
}
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
||||
_logger.LogInformation("Processing Scrobbling reading event for {UserId} on {SeriesName}", userId, series.Name);
|
||||
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
|
||||
|
||||
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
|
||||
ScrobbleEventType.ChapterRead);
|
||||
|
|
@ -338,17 +320,12 @@ public class ScrobblingService : IScrobblingService
|
|||
public async Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
||||
_logger.LogInformation("Processing Scrobbling want-to-read event for {UserId} on {SeriesName}", userId, series.Name);
|
||||
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
|
||||
|
||||
var existing = await _unitOfWork.ScrobbleRepository.Exists(userId, series.Id,
|
||||
onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead);
|
||||
|
|
@ -369,6 +346,21 @@ public class ScrobblingService : IScrobblingService
|
|||
_logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {UserId} ", series.Name, userId);
|
||||
}
|
||||
|
||||
private async Task<bool> CheckIfCanScrobble(int userId, int seriesId, Series series)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
|
||||
{
|
||||
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name,
|
||||
userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return true;
|
||||
if (library.Type == LibraryType.Comic) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<int> GetRateLimit(string license, string aniListToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(aniListToken)) return 0;
|
||||
|
|
|
|||
|
|
@ -65,16 +65,19 @@ public class SeriesService : ISeriesService
|
|||
/// <returns></returns>
|
||||
public static Chapter? GetFirstChapterForMetadata(Series series)
|
||||
{
|
||||
var sortedVolumes = series.Volumes.OrderBy(v => v.Number, ChapterSortComparer.Default);
|
||||
var sortedVolumes = series.Volumes
|
||||
.Where(v => float.TryParse(v.Name, out var parsedValue) && parsedValue != 0.0f)
|
||||
.OrderBy(v => float.TryParse(v.Name, out var parsedValue) ? parsedValue : float.MaxValue);
|
||||
var minVolumeNumber = sortedVolumes
|
||||
.Where(v => v.Number != 0)
|
||||
.MinBy(v => v.Number);
|
||||
.MinBy(v => float.Parse(v.Name));
|
||||
|
||||
var minChapter = series.Volumes
|
||||
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default))
|
||||
|
||||
var allChapters = series.Volumes
|
||||
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)).ToList();
|
||||
var minChapter = allChapters
|
||||
.FirstOrDefault();
|
||||
|
||||
if (minVolumeNumber != null && minChapter != null && float.Parse(minChapter.Number) > minVolumeNumber.Number)
|
||||
if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, out var chapNum) && chapNum >= minVolumeNumber.Number)
|
||||
{
|
||||
return minVolumeNumber.Chapters.MinBy(c => float.Parse(c.Number), ChapterSortComparer.Default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public interface IStatisticService
|
|||
Task<IEnumerable<TopReadDto>> GetTopUsers(int days);
|
||||
Task<IEnumerable<ReadHistoryEvent>> GetReadingHistory(int userId);
|
||||
Task<IEnumerable<PagesReadOnADayCount<DateTime>>> ReadCountByDay(int userId = 0, int days = 0);
|
||||
IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown();
|
||||
IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown(int userId = 0);
|
||||
IEnumerable<StatCount<int>> GetPagesReadCountByYear(int userId = 0);
|
||||
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
|
||||
Task UpdateServerStatistics();
|
||||
|
|
@ -411,11 +411,12 @@ public class StatisticService : IStatisticService
|
|||
return results.OrderBy(r => r.Value);
|
||||
}
|
||||
|
||||
public IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown()
|
||||
public IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown(int userId)
|
||||
{
|
||||
return _context.AppUserProgresses
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.WhereIf(userId > 0, p => p.AppUserId == userId)
|
||||
.GroupBy(p => p.LastModified.DayOfWeek)
|
||||
.OrderBy(g => g.Key)
|
||||
.Select(g => new StatCount<DayOfWeek>{ Value = g.Key, Count = g.Count() })
|
||||
|
|
|
|||
|
|
@ -229,14 +229,20 @@ public class LibraryWatcher : ILibraryWatcher
|
|||
public async Task ProcessChange(string filePath, bool isDirectoryChange = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
_logger.LogDebug("[LibraryWatcher] Processing change of {FilePath}", filePath);
|
||||
_logger.LogTrace("[LibraryWatcher] Processing change of {FilePath}", filePath);
|
||||
try
|
||||
{
|
||||
// If the change occurs in a blacklisted folder path, then abort processing
|
||||
if (Parser.Parser.HasBlacklistedFolderInPath(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If not a directory change AND file is not an archive or book, ignore
|
||||
if (!isDirectoryChange &&
|
||||
!(Parser.Parser.IsArchive(filePath) || Parser.Parser.IsBook(filePath)))
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath);
|
||||
_logger.LogTrace("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -248,10 +254,10 @@ public class LibraryWatcher : ILibraryWatcher
|
|||
.ToList();
|
||||
|
||||
var fullPath = GetFolder(filePath, libraryFolders);
|
||||
_logger.LogDebug("Folder path: {FolderPath}", fullPath);
|
||||
_logger.LogTrace("Folder path: {FolderPath}", fullPath);
|
||||
if (string.IsNullOrEmpty(fullPath))
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath);
|
||||
_logger.LogTrace("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ public class ProcessSeries : IProcessSeries
|
|||
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
|
||||
// parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort)
|
||||
// BUG: This check doesn't work for Books, as books usually have metadata on all files. (#2167)
|
||||
var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, firstInfo);
|
||||
|
||||
UpdateVolumes(series, parsedInfos, forceUpdate);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue