Implemented Publication Status in SeriesMetadata and the ability to filter it. (#915)

This commit is contained in:
Joseph Milazzo 2022-01-08 13:10:03 -08:00 committed by GitHub
parent f8e0fb8a27
commit 2fbcf203aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 3015 additions and 19 deletions

View File

@ -93,6 +93,27 @@ public class MetadataController : BaseApiController
}));
}
/// <summary>
/// Fetches all publication status' from the instance
/// </summary>
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
/// <returns></returns>
[HttpGet("publication-status")]
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllPublicationStatus(string? libraryIds)
{
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
}
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()
{
Title = t.ToDescription(),
Value = t
}));
}
/// <summary>
/// Fetches all age ratings from the instance
/// </summary>

View File

@ -53,7 +53,8 @@ public class OpdsController : BaseApiController
CollectionTags = new List<int>(),
CoverArtist = new List<int>(),
ReadStatus = new ReadStatus(),
SortOptions = null
SortOptions = null,
PublicationStatus = new List<PublicationStatus>()
};
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();

View File

@ -70,6 +70,14 @@ namespace API.DTOs
/// Language for the Chapter/Issue
/// </summary>
public string Language { get; set; }
/// <summary>
/// Number in the TotalCount of issues
/// </summary>
public int Count { get; set; }
/// <summary>
/// Total number of issues for the series
/// </summary>
public int TotalCount { get; set; }
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Penciller { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inker { get; set; } = new List<PersonDto>();

View File

@ -89,6 +89,10 @@ namespace API.DTOs.Filtering
/// Languages (ISO 639-1 code) to filter by. Empty list will return everything back
/// </summary>
public IList<string> Languages { get; init; } = new List<string>();
/// <summary>
/// Publication statuses to filter by. Empty list will return everything back
/// </summary>
public IList<PublicationStatus> PublicationStatus { get; init; } = new List<PublicationStatus>();
}
}

View File

@ -0,0 +1,9 @@
using API.Entities.Enums;
namespace API.DTOs.Metadata;
public class PublicationStatusDto
{
public PublicationStatus Value { get; set; }
public string Title { get; set; }
}

View File

@ -43,6 +43,18 @@ namespace API.DTOs
/// Language of the content (ISO 639-1 code)
/// </summary>
public string Language { get; set; } = string.Empty;
/// <summary>
/// Number in the TotalCount of issues
/// </summary>
public int Count { get; set; }
/// <summary>
/// Total number of issues for the series
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Publication status of the Series
/// </summary>
public PublicationStatus PublicationStatus { get; set; }
public int SeriesId { get; set; }
}

View File

@ -8,13 +8,17 @@ namespace API.Data.Metadata
/// <summary>
/// A representation of a ComicInfo.xml file
/// </summary>
/// <remarks>See reference of the loose spec here: https://github.com/Kussie/ComicInfoStandard/blob/main/ComicInfo.xsd</remarks>
/// <remarks>See reference of the loose spec here: https://anansi-project.github.io/docs/comicinfo/documentation</remarks>
public class ComicInfo
{
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;
/// <summary>
/// The total number of items in the series.
/// </summary>
public int Count { get; set; } = 0;
public string Volume { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
public string Genre { get; set; } = string.Empty;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class CountMetadata : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Count",
table: "SeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Count",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Count",
table: "SeriesMetadata");
migrationBuilder.DropColumn(
name: "Count",
table: "Chapter");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class PublicationStatus : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PublicationStatus",
table: "SeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TotalCount",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PublicationStatus",
table: "SeriesMetadata");
migrationBuilder.DropColumn(
name: "TotalCount",
table: "Chapter");
}
}
}

View File

@ -299,6 +299,9 @@ namespace API.Data.Migrations
b.Property<int>("AgeRating")
.HasColumnType("INTEGER");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.Property<string>("CoverImage")
.HasColumnType("TEXT");
@ -338,6 +341,9 @@ namespace API.Data.Migrations
b.Property<string>("TitleName")
.HasColumnType("TEXT");
b.Property<int>("TotalCount")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
@ -494,9 +500,15 @@ namespace API.Data.Migrations
b.Property<int>("AgeRating")
.HasColumnType("INTEGER");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<int>("PublicationStatus")
.HasColumnType("INTEGER");
b.Property<int>("ReleaseYear")
.HasColumnType("INTEGER");

View File

@ -72,6 +72,7 @@ public interface ISeriesRepository
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
}
public class SeriesRepository : ISeriesRepository
@ -425,7 +426,7 @@ 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 hasAgeRating, out bool hasTagsFilter,
out bool hasLanguageFilter)
out bool hasLanguageFilter, out bool hasPublicationFilter)
{
var formats = filter.GetSqlFilter();
@ -454,6 +455,7 @@ public class SeriesRepository : ISeriesRepository
hasAgeRating = filter.AgeRating.Count > 0;
hasTagsFilter = filter.Tags.Count > 0;
hasLanguageFilter = filter.Languages.Count > 0;
hasPublicationFilter = filter.PublicationStatus.Count > 0;
bool ProgressComparison(int pagesRead, int totalPages)
@ -541,7 +543,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 hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter);
out var seriesIds, out var hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter, out var hasPublicationFilter);
var query = _context.Series
.Where(s => userLibraries.Contains(s.LibraryId)
@ -555,6 +557,7 @@ public class SeriesRepository : ISeriesRepository
&& (!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))
&& (!hasPublicationFilter || filter.PublicationStatus.Contains(s.Metadata.PublicationStatus))
)
.AsNoTracking();
@ -769,4 +772,18 @@ public class SeriesRepository : ISeriesRepository
IsoCode = s
}).ToList();
}
public async Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
{
return await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Select(s => s.Metadata.PublicationStatus)
.Distinct()
.Select(s => new PublicationStatusDto()
{
Value = s,
Title = s.ToDescription()
})
.ToListAsync();
}
}

View File

@ -63,6 +63,14 @@ namespace API.Entities
/// Language for the Chapter/Issue
/// </summary>
public string Language { get; set; }
/// <summary>
/// Total number of issues in the series
/// </summary>
public int TotalCount { get; set; } = 0;
/// <summary>
/// Number in the Total Count
/// </summary>
public int Count { get; set; } = 0;
/// <summary>

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
namespace API.Entities.Enums;
public enum PublicationStatus
{
/// <summary>
/// Default Status. Publication is currently in progress
/// </summary>
[Description("On Going")]
OnGoing = 0,
/// <summary>
/// Series is on temp or indefinite Hiatus
/// </summary>
[Description("Hiatus")]
Hiatus = 1,
/// <summary>
/// Publication has finished releasing
/// </summary>
[Description("Completed")]
Completed = 2
}

View File

@ -14,7 +14,6 @@ namespace API.Entities.Metadata
public string Summary { get; set; }
public ICollection<CollectionTag> CollectionTags { get; set; }
public ICollection<Genre> Genres { get; set; } = new List<Genre>();
@ -36,6 +35,11 @@ namespace API.Entities.Metadata
/// Language of the content (ISO 639-1 code)
/// </summary>
public string Language { get; set; } = string.Empty;
/// <summary>
/// Total number of issues in the series
/// </summary>
public int Count { get; set; } = 0;
public PublicationStatus PublicationStatus { get; set; }
// Relationship
public Series Series { get; set; }

View File

