Linked Series (#1230)
* Implemented the ability to link different series together through Edit Series. CSS pending. * Fixed up the css for related cards to show the relation * Working on making all tabs in edit seris modal save in one go. Taking a break. * Some fixes for Robbie to help with styling on * Linked series pill, center library * Centering library detail and related pill spacing - Library detail cards are now centered if total number of items is > 6 or if mobile. - Added ability to determine if mobile (viewport width <= 480px - Fixed related card spacing - Fixed related card pill spacing * Updating relation form spacing * Fixed a bug in card detail layout when there is no pagination, we create one in a way that all items render at once. * Only auto-close side nav on phones, not tablets * Fixed a bug where we had flipped state on sideNavCollapsed$ * Cleaned up some misleading comments * Implemented RBS back in and now if you have a relationship besides prequel/sequel, the target series will show a link back to it's parent. * Added Parentto pipe * Missed a relationship type Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
7253765f1d
commit
4206ae3e22
47 changed files with 2571 additions and 195 deletions
|
|
@ -6,8 +6,10 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
|
|
@ -339,5 +341,79 @@ namespace API.Controllers
|
|||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the related series for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="relation">Type of Relationship to pull back</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("related")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
|
||||
}
|
||||
|
||||
[HttpGet("all-related")]
|
||||
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
||||
}
|
||||
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpPost("update-related")]
|
||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
|
||||
|
||||
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
|
||||
UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character);
|
||||
UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
|
||||
UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
|
||||
UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
|
||||
UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
|
||||
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
|
||||
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
|
||||
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
|
||||
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
|
||||
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
|
||||
|
||||
return BadRequest("There was an issue updating relationships");
|
||||
}
|
||||
|
||||
private void UpdateRelationForKind(IList<int> dtoTargetSeriesIds, IEnumerable<SeriesRelation> adaptations, Series series, RelationKind kind)
|
||||
{
|
||||
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
|
||||
{
|
||||
// If the seriesId isn't in dto, it means we've removed or reclassified
|
||||
series.Relations.Remove(adaptation);
|
||||
}
|
||||
|
||||
// At this point, we only have things to add
|
||||
foreach (var targetSeriesId in dtoTargetSeriesIds)
|
||||
{
|
||||
// This ensures we don't allow any duplicates to be added
|
||||
if (series.Relations.SingleOrDefault(r =>
|
||||
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
|
||||
null) continue;
|
||||
|
||||
series.Relations.Add(new SeriesRelation()
|
||||
{
|
||||
Series = series,
|
||||
SeriesId = series.Id,
|
||||
TargetSeriesId = targetSeriesId,
|
||||
RelationKind = kind
|
||||
});
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
API/DTOs/SeriesDetail/RelatedSeriesDto.cs
Normal file
25
API/DTOs/SeriesDetail/RelatedSeriesDto.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.Collections.Generic;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
public class RelatedSeriesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The parent relationship Series
|
||||
/// </summary>
|
||||
public int SourceSeriesId { get; set; }
|
||||
|
||||
public IEnumerable<SeriesDto> Sequels { get; set; }
|
||||
public IEnumerable<SeriesDto> Prequels { get; set; }
|
||||
public IEnumerable<SeriesDto> SpinOffs { get; set; }
|
||||
public IEnumerable<SeriesDto> Adaptations { get; set; }
|
||||
public IEnumerable<SeriesDto> SideStories { get; set; }
|
||||
public IEnumerable<SeriesDto> Characters { get; set; }
|
||||
public IEnumerable<SeriesDto> Contains { get; set; }
|
||||
public IEnumerable<SeriesDto> Others { get; set; }
|
||||
public IEnumerable<SeriesDto> AlternativeSettings { get; set; }
|
||||
public IEnumerable<SeriesDto> AlternativeVersions { get; set; }
|
||||
public IEnumerable<SeriesDto> Doujinshis { get; set; }
|
||||
public IEnumerable<SeriesDto> Parent { get; set; }
|
||||
}
|
||||
19
API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs
Normal file
19
API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
public class UpdateRelatedSeriesDto
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public IList<int> Adaptations { get; set; }
|
||||
public IList<int> Characters { get; set; }
|
||||
public IList<int> Contains { get; set; }
|
||||
public IList<int> Others { get; set; }
|
||||
public IList<int> Prequels { get; set; }
|
||||
public IList<int> Sequels { get; set; }
|
||||
public IList<int> SideStories { get; set; }
|
||||
public IList<int> SpinOffs { get; set; }
|
||||
public IList<int> AlternativeSettings { get; set; }
|
||||
public IList<int> AlternativeVersions { get; set; }
|
||||
public IList<int> Doujinshis { get; set; }
|
||||
}
|
||||
|
|
@ -41,24 +41,36 @@ namespace API.Data
|
|||
public DbSet<Genre> Genre { get; set; }
|
||||
public DbSet<Tag> Tag { get; set; }
|
||||
public DbSet<SiteTheme> SiteTheme { get; set; }
|
||||
public DbSet<SeriesRelation> SeriesRelation { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
|
||||
builder.Entity<AppUser>()
|
||||
modelBuilder.Entity<AppUser>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.User)
|
||||
.HasForeignKey(ur => ur.UserId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<AppRole>()
|
||||
modelBuilder.Entity<AppRole>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.Role)
|
||||
.HasForeignKey(ur => ur.RoleId)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.Series)
|
||||
.WithMany(p => p.Relations)
|
||||
.HasForeignKey(pt => pt.SeriesId)
|
||||
.OnDelete(DeleteBehavior.ClientCascade);
|
||||
|
||||
modelBuilder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.TargetSeries)
|
||||
.WithMany(t => t.RelationOf)
|
||||
.HasForeignKey(pt => pt.TargetSeriesId);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
1513
API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs
generated
Normal file
1513
API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
55
API/Data/Migrations/20220421214448_SeriesRelations.cs
Normal file
55
API/Data/Migrations/20220421214448_SeriesRelations.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class SeriesRelations : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesRelation",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
RelationKind = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TargetSeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesRelation", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesRelation_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesRelation_Series_TargetSeriesId",
|
||||
column: x => x.TargetSeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesRelation_SeriesId",
|
||||
table: "SeriesRelation",
|
||||
column: "SeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesRelation_TargetSeriesId",
|
||||
table: "SeriesRelation",
|
||||
column: "TargetSeriesId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesRelation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -592,6 +592,30 @@ namespace API.Data.Migrations
|
|||
b.ToTable("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RelationKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TargetSeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.HasIndex("TargetSeriesId");
|
||||
|
||||
b.ToTable("SeriesRelation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1182,6 +1206,25 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany("Relations")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", "TargetSeries")
|
||||
.WithMany("RelationOf")
|
||||
.HasForeignKey("TargetSeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Series");
|
||||
|
||||
b.Navigation("TargetSeries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -1451,6 +1494,10 @@ namespace API.Data.Migrations
|
|||
|
||||
b.Navigation("Ratings");
|
||||
|
||||
b.Navigation("RelationOf");
|
||||
|
||||
b.Navigation("Relations");
|
||||
|
||||
b.Navigation("Volumes");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Select(c => c)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.Metadata;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
|
|
@ -24,6 +25,17 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
[Flags]
|
||||
public enum SeriesIncludes
|
||||
{
|
||||
None = 1,
|
||||
Volumes = 2,
|
||||
Metadata = 4,
|
||||
Related = 8,
|
||||
//Related = 16,
|
||||
//UserPreferences = 32
|
||||
}
|
||||
|
||||
internal class RecentlyAddedSeries
|
||||
{
|
||||
public int LibraryId { get; init; }
|
||||
|
|
@ -68,7 +80,7 @@ public interface ISeriesRepository
|
|||
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId);
|
||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
Task<bool> DeleteSeriesAsync(int seriesId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
|
||||
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
|
||||
Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds);
|
||||
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
|
||||
|
|
@ -96,6 +108,9 @@ public interface ISeriesRepository
|
|||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
|
|
@ -376,19 +391,35 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Series> GetSeriesByIdAsync(int seriesId)
|
||||
public async Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
|
||||
{
|
||||
return await _context.Series
|
||||
.Include(s => s.Volumes)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.CollectionTags)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
var query = _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
.AsSplitQuery();
|
||||
|
||||
if (includes.HasFlag(SeriesIncludes.Volumes))
|
||||
{
|
||||
query = query.Include(s => s.Volumes);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(SeriesIncludes.Related))
|
||||
{
|
||||
query = query.Include(s => s.Relations)
|
||||
.ThenInclude(r => r.TargetSeries)
|
||||
.Include(s => s.RelationOf);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(SeriesIncludes.Metadata))
|
||||
{
|
||||
query = query.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.CollectionTags)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People);
|
||||
}
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -939,6 +970,79 @@ public class SeriesRepository : ISeriesRepository
|
|||
return seriesMap.Values.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
var usersSeriesIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id);
|
||||
|
||||
var targetSeries = _context.SeriesRelation
|
||||
.Where(sr =>
|
||||
sr.SeriesId == seriesId && sr.RelationKind == kind && usersSeriesIds.Contains(sr.TargetSeriesId))
|
||||
.Include(sr => sr.TargetSeries)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Select(sr => sr.TargetSeriesId);
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => targetSeries.Contains(s.Id))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
var usersSeriesIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id);
|
||||
|
||||
return new RelatedSeriesDto()
|
||||
{
|
||||
SourceSeriesId = seriesId,
|
||||
Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation),
|
||||
Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character),
|
||||
Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel),
|
||||
Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel),
|
||||
Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains),
|
||||
SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory),
|
||||
SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff),
|
||||
Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other),
|
||||
AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting),
|
||||
AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion),
|
||||
Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi),
|
||||
Parent = await _context.Series
|
||||
.SelectMany(s =>
|
||||
s.RelationOf.Where(r => r.TargetSeriesId == seriesId
|
||||
&& usersSeriesIds.Contains(r.TargetSeriesId)
|
||||
&& r.RelationKind != RelationKind.Prequel
|
||||
&& r.RelationKind != RelationKind.Sequel)
|
||||
.Select(sr => sr.Series))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind)
|
||||
{
|
||||
return await _context.Series.SelectMany(s =>
|
||||
s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId))
|
||||
.Select(sr => sr.TargetSeries))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 3000)
|
||||
{
|
||||
var libraries = await _context.AppUser
|
||||
|
|
|
|||
66
API/Entities/Enums/RelationKind.cs
Normal file
66
API/Entities/Enums/RelationKind.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a relationship between Series
|
||||
/// </summary>
|
||||
public enum RelationKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Story that occurred before the original.
|
||||
/// </summary>
|
||||
[Description("Prequel")]
|
||||
Prequel = 1,
|
||||
/// <summary>
|
||||
/// Direct continuation of the story.
|
||||
/// </summary>
|
||||
[Description("Sequel")]
|
||||
Sequel = 2,
|
||||
/// <summary>
|
||||
/// Uses characters of a different series, but is not an alternate setting or story.
|
||||
/// </summary>
|
||||
[Description("Spin Off")]
|
||||
SpinOff = 3,
|
||||
/// <summary>
|
||||
/// Manga/Anime/Light Novel adaptation
|
||||
/// </summary>
|
||||
[Description("Adaptation")]
|
||||
Adaptation = 4,
|
||||
/// <summary>
|
||||
/// Takes place sometime during the parent storyline.
|
||||
/// </summary>
|
||||
[Description("Side Story")]
|
||||
SideStory = 5,
|
||||
/// <summary>
|
||||
/// When characters appear in both series, but is not a spin-off
|
||||
/// </summary>
|
||||
[Description("Character")]
|
||||
Character = 6,
|
||||
/// <summary>
|
||||
/// When the story contains another story, useful for One-Shots
|
||||
/// </summary>
|
||||
[Description("Contains")]
|
||||
Contains = 7,
|
||||
/// <summary>
|
||||
/// When nothing else fits
|
||||
/// </summary>
|
||||
[Description("Other")]
|
||||
Other = 8,
|
||||
/// <summary>
|
||||
/// Same universe/world/reality/timeline, completely different characters
|
||||
/// </summary>
|
||||
[Description("Alternative Setting")]
|
||||
AlternativeSetting = 9,
|
||||
/// <summary>
|
||||
/// Same setting, same characters, story is told differently
|
||||
/// </summary>
|
||||
[Description("Alternative Version")]
|
||||
AlternativeVersion = 10,
|
||||
/// <summary>
|
||||
/// Doujinshi or Fan work
|
||||
/// </summary>
|
||||
[Description("Doujinshi")]
|
||||
Doujinshi = 11
|
||||
|
||||
}
|
||||
25
API/Entities/Metadata/SeriesRelation.cs
Normal file
25
API/Entities/Metadata/SeriesRelation.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Entities.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// A relation flows between one series and another.
|
||||
/// Series ---kind---> target
|
||||
/// </summary>
|
||||
public class SeriesRelation
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public RelationKind RelationKind { get; set; }
|
||||
|
||||
public virtual Series TargetSeries { get; set; }
|
||||
/// <summary>
|
||||
/// A is Sequel to B. In this example, TargetSeries is A. B will hold the foreign key.
|
||||
/// </summary>
|
||||
public int TargetSeriesId { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual Series Series { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
}
|
||||
|
|
@ -66,9 +66,18 @@ public class Series : IEntityDate
|
|||
public DateTime LastChapterAdded { get; set; }
|
||||
|
||||
public SeriesMetadata Metadata { get; set; }
|
||||
|
||||
public ICollection<AppUserRating> Ratings { get; set; } = new List<AppUserRating>();
|
||||
public ICollection<AppUserProgress> Progress { get; set; } = new List<AppUserProgress>();
|
||||
|
||||
/// <summary>
|
||||
/// Relations to other Series, like Sequels, Prequels, etc
|
||||
/// </summary>
|
||||
/// <remarks>1 to Many relationship</remarks>
|
||||
public virtual ICollection<SeriesRelation> Relations { get; set; } = new List<SeriesRelation>();
|
||||
public virtual ICollection<SeriesRelation> RelationOf { get; set; } = new List<SeriesRelation>();
|
||||
|
||||
|
||||
// Relationships
|
||||
public List<Volume> Volumes { get; set; }
|
||||
public Library Library { get; set; }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using API.DTOs.Metadata;
|
|||
using API.DTOs.Reader;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities;
|
||||
|
|
@ -96,6 +97,10 @@ namespace API.Helpers
|
|||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor)));
|
||||
|
||||
// CreateMap<SeriesRelation, RelatedSeriesDto>()
|
||||
// .ForMember(dest => dest.Adaptations,
|
||||
// opt =>
|
||||
// opt.MapFrom(src => src.Where(p => p.Role == PersonRole.Writer)))
|
||||
|
||||
CreateMap<AppUser, UserDto>();
|
||||
CreateMap<SiteTheme, SiteThemeDto>();
|
||||
|
|
|
|||
|
|
@ -272,13 +272,8 @@ public class ScannerService : IScannerService
|
|||
|
||||
|
||||
_logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name);
|
||||
// await _eventHub.SendMessageAsync(SignalREvents.NotificationProgress,
|
||||
// MessageFactory.ScanLibraryProgressEvent(libraryId, 0F));
|
||||
|
||||
|
||||
var (totalFiles, scanElapsedTime, series) = await ScanFiles(library, library.Folders.Select(fp => fp.Path));
|
||||
// var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService);
|
||||
// var series = scanner.ScanLibrariesForSeries(library.Type, library.Folders.Select(fp => fp.Path), out var totalFiles, out var scanElapsedTime);
|
||||
_logger.LogInformation("[ScannerService] Finished file scan. Updating database");
|
||||
|
||||
foreach (var folderPath in library.Folders)
|
||||
|
|
@ -305,8 +300,6 @@ public class ScannerService : IScannerService
|
|||
|
||||
await CleanupDbEntities();
|
||||
|
||||
// await _eventHub.SendMessageAsync(SignalREvents.NotificationProgress,
|
||||
// MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue