Merged v0.5.1 develop into main.
This commit is contained in:
commit
150479e755
256 changed files with 6898 additions and 1833 deletions
|
@ -61,6 +61,11 @@ namespace API.Data
|
|||
};
|
||||
}
|
||||
|
||||
public static SeriesMetadata SeriesMetadata(ComicInfo info)
|
||||
{
|
||||
return SeriesMetadata(Array.Empty<CollectionTag>());
|
||||
}
|
||||
|
||||
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
|
||||
{
|
||||
return new SeriesMetadata()
|
||||
|
|
|
@ -103,17 +103,6 @@ namespace API.Data.Metadata
|
|||
info.Characters = Parser.Parser.CleanAuthor(info.Characters);
|
||||
info.Translator = Parser.Parser.CleanAuthor(info.Translator);
|
||||
info.CoverArtist = Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||
|
||||
|
||||
// if (!string.IsNullOrEmpty(info.Web))
|
||||
// {
|
||||
// // ComicVine stores the Issue number in Number field and does not use Volume.
|
||||
// if (!info.Web.Contains("https://comicvine.gamespot.com/")) return;
|
||||
// if (info.Volume.Equals("1"))
|
||||
// {
|
||||
// info.Volume = Parser.Parser.DefaultVolume;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ using Microsoft.Extensions.Logging;
|
|||
namespace API.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible to migrate existing bookmarks to files
|
||||
/// Responsible to migrate existing bookmarks to files. Introduced in v0.4.9.27
|
||||
/// </summary>
|
||||
public static class MigrateBookmarks
|
||||
{
|
||||
private static readonly Version VersionBookmarksChanged = new Version(0, 4, 9, 27);
|
||||
/// <summary>
|
||||
/// This will migrate existing bookmarks to bookmark folder based
|
||||
/// This will migrate existing bookmarks to bookmark folder based.
|
||||
/// If the bookmarks folder already exists, this will not run.
|
||||
/// </summary>
|
||||
/// <remarks>Bookmark directory is configurable. This will always use the default bookmark directory.</remarks>
|
||||
/// <param name="directoryService"></param>
|
||||
|
|
30
API/Data/MigrateChangePasswordRoles.cs
Normal file
30
API/Data/MigrateChangePasswordRoles.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace API.Data;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.5.1. Adds the role to all users.
|
||||
/// </summary>
|
||||
public static class MigrateChangePasswordRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Will not run if any users have the ChangePassword role already
|
||||
/// </summary>
|
||||
/// <param name="unitOfWork"></param>
|
||||
/// <param name="userManager"></param>
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
||||
{
|
||||
var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangePasswordRole);
|
||||
if (usersWithRole.Count != 0) return;
|
||||
|
||||
var allUsers = await unitOfWork.UserRepository.GetAllUsers();
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
await userManager.RemoveFromRoleAsync(user, "ChangePassword");
|
||||
await userManager.AddToRoleAsync(user, PolicyConstants.ChangePasswordRole);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ public interface IAppUserProgressRepository
|
|||
Task<int> CleanupAbandonedChapters();
|
||||
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
|
||||
Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId);
|
||||
Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId);
|
||||
}
|
||||
|
||||
public class AppUserProgressRepository : IAppUserProgressRepository
|
||||
|
@ -76,6 +77,12 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
.AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId);
|
||||
}
|
||||
|
||||
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
|
|
|
@ -67,6 +67,7 @@ public class GenreRepository : IGenreRepository
|
|||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.Genres)
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Title)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ public interface ILibraryRepository
|
|||
Task<bool> DeleteLibrary(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds);
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
|
@ -108,6 +109,13 @@ public class LibraryRepository : ILibraryRepository
|
|||
.SingleAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds)
|
||||
{
|
||||
return await _context.Library
|
||||
.Where(x => libraryIds.Contains(x.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
|
||||
{
|
||||
return await _context.Library
|
||||
|
|
|
@ -66,6 +66,8 @@ public class PersonRepository : IPersonRepository
|
|||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.People)
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Name)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -74,6 +76,7 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IList<Person>> GetAllPeople()
|
||||
{
|
||||
return await _context.Person
|
||||
.OrderBy(p => p.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ using API.DTOs;
|
|||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Search;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
|
@ -21,6 +23,23 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
internal class RecentlyAddedSeries
|
||||
{
|
||||
public int LibraryId { get; init; }
|
||||
public LibraryType LibraryType { get; init; }
|
||||
public DateTime Created { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public string SeriesName { get; init; }
|
||||
public MangaFormat Format { get; init; }
|
||||
public int ChapterId { get; init; }
|
||||
public int VolumeId { get; init; }
|
||||
public string ChapterNumber { get; init; }
|
||||
public string ChapterRange { get; init; }
|
||||
public string ChapterTitle { get; init; }
|
||||
public bool IsSpecial { get; init; }
|
||||
public int VolumeNumber { get; init; }
|
||||
}
|
||||
|
||||
public interface ISeriesRepository
|
||||
{
|
||||
void Attach(Series series);
|
||||
|
@ -39,10 +58,12 @@ public interface ISeriesRepository
|
|||
/// <summary>
|
||||
/// Does not add user information like progress, ratings, etc.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="isAdmin"></param>
|
||||
/// <param name="libraryIds"></param>
|
||||
/// <param name="searchQuery">Series name to search for</param>
|
||||
/// <param name="searchQuery"></param>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery);
|
||||
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery);
|
||||
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId);
|
||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
Task<bool> DeleteSeriesAsync(int seriesId);
|
||||
|
@ -73,6 +94,8 @@ public interface ISeriesRepository
|
|||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
|
||||
Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
|
@ -124,6 +147,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.CountAsync() > 1;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId)
|
||||
{
|
||||
return await _context.Series
|
||||
|
@ -209,15 +233,18 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all series
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||
{
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
||||
|
||||
if (filter.SortOptions == null)
|
||||
{
|
||||
query = query.OrderBy(s => s.SortName);
|
||||
}
|
||||
|
||||
var retSeries = query
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
|
@ -244,9 +271,25 @@ public class SeriesRepository : ISeriesRepository
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery)
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery)
|
||||
{
|
||||
return await _context.Series
|
||||
|
||||
var result = new SearchResultGroupDto();
|
||||
|
||||
var seriesIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
result.Libraries = await _context.Library
|
||||
.Where(l => libraryIds.Contains(l.Id))
|
||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%"))
|
||||
.OrderBy(l => l.Name)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Series = await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
|
||||
|
@ -254,17 +297,56 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(s => s.Library)
|
||||
.OrderBy(s => s.SortName)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.ReadingLists = await _context.ReadingList
|
||||
.Where(rl => rl.AppUserId == userId || rl.Promoted)
|
||||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Collections = await _context.CollectionTag
|
||||
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
||||
.Where(s => s.Promoted || isAdmin)
|
||||
.OrderBy(s => s.Title)
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Persons = await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Genres = await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.Distinct()
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Tags = await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.Distinct()
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId)
|
||||
{
|
||||
var series = await _context.Series.Where(x => x.Id == seriesId)
|
||||
|
@ -277,9 +359,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
return seriesList[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<bool> DeleteSeriesAsync(int seriesId)
|
||||
{
|
||||
var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync();
|
||||
|
@ -345,7 +424,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This returns a dictonary mapping seriesId -> list of chapters back for each series id passed
|
||||
/// This returns a dictionary mapping seriesId -> list of chapters back for each series id passed
|
||||
/// </summary>
|
||||
/// <param name="seriesIds"></param>
|
||||
/// <returns></returns>
|
||||
|
@ -452,7 +531,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
allPeopleIds.AddRange(filter.Publisher);
|
||||
allPeopleIds.AddRange(filter.CoverArtist);
|
||||
allPeopleIds.AddRange(filter.Translators);
|
||||
//allPeopleIds.AddRange(filter.Artist);
|
||||
|
||||
hasPeopleFilter = allPeopleIds.Count > 0;
|
||||
hasGenresFilter = filter.Genres.Count > 0;
|
||||
|
@ -566,7 +644,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
&& (!hasPeopleFilter || s.Metadata.People.Any(p => allPeopleIds.Contains(p.Id)))
|
||||
&& (!hasCollectionTagFilter ||
|
||||
s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id)))
|
||||
&& (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating))
|
||||
&& (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating && r.AppUserId == userId))
|
||||
&& (!hasProgressFilter || seriesIds.Contains(s.Id))
|
||||
&& (!hasAgeRating || filter.AgeRating.Contains(s.Metadata.AgeRating))
|
||||
&& (!hasTagsFilter || s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
|
||||
|
@ -575,34 +653,32 @@ public class SeriesRepository : ISeriesRepository
|
|||
)
|
||||
.AsNoTracking();
|
||||
|
||||
if (filter.SortOptions != null)
|
||||
// If no sort options, default to using SortName
|
||||
filter.SortOptions ??= new SortOptions()
|
||||
{
|
||||
if (filter.SortOptions.IsAscending)
|
||||
IsAscending = true,
|
||||
SortField = SortField.SortName
|
||||
};
|
||||
|
||||
if (filter.SortOptions.IsAscending)
|
||||
{
|
||||
query = filter.SortOptions.SortField switch
|
||||
{
|
||||
if (filter.SortOptions.SortField == SortField.SortName)
|
||||
{
|
||||
query = query.OrderBy(s => s.SortName);
|
||||
} else if (filter.SortOptions.SortField == SortField.CreatedDate)
|
||||
{
|
||||
query = query.OrderBy(s => s.Created);
|
||||
} else if (filter.SortOptions.SortField == SortField.LastModifiedDate)
|
||||
{
|
||||
query = query.OrderBy(s => s.LastModified);
|
||||
}
|
||||
}
|
||||
else
|
||||
SortField.SortName => query.OrderBy(s => s.SortName),
|
||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
query = filter.SortOptions.SortField switch
|
||||
{
|
||||
if (filter.SortOptions.SortField == SortField.SortName)
|
||||
{
|
||||
query = query.OrderByDescending(s => s.SortName);
|
||||
} else if (filter.SortOptions.SortField == SortField.CreatedDate)
|
||||
{
|
||||
query = query.OrderByDescending(s => s.Created);
|
||||
} else if (filter.SortOptions.SortField == SortField.LastModifiedDate)
|
||||
{
|
||||
query = query.OrderByDescending(s => s.LastModified);
|
||||
}
|
||||
}
|
||||
SortField.SortName => query.OrderByDescending(s => s.SortName),
|
||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
|
||||
return query;
|
||||
|
@ -777,6 +853,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
var ret = await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Metadata.Language)
|
||||
.AsNoTracking()
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -786,7 +863,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
Title = CultureInfo.GetCultureInfo(s).DisplayName,
|
||||
IsoCode = s
|
||||
}).ToList();
|
||||
})
|
||||
.OrderBy(s => s.Title)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
|
||||
|
@ -800,6 +879,154 @@ public class SeriesRepository : ISeriesRepository
|
|||
Value = s,
|
||||
Title = s.ToDescription()
|
||||
})
|
||||
.OrderBy(s => s.Title)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static string RecentlyAddedItemTitle(RecentlyAddedSeries item)
|
||||
{
|
||||
switch (item.LibraryType)
|
||||
{
|
||||
case LibraryType.Book:
|
||||
return string.Empty;
|
||||
case LibraryType.Comic:
|
||||
return "Issue";
|
||||
case LibraryType.Manga:
|
||||
default:
|
||||
return "Chapter";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show all recently added chapters. Provide some mapping for chapter 0 -> Volume 1
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId)
|
||||
{
|
||||
var ret = await GetRecentlyAddedChaptersQuery(userId);
|
||||
|
||||
var items = new List<RecentlyAddedItemDto>();
|
||||
foreach (var item in ret)
|
||||
{
|
||||
var dto = new RecentlyAddedItemDto()
|
||||
{
|
||||
LibraryId = item.LibraryId,
|
||||
LibraryType = item.LibraryType,
|
||||
SeriesId = item.SeriesId,
|
||||
SeriesName = item.SeriesName,
|
||||
Created = item.Created,
|
||||
Id = items.Count,
|
||||
Format = item.Format
|
||||
};
|
||||
|
||||
// Add title and Volume/Chapter Id
|
||||
var chapterTitle = RecentlyAddedItemTitle(item);
|
||||
string title;
|
||||
if (item.ChapterNumber.Equals(Parser.Parser.DefaultChapter))
|
||||
{
|
||||
if ((item.VolumeNumber + string.Empty).Equals(Parser.Parser.DefaultChapter))
|
||||
{
|
||||
title = item.ChapterTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
title = "Volume " + item.VolumeNumber;
|
||||
}
|
||||
|
||||
dto.VolumeId = item.VolumeId;
|
||||
}
|
||||
else
|
||||
{
|
||||
title = item.IsSpecial
|
||||
? item.ChapterRange
|
||||
: $"{chapterTitle} {item.ChapterRange}";
|
||||
dto.ChapterId = item.ChapterId;
|
||||
}
|
||||
|
||||
dto.Title = title;
|
||||
items.Add(dto);
|
||||
}
|
||||
|
||||
|
||||
return items;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
|
||||
/// </summary>
|
||||
/// <param name="userId">Used to ensure user has access to libraries</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
|
||||
{
|
||||
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
|
||||
|
||||
|
||||
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
|
||||
var index = 0;
|
||||
foreach (var item in ret)
|
||||
{
|
||||
if (seriesMap.ContainsKey(item.SeriesName))
|
||||
{
|
||||
seriesMap[item.SeriesName].Count += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
seriesMap[item.SeriesName] = new GroupedSeriesDto()
|
||||
{
|
||||
LibraryId = item.LibraryId,
|
||||
LibraryType = item.LibraryType,
|
||||
SeriesId = item.SeriesId,
|
||||
SeriesName = item.SeriesName,
|
||||
Created = item.Created,
|
||||
Id = index,
|
||||
Format = item.Format,
|
||||
Count = 1
|
||||
};
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return seriesMap.Values.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
|
||||
{
|
||||
var libraries = await _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(u => u.Libraries.Select(l => new {LibraryId = l.Id, LibraryType = l.Type}))
|
||||
.ToListAsync();
|
||||
var libraryIds = libraries.Select(l => l.LibraryId).ToList();
|
||||
|
||||
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
|
||||
var ret = await _context.Chapter
|
||||
.Where(c => c.Created >= withinLastWeek)
|
||||
.AsNoTracking()
|
||||
.Include(c => c.Volume)
|
||||
.ThenInclude(v => v.Series)
|
||||
.ThenInclude(s => s.Library)
|
||||
.OrderByDescending(c => c.Created)
|
||||
.Select(c => new RecentlyAddedSeries()
|
||||
{
|
||||
LibraryId = c.Volume.Series.LibraryId,
|
||||
LibraryType = c.Volume.Series.Library.Type,
|
||||
Created = c.Created,
|
||||
SeriesId = c.Volume.Series.Id,
|
||||
SeriesName = c.Volume.Series.Name,
|
||||
VolumeId = c.VolumeId,
|
||||
ChapterId = c.Id,
|
||||
Format = c.Volume.Series.Format,
|
||||
ChapterNumber = c.Number,
|
||||
ChapterRange = c.Range,
|
||||
IsSpecial = c.IsSpecial,
|
||||
VolumeNumber = c.Volume.Number,
|
||||
ChapterTitle = c.Title
|
||||
})
|
||||
.Take(maxRecords)
|
||||
.Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId))
|
||||
.ToListAsync();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,13 +50,13 @@ public class TagRepository : ITagRepository
|
|||
|
||||
public async Task RemoveAllTagNoLongerAssociated(bool removeExternal = false)
|
||||
{
|
||||
var TagsWithNoConnections = await _context.Tag
|
||||
var tagsWithNoConnections = await _context.Tag
|
||||
.Include(p => p.SeriesMetadatas)
|
||||
.Include(p => p.Chapters)
|
||||
.Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0 && p.ExternalTag == removeExternal)
|
||||
.ToListAsync();
|
||||
|
||||
_context.Tag.RemoveRange(TagsWithNoConnections);
|
||||
_context.Tag.RemoveRange(tagsWithNoConnections);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
@ -67,6 +67,8 @@ public class TagRepository : ITagRepository
|
|||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.Tags)
|
||||
.Distinct()
|
||||
.OrderBy(t => t.Title)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -80,6 +82,7 @@ public class TagRepository : ITagRepository
|
|||
{
|
||||
return await _context.Tag
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.Title)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
|
@ -20,7 +21,8 @@ public enum AppUserIncludes
|
|||
Progress = 2,
|
||||
Bookmarks = 4,
|
||||
ReadingLists = 8,
|
||||
Ratings = 16
|
||||
Ratings = 16,
|
||||
UserPreferences = 32
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
|
@ -29,7 +31,9 @@ public interface IUserRepository
|
|||
void Update(AppUserPreferences preferences);
|
||||
void Update(AppUserBookmark bookmark);
|
||||
public void Delete(AppUser user);
|
||||
Task<IEnumerable<MemberDto>> GetMembersAsync();
|
||||
void Delete(AppUserBookmark bookmark);
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync();
|
||||
Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync();
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<IEnumerable<AppUser>> GetNonAdminUsersAsync();
|
||||
Task<bool> IsUserAdminAsync(AppUser user);
|
||||
|
@ -48,6 +52,9 @@ public interface IUserRepository
|
|||
Task<int> GetUserIdByUsernameAsync(string username);
|
||||
Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username);
|
||||
Task<IList<AppUserBookmark>> GetAllBookmarksByIds(IList<int> bookmarkIds);
|
||||
Task<AppUser> GetUserByEmailAsync(string email);
|
||||
Task<IEnumerable<AppUser>> GetAllUsers();
|
||||
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
|
@ -83,6 +90,11 @@ public class UserRepository : IUserRepository
|
|||
_context.AppUser.Remove(user);
|
||||
}
|
||||
|
||||
public void Delete(AppUserBookmark bookmark)
|
||||
{
|
||||
_context.AppUserBookmark.Remove(bookmark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
|
||||
/// </summary>
|
||||
|
@ -156,6 +168,13 @@ public class UserRepository : IUserRepository
|
|||
query = query.Include(u => u.Ratings);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
|
||||
{
|
||||
query = query.Include(u => u.UserPreferences);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
@ -198,6 +217,16 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUser> GetUserByEmailAsync(string email)
|
||||
{
|
||||
return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(email.ToLower()));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAllUsers()
|
||||
{
|
||||
return await _context.AppUser.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
|
@ -280,9 +309,10 @@ public class UserRepository : IUserRepository
|
|||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
||||
public async Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync()
|
||||
{
|
||||
return await _context.Users
|
||||
.Where(u => u.EmailConfirmed)
|
||||
.Include(x => x.Libraries)
|
||||
.Include(r => r.UserRoles)
|
||||
.ThenInclude(r => r.Role)
|
||||
|
@ -291,6 +321,7 @@ public class UserRepository : IUserRepository
|
|||
{
|
||||
Id = u.Id,
|
||||
Username = u.UserName,
|
||||
Email = u.Email,
|
||||
Created = u.Created,
|
||||
LastActive = u.LastActive,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
|
||||
|
@ -305,4 +336,42 @@ public class UserRepository : IUserRepository
|
|||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync()
|
||||
{
|
||||
return await _context.Users
|
||||
.Where(u => !u.EmailConfirmed)
|
||||
.Include(x => x.Libraries)
|
||||
.Include(r => r.UserRoles)
|
||||
.ThenInclude(r => r.Role)
|
||||
.OrderBy(u => u.UserName)
|
||||
.Select(u => new MemberDto
|
||||
{
|
||||
Id = u.Id,
|
||||
Username = u.UserName,
|
||||
Email = u.Email,
|
||||
Created = u.Created,
|
||||
LastActive = u.LastActive,
|
||||
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
|
||||
Libraries = u.Libraries.Select(l => new LibraryDto
|
||||
{
|
||||
Name = l.Name,
|
||||
Type = l.Type,
|
||||
LastScanned = l.LastScanned,
|
||||
Folders = l.Folders.Select(x => x.Path).ToList()
|
||||
}).ToList()
|
||||
})
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateUserExists(string username)
|
||||
{
|
||||
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == username.ToUpper()))
|
||||
{
|
||||
throw new ValidationException("Username is taken.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
|
|
|
@ -60,6 +60,7 @@ namespace API.Data
|
|||
new () {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
new () {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new () {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new () {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
};
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
|
@ -92,12 +93,9 @@ namespace API.Data
|
|||
await context.Database.EnsureCreatedAsync();
|
||||
|
||||
var users = await context.AppUser.ToListAsync();
|
||||
foreach (var user in users)
|
||||
foreach (var user in users.Where(user => string.IsNullOrEmpty(user.ApiKey)))
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.ApiKey))
|
||||
{
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
}
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue