Kavita+ Overhaul & New Changelog (#3507)
This commit is contained in:
parent
d880c1690c
commit
a5707617f2
249 changed files with 14775 additions and 2300 deletions
37
API/Data/Repositories/EmailHistoryRepository.cs
Normal file
37
API/Data/Repositories/EmailHistoryRepository.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Email;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
public interface IEmailHistoryRepository
|
||||
{
|
||||
Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams);
|
||||
}
|
||||
|
||||
public class EmailHistoryRepository : IEmailHistoryRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public EmailHistoryRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams)
|
||||
{
|
||||
return await _context.EmailHistory
|
||||
.OrderByDescending(h => h.SendDate)
|
||||
.ProjectTo<EmailHistoryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.KavitaPlus.Manage;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
@ -31,14 +32,12 @@ public interface IExternalSeriesMetadataRepository
|
|||
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
|
||||
void Remove(ExternalSeriesMetadata metadata);
|
||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task<bool> NeedsDataRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task LinkRecommendationsToSeries(Series series);
|
||||
Task LinkRecommendationsToSeries(int seriesId);
|
||||
Task<bool> IsBlacklistedSeries(int seriesId);
|
||||
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
|
||||
Task RemoveFromBlacklist(int seriesId);
|
||||
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
|
||||
Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
|
||||
}
|
||||
|
||||
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
|
||||
|
@ -107,7 +106,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
|
||||
public async Task<bool> NeedsDataRefresh(int seriesId)
|
||||
{
|
||||
var row = await _context.ExternalSeriesMetadata
|
||||
.Where(s => s.SeriesId == seriesId)
|
||||
|
@ -115,7 +114,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
|
||||
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId)
|
||||
{
|
||||
var seriesDetailDto = await _context.ExternalSeriesMetadata
|
||||
.Where(m => m.SeriesId == seriesId)
|
||||
|
@ -180,13 +179,6 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
return seriesDetailPlusDto;
|
||||
}
|
||||
|
||||
public async Task LinkRecommendationsToSeries(int seriesId)
|
||||
{
|
||||
var series = await _context.Series.Where(s => s.Id == seriesId).AsNoTracking().SingleOrDefaultAsync();
|
||||
if (series == null) return;
|
||||
await LinkRecommendationsToSeries(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
|
||||
/// </summary>
|
||||
|
@ -210,45 +202,12 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
|
||||
public Task<bool> IsBlacklistedSeries(int seriesId)
|
||||
{
|
||||
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
|
||||
return _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.Select(s => s.IsBlacklisted)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance against SeriesId and Saves to the DB
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="saveChanges"></param>
|
||||
public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true)
|
||||
{
|
||||
if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return;
|
||||
|
||||
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
|
||||
{
|
||||
SeriesId = seriesId
|
||||
});
|
||||
if (saveChanges)
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Series from Blacklist and Saves to the DB
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
public async Task RemoveFromBlacklist(int seriesId)
|
||||
{
|
||||
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
|
||||
|
||||
if (seriesBlacklist != null)
|
||||
{
|
||||
// Remove the SeriesBlacklist entity from the context
|
||||
_context.SeriesBlacklist.Remove(seriesBlacklist);
|
||||
|
||||
// Save the changes to the database
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
|
||||
{
|
||||
|
@ -261,4 +220,14 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||
.FilterMatchState(filter.MatchStateOption)
|
||||
.OrderBy(s => s.NormalizedName)
|
||||
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ public interface IScrobbleRepository
|
|||
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
||||
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
|
||||
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
|
||||
Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -153,4 +154,10 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
|
||||
return await PagedList<ScrobbleEventDto>.CreateAsync(query, pagination.PageNumber, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId)
|
||||
{
|
||||
return await _context.ScrobbleEvent.Where(e => e.SeriesId == seriesId)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
@ -165,6 +166,7 @@ public interface ISeriesRepository
|
|||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None);
|
||||
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
|
||||
Task<int> GetCountAsync();
|
||||
Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
|
@ -709,7 +711,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(s => s.Id == seriesId)
|
||||
.Select(series => new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
||||
|
@ -2037,9 +2039,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// Uses multiple names to find a match against a series. If not, returns null.
|
||||
/// </summary>
|
||||
/// <remarks>This does not restrict to the user at all. That is handled at the API level.</remarks>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="names"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
|
||||
{
|
||||
var libraryIds = await _context.Library
|
||||
|
@ -2073,6 +2072,47 @@ public class SeriesRepository : ISeriesRepository
|
|||
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
|
||||
}
|
||||
|
||||
public async Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries)
|
||||
{
|
||||
var libraryIds = await _context.Library
|
||||
.Where(lib => externalSeries.PlusMediaFormat.ConvertToLibraryTypes().Contains(lib.Type))
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var normalizedNames = (externalSeries.Synonyms ?? Enumerable.Empty<string>())
|
||||
.Prepend(externalSeries.Name)
|
||||
.Select(n => n.ToNormalized())
|
||||
.ToList();
|
||||
|
||||
var aniListWebLink =
|
||||
ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, externalSeries.AniListId);
|
||||
var malWebLink =
|
||||
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, externalSeries.MALId);
|
||||
|
||||
Series? result = null;
|
||||
if (!string.IsNullOrEmpty(aniListWebLink) || !string.IsNullOrEmpty(malWebLink))
|
||||
{
|
||||
result = await _context.Series
|
||||
.Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks))
|
||||
.Where(s => libraryIds.Contains(s.Library.Id))
|
||||
.WhereIf(!string.IsNullOrEmpty(aniListWebLink), s => s.Metadata.WebLinks.Contains(aniListWebLink))
|
||||
.WhereIf(!string.IsNullOrEmpty(malWebLink), s => s.Metadata.WebLinks.Contains(malWebLink))
|
||||
.Include(s => s.Metadata)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
if (result != null) return result;
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => normalizedNames.Contains(s.NormalizedName) ||
|
||||
normalizedNames.Contains(s.NormalizedLocalizedName))
|
||||
.Where(s => libraryIds.Contains(s.Library.Id))
|
||||
.AsSplitQuery()
|
||||
.Include(s => s.Metadata)
|
||||
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Average rating for all users within Kavita instance
|
||||
/// </summary>
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.DTOs;
|
|||
using API.DTOs.Account;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.KavitaPlus.Account;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
@ -15,6 +16,7 @@ using API.Entities;
|
|||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Extensions.QueryExtensions.Filtering;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
@ -96,6 +98,8 @@ public interface IUserRepository
|
|||
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
|
||||
Task<IList<AppUserSideNavStream>> GetDashboardStreamsByIds(IList<int> streamIds);
|
||||
Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo();
|
||||
Task<AppUser?> GetUserByDeviceEmail(string deviceEmail);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
|
@ -490,6 +494,43 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo()
|
||||
{
|
||||
var users = await _context.AppUser
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.UserName,
|
||||
u.AniListAccessToken, // JWT Token
|
||||
u.MalAccessToken // JWT Token
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var userTokenInfos = users.Select(user => new UserTokenInfo
|
||||
{
|
||||
UserId = user.Id,
|
||||
Username = user.UserName,
|
||||
IsAniListTokenSet = !string.IsNullOrEmpty(user.AniListAccessToken),
|
||||
AniListValidUntilUtc = JwtHelper.GetTokenExpiry(user.AniListAccessToken),
|
||||
IsAniListTokenValid = JwtHelper.IsTokenValid(user.AniListAccessToken),
|
||||
IsMalTokenSet = !string.IsNullOrEmpty(user.MalAccessToken),
|
||||
});
|
||||
|
||||
return userTokenInfos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first user with a device email matching
|
||||
/// </summary>
|
||||
/// <param name="deviceEmail"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserByDeviceEmail(string deviceEmail)
|
||||
{
|
||||
return await _context.AppUser
|
||||
.Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue