More Filtering and Support for ComicInfo v2.1 (draft) Tags (#851)
* Added a reoccuring task to cleanup db entries that might be abandoned. On library page, the Library in question will be prepoulated. * Laid out the foundation for customized sorting. Added all series page to the UI when clicking on Libraries section header on home page so user can apply any filtering they like. * When filtering, the current library filter will now automatically filter out the options for people and genres. * Implemented Sorting controls * Clear now clears sorting and read progress. Sorting is disabled on deck and recently added. * Fixed an issue where all-series page couldn't click to open series * Don't let the user unselect the last read progress. Added new comicinfo v2.1 draft tags. * Hooked in Translator tag into backend and UI. * Fixed an issue where you could open multiple typeaheads at the same time * Integrated Translator and Tags ComicInfo extension fields. Started work on a badge expander. * Reworked a bit more on badge expander. Added the UI code for Age Rating and Tags * Integrated backend for Tags, Translator, and Age Rating * Metadata tags now collapse if more than 4 present * Some code cleanup * Made the not read badge slightly smaller
This commit is contained in:
parent
21da5d8134
commit
94bad97511
71 changed files with 4324 additions and 207 deletions
|
@ -39,6 +39,7 @@ namespace API.Data
|
|||
public DbSet<ReadingListItem> ReadingListItem { get; set; }
|
||||
public DbSet<Person> Person { get; set; }
|
||||
public DbSet<Genre> Genre { get; set; }
|
||||
public DbSet<Tag> Tag { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
|
|
@ -91,6 +91,16 @@ namespace API.Data
|
|||
};
|
||||
}
|
||||
|
||||
public static Tag Tag(string name, bool external)
|
||||
{
|
||||
return new Tag()
|
||||
{
|
||||
Title = name.Trim().SentenceCase(),
|
||||
NormalizedTitle = Parser.Parser.Normalize(name),
|
||||
ExternalTag = external
|
||||
};
|
||||
}
|
||||
|
||||
public static Person Person(string name, PersonRole role)
|
||||
{
|
||||
return new Person()
|
||||
|
|
|
@ -20,6 +20,9 @@ namespace API.Data.Metadata
|
|||
public string Genre { get; set; } = string.Empty;
|
||||
public int PageCount { get; set; }
|
||||
// ReSharper disable once InconsistentNaming
|
||||
/// <summary>
|
||||
/// ISO 639-1 Code to represent the language of the content
|
||||
/// </summary>
|
||||
public string LanguageISO { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This is the link to where the data was scraped from
|
||||
|
@ -51,8 +54,16 @@ namespace API.Data.Metadata
|
|||
/// </summary>
|
||||
public string TitleSort { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
|
||||
/// </summary>
|
||||
/// See https://github.com/anansi-project/comicinfo/issues/2 for information about this tag
|
||||
public string Translator { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Misc tags. This is part of ComicInfo.xml draft v2.1
|
||||
/// </summary>
|
||||
/// See https://github.com/anansi-project/comicinfo/issues/1 for information about this tag
|
||||
public string Tags { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
|
||||
|
|
1311
API/Data/Migrations/20211216150752_seriesAndChapterTags.Designer.cs
generated
Normal file
1311
API/Data/Migrations/20211216150752_seriesAndChapterTags.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
103
API/Data/Migrations/20211216150752_seriesAndChapterTags.cs
Normal file
103
API/Data/Migrations/20211216150752_seriesAndChapterTags.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class seriesAndChapterTags : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tag",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
NormalizedTitle = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ExternalTag = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tag", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChapterTag",
|
||||
columns: table => new
|
||||
{
|
||||
ChaptersId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TagsId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChapterTag", x => new { x.ChaptersId, x.TagsId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ChapterTag_Chapter_ChaptersId",
|
||||
column: x => x.ChaptersId,
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChapterTag_Tag_TagsId",
|
||||
column: x => x.TagsId,
|
||||
principalTable: "Tag",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesMetadataTag",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesMetadatasId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TagsId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesMetadataTag", x => new { x.SeriesMetadatasId, x.TagsId });
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesMetadataTag_SeriesMetadata_SeriesMetadatasId",
|
||||
column: x => x.SeriesMetadatasId,
|
||||
principalTable: "SeriesMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesMetadataTag_Tag_TagsId",
|
||||
column: x => x.TagsId,
|
||||
principalTable: "Tag",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChapterTag_TagsId",
|
||||
table: "ChapterTag",
|
||||
column: "TagsId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesMetadataTag_TagsId",
|
||||
table: "SeriesMetadataTag",
|
||||
column: "TagsId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tag_NormalizedTitle_ExternalTag",
|
||||
table: "Tag",
|
||||
columns: new[] { "NormalizedTitle", "ExternalTag" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChapterTag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesMetadataTag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tag");
|
||||
}
|
||||
}
|
||||
}
|
1314
API/Data/Migrations/20211216191436_seriesLanguage.Designer.cs
generated
Normal file
1314
API/Data/Migrations/20211216191436_seriesLanguage.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
25
API/Data/Migrations/20211216191436_seriesLanguage.cs
Normal file
25
API/Data/Migrations/20211216191436_seriesLanguage.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class seriesLanguage : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Language",
|
||||
table: "SeriesMetadata",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Language",
|
||||
table: "SeriesMetadata");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.0");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
@ -490,6 +490,9 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("AgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ReleaseYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -668,6 +671,29 @@ namespace API.Data.Migrations
|
|||
b.ToTable("ServerSetting");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ExternalTag")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NormalizedTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedTitle", "ExternalTag")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -732,6 +758,21 @@ namespace API.Data.Migrations
|
|||
b.ToTable("ChapterPerson");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
{
|
||||
b.Property<int>("ChaptersId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TagsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ChaptersId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("ChapterTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("CollectionTagsId")
|
||||
|
@ -861,6 +902,21 @@ namespace API.Data.Migrations
|
|||
b.ToTable("PersonSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
{
|
||||
b.Property<int>("SeriesMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TagsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("SeriesMetadatasId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("SeriesMetadataTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
@ -1082,6 +1138,21 @@ namespace API.Data.Migrations
|
|||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ChaptersId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.CollectionTag", null)
|
||||
|
@ -1163,6 +1234,21 @@ namespace API.Data.Migrations
|
|||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Metadata.SeriesMetadata", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
|
|
|
@ -17,6 +17,7 @@ public interface IGenreRepository
|
|||
Task<IList<Genre>> GetAllGenresAsync();
|
||||
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
|
||||
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
||||
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
|
||||
}
|
||||
|
||||
public class GenreRepository : IGenreRepository
|
||||
|
@ -60,6 +61,16 @@ public class GenreRepository : IGenreRepository
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.Genres)
|
||||
.Distinct()
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Genre>> GetAllGenresAsync()
|
||||
{
|
||||
return await _context.Genre.ToListAsync();
|
||||
|
@ -68,6 +79,7 @@ public class GenreRepository : IGenreRepository
|
|||
public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync()
|
||||
{
|
||||
return await _context.Genre
|
||||
.AsNoTracking()
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
@ -13,6 +15,7 @@ public interface IPersonRepository
|
|||
void Remove(Person person);
|
||||
Task<IList<Person>> GetAllPeople();
|
||||
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
|
||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
|
||||
}
|
||||
|
||||
public class PersonRepository : IPersonRepository
|
||||
|
@ -57,6 +60,16 @@ public class PersonRepository : IPersonRepository
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.People)
|
||||
.Distinct()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IList<Person>> GetAllPeople()
|
||||
{
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Scanner;
|
||||
using API.DTOs;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
|
@ -14,6 +16,7 @@ using API.Helpers;
|
|||
using API.Services.Tasks;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
@ -67,6 +70,8 @@ public interface ISeriesRepository
|
|||
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
|
||||
Task<Chunk> GetChunkInfo(int libraryId = 0);
|
||||
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
|
@ -135,13 +140,23 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
var query = _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(cm => cm.People)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
|
@ -168,6 +183,14 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(cm => cm.People)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(cm => cm.Tags)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
|
@ -179,8 +202,12 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
||||
|
||||
if (filter.SortOptions == null)
|
||||
{
|
||||
query = query.OrderBy(s => s.SortName);
|
||||
}
|
||||
|
||||
var retSeries = query
|
||||
.OrderByDescending(s => s.SortName)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
@ -387,7 +414,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
private IList<MangaFormat> ExtractFilters(int libraryId, int userId, FilterDto filter, ref List<int> userLibraries,
|
||||
out List<int> allPeopleIds, out bool hasPeopleFilter, out bool hasGenresFilter, out bool hasCollectionTagFilter,
|
||||
out bool hasRatingFilter, out bool hasProgressFilter, out IList<int> seriesIds)
|
||||
out bool hasRatingFilter, out bool hasProgressFilter, out IList<int> seriesIds, out bool hasAgeRating, out bool hasTagsFilter,
|
||||
out bool hasLanguageFilter)
|
||||
{
|
||||
var formats = filter.GetSqlFilter();
|
||||
|
||||
|
@ -406,12 +434,16 @@ public class SeriesRepository : ISeriesRepository
|
|||
allPeopleIds.AddRange(filter.Penciller);
|
||||
allPeopleIds.AddRange(filter.Publisher);
|
||||
allPeopleIds.AddRange(filter.CoverArtist);
|
||||
allPeopleIds.AddRange(filter.Translators);
|
||||
|
||||
hasPeopleFilter = allPeopleIds.Count > 0;
|
||||
hasGenresFilter = filter.Genres.Count > 0;
|
||||
hasCollectionTagFilter = filter.CollectionTags.Count > 0;
|
||||
hasRatingFilter = filter.Rating > 0;
|
||||
hasProgressFilter = !filter.ReadStatus.Read || !filter.ReadStatus.InProgress || !filter.ReadStatus.NotRead;
|
||||
hasAgeRating = filter.AgeRating.Count > 0;
|
||||
hasTagsFilter = filter.Tags.Count > 0;
|
||||
hasLanguageFilter = filter.Languages.Count > 0;
|
||||
|
||||
|
||||
bool ProgressComparison(int pagesRead, int totalPages)
|
||||
|
@ -499,7 +531,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
|
||||
out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
|
||||
out var hasCollectionTagFilter, out var hasRatingFilter, out var hasProgressFilter,
|
||||
out var seriesIds);
|
||||
out var seriesIds, out var hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter);
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => userLibraries.Contains(s.LibraryId)
|
||||
|
@ -510,42 +542,41 @@ public class SeriesRepository : ISeriesRepository
|
|||
s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id)))
|
||||
&& (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating))
|
||||
&& (!hasProgressFilter || seriesIds.Contains(s.Id))
|
||||
&& (!hasAgeRating || filter.AgeRating.Contains(s.Metadata.AgeRating))
|
||||
&& (!hasTagsFilter || s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
|
||||
&& (!hasLanguageFilter || filter.Languages.Contains(s.Metadata.Language))
|
||||
)
|
||||
.AsNoTracking();
|
||||
// IQueryable<FilterableQuery> newFilter = null;
|
||||
// if (hasProgressFilter)
|
||||
// {
|
||||
// newFilter = query
|
||||
// .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
||||
// new
|
||||
// {
|
||||
// Series = s,
|
||||
// PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
|
||||
// .Sum(s1 => s1.PagesRead),
|
||||
// progress.AppUserId,
|
||||
// LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId)
|
||||
// .Max(p => p.LastModified)
|
||||
// })
|
||||
// .Select(d => new FilterableQuery()
|
||||
// {
|
||||
// Series = d.Series,
|
||||
// AppUserId = d.AppUserId,
|
||||
// LastModified = d.LastModified,
|
||||
// PagesRead = d.PagesRead
|
||||
// })
|
||||
// .Where(d => seriesIds.Contains(d.Series.Id));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// newFilter = query.Select(s => new FilterableQuery()
|
||||
// {
|
||||
// Series = s,
|
||||
// LastModified = DateTime.Now, // TODO: Figure this out
|
||||
// AppUserId = userId,
|
||||
// PagesRead = 0
|
||||
// });
|
||||
// }
|
||||
|
||||
if (filter.SortOptions != null)
|
||||
{
|
||||
if (filter.SortOptions.IsAscending)
|
||||
{
|
||||
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
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
@ -555,13 +586,15 @@ public class SeriesRepository : ISeriesRepository
|
|||
var metadataDto = await _context.SeriesMetadata
|
||||
.Where(metadata => metadata.SeriesId == seriesId)
|
||||
.Include(m => m.Genres)
|
||||
.Include(m => m.Tags)
|
||||
.Include(m => m.People)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesMetadataDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (metadataDto != null)
|
||||
{
|
||||
metadataDto.Tags = await _context.CollectionTag
|
||||
metadataDto.CollectionTags = await _context.CollectionTag
|
||||
.Include(t => t.SeriesMetadatas)
|
||||
.Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId))
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -694,4 +727,35 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(sm => sm.CollectionTags)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Metadata.AgeRating)
|
||||
.Distinct()
|
||||
.Select(s => new AgeRatingDto()
|
||||
{
|
||||
Value = s,
|
||||
Title = s.ToDescription()
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds)
|
||||
{
|
||||
var ret = await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Metadata.Language)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
return ret
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(s => new LanguageDto()
|
||||
{
|
||||
Title = CultureInfo.GetCultureInfo(s).DisplayName,
|
||||
IsoCode = s
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
|
86
API/Data/Repositories/TagRepository.cs
Normal file
86
API/Data/Repositories/TagRepository.cs
Normal file
|
@ -0,0 +1,86 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
public interface ITagRepository
|
||||
{
|
||||
void Attach(Tag tag);
|
||||
void Remove(Tag tag);
|
||||
Task<Tag> FindByNameAsync(string tagName);
|
||||
Task<IList<Tag>> GetAllTagsAsync();
|
||||
Task<IList<TagDto>> GetAllTagDtosAsync();
|
||||
Task RemoveAllTagNoLongerAssociated(bool removeExternal = false);
|
||||
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds);
|
||||
}
|
||||
|
||||
public class TagRepository : ITagRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public TagRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Attach(Tag tag)
|
||||
{
|
||||
_context.Tag.Attach(tag);
|
||||
}
|
||||
|
||||
public void Remove(Tag tag)
|
||||
{
|
||||
_context.Tag.Remove(tag);
|
||||
}
|
||||
|
||||
public async Task<Tag> FindByNameAsync(string tagName)
|
||||
{
|
||||
var normalizedName = Parser.Parser.Normalize(tagName);
|
||||
return await _context.Tag
|
||||
.FirstOrDefaultAsync(g => g.NormalizedTitle.Equals(normalizedName));
|
||||
}
|
||||
|
||||
public async Task RemoveAllTagNoLongerAssociated(bool removeExternal = false)
|
||||
{
|
||||
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);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.Tags)
|
||||
.Distinct()
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Tag>> GetAllTagsAsync()
|
||||
{
|
||||
return await _context.Tag.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<TagDto>> GetAllTagDtosAsync()
|
||||
{
|
||||
return await _context.Tag
|
||||
.AsNoTracking()
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
|
@ -173,6 +173,8 @@ public class VolumeRepository : IVolumeRepository
|
|||
.Where(vol => vol.SeriesId == seriesId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.People)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Tags)
|
||||
.OrderBy(volume => volume.Number)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
|
|
|
@ -20,6 +20,7 @@ public interface IUnitOfWork
|
|||
ISeriesMetadataRepository SeriesMetadataRepository { get; }
|
||||
IPersonRepository PersonRepository { get; }
|
||||
IGenreRepository GenreRepository { get; }
|
||||
ITagRepository TagRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
|
@ -54,6 +55,7 @@ public class UnitOfWork : IUnitOfWork
|
|||
public ISeriesMetadataRepository SeriesMetadataRepository => new SeriesMetadataRepository(_context);
|
||||
public IPersonRepository PersonRepository => new PersonRepository(_context, _mapper);
|
||||
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
|
||||
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue