mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Metadata Fixes (#3533)
Co-authored-by: Midhun Sudhir <60651970+midhun3301@users.noreply.github.com>
This commit is contained in:
parent
40bbdcb5f0
commit
bb9621a588
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.KavitaPlus.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
@ -217,6 +218,12 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
|
||||
v => JsonSerializer.Deserialize<List<string>>(v, JsonSerializerOptions.Default)
|
||||
);
|
||||
builder.Entity<MetadataSettings>()
|
||||
.Property(x => x.Whitelist)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
|
||||
v => JsonSerializer.Deserialize<List<string>>(v, JsonSerializerOptions.Default)
|
||||
);
|
||||
|
||||
// Configure one-to-many relationship
|
||||
builder.Entity<MetadataSettings>()
|
||||
@ -224,6 +231,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
.WithOne(x => x.MetadataSettings)
|
||||
.HasForeignKey(x => x.MetadataSettingsId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<MetadataSettings>()
|
||||
.Property(b => b.Enabled)
|
||||
.HasDefaultValue(true);
|
||||
|
@ -224,6 +224,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
|
||||
{
|
||||
return await _context.Series
|
||||
.Include(s => s.Library)
|
||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||
.FilterMatchState(filter.MatchStateOption)
|
||||
.OrderBy(s => s.NormalizedName)
|
||||
|
@ -308,7 +308,7 @@ public static class Seed
|
||||
EnableTags = false,
|
||||
EnableGenres = true,
|
||||
EnableLocalizedName = false,
|
||||
FirstLastPeopleNaming = false,
|
||||
FirstLastPeopleNaming = true,
|
||||
PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character]
|
||||
};
|
||||
await context.MetadataSettings.AddAsync(existing);
|
||||
|
@ -288,11 +288,15 @@ public static class QueryableExtensions
|
||||
return stateOption switch
|
||||
{
|
||||
MatchStateOption.All => query,
|
||||
MatchStateOption.Matched => query.Where(s => s.ExternalSeriesMetadata != null && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue && !s.IsBlacklisted),
|
||||
MatchStateOption.NotMatched => query.Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted),
|
||||
MatchStateOption.Matched => query
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
.Where(s => s.ExternalSeriesMetadata != null && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue && !s.IsBlacklisted),
|
||||
MatchStateOption.NotMatched => query.
|
||||
Include(s => s.ExternalSeriesMetadata)
|
||||
.Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted),
|
||||
MatchStateOption.Error => query.Where(s => s.IsBlacklisted),
|
||||
MatchStateOption.DontMatch => query.Where(s => s.DontMatch),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(stateOption), stateOption, null)
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -345,10 +345,13 @@ public class AutoMapperProfiles : Profile
|
||||
opt.MapFrom(src => src))
|
||||
.ForMember(dest => dest.IsMatched,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0 && src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue))
|
||||
opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0
|
||||
&& src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue))
|
||||
.ForMember(dest => dest.ValidUntilUtc,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.ExternalSeriesMetadata.ValidUntilUtc));
|
||||
opt => opt.MapFrom(src =>
|
||||
src.ExternalSeriesMetadata != null
|
||||
? src.ExternalSeriesMetadata.ValidUntilUtc
|
||||
: DateTime.MinValue));
|
||||
|
||||
|
||||
CreateMap<MangaFile, FileExtensionExportDto>();
|
||||
@ -361,10 +364,14 @@ public class AutoMapperProfiles : Profile
|
||||
.ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId))
|
||||
.ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type));
|
||||
|
||||
CreateMap<MetadataFieldMapping, MetadataFieldMappingDto>();
|
||||
|
||||
CreateMap<MetadataSettings, MetadataSettingsDto>()
|
||||
.ForMember(dest => dest.Blacklist, opt => opt.MapFrom(src => src.Blacklist ?? new List<string>()))
|
||||
.ForMember(dest => dest.Whitelist, opt => opt.MapFrom(src => src.Whitelist ?? new List<string>()));
|
||||
CreateMap<MetadataFieldMapping, MetadataFieldMappingDto>();
|
||||
.ForMember(dest => dest.Whitelist, opt => opt.MapFrom(src => src.Whitelist ?? new List<string>()))
|
||||
.ForMember(dest => dest.AgeRatingMappings, opt => opt.MapFrom(src => src.AgeRatingMappings ?? new Dictionary<string, AgeRating>()));
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -195,6 +195,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId,
|
||||
SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata);
|
||||
if (series == null) return [];
|
||||
|
||||
var potentialAnilistId = ScrobblingService.ExtractId<int?>(dto.Query, ScrobblingService.AniListWeblinkWebsite);
|
||||
var potentialMalId = ScrobblingService.ExtractId<long?>(dto.Query, ScrobblingService.MalWeblinkWebsite);
|
||||
@ -512,7 +513,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
madeModification = true;
|
||||
}
|
||||
|
||||
if (settings.EnableStartDate && externalMetadata.StartDate.HasValue)
|
||||
if (settings.EnableStartDate && !series.Metadata.ReleaseYearLocked && externalMetadata.StartDate.HasValue)
|
||||
{
|
||||
series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year;
|
||||
madeModification = true;
|
||||
@ -526,7 +527,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
// Process Genres
|
||||
if (externalMetadata.Genres != null)
|
||||
{
|
||||
foreach (var genre in externalMetadata.Genres.Where(g => !settings.Blacklist.Contains(g)))
|
||||
foreach (var genre in externalMetadata.Genres)
|
||||
{
|
||||
// Apply field mappings
|
||||
var mappedGenre = ApplyFieldMapping(genre, MetadataFieldType.Genre, settings.FieldMappings);
|
||||
@ -537,9 +538,12 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
}
|
||||
|
||||
// Strip blacklisted items from processedGenres
|
||||
processedGenres = processedGenres.Distinct().Where(g => !settings.Blacklist.Contains(g)).ToList();
|
||||
processedGenres = processedGenres
|
||||
.Distinct()
|
||||
.Where(g => !settings.Blacklist.Contains(g))
|
||||
.ToList();
|
||||
|
||||
if (settings.EnableGenres && processedGenres.Count > 0)
|
||||
if (settings.EnableGenres && !series.Metadata.GenresLocked && processedGenres.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Found {GenreCount} genres for {SeriesName}", processedGenres.Count, series.Name);
|
||||
var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(processedGenres.Select(Parser.Normalize))).ToList();
|
||||
@ -567,13 +571,14 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
}
|
||||
|
||||
// Strip blacklisted items from processedTags
|
||||
processedTags = processedTags.Distinct()
|
||||
processedTags = processedTags
|
||||
.Distinct()
|
||||
.Where(g => !settings.Blacklist.Contains(g))
|
||||
.Where(g => settings.Whitelist.Count == 0 || settings.Whitelist.Contains(g))
|
||||
.ToList();
|
||||
|
||||
// Set the tags for the series and ensure they are in the DB
|
||||
if (settings.EnableTags && processedTags.Count > 0)
|
||||
if (settings.EnableTags && !series.Metadata.TagsLocked && processedTags.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Found {TagCount} tags for {SeriesName}", processedTags.Count, series.Name);
|
||||
var allTags = (await _unitOfWork.TagRepository.GetAllTagsByNameAsync(processedTags.Select(Parser.Normalize)))
|
||||
@ -591,22 +596,36 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
|
||||
#region Age Rating
|
||||
|
||||
// Determine Age Rating
|
||||
var ageRating = DetermineAgeRating(processedGenres.Concat(processedTags), settings.AgeRatingMappings);
|
||||
if (!series.Metadata.AgeRatingLocked && series.Metadata.AgeRating <= ageRating)
|
||||
if (!series.Metadata.AgeRatingLocked)
|
||||
{
|
||||
series.Metadata.AgeRating = ageRating;
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
madeModification = true;
|
||||
}
|
||||
try
|
||||
{
|
||||
// Determine Age Rating
|
||||
var totalTags = processedGenres
|
||||
.Concat(processedTags)
|
||||
.Concat(series.Metadata.Genres.Select(g => g.Title))
|
||||
.Concat(series.Metadata.Tags.Select(g => g.Title));
|
||||
|
||||
var ageRating = DetermineAgeRating(totalTags, settings.AgeRatingMappings);
|
||||
if (!series.Metadata.AgeRatingLocked && series.Metadata.AgeRating <= ageRating)
|
||||
{
|
||||
series.Metadata.AgeRating = ageRating;
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
madeModification = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue determining Age Rating for Series {SeriesName} ({SeriesId})", series.Name, series.Id);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region People
|
||||
|
||||
if (settings.EnablePeople)
|
||||
{
|
||||
series.Metadata.People ??= new List<SeriesMetadataPeople>();
|
||||
series.Metadata.People ??= [];
|
||||
|
||||
// Ensure all people are named correctly
|
||||
externalMetadata.Staff = externalMetadata.Staff.Select(s =>
|
||||
@ -635,7 +654,10 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
Name = w.Name,
|
||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListStaffWebsite),
|
||||
Description = CleanSummary(w.Description),
|
||||
}).ToList();
|
||||
})
|
||||
.Concat(series.Metadata.People.Where(p => p.Role == PersonRole.Writer).Select(p => _mapper.Map<PersonDto>(p)))
|
||||
.DistinctBy(p => Parser.Normalize(p.Name))
|
||||
.ToList();
|
||||
|
||||
|
||||
// NOTE: PersonRoles can be a hashset
|
||||
@ -661,7 +683,10 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
Name = w.Name,
|
||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListStaffWebsite),
|
||||
Description = CleanSummary(w.Description),
|
||||
}).ToList();
|
||||
})
|
||||
.Concat(series.Metadata.People.Where(p => p.Role == PersonRole.CoverArtist).Select(p => _mapper.Map<PersonDto>(p)))
|
||||
.DistinctBy(p => Parser.Normalize(p.Name))
|
||||
.ToList();
|
||||
|
||||
if (!series.Metadata.CoverArtistLocked && artists.Count > 0 && settings.PersonRoles.Contains(PersonRole.CoverArtist))
|
||||
{
|
||||
@ -684,7 +709,10 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
Name = w.Name,
|
||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListCharacterWebsite),
|
||||
Description = CleanSummary(w.Description),
|
||||
}).ToList();
|
||||
})
|
||||
.Concat(series.Metadata.People.Where(p => p.Role == PersonRole.Character).Select(p => _mapper.Map<PersonDto>(p)))
|
||||
.DistinctBy(p => Parser.Normalize(p.Name))
|
||||
.ToList();
|
||||
|
||||
|
||||
if (!series.Metadata.CharacterLocked && characters.Count > 0)
|
||||
@ -713,13 +741,27 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
|
||||
#endregion
|
||||
|
||||
#region Publication Status
|
||||
|
||||
if (!series.Metadata.PublicationStatusLocked && settings.EnablePublicationStatus)
|
||||
{
|
||||
var chapters = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Chapters))!.Volumes.SelectMany(v => v.Chapters).ToList();
|
||||
var wasChanged = DeterminePublicationStatus(series, chapters, externalMetadata);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
madeModification = madeModification || wasChanged;
|
||||
try
|
||||
{
|
||||
var chapters =
|
||||
(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Chapters))!.Volumes
|
||||
.SelectMany(v => v.Chapters).ToList();
|
||||
var wasChanged = DeterminePublicationStatus(series, chapters, externalMetadata);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
madeModification = madeModification || wasChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue determining Publication Status for Series {SeriesName} ({SeriesId})", series.Name, series.Id);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Relationships
|
||||
|
||||
if (settings.EnableRelationships && externalMetadata.Relations != null && defaultAdmin != null)
|
||||
{
|
||||
@ -773,6 +815,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
madeModification = true;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
return madeModification;
|
||||
}
|
||||
@ -889,6 +932,8 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
private static AgeRating DetermineAgeRating(IEnumerable<string> values, Dictionary<string, AgeRating> mappings)
|
||||
{
|
||||
// Find highest age rating from mappings
|
||||
mappings ??= new Dictionary<string, AgeRating>();
|
||||
|
||||
return values
|
||||
.Select(v => mappings.TryGetValue(v, out var mapping) ? mapping : AgeRating.Unknown)
|
||||
.DefaultIfEmpty(AgeRating.Unknown)
|
||||
@ -913,6 +958,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
};
|
||||
series.ExternalSeriesMetadata = externalSeriesMetadata;
|
||||
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
|
||||
|
||||
return externalSeriesMetadata;
|
||||
}
|
||||
|
||||
|
@ -349,7 +349,8 @@ public class SeriesService : ISeriesService
|
||||
var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames);
|
||||
|
||||
// Use a dictionary for quick lookups
|
||||
var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName).ToDictionary(p => p.NormalizedName, p => p);
|
||||
var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName)
|
||||
.ToDictionary(p => p.NormalizedName, p => p);
|
||||
|
||||
// List to track people that will be added to the metadata
|
||||
var peopleToAdd = new List<Person>();
|
||||
|
@ -450,12 +450,12 @@ public class ScannerService : IScannerService
|
||||
// That way logging and UI informing is all in one place with full context
|
||||
_logger.LogError("[ScannerService] Some of the root folders for the library are empty. " +
|
||||
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
|
||||
"Scan has be aborted. " +
|
||||
"Scan has been aborted. " +
|
||||
"Check that your mount is connected or change the library's root folder and rescan");
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.",
|
||||
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
|
||||
"Scan has be aborted. " +
|
||||
"Scan has been aborted. " +
|
||||
"Check that your mount is connected or change the library's root folder and rescan"));
|
||||
|
||||
return false;
|
||||
|
@ -114,6 +114,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
||||
|
||||
var nightlyDto = new UpdateNotificationDto
|
||||
{
|
||||
// TODO: I should pass Title to the FE so that Nightly Release can be localized
|
||||
UpdateTitle = $"Nightly Release {nightly.Version} - {prInfo.Title}",
|
||||
UpdateVersion = nightly.Version,
|
||||
CurrentVersion = dto.CurrentVersion,
|
||||
@ -446,7 +447,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
||||
{
|
||||
var sections = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
var lines = body.Split('\n');
|
||||
string currentSection = null;
|
||||
string? currentSection = null;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
|
@ -37,6 +37,11 @@ export interface ExternalSeriesDetail {
|
||||
summary?: string;
|
||||
volumeCount?: number;
|
||||
chapterCount?: number;
|
||||
/**
|
||||
* These are duplicated with volumeCount based on where it's being invoked.
|
||||
*/
|
||||
volumes?: number;
|
||||
chapters?: number;
|
||||
staff: Array<SeriesStaff>;
|
||||
tags: Array<MetadataTagDto>;
|
||||
provider: ScrobbleProvider;
|
||||
|
@ -63,7 +63,7 @@
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="webLinks" [title]="t('weblinks-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<a class="me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
|
||||
<a class="me-1" [href]="item | safeUrl" target="_blank" rel="noopener noreferrer" [title]="item">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(item)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
|
@ -22,6 +22,7 @@ import {SeriesFormatComponent} from "../../shared/series-format/series-format.co
|
||||
import {MangaFormatPipe} from "../../_pipes/manga-format.pipe";
|
||||
import {LanguageNamePipe} from "../../_pipes/language-name.pipe";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {SafeUrlPipe} from "../../_pipes/safe-url.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-tab',
|
||||
@ -39,7 +40,8 @@ import {AsyncPipe} from "@angular/common";
|
||||
SeriesFormatComponent,
|
||||
MangaFormatPipe,
|
||||
LanguageNamePipe,
|
||||
AsyncPipe
|
||||
AsyncPipe,
|
||||
SafeUrlPipe
|
||||
],
|
||||
templateUrl: './details-tab.component.html',
|
||||
styleUrl: './details-tab.component.scss',
|
||||
|
@ -32,9 +32,9 @@
|
||||
} @else {
|
||||
<div class="d-flex p-1 justify-content-between">
|
||||
<span class="me-1"><a (click)="$event.stopPropagation()" [href]="item.series.siteUrl" rel="noreferrer noopener" target="_blank">{{t('details')}}</a></span>
|
||||
@if ((item.series.volumeCount || 0) > 0 || (item.series.chapterCount || 0) > 0) {
|
||||
<span class="me-1">{{t('volume-count', {num: item.series.volumeCount})}}</span>
|
||||
<span class="me-1">{{t('chapter-count', {num: item.series.chapterCount})}}</span>
|
||||
@if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) {
|
||||
<span class="me-1">{{t('volume-count', {num: item.series.volumes})}}</span>
|
||||
<span class="me-1">{{t('chapter-count', {num: item.series.chapters})}}</span>
|
||||
} @else {
|
||||
<span class="me-1">{{t('releasing')}}</span>
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<a class="position-absolute custom-position btn btn-primary-outline" [href]="WikiLink.KavitaPlusFAQ" target="_blank" rel="noreferrer nofollow">{{t('faq-title')}}</a>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div>
|
||||
<p>{{t('kavita+-desc-part-1')}} <a [href]="WikiLink.KavitaPlus" target="_blank" rel="noreferrer nofollow">{{t('kavita+-desc-part-2')}}</a> {{t('kavita+-desc-part-3')}}</p>
|
||||
|
||||
<form [formGroup]="formGroup">
|
||||
|
@ -31,6 +31,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
|
||||
constructor(private accountService: AccountService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// TODO: Come back and implement this one day
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.hubConnection = new HubConnectionBuilder()
|
||||
|
@ -25,7 +25,7 @@ const ValidIpAddress = /^(\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:
|
||||
styleUrls: ['./manage-settings.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ReactiveFormsModule, NgbTooltip, TitleCasePipe, TranslocoModule, NgTemplateOutlet, PageLayoutModePipe, SettingItemComponent, SettingSwitchComponent, SafeHtmlPipe]
|
||||
imports: [ReactiveFormsModule, TitleCasePipe, TranslocoModule, SettingItemComponent, SettingSwitchComponent]
|
||||
})
|
||||
export class ManageSettingsComponent implements OnInit {
|
||||
|
||||
|
@ -18,7 +18,6 @@ import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component";
|
||||
import {DefaultModalOptions} from "../../_models/default-modal-options";
|
||||
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
|
||||
@ -38,7 +37,8 @@ interface AdhocTask {
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ReactiveFormsModule, AsyncPipe, TitleCasePipe, DefaultValuePipe,
|
||||
TranslocoModule, TranslocoLocaleModule, UtcToLocalTimePipe, SettingItemComponent, SettingButtonComponent, NgxDatatableModule]
|
||||
TranslocoModule, TranslocoLocaleModule, UtcToLocalTimePipe, SettingItemComponent,
|
||||
SettingButtonComponent, NgxDatatableModule]
|
||||
})
|
||||
export class ManageTasksSettingsComponent implements OnInit {
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.blog-content {
|
||||
:host ::ng-deep .blog-content {
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
|
@ -27,4 +27,5 @@ export class ChangelogUpdateItemComponent {
|
||||
@Input({required:true}) update: UpdateVersionEvent | null = null;
|
||||
@Input() index: number = 0;
|
||||
@Input() showExtras: boolean = true;
|
||||
|
||||
}
|
||||
|
@ -103,9 +103,7 @@ const blackList = [Action.Edit, Action.Info, Action.IncognitoRead, Action.Read,
|
||||
MangaFormatPipe,
|
||||
DefaultDatePipe,
|
||||
TimeAgoPipe,
|
||||
TagBadgeComponent,
|
||||
PublicationStatusPipe,
|
||||
NgbTooltip,
|
||||
BytesPipe,
|
||||
ImageComponent,
|
||||
NgbCollapse,
|
||||
@ -117,7 +115,6 @@ const blackList = [Action.Edit, Action.Info, Action.IncognitoRead, Action.Read,
|
||||
EditListComponent,
|
||||
SettingButtonComponent,
|
||||
SettingItemComponent,
|
||||
ReadTimePipe,
|
||||
],
|
||||
templateUrl: './edit-series-modal.component.html',
|
||||
styleUrls: ['./edit-series-modal.component.scss'],
|
||||
@ -655,6 +652,11 @@ export class EditSeriesModalComponent implements OnInit {
|
||||
case Action.Download:
|
||||
this.downloadService.download('series', this.series);
|
||||
break;
|
||||
case Action.Match:
|
||||
this.actionService.matchSeries(this.series, _ => {
|
||||
this.modal.close({success: true, series: this.series, coverImageUpdate: false, updateExternal: true});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,13 @@
|
||||
<h2 class="title text-break">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="personActions" [labelBy]="person.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<span>{{person.name}}</span>
|
||||
|
||||
@if (person.aniListId) {
|
||||
<a class="ms-1" [href]="anilistUrl | safeUrl" target="_blank" rel="noopener noreferrer">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(anilistUrl)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
}
|
||||
</h2>
|
||||
</ng-container>
|
||||
</app-side-nav-companion-bar>
|
||||
@ -45,6 +52,7 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,6 +41,7 @@ import {ThemeService} from "../_services/theme.service";
|
||||
import {DefaultModalOptions} from "../_models/default-modal-options";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {LicenseService} from "../_services/license.service";
|
||||
import {SafeUrlPipe} from "../_pipes/safe-url.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-person-detail',
|
||||
@ -49,16 +50,15 @@ import {LicenseService} from "../_services/license.service";
|
||||
AsyncPipe,
|
||||
ImageComponent,
|
||||
SideNavCompanionBarComponent,
|
||||
NgStyle,
|
||||
ReadMoreComponent,
|
||||
TagBadgeComponent,
|
||||
PersonRolePipe,
|
||||
CarouselReelComponent,
|
||||
SeriesCardComponent,
|
||||
CardItemComponent,
|
||||
CardActionablesComponent,
|
||||
TranslocoDirective,
|
||||
ChapterCardComponent
|
||||
ChapterCardComponent,
|
||||
SafeUrlPipe
|
||||
],
|
||||
templateUrl: './person-detail.component.html',
|
||||
styleUrl: './person-detail.component.scss',
|
||||
@ -93,8 +93,14 @@ export class PersonDetailComponent {
|
||||
filter: SeriesFilterV2 | null = null;
|
||||
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
|
||||
chaptersByRole: any = {};
|
||||
anilistUrl: string = '';
|
||||
private readonly personSubject = new BehaviorSubject<Person | null>(null);
|
||||
protected readonly person$ = this.personSubject.asObservable();
|
||||
protected readonly person$ = this.personSubject.asObservable().pipe(tap(p => {
|
||||
if (p?.aniListId) {
|
||||
this.anilistUrl = translate('person-detail.anilist-url').replace('{AniListId}', p!.aniListId! + '');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}));
|
||||
|
||||
get HasCoverImage() {
|
||||
return (this.person as Person).coverImage;
|
||||
|
@ -1,11 +1,11 @@
|
||||
<div class="main-container container-fluid">
|
||||
<ng-container *transloco="let t; read:'settings'">
|
||||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
<h2 class="container-fluid" title>
|
||||
{{fragment | settingFragment}}
|
||||
</h2>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="row col-me-4 pb-3">
|
||||
<div class="container-fluid row col-me-4 pb-3">
|
||||
|
||||
@if (accountService.currentUser$ | async; as user) {
|
||||
@if (accountService.hasAdminRole(user)) {
|
||||
|
@ -763,6 +763,7 @@
|
||||
"matched-status-label": "Matched",
|
||||
"unmatched-status-label": "Not Matched",
|
||||
"blacklist-status-label": "Needs Manual Match",
|
||||
"dont-match-status-label": "{{dont-match-label}}",
|
||||
"all-status-label": "All",
|
||||
"dont-match-label": "Don't Match",
|
||||
"no-data": "{{common.no-data}}"
|
||||
@ -1071,7 +1072,8 @@
|
||||
"individual-role-title": "As a {{role}}",
|
||||
"browse-person-title": "All Works of {{name}}",
|
||||
"browse-person-by-role-title": "All Works of {{name}} as a {{role}}",
|
||||
"all-roles": "Roles"
|
||||
"all-roles": "Roles",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
},
|
||||
|
||||
"library-settings-modal": {
|
||||
@ -2677,7 +2679,7 @@
|
||||
"title": "Actions",
|
||||
"copy-settings": "Copy Settings From",
|
||||
"match": "Match",
|
||||
"match-description": "Match Series with Kavita+ manually"
|
||||
"match-tooltip": "Match Series with Kavita+ manually"
|
||||
},
|
||||
|
||||
"preferences": {
|
||||
|
@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "Kavita",
|
||||
"description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.9",
|
||||
"description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.10",
|
||||
"license": {
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
|
Loading…
x
Reference in New Issue
Block a user