@ -107,6 +107,19 @@ public class MetadataService : IMetadataService
chapter.Language = comicInfo.LanguageISO;
}
if (comicInfo.Count > 0)
{
chapter.TotalCount = comicInfo.Count;
}
if (int.Parse(comicInfo.Number) > 0)
{
chapter.Count = int.Parse(comicInfo.Number);
}
if (comicInfo.Year > 0)
{
var day = Math.Max(comicInfo.Day, 1);
@ -295,6 +308,13 @@ public class MetadataService : IMetadataService
series.Metadata.AgeRating = chapters.Max(chapter => chapter.AgeRating);
series.Metadata.Count = chapters.Max(chapter => chapter.TotalCount);
series.Metadata.PublicationStatus = PublicationStatus.OnGoing;
if (chapters.Max(chapter => chapter.Count) >= series.Metadata.Count)
{
series.Metadata.PublicationStatus = PublicationStatus.Completed;
}
if (!string.IsNullOrEmpty(firstChapter.Summary))
{
series.Metadata.Summary = firstChapter.Summary;

View File

@ -0,0 +1,6 @@
import { PublicationStatus } from "./publication-status";
export interface PublicationStatusDto {
value: PublicationStatus;
title: string;
}

View File

@ -0,0 +1,5 @@
export enum PublicationStatus {
OnGoing = 0,
Hiatus = 1,
Completed = 2
}

View File

@ -27,6 +27,7 @@ export interface SeriesFilter {
sortOptions: SortOptions | null;
tags: Array<number>;
languages: Array<string>;
publicationStatus: Array<number>;
}
export interface SortOptions {

View File

@ -1,6 +1,7 @@
import { CollectionTag } from "./collection-tag";
import { Genre } from "./genre";
import { AgeRating } from "./metadata/age-rating";
import { PublicationStatus } from "./metadata/publication-status";
import { Person } from "./person";
import { Tag } from "./tag";
@ -24,4 +25,5 @@ export interface SeriesMetadata {
releaseYear: number;
language: string;
seriesId: number;
publicationStatus: PublicationStatus;
}

View File

@ -8,6 +8,7 @@ import { Genre } from '../_models/genre';
import { AgeRating } from '../_models/metadata/age-rating';
import { AgeRatingDto } from '../_models/metadata/age-rating-dto';
import { Language } from '../_models/metadata/language';
import { PublicationStatusDto } from '../_models/metadata/publication-status-dto';
import { Person } from '../_models/person';
import { Tag } from '../_models/tag';
@ -44,6 +45,14 @@ export class MetadataService {
return this.httpClient.get<Array<AgeRatingDto>>(this.baseUrl + method);;
}
getAllPublicationStatus(libraries?: Array<number>) {
let method = 'metadata/publication-status'
if (libraries != undefined && libraries.length > 0) {
method += '?libraryIds=' + libraries.join(',');
}
return this.httpClient.get<Array<PublicationStatusDto>>(this.baseUrl + method);;
}
getAllTags(libraries?: Array<number>) {
let method = 'metadata/tags'
if (libraries != undefined && libraries.length > 0) {

View File

@ -212,7 +212,8 @@ export class SeriesService {
sortOptions: null,
ageRating: [],
tags: [],
languages: []
languages: [],
publicationStatus: [],
};
if (filter === undefined) return data;

View File

@ -35,6 +35,7 @@ import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.
import { PersonRolePipe } from './person-role.pipe';
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
import { AllSeriesComponent } from './all-series/all-series.component';
import { PublicationStatusPipe } from './publication-status.pipe';
@NgModule({
@ -52,6 +53,7 @@ import { AllSeriesComponent } from './all-series/all-series.component';
DashboardComponent,
NavEventsToggleComponent,
PersonRolePipe,
PublicationStatusPipe,
SeriesMetadataDetailComponent,
AllSeriesComponent,
],

View File

@ -309,6 +309,19 @@
</app-typeahead>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.publicationStatusDisabled">
<label for="publication-status">Publication Status</label>
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="row justify-content-center no-gutters">
<div class="col-md-2 mr-3" *ngIf="!filterSettings.sortDisabled">
<form [formGroup]="sortGroup">
<div class="form-group">
@ -326,16 +339,13 @@
</select>
</div>
</form>
</div>
</div>
<div class="row justify-content-center no-gutters">
<div class="col-md-3 mr-3">
</div>
<div class="col-md-2 mr-3"></div>
<div class="col-md-2 mr-3"></div>
<div class="col-md-2 mr-3 mt-4">
<button class="btn btn-secondary btn-block" (click)="clear()">Clear</button>
</div>
<div class="col-md-3">
<div class="col-md-2 mr-3 mt-4">
<button class="btn btn-primary btn-block" (click)="apply()">Apply</button>
</div>
</div>

View File

@ -11,6 +11,7 @@ import { MangaFormat } from 'src/app/_models/manga-format';
import { AgeRating } from 'src/app/_models/metadata/age-rating';
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
import { Language } from 'src/app/_models/metadata/language';
import { PublicationStatusDto } from 'src/app/_models/metadata/publication-status-dto';
import { Pagination } from 'src/app/_models/pagination';
import { Person, PersonRole } from 'src/app/_models/person';
import { FilterItem, mangaFormatFilters, SeriesFilter, SortField } from 'src/app/_models/series-filter';
@ -39,6 +40,7 @@ export class FilterSettings {
ageRatingDisabled = false;
tagsDisabled = false;
languageDisabled = false;
publicationStatusDisabled = false;
}
@Component({
@ -70,6 +72,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
genreSettings: TypeaheadSettings<FilterItem<Genre>> = new TypeaheadSettings();
collectionSettings: TypeaheadSettings<FilterItem<CollectionTag>> = new TypeaheadSettings();
ageRatingSettings: TypeaheadSettings<FilterItem<AgeRatingDto>> = new TypeaheadSettings();
publicationStatusSettings: TypeaheadSettings<FilterItem<PublicationStatusDto>> = new TypeaheadSettings();
tagsSettings: TypeaheadSettings<FilterItem<Tag>> = new TypeaheadSettings();
languageSettings: TypeaheadSettings<FilterItem<Language>> = new TypeaheadSettings();
peopleSettings: {[PersonRole: string]: TypeaheadSettings<FilterItem<Person>>} = {};
@ -84,7 +87,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
libraries: Array<FilterItem<Library>> = [];
genres: Array<FilterItem<Genre>> = [];
persons: Array<FilterItem<Person>> = [];
//collectionTags: Array<FilterItem<CollectionTag>> = [];
readProgressGroup!: FormGroup;
sortGroup!: FormGroup;
@ -171,6 +174,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
this.setupCollectionTagTypeahead();
this.setupPersonTypeahead();
this.setupAgeRatingSettings();
this.setupPublicationStatusSettings();
this.setupTagSettings();
this.setupLanguageSettings();
}
@ -261,6 +265,29 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
}
}
setupPublicationStatusSettings() {
this.publicationStatusSettings.minCharacters = 0;
this.publicationStatusSettings.multiple = true;
this.publicationStatusSettings.id = 'publication-status';
this.publicationStatusSettings.unique = true;
this.publicationStatusSettings.addIfNonExisting = false;
this.publicationStatusSettings.fetchFn = (filter: string) => {
return this.metadataService.getAllPublicationStatus(this.filter.libraries).pipe(map(statuses => {
return statuses.map(status => {
return {
title: status.title,
value: status,
selected: false,
}
})
}));
};
this.publicationStatusSettings.compareFn = (options: FilterItem<PublicationStatusDto>[], filter: string) => {
const f = filter.toLowerCase();
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
}
}
setupTagSettings() {
this.tagsSettings.minCharacters = 0;
this.tagsSettings.multiple = true;
@ -475,9 +502,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
case PersonRole.Colorist:
this.filter.colorist = persons.map(p => p.value.id);
break;
// case PersonRole.Artist:
// this.filter.artist = persons.map(p => p.value.id);
// break;
case PersonRole.Editor:
this.filter.editor = persons.map(p => p.value.id);
break;
@ -514,12 +538,15 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
this.filter.ageRating = ratingDtos.map(item => item.value.value) || [];
}
updatePublicationStatus(dtos: FilterItem<PublicationStatusDto>[]) {
this.filter.publicationStatus = dtos.map(item => item.value.value) || [];
}
updateLanguageRating(languages: FilterItem<Language>[]) {
this.filter.languages = languages.map(item => item.value.isoCode) || [];
}
updateReadStatus(status: string) {
console.log('readstatus: ', this.filter.readStatus);
if (status === 'read') {
this.filter.readStatus.read = !this.filter.readStatus.read;
} else if (status === 'inProgress') {

View File

@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';
import { PublicationStatus } from './_models/metadata/publication-status';
@Pipe({
name: 'publicationStatus'
})
export class PublicationStatusPipe implements PipeTransform {
transform(value: PublicationStatus): string {
switch (value) {
case PublicationStatus.OnGoing: return 'Ongoing';
case PublicationStatus.Hiatus: return 'Hiatus';
case PublicationStatus.Completed: return 'Completed';
default: return '';
}
}
}

View File

@ -10,6 +10,7 @@
<!-- tooltip here explaining how this is year of first issue -->
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date">{{seriesMetadata.releaseYear}}</app-tag-badge>
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed">
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
</app-tag-badge>