diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs index e6abf9143..a4fe08381 100644 --- a/API.Tests/Parser/ComicParserTests.cs +++ b/API.Tests/Parser/ComicParserTests.cs @@ -77,6 +77,7 @@ namespace API.Tests.Parser [InlineData("Max_l_explorateur-_Tome_0", "Max l explorateur")] [InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "Chevaliers d'Héliopolis")] [InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")] + [InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")] public void ParseComicSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename)); diff --git a/API.Tests/Parser/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs index 4badd1c8a..a9c593de9 100644 --- a/API.Tests/Parser/DefaultParserTests.cs +++ b/API.Tests/Parser/DefaultParserTests.cs @@ -206,6 +206,14 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }); + filepath = @"E:\Manga\Air Gear\Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz"; + expected.Add(filepath, new ParserInfo + { + Series = "Air Gear", Volumes = "1", Edition = "Omnibus", + Chapters = "0", Filename = "Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", Format = MangaFormat.Archive, + FullFilePath = filepath, IsSpecial = false + }); + foreach (var file in expected.Keys) { diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index b1f637682..d088901e5 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -169,6 +169,7 @@ namespace API.Tests.Parser [InlineData("Battle Royale, v01 (2000) [TokyoPop] [Manga-Sketchbook]", "Battle Royale")] [InlineData("Kaiju No. 8 036 (2021) (Digital)", "Kaiju No. 8")] [InlineData("Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz", "Seraph of the End - Vampire Reign")] + [InlineData("Love Hina - Volume 01 [Scans].pdf", "Love Hina")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); @@ -258,6 +259,7 @@ namespace API.Tests.Parser [InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")] [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "")] [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")] + [InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")] public void ParseEditionTest(string input, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseEdition(input)); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 26d036f16..57a5c13d5 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -11,6 +11,7 @@ using API.DTOs.CollectionTags; using API.DTOs.Filtering; using API.DTOs.OPDS; using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Helpers; using API.Services; @@ -33,7 +34,7 @@ public class OpdsController : BaseApiController private const string Prefix = "/api/opds/"; private readonly FilterDto _filterDto = new FilterDto() { - MangaFormat = null + Formats = new List() }; private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer(); diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 1b206cfba..d4abe7ba3 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -233,6 +233,23 @@ namespace API.Controllers return Ok(series); } + [HttpPost("all")] + public async Task>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) + { + var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var series = + await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); + + // Apply progress/rating information (I can't work out how to do this in initial query) + if (series == null) return BadRequest("Could not get series"); + + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); + + Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); + + return Ok(series); + } + /// /// Fetches series that are on deck aka have progress on them. /// diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs index c5956f17d..6e52ef45c 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/API/DTOs/Filtering/FilterDto.cs @@ -1,13 +1,15 @@ -using API.Entities.Enums; +using System.Collections; +using System.Collections.Generic; +using API.Entities.Enums; namespace API.DTOs.Filtering { public class FilterDto { /// - /// Pass null if you want all formats + /// The type of Formats you want to be returned. An empty list will return all formats back /// - public MangaFormat? MangaFormat { get; init; } = null; + public IList Formats { get; init; } = new List(); } } diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index 9f86e0c6e..bab5f2bc4 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -11,20 +11,20 @@ namespace API.Data.Metadata /// See reference of the loose spec here: https://github.com/Kussie/ComicInfoStandard/blob/main/ComicInfo.xsd public class ComicInfo { - public string Summary { get; set; } - public string Title { get; set; } - public string Series { get; set; } - public string Number { get; set; } - public string Volume { get; set; } - public string Notes { get; set; } - public string Genre { get; set; } + public string Summary { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Series { get; set; } = string.Empty; + public string Number { get; set; } = string.Empty; + public string Volume { get; set; } = string.Empty; + public string Notes { get; set; } = string.Empty; + public string Genre { get; set; } = string.Empty; public int PageCount { get; set; } // ReSharper disable once InconsistentNaming - public string LanguageISO { get; set; } + public string LanguageISO { get; set; } = string.Empty; /// /// This is the link to where the data was scraped from /// - public string Web { get; set; } + public string Web { get; set; } = string.Empty; public int Day { get; set; } public int Month { get; set; } public int Year { get; set; } @@ -33,29 +33,23 @@ namespace API.Data.Metadata /// /// Rating based on the content. Think PG-13, R for movies. See for valid types /// - public string AgeRating { get; set; } - - // public AgeRating AgeRating - // { - // get => ConvertAgeRatingToEnum(_AgeRating); - // set => ConvertAgeRatingToEnum(value); - // } + public string AgeRating { get; set; } = string.Empty; /// /// User's rating of the content /// public float UserRating { get; set; } - public string AlternateSeries { get; set; } - public string StoryArc { get; set; } - public string SeriesGroup { get; set; } - public string AlternativeSeries { get; set; } - public string AlternativeNumber { get; set; } + public string AlternateSeries { get; set; } = string.Empty; + public string StoryArc { get; set; } = string.Empty; + public string SeriesGroup { get; set; } = string.Empty; + public string AlternativeSeries { get; set; } = string.Empty; + public string AlternativeNumber { get; set; } = string.Empty; /// /// This is Epub only: calibre:title_sort /// Represents the sort order for the title /// - public string TitleSort { get; set; } + public string TitleSort { get; set; } = string.Empty; @@ -63,14 +57,14 @@ namespace API.Data.Metadata /// /// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple. /// - public string Writer { get; set; } - public string Penciller { get; set; } - public string Inker { get; set; } - public string Colorist { get; set; } - public string Letterer { get; set; } - public string CoverArtist { get; set; } - public string Editor { get; set; } - public string Publisher { get; set; } + public string Writer { get; set; } = string.Empty; + public string Penciller { get; set; } = string.Empty; + public string Inker { get; set; } = string.Empty; + public string Colorist { get; set; } = string.Empty; + public string Letterer { get; set; } = string.Empty; + public string CoverArtist { get; set; } = string.Empty; + public string Editor { get; set; } = string.Empty; + public string Publisher { get; set; } = string.Empty; public static AgeRating ConvertAgeRatingToEnum(string value) { diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 2dcddd5da..8955635b0 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -178,8 +178,11 @@ public class SeriesRepository : ISeriesRepository public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter) { var formats = filter.GetSqlFilter(); + + var userLibraries = await GetUserLibraries(libraryId, userId); + var query = _context.Series - .Where(s => s.LibraryId == libraryId && formats.Contains(s.Format)) + .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) .OrderBy(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking(); @@ -187,6 +190,24 @@ public class SeriesRepository : ISeriesRepository return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } + private async Task> GetUserLibraries(int libraryId, int userId) + { + if (libraryId == 0) + { + return await _context.Library + .Include(l => l.AppUsers) + .Where(library => library.AppUsers.Any(user => user.Id == userId)) + .AsNoTracking() + .Select(library => library.Id) + .ToListAsync(); + } + + return new List() + { + libraryId + }; + } + public async Task> SearchSeries(int[] libraryIds, string searchQuery) { return await _context.Series @@ -357,26 +378,10 @@ public class SeriesRepository : ISeriesRepository { var formats = filter.GetSqlFilter(); - if (libraryId == 0) - { - var userLibraries = _context.Library - .Include(l => l.AppUsers) - .Where(library => library.AppUsers.Any(user => user.Id == userId)) - .AsNoTracking() - .Select(library => library.Id) - .ToList(); - - var allQuery = _context.Series - .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) - .OrderByDescending(s => s.Created) - .ProjectTo(_mapper.ConfigurationProvider) - .AsNoTracking(); - - return await PagedList.CreateAsync(allQuery, userParams.PageNumber, userParams.PageSize); - } + var userLibraries = await GetUserLibraries(libraryId, userId); var query = _context.Series - .Where(s => s.LibraryId == libraryId && formats.Contains(s.Format)) + .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) .OrderByDescending(s => s.Created) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -397,20 +402,8 @@ public class SeriesRepository : ISeriesRepository public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter) { var formats = filter.GetSqlFilter(); - IList userLibraries; - if (libraryId == 0) - { - userLibraries = _context.Library - .Include(l => l.AppUsers) - .Where(library => library.AppUsers.Any(user => user.Id == userId)) - .AsNoTracking() - .Select(library => library.Id) - .ToList(); - } - else - { - userLibraries = new List() {libraryId}; - } + + var userLibraries = await GetUserLibraries(libraryId, userId); var series = _context.Series .Where(s => formats.Contains(s.Format) && userLibraries.Contains(s.LibraryId)) diff --git a/API/Extensions/FilterDtoExtensions.cs b/API/Extensions/FilterDtoExtensions.cs index 7e5a818ec..b0d9f80f6 100644 --- a/API/Extensions/FilterDtoExtensions.cs +++ b/API/Extensions/FilterDtoExtensions.cs @@ -11,15 +11,12 @@ namespace API.Extensions public static IList GetSqlFilter(this FilterDto filter) { - var format = filter.MangaFormat; - if (format != null) + if (filter.Formats == null || filter.Formats.Count == 0) { - return new List() - { - (MangaFormat) format - }; + return AllFormats; } - return AllFormats; + + return filter.Formats; } } } diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index c4dd2caf7..5ebac08cf 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -237,9 +237,13 @@ namespace API.Parser private static readonly Regex[] ComicSeriesRegex = new[] { + // Tintin - T22 Vol 714 pour Sydney + new Regex( + @"(?.+?)\s?(\b|_|-)\s?((vol|tome|t)\.?)(?\d+(-\d+)?)", + MatchOptions, RegexTimeout), // Invincible Vol 01 Family matters (2005) (Digital) new Regex( - @"(?.*)(\b|_)((vol|tome|t)\.?)( |_)(?\d+(-\d+)?)", + @"(?.+?)(\b|_)((vol|tome|t)\.?)(\s|_)(?\d+(-\d+)?)", MatchOptions, RegexTimeout), // Batman Beyond 2.0 001 (2013) new Regex( diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index f10e9a65e..eff48bb72 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -287,7 +287,7 @@ public class MetadataService : IMetadataService series.Metadata.ReleaseYear = series.Volumes .SelectMany(volume => volume.Chapters).Min(c => c.ReleaseDate.Year); - var genres = comicInfos.SelectMany(i => i.Genre.Split(",")).Distinct().ToList(); + var genres = comicInfos.SelectMany(i => i?.Genre.Split(",")).Distinct().ToList(); var people = series.Volumes.SelectMany(volume => volume.Chapters).SelectMany(c => c.People).ToList(); diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index ac1707592..21f688931 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -5,7 +5,7 @@ "TokenKey": "super secret unguessable key", "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Error", "Hangfire": "Information", diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 801b4ac6c..308057606 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -12384,7 +12384,8 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "strip-ansi": { @@ -12589,7 +12590,8 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "ansi-styles": { diff --git a/UI/Web/src/app/_models/series-filter.ts b/UI/Web/src/app/_models/series-filter.ts index a81cfe381..f4eef7b1c 100644 --- a/UI/Web/src/app/_models/series-filter.ts +++ b/UI/Web/src/app/_models/series-filter.ts @@ -7,7 +7,7 @@ export interface FilterItem { } export interface SeriesFilter { - mangaFormat: MangaFormat | null; + formats: Array; } export const mangaFormatFilters = [ diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 8ed258c45..5fd621928 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -177,11 +177,13 @@ export class SeriesService { createSeriesFilter(filter?: SeriesFilter) { const data: SeriesFilter = { - mangaFormat: null + formats: [] }; if (filter) { - data.mangaFormat = filter.mangaFormat; + if (filter.formats != null) { + data.formats = filter.formats; + } } return data; diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index 2b4f6c4a4..3799871c1 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -41,7 +41,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { isAdmin: boolean = false; filters: Array = mangaFormatFilters; filter: SeriesFilter = { - mangaFormat: null + formats: [] }; private onDestory: Subject = new Subject(); @@ -175,7 +175,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy { } updateFilter(data: UpdateFilterEvent) { - this.filter.mangaFormat = data.filterItem.value; + this.filter.formats = [data.filterItem.value]; if (this.seriesPagination !== undefined && this.seriesPagination !== null) { this.seriesPagination.currentPage = 1; this.onPageChange(this.seriesPagination); diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index 810f20131..374b7685e 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -32,7 +32,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { actions: ActionItem[] = []; filters: Array = mangaFormatFilters; filter: SeriesFilter = { - mangaFormat: null + formats: [] }; onDestroy: Subject = new Subject(); @@ -135,7 +135,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy { } updateFilter(data: UpdateFilterEvent) { - this.filter.mangaFormat = data.filterItem.value; + this.filter.formats = [data.filterItem.value]; if (this.pagination !== undefined && this.pagination !== null) { this.pagination.currentPage = 1; this.onPageChange(this.pagination); diff --git a/UI/Web/src/app/on-deck/on-deck.component.ts b/UI/Web/src/app/on-deck/on-deck.component.ts index 6ed72561b..7c8bb7fc2 100644 --- a/UI/Web/src/app/on-deck/on-deck.component.ts +++ b/UI/Web/src/app/on-deck/on-deck.component.ts @@ -25,7 +25,7 @@ export class OnDeckComponent implements OnInit { libraryId!: number; filters: Array = mangaFormatFilters; filter: SeriesFilter = { - mangaFormat: null + formats: [] }; constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title, @@ -64,7 +64,7 @@ export class OnDeckComponent implements OnInit { } updateFilter(data: UpdateFilterEvent) { - this.filter.mangaFormat = data.filterItem.value; + this.filter.formats = [data.filterItem.value]; if (this.pagination !== undefined && this.pagination !== null) { this.pagination.currentPage = 1; this.onPageChange(this.pagination); diff --git a/UI/Web/src/app/recently-added/recently-added.component.ts b/UI/Web/src/app/recently-added/recently-added.component.ts index 5f3164cf4..a517cbf30 100644 --- a/UI/Web/src/app/recently-added/recently-added.component.ts +++ b/UI/Web/src/app/recently-added/recently-added.component.ts @@ -32,7 +32,7 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy { filters: Array = mangaFormatFilters; filter: SeriesFilter = { - mangaFormat: null + formats: [] }; onDestroy: Subject = new Subject(); @@ -82,7 +82,8 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy { } updateFilter(data: UpdateFilterEvent) { - this.filter.mangaFormat = data.filterItem.value; + // TODO: Move this into card-layout component. It's the same except for callback + this.filter.formats = [data.filterItem.value]; if (this.pagination !== undefined && this.pagination !== null) { this.pagination.currentPage = 1; this.onPageChange(this.pagination); diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index 2da355765..7763ddb55 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -213,21 +213,18 @@ - +
- +
- - - - - +

All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.

+ + +