Merged develop in

This commit is contained in:
Joseph Milazzo 2025-04-26 16:17:05 -05:00
commit d12a79892f
1443 changed files with 215765 additions and 44113 deletions

View file

@ -1,17 +1,24 @@
using System;
using System.Collections.Generic;
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;
using API.Entities.History;
using API.Entities.Interfaces;
using API.Entities.Metadata;
using API.Entities.MetadataMatching;
using API.Entities.Person;
using API.Entities.Scrobble;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Data;
@ -36,6 +43,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ServerSetting> ServerSetting { get; set; } = null!;
public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!;
public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!;
[Obsolete]
public DbSet<CollectionTag> CollectionTag { get; set; } = null!;
public DbSet<AppUserBookmark> AppUserBookmark { get; set; } = null!;
public DbSet<ReadingList> ReadingList { get; set; } = null!;
@ -64,7 +72,12 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!;
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!;
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!;
public DbSet<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = null!;
public DbSet<EmailHistory> EmailHistory { get; set; } = null!;
public DbSet<MetadataSettings> MetadataSettings { get; set; } = null!;
public DbSet<MetadataFieldMapping> MetadataFieldMapping { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
{
@ -114,10 +127,22 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
.Property(b => b.Locale)
.IsRequired(true)
.HasDefaultValue("en");
builder.Entity<AppUserPreferences>()
.Property(b => b.AniListScrobblingEnabled)
.HasDefaultValue(true);
builder.Entity<AppUserPreferences>()
.Property(b => b.WantToReadSync)
.HasDefaultValue(true);
builder.Entity<AppUserPreferences>()
.Property(b => b.AllowAutomaticWebtoonReaderDetection)
.HasDefaultValue(true);
builder.Entity<Library>()
.Property(b => b.AllowScrobbling)
.HasDefaultValue(true);
builder.Entity<Library>()
.Property(b => b.AllowMetadataMatching)
.HasDefaultValue(true);
builder.Entity<Chapter>()
.Property(b => b.WebLinks)
@ -149,6 +174,85 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
.WithOne(s => s.ExternalSeriesMetadata)
.HasForeignKey<ExternalSeriesMetadata>(em => em.SeriesId)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<AppUserCollection>()
.Property(b => b.AgeRating)
.HasDefaultValue(AgeRating.Unknown);
// Configure the many-to-many relationship for Movie and Person
builder.Entity<ChapterPeople>()
.HasKey(cp => new { cp.ChapterId, cp.PersonId, cp.Role });
builder.Entity<ChapterPeople>()
.HasOne(cp => cp.Chapter)
.WithMany(c => c.People)
.HasForeignKey(cp => cp.ChapterId);
builder.Entity<ChapterPeople>()
.HasOne(cp => cp.Person)
.WithMany(p => p.ChapterPeople)
.HasForeignKey(cp => cp.PersonId)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<SeriesMetadataPeople>()
.HasKey(smp => new { smp.SeriesMetadataId, smp.PersonId, smp.Role });
builder.Entity<SeriesMetadataPeople>()
.HasOne(smp => smp.SeriesMetadata)
.WithMany(sm => sm.People)
.HasForeignKey(smp => smp.SeriesMetadataId);
builder.Entity<SeriesMetadataPeople>()
.HasOne(smp => smp.Person)
.WithMany(p => p.SeriesMetadataPeople)
.HasForeignKey(smp => smp.PersonId)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<SeriesMetadataPeople>()
.Property(b => b.OrderWeight)
.HasDefaultValue(0);
builder.Entity<MetadataSettings>()
.Property(x => x.AgeRatingMappings)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<Dictionary<string, AgeRating>>(v, JsonSerializerOptions.Default) ?? new Dictionary<string, AgeRating>()
);
// Ensure blacklist is stored as a JSON array
builder.Entity<MetadataSettings>()
.Property(x => x.Blacklist)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<List<string>>(v, JsonSerializerOptions.Default) ?? new List<string>()
);
builder.Entity<MetadataSettings>()
.Property(x => x.Whitelist)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<List<string>>(v, JsonSerializerOptions.Default) ?? new List<string>()
);
builder.Entity<MetadataSettings>()
.Property(x => x.Overrides)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<List<MetadataSettingField>>(v, JsonSerializerOptions.Default) ?? new List<MetadataSettingField>()
);
// Configure one-to-many relationship
builder.Entity<MetadataSettings>()
.HasMany(x => x.FieldMappings)
.WithOne(x => x.MetadataSettings)
.HasForeignKey(x => x.MetadataSettingsId)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<MetadataSettings>()
.Property(b => b.Enabled)
.HasDefaultValue(true);
builder.Entity<MetadataSettings>()
.Property(b => b.EnableCoverImage)
.HasDefaultValue(true);
}
#nullable enable
@ -156,10 +260,15 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
{
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
entity.Created = DateTime.Now;
entity.LastModified = DateTime.Now;
entity.CreatedUtc = DateTime.UtcNow;
entity.LastModifiedUtc = DateTime.UtcNow;
// This allows for mocking
if (entity.Created == DateTime.MinValue)
{
entity.Created = DateTime.Now;
entity.CreatedUtc = DateTime.UtcNow;
}
}
private static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e)

View file

@ -15,9 +15,8 @@ public static class MigrateLibrariesToHaveAllFileTypes
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.Library.AnyAsync(l => l.LibraryFileTypes.Count == 0))
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLibrariesToHaveAllFileTypes"))
{
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error");
return;
}

View file

@ -3,7 +3,11 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.DTOs.Filtering.v2;
using API.Entities;
using API.Entities.History;
using API.Helpers;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
@ -21,10 +25,14 @@ public static class MigrateSmartFilterEncoding
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateSmartFilterEncoding"))
{
return;
}
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Please be patient, this may take some time. This is not an error");
var smartFilters = dataContext.AppUserSmartFilter.ToList();
var smartFilters = await dataContext.AppUserSmartFilter.ToListAsync();
foreach (var filter in smartFilters)
{
if (!ShouldMigrateFilter(filter.Filter)) continue;
@ -38,6 +46,14 @@ public static class MigrateSmartFilterEncoding
await unitOfWork.CommitAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateSmartFilterEncoding",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Completed. This is not an error");
}

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -21,10 +21,11 @@ public static class MigrateEmailTemplates
var files = directoryService.GetFiles(directoryService.CustomizedTemplateDirectory);
if (files.Any())
{
logger.LogCritical("Running MigrateEmailTemplates migration - Completed. This is not an error");
return;
}
logger.LogCritical("Running MigrateEmailTemplates migration - Please be patient, this may take some time. This is not an error");
// Write files to directory
await DownloadAndWriteToFile(EmailChange, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailChange.html"), logger);
await DownloadAndWriteToFile(EmailConfirm, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailConfirm.html"), logger);
@ -33,8 +34,7 @@ public static class MigrateEmailTemplates
await DownloadAndWriteToFile(EmailTest, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailTest.html"), logger);
logger.LogCritical("Running MigrateEmailTemplates migration - Please be patient, this may take some time. This is not an error");
logger.LogCritical("Running MigrateEmailTemplates migration - Completed. This is not an error");
}
private static async Task DownloadAndWriteToFile(string url, string filePath, ILogger<Program> logger)

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -16,8 +17,6 @@ public static class MigrateManualHistory
{
if (await dataContext.ManualMigrationHistory.AnyAsync())
{
logger.LogCritical(
"Running MigrateManualHistory migration - Completed. This is not an error");
return;
}

View file

@ -0,0 +1,42 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
public static class MigrateVolumeLookupName
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateVolumeLookupName"))
{
return;
}
logger.LogCritical(
"Running MigrateVolumeLookupName migration - Please be patient, this may take some time. This is not an error");
// Update all volumes to have LookupName as after this migration, name isn't used for lookup
var volumes = dataContext.Volume.ToList();
foreach (var volume in volumes)
{
volume.LookupName = volume.Name;
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateVolumeLookupName",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateVolumeLookupName migration - Completed. This is not an error");
}
}

View file

@ -13,8 +13,13 @@ namespace API.Data.ManualMigrations;
/// </summary>
public static class MigrateVolumeNumber
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateVolumeNumber"))
{
return;
}
if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0))
{
logger.LogCritical(

View file

@ -20,6 +20,12 @@ public static class MigrateWantToReadExport
{
try
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadExport"))
{
return;
}
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
if (File.Exists(importFile))
{

View file

@ -6,6 +6,7 @@ using API.Data.Repositories;
using API.Entities;
using API.Services;
using CsvHelper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
@ -15,8 +16,14 @@ namespace API.Data.ManualMigrations;
/// </summary>
public static class MigrateWantToReadImport
{
public static async Task Migrate(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger<Program> logger)
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadImport"))
{
return;
}
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv");

View file

@ -14,6 +14,10 @@ public static class MigrateUserLibrarySideNavStream
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateUserLibrarySideNavStream"))
{
return;
}
var usersWithLibraryStreams = await dataContext.AppUser
.Include(u => u.SideNavStreams)

View file

@ -0,0 +1,188 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.0 migration to move loose leaf chapters into their own volume and retain user progress.
/// </summary>
public static class MigrateLooseLeafChapters
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLooseLeafChapters"))
{
return;
}
logger.LogCritical(
"Running MigrateLooseLeafChapters migration - Please be patient, this may take some time. This is not an error");
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var extension = settings.EncodeMediaAs.GetExtension();
var progress = await dataContext.AppUserProgresses
.Join(dataContext.Chapter, p => p.ChapterId, c => c.Id, (p, c) => new UserProgressCsvRecord
{
IsSpecial = c.IsSpecial,
AppUserId = p.AppUserId,
PagesRead = p.PagesRead,
Range = c.Range,
Number = c.Number,
MinNumber = c.MinNumber,
SeriesId = p.SeriesId,
VolumeId = p.VolumeId,
ProgressId = p.Id
})
.Where(d => !d.IsSpecial)
.Join(dataContext.Volume, d => d.VolumeId, v => v.Id, (d, v) => new
{
ProgressRecord = d,
Volume = v
})
.Where(d => d.Volume.Name == "0")
.ToListAsync();
// First, group all the progresses into different series
logger.LogCritical("Migrating {Count} progress events to new Volume structure for Loose leafs - This may take over 10 minutes depending on size of DB. Please wait", progress.Count);
var progressesGroupedBySeries = progress
.GroupBy(p => p.ProgressRecord.SeriesId);
foreach (var seriesGroup in progressesGroupedBySeries)
{
// Get each series and move the loose leafs from the old volume to the new Volume
var seriesId = seriesGroup.Key;
// Handle All Loose Leafs
var looseLeafsInSeries = seriesGroup
.Where(p => !p.ProgressRecord.IsSpecial)
.ToList();
// Get distinct Volumes by Id. For each one, create it then create the progress events
var distinctVolumes = looseLeafsInSeries.DistinctBy(d => d.Volume.Id);
foreach (var distinctVolume in distinctVolumes)
{
// Create a new volume for each series with the appropriate number (-100000)
var chapters = await dataContext.Chapter
.Where(c => c.VolumeId == distinctVolume.Volume.Id && !c.IsSpecial).ToListAsync();
var newVolume = new VolumeBuilder(Parser.LooseLeafVolume)
.WithSeriesId(seriesId)
.WithCreated(distinctVolume.Volume.Created)
.WithLastModified(distinctVolume.Volume.LastModified)
.Build();
newVolume.Pages = chapters.Sum(c => c.Pages);
newVolume.WordCount = chapters.Sum(c => c.WordCount);
newVolume.MinHoursToRead = chapters.Sum(c => c.MinHoursToRead);
newVolume.MaxHoursToRead = chapters.Sum(c => c.MaxHoursToRead);
newVolume.AvgHoursToRead = chapters.Sum(c => c.AvgHoursToRead);
dataContext.Volume.Add(newVolume);
await dataContext.SaveChangesAsync(); // Save changes to generate the newVolumeId
// Migrate the progress event to the new volume
var oldVolumeProgresses = await dataContext.AppUserProgresses
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
foreach (var oldProgress in oldVolumeProgresses)
{
oldProgress.VolumeId = newVolume.Id;
}
logger.LogInformation("Moving {Count} chapters from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
chapters.Count, distinctVolume.Volume.Id, newVolume.Id);
// Move the loose leaf chapters from the old volume to the new Volume
foreach (var chapter in chapters)
{
// Update the VolumeId on the existing progress event
chapter.VolumeId = newVolume.Id;
// We need to migrate cover images as well
//UpdateCoverImage(directoryService, logger, chapter, extension, newVolume);
}
var oldVolumeBookmarks = await dataContext.AppUserBookmark
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
logger.LogInformation("Moving {Count} existing Bookmarks from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
oldVolumeBookmarks.Count, distinctVolume.Volume.Id, newVolume.Id);
foreach (var bookmark in oldVolumeBookmarks)
{
bookmark.VolumeId = newVolume.Id;
}
var oldVolumePersonalToC = await dataContext.AppUserTableOfContent
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
oldVolumePersonalToC.Count, distinctVolume.Volume.Id, newVolume.Id);
foreach (var pToc in oldVolumePersonalToC)
{
pToc.VolumeId = newVolume.Id;
}
var oldVolumeReadingListItems = await dataContext.ReadingListItem
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
oldVolumeReadingListItems.Count, distinctVolume.Volume.Id, newVolume.Id);
foreach (var readingListItem in oldVolumeReadingListItems)
{
readingListItem.VolumeId = newVolume.Id;
}
await dataContext.SaveChangesAsync();
}
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateLooseLeafChapters",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateLooseLeafChapters migration - Completed. This is not an error");
}
private static void UpdateCoverImage(IDirectoryService directoryService, ILogger<Program> logger, Chapter chapter,
string extension, Volume newVolume)
{
var existingCover = ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId) + extension;
var newCover = ImageService.GetChapterFormat(chapter.Id, newVolume.Id) + extension;
try
{
if (!chapter.CoverImageLocked)
{
// First rename existing cover
File.Copy(Path.Join(directoryService.CoverImageDirectory, existingCover), Path.Join(directoryService.CoverImageDirectory, newCover));
chapter.CoverImage = newCover;
}
} catch (Exception ex)
{
logger.LogError(ex, "Unable to rename {OldCover} to {NewCover}, this cover will need manual refresh", existingCover, newCover);
}
}
}

View file

@ -0,0 +1,207 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
public class UserProgressCsvRecord
{
public bool IsSpecial { get; set; }
public int AppUserId { get; set; }
public int PagesRead { get; set; }
public string Range { get; set; }
public string Number { get; set; }
public float MinNumber { get; set; }
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public int ProgressId { get; set; }
}
/// <summary>
/// v0.8.0 migration to move Specials into their own volume and retain user progress.
/// </summary>
public static class MigrateMixedSpecials
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateMixedSpecials"))
{
return;
}
logger.LogCritical(
"Running ManualMigrateMixedSpecials migration - Please be patient, this may take some time. This is not an error");
// First, group all the progresses into different series
// Get each series and move the specials from old volume to the new Volume()
// Create a new progress event from existing and store the Id of existing progress event to delete it
// Save per series
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var extension = settings.EncodeMediaAs.GetExtension();
var progress = await dataContext.AppUserProgresses
.Join(dataContext.Chapter, p => p.ChapterId, c => c.Id, (p, c) => new UserProgressCsvRecord
{
IsSpecial = c.IsSpecial,
AppUserId = p.AppUserId,
PagesRead = p.PagesRead,
Range = c.Range,
Number = c.Number,
MinNumber = c.MinNumber,
SeriesId = p.SeriesId,
VolumeId = p.VolumeId,
ProgressId = p.Id
})
.Where(d => d.IsSpecial || d.Number == "0")
.Join(dataContext.Volume, d => d.VolumeId, v => v.Id,
(d, v) => new
{
ProgressRecord = d,
Volume = v
})
.Where(d => d.Volume.Name == "0")
.ToListAsync();
// First, group all the progresses into different series
logger.LogCritical("Migrating {Count} progress events to new Volume structure for Specials - This may take over 10 minutes depending on size of DB. Please wait", progress.Count);
var progressesGroupedBySeries = progress.GroupBy(p => p.ProgressRecord.SeriesId);
foreach (var seriesGroup in progressesGroupedBySeries)
{
// Get each series and move the specials from the old volume to the new Volume
var seriesId = seriesGroup.Key;
// Handle All Specials
var specialsInSeries = seriesGroup
.Where(p => p.ProgressRecord.IsSpecial)
.ToList();
// Get distinct Volumes by Id. For each one, create it then create the progress events
var distinctVolumes = specialsInSeries.DistinctBy(d => d.Volume.Id);
foreach (var distinctVolume in distinctVolumes)
{
// Create a new volume for each series with the appropriate number (-100000)
var chapters = await dataContext.Chapter
.Where(c => c.VolumeId == distinctVolume.Volume.Id && c.IsSpecial).ToListAsync();
var newVolume = new VolumeBuilder(Parser.SpecialVolume)
.WithSeriesId(seriesId)
.WithCreated(distinctVolume.Volume.Created)
.WithLastModified(distinctVolume.Volume.LastModified)
.Build();
newVolume.Pages = chapters.Sum(c => c.Pages);
newVolume.WordCount = chapters.Sum(c => c.WordCount);
newVolume.MinHoursToRead = chapters.Sum(c => c.MinHoursToRead);
newVolume.MaxHoursToRead = chapters.Sum(c => c.MaxHoursToRead);
newVolume.AvgHoursToRead = chapters.Sum(c => c.AvgHoursToRead);
dataContext.Volume.Add(newVolume);
await dataContext.SaveChangesAsync(); // Save changes to generate the newVolumeId
// Migrate the progress event to the new volume
var oldVolumeProgresses = await dataContext.AppUserProgresses
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
foreach (var oldProgress in oldVolumeProgresses)
{
oldProgress.VolumeId = newVolume.Id;
}
logger.LogInformation("Moving {Count} chapters from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
chapters.Count, distinctVolume.Volume.Id, newVolume.Id);
// Move the special chapters from the old volume to the new Volume
foreach (var specialChapter in chapters)
{
// Update the VolumeId on the existing progress event
specialChapter.VolumeId = newVolume.Id;
//UpdateCoverImage(directoryService, logger, specialChapter, extension, newVolume);
}
var oldVolumeBookmarks = await dataContext.AppUserBookmark
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
logger.LogInformation("Moving {Count} existing Bookmarks from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
oldVolumeBookmarks.Count, distinctVolume.Volume.Id, newVolume.Id);
foreach (var bookmark in oldVolumeBookmarks)
{
bookmark.VolumeId = newVolume.Id;
}
var oldVolumePersonalToC = await dataContext.AppUserTableOfContent
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
oldVolumePersonalToC.Count, distinctVolume.Volume.Id, newVolume.Id);
foreach (var pToc in oldVolumePersonalToC)
{
pToc.VolumeId = newVolume.Id;
}
var oldVolumeReadingListItems = await dataContext.ReadingListItem
.Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync();
logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
oldVolumeReadingListItems.Count, distinctVolume.Volume.Id, newVolume.Id);
foreach (var readingListItem in oldVolumeReadingListItems)
{
readingListItem.VolumeId = newVolume.Id;
}
await dataContext.SaveChangesAsync();
}
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "ManualMigrateMixedSpecials",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running ManualMigrateMixedSpecials migration - Completed. This is not an error");
}
private static void UpdateCoverImage(IDirectoryService directoryService, ILogger<Program> logger, Chapter specialChapter,
string extension, Volume newVolume)
{
// We need to migrate cover images as well
var existingCover = ImageService.GetChapterFormat(specialChapter.Id, specialChapter.VolumeId) + extension;
var newCover = ImageService.GetChapterFormat(specialChapter.Id, newVolume.Id) + extension;
try
{
if (!specialChapter.CoverImageLocked)
{
// First rename existing cover
File.Copy(Path.Join(directoryService.CoverImageDirectory, existingCover), Path.Join(directoryService.CoverImageDirectory, newCover));
specialChapter.CoverImage = newCover;
}
} catch (Exception ex)
{
logger.LogError(ex, "Unable to rename {OldCover} to {NewCover}, this cover will need manual refresh", existingCover, newCover);
}
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// Introduced in v0.8.0, this migrates the existing Chapter and Volume 0 -> Parser defined, MangaFile.FileName
/// </summary>
public static class MigrateChapterFields
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterFields"))
{
return;
}
logger.LogCritical(
"Running MigrateChapterFields migration - Please be patient, this may take some time. This is not an error");
// Update all volumes only have specials in them (rare)
var volumesWithJustSpecials = dataContext.Volume
.Include(v => v.Chapters)
.Where(v => v.Name == "0" && v.Chapters.All(c => c.IsSpecial))
.ToList();
logger.LogCritical(
"Running MigrateChapterFields migration - Updating {Count} volumes that only have specials in them", volumesWithJustSpecials.Count);
foreach (var volume in volumesWithJustSpecials)
{
volume.Name = $"{Parser.SpecialVolumeNumber}";
volume.MinNumber = Parser.SpecialVolumeNumber;
volume.MaxNumber = Parser.SpecialVolumeNumber;
}
// Update all volumes that only have loose leafs in them
var looseLeafVolumes = dataContext.Volume
.Include(v => v.Chapters)
.Where(v => v.Name == "0" && v.Chapters.All(c => !c.IsSpecial))
.ToList();
logger.LogCritical(
"Running MigrateChapterFields migration - Updating {Count} volumes that only have loose leaf chapters in them", looseLeafVolumes.Count);
foreach (var volume in looseLeafVolumes)
{
volume.Name = $"{Parser.DefaultChapterNumber}";
volume.MinNumber = Parser.DefaultChapterNumber;
volume.MaxNumber = Parser.DefaultChapterNumber;
}
// Update all MangaFile
logger.LogCritical(
"Running MigrateChapterFields migration - Updating all MangaFiles");
foreach (var mangaFile in dataContext.MangaFile)
{
mangaFile.FileName = Parser.RemoveExtensionIfSupported(mangaFile.FilePath);
}
var looseLeafChapters = await dataContext.Chapter.Where(c => c.Number == "0").ToListAsync();
logger.LogCritical(
"Running MigrateChapterFields migration - Updating {Count} loose leaf chapters", looseLeafChapters.Count);
foreach (var chapter in looseLeafChapters)
{
chapter.Number = Parser.DefaultChapter;
chapter.MinNumber = Parser.DefaultChapterNumber;
chapter.MaxNumber = Parser.DefaultChapterNumber;
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateChapterFields",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateChapterFields migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// Introduced in v0.8.0, this migrates the existing Chapter Range -> Chapter Min/Max Number
/// </summary>
public static class MigrateChapterNumber
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterNumber"))
{
return;
}
logger.LogCritical(
"Running MigrateChapterNumber migration - Please be patient, this may take some time. This is not an error");
// Get all volumes
foreach (var chapter in dataContext.Chapter)
{
if (chapter.IsSpecial)
{
chapter.MinNumber = Parser.DefaultChapterNumber;
chapter.MaxNumber = Parser.DefaultChapterNumber;
continue;
}
chapter.MinNumber = Parser.MinNumberFromRange(chapter.Range);
chapter.MaxNumber = Parser.MaxNumberFromRange(chapter.Range);
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateChapterNumber",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateChapterNumber migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.0 changed the range to that it doesn't have filename by default
/// </summary>
public static class MigrateChapterRange
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterRange"))
{
return;
}
logger.LogCritical(
"Running MigrateChapterRange migration - Please be patient, this may take some time. This is not an error");
var chapters = await dataContext.Chapter.ToListAsync();
foreach (var chapter in chapters)
{
if (Parser.MinNumberFromRange(chapter.Range).Is(0.0f))
{
chapter.Range = chapter.GetNumberTitle();
}
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateChapterRange",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateChapterRange migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,82 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using API.Extensions.QueryExtensions;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.0 refactored User Collections
/// </summary>
public static class MigrateCollectionTagToUserCollections
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateCollectionTagToUserCollections") ||
!await dataContext.AppUser.AnyAsync())
{
return;
}
logger.LogCritical(
"Running MigrateCollectionTagToUserCollections migration - Please be patient, this may take some time. This is not an error");
// Find the first user that is an admin
var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(AppUserIncludes.Collections);
if (defaultAdmin == null)
{
await CompleteMigration(dataContext, logger);
return;
}
// For all collectionTags, move them over to said user
var existingCollections = await dataContext.CollectionTag
.OrderBy(c => c.NormalizedTitle)
.Includes(CollectionTagIncludes.SeriesMetadataWithSeries)
.ToListAsync();
foreach (var existingCollectionTag in existingCollections)
{
var collection = new AppUserCollection()
{
Title = existingCollectionTag.Title,
NormalizedTitle = existingCollectionTag.Title.Normalize(),
CoverImage = existingCollectionTag.CoverImage,
CoverImageLocked = existingCollectionTag.CoverImageLocked,
Promoted = existingCollectionTag.Promoted,
AgeRating = AgeRating.Unknown,
Summary = existingCollectionTag.Summary,
Items = existingCollectionTag.SeriesMetadatas.Select(s => s.Series).ToList()
};
collection.AgeRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(collection.Items.Select(s => s.Id));
defaultAdmin.Collections.Add(collection);
}
unitOfWork.UserRepository.Update(defaultAdmin);
await unitOfWork.CommitAsync();
await CompleteMigration(dataContext, logger);
}
private static async Task CompleteMigration(DataContext dataContext, ILogger<Program> logger)
{
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateCollectionTagToUserCollections",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateCollectionTagToUserCollections migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn.
/// </summary>
public static class MigrateDuplicateDarkTheme
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateDuplicateDarkTheme"))
{
return;
}
logger.LogCritical(
"Running MigrateDuplicateDarkTheme migration - Please be patient, this may take some time. This is not an error");
var darkThemes = await dataContext.SiteTheme.Where(t => t.Name == "Dark").ToListAsync();
if (darkThemes.Count > 1)
{
var correctDarkTheme = darkThemes.First(d => !string.IsNullOrEmpty(d.Description));
// Get users
var users = await dataContext.AppUser
.Include(u => u.UserPreferences)
.ThenInclude(p => p.Theme)
.Where(u => u.UserPreferences.Theme.Name == "Dark")
.ToListAsync();
// Find any users that have a duplicate Dark theme as default and switch to the correct one
foreach (var user in users)
{
if (string.IsNullOrEmpty(user.UserPreferences.Theme.Description))
{
user.UserPreferences.Theme = correctDarkTheme;
}
}
await dataContext.SaveChangesAsync();
// Now remove the bad themes
dataContext.SiteTheme.RemoveRange(darkThemes.Where(d => string.IsNullOrEmpty(d.Description)));
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateDuplicateDarkTheme",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateDuplicateDarkTheme migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn.
/// </summary>
public static class MigrateMangaFilePath
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateMangaFilePath"))
{
return;
}
logger.LogCritical(
"Running MigrateMangaFilePath migration - Please be patient, this may take some time. This is not an error");
foreach(var file in dataContext.MangaFile)
{
file.FilePath = Parser.NormalizePath(file.FilePath);
}
await dataContext.SaveChangesAsync();
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateMangaFilePath",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateMangaFilePath migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,124 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
public class ProgressExport
{
[Name("Library Id")]
public int LibraryId { get; set; }
[Name("Library Name")]
public string LibraryName { get; set; }
[Name("Series Name")]
public string SeriesName { get; set; }
[Name("Volume Number")]
public string VolumeRange { get; set; }
[Name("Volume LookupName")]
public string VolumeLookupName { get; set; }
[Name("Chapter Number")]
public string ChapterRange { get; set; }
[Name("FileName")]
public string MangaFileName { get; set; }
[Name("FilePath")]
public string MangaFilePath { get; set; }
[Name("AppUser Name")]
public string AppUserName { get; set; }
[Name("AppUser Id")]
public int AppUserId { get; set; }
[Name("Pages Read")]
public int PagesRead { get; set; }
[Name("BookScrollId")]
public string BookScrollId { get; set; }
[Name("Progress Created")]
public DateTime Created { get; set; }
[Name("Progress LastModified")]
public DateTime LastModified { get; set; }
}
/// <summary>
/// v0.8.0 - Progress is extracted and saved in a csv
/// </summary>
public static class MigrateProgressExport
{
public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
{
try
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateProgressExport"))
{
return;
}
logger.LogCritical(
"Running MigrateProgressExport migration - Please be patient, this may take some time. This is not an error");
var data = await dataContext.AppUserProgresses
.Join(dataContext.Series, progress => progress.SeriesId, series => series.Id, (progress, series) => new { progress, series })
.Join(dataContext.Volume, ps => ps.progress.VolumeId, volume => volume.Id, (ps, volume) => new { ps.progress, ps.series, volume })
.Join(dataContext.Chapter, psv => psv.progress.ChapterId, chapter => chapter.Id, (psv, chapter) => new { psv.progress, psv.series, psv.volume, chapter })
.Join(dataContext.MangaFile, psvc => psvc.chapter.Id, mangaFile => mangaFile.ChapterId, (psvc, mangaFile) => new { psvc.progress, psvc.series, psvc.volume, psvc.chapter, mangaFile })
.Join(dataContext.AppUser, psvcm => psvcm.progress.AppUserId, appUser => appUser.Id, (psvcm, appUser) => new
{
LibraryId = psvcm.series.LibraryId,
LibraryName = psvcm.series.Library.Name,
SeriesName = psvcm.series.Name,
VolumeRange = psvcm.volume.MinNumber + "-" + psvcm.volume.MaxNumber,
VolumeLookupName = psvcm.volume.Name,
ChapterRange = psvcm.chapter.Range,
MangaFileName = psvcm.mangaFile.FileName,
MangaFilePath = psvcm.mangaFile.FilePath,
AppUserName = appUser.UserName,
AppUserId = appUser.Id,
PagesRead = psvcm.progress.PagesRead,
BookScrollId = psvcm.progress.BookScrollId,
ProgressCreated = psvcm.progress.Created,
ProgressLastModified = psvcm.progress.LastModified
}).ToListAsync();
// Write the mapped data to a CSV file
await using var writer = new StreamWriter(Path.Join(directoryService.ConfigDirectory, "progress_export.csv"));
await using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
await csv.WriteRecordsAsync(data);
logger.LogCritical(
"Running MigrateProgressExport migration - Completed. This is not an error");
}
catch (Exception ex)
{
// On new installs, the db isn't setup yet, so this has nothing to do
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateProgressExport",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.0 released with a bug around LowestSeriesPath. This resets it for all users.
/// </summary>
public static class MigrateLowestSeriesFolderPath
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLowestSeriesFolderPath"))
{
return;
}
logger.LogCritical(
"Running MigrateLowestSeriesFolderPath migration - Please be patient, this may take some time. This is not an error");
var series = await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)).ToListAsync();
foreach (var s in series)
{
s.LowestFolderPath = string.Empty;
unitOfWork.SeriesRepository.Update(s);
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateLowestSeriesFolderPath",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateLowestSeriesFolderPath migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.2 switches Default Kavita installs to WAL
/// </summary>
public static class ManualMigrateSwitchToWal
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateSwitchToWal"))
{
return;
}
logger.LogCritical("Running ManualMigrateSwitchToWal migration - Please be patient, this may take some time. This is not an error");
try
{
var connection = context.Database.GetDbConnection();
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "PRAGMA journal_mode=WAL;";
await command.ExecuteNonQueryAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Error setting WAL");
/* Swallow */
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateSwitchToWal",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateSwitchToWal migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.2 introduced Theme repo viewer, this adds Description to existing SiteTheme defaults
/// </summary>
public static class ManualMigrateThemeDescription
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateThemeDescription"))
{
return;
}
logger.LogCritical("Running ManualMigrateThemeDescription migration - Please be patient, this may take some time. This is not an error");
var theme = await context.SiteTheme.FirstOrDefaultAsync(t => t.Name == "Dark");
if (theme != null)
{
theme.Description = Seed.DefaultThemes.First().Description;
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateThemeDescription",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateThemeDescription migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using API.Services;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.2 I started collecting information on when the user first installed Kavita as a nice to have info for the user
/// </summary>
public static class MigrateInitialInstallData
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger, IDirectoryService directoryService)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateInitialInstallData"))
{
return;
}
logger.LogCritical(
"Running MigrateInitialInstallData migration - Please be patient, this may take some time. This is not an error");
var settings = await dataContext.ServerSetting.ToListAsync();
// Get the Install Date as Date the DB was written
var dbFile = Path.Join(directoryService.ConfigDirectory, "kavita.db");
if (!string.IsNullOrEmpty(dbFile) && directoryService.FileSystem.File.Exists(dbFile))
{
var fi = directoryService.FileSystem.FileInfo.New(dbFile);
var setting = settings.First(s => s.Key == ServerSettingKey.FirstInstallDate);
setting.Value = fi.CreationTimeUtc.ToString(CultureInfo.InvariantCulture);
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateInitialInstallData",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateInitialInstallData migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
#nullable enable
/// <summary>
/// Some linux-based users are having non-rooted LowestFolderPaths. This will attempt to fix it or null them.
/// Fixed in v0.8.2
/// </summary>
public static class MigrateSeriesLowestFolderPath
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger, IDirectoryService directoryService)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateSeriesLowestFolderPath"))
{
return;
}
logger.LogCritical("Running MigrateSeriesLowestFolderPath migration - Please be patient, this may take some time. This is not an error");
var seriesWithFolderPath =
await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath))
.Include(s => s.Library)
.ThenInclude(l => l.Folders)
.ToListAsync();
foreach (var series in seriesWithFolderPath)
{
var isValidPath = series.Library.Folders
.Any(folder => Parser.NormalizePath(series.LowestFolderPath!).StartsWith(Parser.NormalizePath(folder.Path), StringComparison.OrdinalIgnoreCase));
if (isValidPath) continue;
series.LowestFolderPath = null;
dataContext.Entry(series).State = EntityState.Modified;
}
await dataContext.SaveChangesAsync();
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateSeriesLowestFolderPath",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical("Running MigrateSeriesLowestFolderPath migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using Flurl.Util;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// At some point, encoding settings wrote bad data to the backend, maybe in v0.8.0. This just fixes any bad data.
/// </summary>
public static class ManualMigrateEncodeSettings
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateEncodeSettings"))
{
return;
}
logger.LogCritical("Running ManualMigrateEncodeSettings migration - Please be patient, this may take some time. This is not an error");
var encodeAs = await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.EncodeMediaAs);
var coverSize = await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CoverImageSize);
var encodeMap = new Dictionary<string, string>
{
{ EncodeFormat.WEBP.ToString(), ((int)EncodeFormat.WEBP).ToString() },
{ EncodeFormat.PNG.ToString(), ((int)EncodeFormat.PNG).ToString() },
{ EncodeFormat.AVIF.ToString(), ((int)EncodeFormat.AVIF).ToString() }
};
if (encodeMap.TryGetValue(encodeAs.Value, out var encodedValue))
{
encodeAs.Value = encodedValue;
context.ServerSetting.Update(encodeAs);
}
if (coverSize.Value == "0")
{
coverSize.Value = ((int)CoverImageSize.Default).ToString();
context.ServerSetting.Update(coverSize);
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateEncodeSettings",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateEncodeSettings migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// Due to a bug in the initial merge of People/Scanner rework, people got messed up bad. This migration will clear out the table only for nightly users: 0.8.3.15/0.8.3.16
/// </summary>
public static class ManualMigrateRemovePeople
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateRemovePeople"))
{
return;
}
var version = BuildInfo.Version.ToString();
if (version != "0.8.3.15" && version != "0.8.3.16")
{
return;
}
logger.LogCritical("Running ManualMigrateRemovePeople migration - Please be patient, this may take some time. This is not an error");
context.Person.RemoveRange(context.Person);
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateRemovePeople",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateRemovePeople migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// When I removed Scrobble support for Book libraries, I forgot to turn the setting off for said libraries.
/// </summary>
public static class ManualMigrateUnscrobbleBookLibraries
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateUnscrobbleBookLibraries"))
{
return;
}
logger.LogCritical("Running ManualMigrateUnscrobbleBookLibraries migration - Please be patient, this may take some time. This is not an error");
var libs = await context.Library.Where(l => l.Type == LibraryType.Book).ToListAsync();
foreach (var lib in libs)
{
lib.AllowScrobbling = false;
context.Entry(lib).State = EntityState.Modified;
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateUnscrobbleBookLibraries",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateUnscrobbleBookLibraries migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.3 still had a bug around LowestSeriesPath. This resets it for all users.
/// </summary>
public static class MigrateLowestSeriesFolderPath2
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLowestSeriesFolderPath2"))
{
return;
}
logger.LogCritical(
"Running MigrateLowestSeriesFolderPath2 migration - Please be patient, this may take some time. This is not an error");
var series = await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)).ToListAsync();
foreach (var s in series)
{
s.LowestFolderPath = string.Empty;
unitOfWork.SeriesRepository.Update(s);
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateLowestSeriesFolderPath2",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateLowestSeriesFolderPath2 migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Entities.Metadata;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.5 - Migrating Kavita+ BlacklistedSeries table to Series entity to streamline implementation and generate a "Needs Manual Match" entry for the Series
/// </summary>
public static class ManualMigrateBlacklistTableToSeries
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateBlacklistTableToSeries"))
{
return;
}
logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Please be patient, this may take some time. This is not an error");
// Get all series in the Blacklist table and set their IsBlacklist = true
var blacklistedSeries = await context.SeriesBlacklist
.Include(s => s.Series.ExternalSeriesMetadata)
.Select(s => s.Series)
.ToListAsync();
foreach (var series in blacklistedSeries)
{
series.IsBlacklisted = true;
series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() { SeriesId = series.Id };
if (series.ExternalSeriesMetadata.AniListId > 0)
{
series.IsBlacklisted = false;
logger.LogInformation("{SeriesName} was in Blacklist table, but has valid AniList Id, not blacklisting", series.Name);
}
context.Series.Entry(series).State = EntityState.Modified;
}
// Remove everything in SeriesBlacklist (it will be removed in another migration)
context.SeriesBlacklist.RemoveRange(context.SeriesBlacklist);
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateBlacklistTableToSeries",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,53 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using API.Entities.Metadata;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.5 - Migrating Kavita+ Series that are Blacklisted but have valid ExternalSeries row
/// </summary>
public static class ManualMigrateInvalidBlacklistSeries
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateInvalidBlacklistSeries"))
{
return;
}
logger.LogCritical("Running ManualMigrateInvalidBlacklistSeries migration - Please be patient, this may take some time. This is not an error");
// Get all series in the Blacklist table and set their IsBlacklist = true
var blacklistedSeries = await context.Series
.Include(s => s.ExternalSeriesMetadata)
.Where(s => s.IsBlacklisted && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue)
.ToListAsync();
foreach (var series in blacklistedSeries)
{
series.IsBlacklisted = false;
context.Series.Entry(series).State = EntityState.Modified;
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateInvalidBlacklistSeries",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateInvalidBlacklistSeries migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.KavitaPlus.Manage;
using API.Entities.History;
using API.Entities.Metadata;
using API.Extensions.QueryExtensions;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.5 - After user testing, the needs manual match has some edge cases from migrations and for best user experience,
/// should be reset to allow the upgraded system to process better.
/// </summary>
public static class ManualMigrateNeedsManualMatch
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateNeedsManualMatch"))
{
return;
}
logger.LogCritical("Running ManualMigrateNeedsManualMatch migration - Please be patient, this may take some time. This is not an error");
// Get all series in the Blacklist table and set their IsBlacklist = true
var series = await context.Series
.FilterMatchState(MatchStateOption.Error)
.ToListAsync();
foreach (var seriesEntry in series)
{
seriesEntry.IsBlacklisted = false;
context.Series.Update(seriesEntry);
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateNeedsManualMatch",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateNeedsManualMatch migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.5 - There seems to be some scrobble events that are pre-scrobble error table that can be processed over and over.
/// This will take the given year and minus 1 from it and clear everything from that and anything that is errored.
/// </summary>
public static class ManualMigrateScrobbleErrors
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleErrors"))
{
return;
}
logger.LogCritical("Running ManualMigrateScrobbleErrors migration - Please be patient, this may take some time. This is not an error");
// Get all series in the Blacklist table and set their IsBlacklist = true
var events = await context.ScrobbleEvent
.Where(se => se.LastModifiedUtc <= DateTime.UtcNow.AddYears(-1) || se.IsErrored)
.ToListAsync();
context.ScrobbleEvent.RemoveRange(events);
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
logger.LogInformation("Removed {Count} old scrobble events", events.Count);
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateScrobbleErrors",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateScrobbleErrors migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.5 - Progress is extracted and saved in a csv since PDF parser has massive changes
/// </summary>
public static class MigrateProgressExportForV085
{
public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
{
try
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateProgressExportForV085"))
{
return;
}
logger.LogCritical(
"Running MigrateProgressExportForV085 migration - Please be patient, this may take some time. This is not an error");
var data = await dataContext.AppUserProgresses
.Join(dataContext.Series, progress => progress.SeriesId, series => series.Id, (progress, series) => new { progress, series })
.Join(dataContext.Volume, ps => ps.progress.VolumeId, volume => volume.Id, (ps, volume) => new { ps.progress, ps.series, volume })
.Join(dataContext.Chapter, psv => psv.progress.ChapterId, chapter => chapter.Id, (psv, chapter) => new { psv.progress, psv.series, psv.volume, chapter })
.Join(dataContext.MangaFile, psvc => psvc.chapter.Id, mangaFile => mangaFile.ChapterId, (psvc, mangaFile) => new { psvc.progress, psvc.series, psvc.volume, psvc.chapter, mangaFile })
.Join(dataContext.AppUser, psvcm => psvcm.progress.AppUserId, appUser => appUser.Id, (psvcm, appUser) => new
{
LibraryId = psvcm.series.LibraryId,
LibraryName = psvcm.series.Library.Name,
SeriesName = psvcm.series.Name,
VolumeRange = psvcm.volume.MinNumber + "-" + psvcm.volume.MaxNumber,
VolumeLookupName = psvcm.volume.Name,
ChapterRange = psvcm.chapter.Range,
MangaFileName = psvcm.mangaFile.FileName,
MangaFilePath = psvcm.mangaFile.FilePath,
AppUserName = appUser.UserName,
AppUserId = appUser.Id,
PagesRead = psvcm.progress.PagesRead,
BookScrollId = psvcm.progress.BookScrollId,
ProgressCreated = psvcm.progress.Created,
ProgressLastModified = psvcm.progress.LastModified
}).ToListAsync();
// Write the mapped data to a CSV file
await using var writer = new StreamWriter(Path.Join(directoryService.ConfigDirectory, "progress_export-v0.8.5.csv"));
await using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
await csv.WriteRecordsAsync(data);
logger.LogCritical(
"Running MigrateProgressExportForV085 migration - Completed. This is not an error");
}
catch (Exception ex)
{
// On new installs, the db isn't setup yet, so this has nothing to do
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateProgressExportForV085",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.6 - Manually check when a user triggers scrobble event generation
/// </summary>
public static class ManualMigrateScrobbleEventGen
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleEventGen"))
{
return;
}
logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Please be patient, this may take some time. This is not an error");
var users = await context.Users
.Where(u => u.AniListAccessToken != null)
.ToListAsync();
foreach (var user in users)
{
if (await context.ScrobbleEvent.AnyAsync(se => se.AppUserId == user.Id))
{
user.HasRunScrobbleEventGeneration = true;
user.ScrobbleEventGenerationRan = DateTime.UtcNow;
context.AppUser.Update(user);
}
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateScrobbleEventGen",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.6 - Change to not scrobble specials as they will never process, this migration removes all existing scrobble events
/// </summary>
public static class ManualMigrateScrobbleSpecials
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleSpecials"))
{
return;
}
logger.LogCritical("Running ManualMigrateScrobbleSpecials migration - Please be patient, this may take some time. This is not an error");
// Get all series in the Blacklist table and set their IsBlacklist = true
var events = await context.ScrobbleEvent
.Where(se => se.VolumeNumber == Parser.SpecialVolumeNumber)
.ToListAsync();
context.ScrobbleEvent.RemoveRange(events);
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
logger.LogInformation("Removed {Count} scrobble events that were specials", events.Count);
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateScrobbleSpecials",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateScrobbleSpecials migration - Completed. This is not an error");
}
}

View file

@ -127,7 +127,11 @@ public class ComicInfo
public string CoverArtist { get; set; } = string.Empty;
public string Editor { get; set; } = string.Empty;
public string Publisher { get; set; } = string.Empty;
public string Imprint { get; set; } = string.Empty;
public string Characters { get; set; } = string.Empty;
public string Teams { get; set; } = string.Empty;
public string Locations { get; set; } = string.Empty;
public static AgeRating ConvertAgeRatingToEnum(string value)
{
@ -151,9 +155,12 @@ public class ComicInfo
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
info.Imprint = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Imprint);
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
info.Teams = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Teams);
info.Locations = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Locations);
// We need to convert GTIN to ISBN
if (!string.IsNullOrEmpty(info.GTIN))
@ -174,7 +181,12 @@ public class ComicInfo
if (!string.IsNullOrEmpty(info.Number))
{
info.Number = info.Number.Replace(",", "."); // Corrective measure for non English OSes
info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes
}
if (!string.IsNullOrEmpty(info.Volume))
{
info.Volume = info.Volume.Trim();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterNumber : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "MaxNumber",
table: "Chapter",
type: "REAL",
nullable: false,
defaultValue: 0f);
migrationBuilder.AddColumn<float>(
name: "MinNumber",
table: "Chapter",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MaxNumber",
table: "Chapter");
migrationBuilder.DropColumn(
name: "MinNumber",
table: "Chapter");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class MangaFileNameTemp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FileName",
table: "MangaFile",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FileName",
table: "MangaFile");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterIssueSort : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "SortOrder",
table: "Chapter",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SortOrder",
table: "Chapter");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class VolumeLookupName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "LookupName",
table: "Volume",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LookupName",
table: "Volume");
}
}
}

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SeriesLowestFolderPath : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "LowestFolderPath",
table: "Series",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LowestFolderPath",
table: "Series");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class TeamsAndLocations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "LocationLocked",
table: "SeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "TeamLocked",
table: "SeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LocationLocked",
table: "SeriesMetadata");
migrationBuilder.DropColumn(
name: "TeamLocked",
table: "SeriesMetadata");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class UserMalToken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "MalAccessToken",
table: "AspNetUsers",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "MalUserName",
table: "AspNetUsers",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MalAccessToken",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "MalUserName",
table: "AspNetUsers");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PdfSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PdfLayoutMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfScrollMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfSpreadMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "PdfTheme",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PdfLayoutMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfScrollMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfSpreadMode",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "PdfTheme",
table: "AppUserPreferences");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class UserBasedCollections : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppUserCollection",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: true),
NormalizedTitle = table.Column<string>(type: "TEXT", nullable: true),
Summary = table.Column<string>(type: "TEXT", nullable: true),
Promoted = table.Column<bool>(type: "INTEGER", nullable: false),
CoverImage = table.Column<string>(type: "TEXT", nullable: true),
CoverImageLocked = table.Column<bool>(type: "INTEGER", nullable: false),
AgeRating = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastSyncUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
Source = table.Column<int>(type: "INTEGER", nullable: false),
SourceUrl = table.Column<string>(type: "TEXT", nullable: true),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppUserCollection", x => x.Id);
table.ForeignKey(
name: "FK_AppUserCollection_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AppUserCollectionSeries",
columns: table => new
{
CollectionsId = table.Column<int>(type: "INTEGER", nullable: false),
ItemsId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppUserCollectionSeries", x => new { x.CollectionsId, x.ItemsId });
table.ForeignKey(
name: "FK_AppUserCollectionSeries_AppUserCollection_CollectionsId",
column: x => x.CollectionsId,
principalTable: "AppUserCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AppUserCollectionSeries_Series_ItemsId",
column: x => x.ItemsId,
principalTable: "Series",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AppUserCollection_AppUserId",
table: "AppUserCollection",
column: "AppUserId");
migrationBuilder.CreateIndex(
name: "IX_AppUserCollectionSeries_ItemsId",
table: "AppUserCollectionSeries",
column: "ItemsId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppUserCollectionSeries");
migrationBuilder.DropTable(
name: "AppUserCollection");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterSortOrderLock : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PdfLayoutMode",
table: "AppUserPreferences");
migrationBuilder.AddColumn<bool>(
name: "SortOrderLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SortOrderLocked",
table: "Chapter");
migrationBuilder.AddColumn<int>(
name: "PdfLayoutMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SmartCollectionFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "MissingSeriesFromSource",
table: "AppUserCollection",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "TotalSourceCount",
table: "AppUserCollection",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MissingSeriesFromSource",
table: "AppUserCollection");
migrationBuilder.DropColumn(
name: "TotalSourceCount",
table: "AppUserCollection");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,78 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SiteThemeFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Author",
table: "SiteTheme",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CompatibleVersion",
table: "SiteTheme",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "SiteTheme",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "GitHubPath",
table: "SiteTheme",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PreviewUrls",
table: "SiteTheme",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ShaHash",
table: "SiteTheme",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Author",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "CompatibleVersion",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "Description",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "GitHubPath",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "PreviewUrls",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "ShaHash",
table: "SiteTheme");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,91 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PersonFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AniListId",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "Asin",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CoverImage",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "CoverImageLocked",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "HardcoverId",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "MalId",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AniListId",
table: "Person");
migrationBuilder.DropColumn(
name: "Asin",
table: "Person");
migrationBuilder.DropColumn(
name: "CoverImage",
table: "Person");
migrationBuilder.DropColumn(
name: "CoverImageLocked",
table: "Person");
migrationBuilder.DropColumn(
name: "Description",
table: "Person");
migrationBuilder.DropColumn(
name: "HardcoverId",
table: "Person");
migrationBuilder.DropColumn(
name: "MalId",
table: "Person");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,138 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class CoverPrimaryColors : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "Volume",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "Volume",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "Series",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "Series",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "ReadingList",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "ReadingList",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "Library",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "Library",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "Chapter",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "Chapter",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "AppUserCollection",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "AppUserCollection",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "Volume");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "Volume");
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "Series");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "Series");
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "ReadingList");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "ReadingList");
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "Library");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "Library");
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "Chapter");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "Chapter");
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "AppUserCollection");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "AppUserCollection");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,249 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterMetadataLocks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AgeRatingLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CharacterLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "ColoristLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CoverArtistLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EditorLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "GenresLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "ISBNLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "ImprintLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "InkerLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "LanguageLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "LettererLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "LocationLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "PencillerLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "PublisherLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "ReleaseDateLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "SummaryLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "TagsLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "TeamLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "TitleNameLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "TranslatorLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "WriterLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AgeRatingLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "CharacterLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "ColoristLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "CoverArtistLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "EditorLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "GenresLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "ISBNLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "ImprintLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "InkerLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "LanguageLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "LettererLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "LocationLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "PencillerLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "PublisherLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "ReleaseDateLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "SummaryLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "TagsLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "TeamLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "TitleNameLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "TranslatorLocked",
table: "Chapter");
migrationBuilder.DropColumn(
name: "WriterLocked",
table: "Chapter");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class VolumeCoverLocked : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "CoverImageLocked",
table: "Volume",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CoverImageLocked",
table: "Volume");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class AvgReadingTimeFloat : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<float>(
name: "AvgHoursToRead",
table: "Volume",
type: "REAL",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<float>(
name: "AvgHoursToRead",
table: "Series",
type: "REAL",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<float>(
name: "AvgHoursToRead",
table: "Chapter",
type: "REAL",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "AvgHoursToRead",
table: "Volume",
type: "INTEGER",
nullable: false,
oldClrType: typeof(float),
oldType: "REAL");
migrationBuilder.AlterColumn<int>(
name: "AvgHoursToRead",
table: "Series",
type: "INTEGER",
nullable: false,
oldClrType: typeof(float),
oldType: "REAL");
migrationBuilder.AlterColumn<int>(
name: "AvgHoursToRead",
table: "Chapter",
type: "INTEGER",
nullable: false,
oldClrType: typeof(float),
oldType: "REAL");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,159 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PeopleOverhaulPart1 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ChapterPerson");
migrationBuilder.DropTable(
name: "PersonSeriesMetadata");
migrationBuilder.DropColumn(
name: "Role",
table: "Person");
migrationBuilder.CreateTable(
name: "ChapterPeople",
columns: table => new
{
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
PersonId = table.Column<int>(type: "INTEGER", nullable: false),
Role = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChapterPeople", x => new { x.ChapterId, x.PersonId, x.Role });
table.ForeignKey(
name: "FK_ChapterPeople_Chapter_ChapterId",
column: x => x.ChapterId,
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChapterPeople_Person_PersonId",
column: x => x.PersonId,
principalTable: "Person",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SeriesMetadataPeople",
columns: table => new
{
SeriesMetadataId = table.Column<int>(type: "INTEGER", nullable: false),
PersonId = table.Column<int>(type: "INTEGER", nullable: false),
Role = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SeriesMetadataPeople", x => new { x.SeriesMetadataId, x.PersonId, x.Role });
table.ForeignKey(
name: "FK_SeriesMetadataPeople_Person_PersonId",
column: x => x.PersonId,
principalTable: "Person",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SeriesMetadataPeople_SeriesMetadata_SeriesMetadataId",
column: x => x.SeriesMetadataId,
principalTable: "SeriesMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ChapterPeople_PersonId",
table: "ChapterPeople",
column: "PersonId");
migrationBuilder.CreateIndex(
name: "IX_SeriesMetadataPeople_PersonId",
table: "SeriesMetadataPeople",
column: "PersonId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ChapterPeople");
migrationBuilder.DropTable(
name: "SeriesMetadataPeople");
migrationBuilder.AddColumn<int>(
name: "Role",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "ChapterPerson",
columns: table => new
{
ChapterMetadatasId = table.Column<int>(type: "INTEGER", nullable: false),
PeopleId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChapterPerson", x => new { x.ChapterMetadatasId, x.PeopleId });
table.ForeignKey(
name: "FK_ChapterPerson_Chapter_ChapterMetadatasId",
column: x => x.ChapterMetadatasId,
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChapterPerson_Person_PeopleId",
column: x => x.PeopleId,
principalTable: "Person",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PersonSeriesMetadata",
columns: table => new
{
PeopleId = table.Column<int>(type: "INTEGER", nullable: false),
SeriesMetadatasId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PersonSeriesMetadata", x => new { x.PeopleId, x.SeriesMetadatasId });
table.ForeignKey(
name: "FK_PersonSeriesMetadata_Person_PeopleId",
column: x => x.PeopleId,
principalTable: "Person",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PersonSeriesMetadata_SeriesMetadata_SeriesMetadatasId",
column: x => x.SeriesMetadatasId,
principalTable: "SeriesMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ChapterPerson_PeopleId",
table: "ChapterPerson",
column: "PeopleId");
migrationBuilder.CreateIndex(
name: "IX_PersonSeriesMetadata_SeriesMetadatasId",
table: "PersonSeriesMetadata",
column: "SeriesMetadatasId");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PeopleOverhaulPart2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CoverImage",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "CoverImageLocked",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "PrimaryColor",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryColor",
table: "Person",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CoverImage",
table: "Person");
migrationBuilder.DropColumn(
name: "CoverImageLocked",
table: "Person");
migrationBuilder.DropColumn(
name: "PrimaryColor",
table: "Person");
migrationBuilder.DropColumn(
name: "SecondaryColor",
table: "Person");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class PeopleOverhaulPart3 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AniListId",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "Asin",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "HardcoverId",
table: "Person",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "MalId",
table: "Person",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AniListId",
table: "Person");
migrationBuilder.DropColumn(
name: "Asin",
table: "Person");
migrationBuilder.DropColumn(
name: "Description",
table: "Person");
migrationBuilder.DropColumn(
name: "HardcoverId",
table: "Person");
migrationBuilder.DropColumn(
name: "MalId",
table: "Person");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SeriesDontMatchAndBlacklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DontMatch",
table: "Series",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsBlacklisted",
table: "Series",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DontMatch",
table: "Series");
migrationBuilder.DropColumn(
name: "IsBlacklisted",
table: "Series");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class EmailHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EmailHistory",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Sent = table.Column<bool>(type: "INTEGER", nullable: false),
SendDate = table.Column<DateTime>(type: "TEXT", nullable: false),
EmailTemplate = table.Column<string>(type: "TEXT", nullable: true),
Subject = table.Column<string>(type: "TEXT", nullable: true),
Body = table.Column<string>(type: "TEXT", nullable: true),
DeliveryStatus = table.Column<string>(type: "TEXT", nullable: true),
ErrorMessage = table.Column<string>(type: "TEXT", nullable: true),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EmailHistory", x => x.Id);
table.ForeignKey(
name: "FK_EmailHistory_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EmailHistory_AppUserId",
table: "EmailHistory",
column: "AppUserId");
migrationBuilder.CreateIndex(
name: "IX_EmailHistory_Sent_AppUserId_EmailTemplate_SendDate",
table: "EmailHistory",
columns: new[] { "Sent", "AppUserId", "EmailTemplate", "SendDate" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EmailHistory");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,112 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class KavitaPlusUserAndMetadataSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AllowMetadataMatching",
table: "Library",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "AniListScrobblingEnabled",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "WantToReadSync",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.CreateTable(
name: "MetadataSettings",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
EnableSummary = table.Column<bool>(type: "INTEGER", nullable: false),
EnablePublicationStatus = table.Column<bool>(type: "INTEGER", nullable: false),
EnableRelationships = table.Column<bool>(type: "INTEGER", nullable: false),
EnablePeople = table.Column<bool>(type: "INTEGER", nullable: false),
EnableStartDate = table.Column<bool>(type: "INTEGER", nullable: false),
EnableLocalizedName = table.Column<bool>(type: "INTEGER", nullable: false),
EnableGenres = table.Column<bool>(type: "INTEGER", nullable: false),
EnableTags = table.Column<bool>(type: "INTEGER", nullable: false),
FirstLastPeopleNaming = table.Column<bool>(type: "INTEGER", nullable: false),
AgeRatingMappings = table.Column<string>(type: "TEXT", nullable: true),
Blacklist = table.Column<string>(type: "TEXT", nullable: true),
Whitelist = table.Column<string>(type: "TEXT", nullable: true),
PersonRoles = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MetadataFieldMapping",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SourceType = table.Column<int>(type: "INTEGER", nullable: false),
DestinationType = table.Column<int>(type: "INTEGER", nullable: false),
SourceValue = table.Column<string>(type: "TEXT", nullable: true),
DestinationValue = table.Column<string>(type: "TEXT", nullable: true),
ExcludeFromSource = table.Column<bool>(type: "INTEGER", nullable: false),
MetadataSettingsId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataFieldMapping", x => x.Id);
table.ForeignKey(
name: "FK_MetadataFieldMapping_MetadataSettings_MetadataSettingsId",
column: x => x.MetadataSettingsId,
principalTable: "MetadataSettings",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MetadataFieldMapping_MetadataSettingsId",
table: "MetadataFieldMapping",
column: "MetadataSettingsId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MetadataFieldMapping");
migrationBuilder.DropTable(
name: "MetadataSettings");
migrationBuilder.DropColumn(
name: "AllowMetadataMatching",
table: "Library");
migrationBuilder.DropColumn(
name: "AniListScrobblingEnabled",
table: "AppUserPreferences");
migrationBuilder.DropColumn(
name: "WantToReadSync",
table: "AppUserPreferences");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class MoreMetadtaSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "KavitaPlusConnection",
table: "SeriesMetadataPeople",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "OrderWeight",
table: "SeriesMetadataPeople",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "EnableCoverImage",
table: "MetadataSettings",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<string>(
name: "Overrides",
table: "MetadataSettings",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "KavitaPlusConnection",
table: "SeriesMetadataPeople");
migrationBuilder.DropColumn(
name: "OrderWeight",
table: "SeriesMetadataPeople");
migrationBuilder.DropColumn(
name: "EnableCoverImage",
table: "MetadataSettings");
migrationBuilder.DropColumn(
name: "Overrides",
table: "MetadataSettings");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class AutomaticWebtoonReaderMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AllowAutomaticWebtoonReaderDetection",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AllowAutomaticWebtoonReaderDetection",
table: "AppUserPreferences");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ScrobbleGenerationDbCapture : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "HasRunScrobbleEventGeneration",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "ScrobbleEventGenerationRan",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HasRunScrobbleEventGeneration",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "ScrobbleEventGenerationRan",
table: "AspNetUsers");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class KavitaPlusCBR : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "EnableChapterCoverImage",
table: "MetadataSettings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableChapterPublisher",
table: "MetadataSettings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableChapterReleaseDate",
table: "MetadataSettings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableChapterSummary",
table: "MetadataSettings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableChapterTitle",
table: "MetadataSettings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "CbrId",
table: "ExternalSeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "KavitaPlusConnection",
table: "ChapterPeople",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "OrderWeight",
table: "ChapterPeople",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EnableChapterCoverImage",
table: "MetadataSettings");
migrationBuilder.DropColumn(
name: "EnableChapterPublisher",
table: "MetadataSettings");
migrationBuilder.DropColumn(
name: "EnableChapterReleaseDate",
table: "MetadataSettings");
migrationBuilder.DropColumn(
name: "EnableChapterSummary",
table: "MetadataSettings");
migrationBuilder.DropColumn(
name: "EnableChapterTitle",
table: "MetadataSettings");
migrationBuilder.DropColumn(
name: "CbrId",
table: "ExternalSeriesMetadata");
migrationBuilder.DropColumn(
name: "KavitaPlusConnection",
table: "ChapterPeople");
migrationBuilder.DropColumn(
name: "OrderWeight",
table: "ChapterPeople");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,10 @@ using System.Text;
using System.Threading.Tasks;
using API.Data.ManualMigrations;
using API.DTOs;
using API.DTOs.Progress;
using API.Entities;
using API.Entities.Enums;
using API.Extensions.QueryExtensions;
using API.Services.Tasks.Scanner.Parser;
using AutoMapper;
using AutoMapper.QueryableExtensions;
@ -14,9 +16,11 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IAppUserProgressRepository
{
void Update(AppUserProgress userProgress);
void Remove(AppUserProgress userProgress);
Task<int> CleanupAbandonedChapters();
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId);
@ -36,8 +40,9 @@ public interface IAppUserProgressRepository
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
Task UpdateAllProgressThatAreMoreThanChapterPages();
Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0);
}
#nullable disable
public class AppUserProgressRepository : IAppUserProgressRepository
{
private readonly DataContext _context;
@ -54,6 +59,11 @@ public class AppUserProgressRepository : IAppUserProgressRepository
_context.Entry(userProgress).State = EntityState.Modified;
}
public void Remove(AppUserProgress userProgress)
{
_context.Remove(userProgress);
}
/// <summary>
/// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well.
/// </summary>
@ -167,9 +177,10 @@ public class AppUserProgressRepository : IAppUserProgressRepository
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
p.appUserProgresses.PagesRead >= p.chapter.Pages)
.Select(p => p.chapter.Range)
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
.Select(p => p.chapter.MaxNumber)
.ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d)));
return list.Count == 0 ? 0 : (int) list.DefaultIfEmpty().Max(d => d);
}
public async Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
@ -179,8 +190,10 @@ public class AppUserProgressRepository : IAppUserProgressRepository
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
p.appUserProgresses.PagesRead >= p.chapter.Pages)
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
.Select(p => p.chapter.Volume.MaxNumber)
.ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
}
@ -231,6 +244,33 @@ public class AppUserProgressRepository : IAppUserProgressRepository
await _context.Database.ExecuteSqlRawAsync(batchSql);
}
/// <summary>
///
/// </summary>
/// <param name="chapterId"></param>
/// <param name="userId">If 0, will pull all records</param>
/// <returns></returns>
public async Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0)
{
return await _context.AppUserProgresses
.WhereIf(userId > 0, p => p.AppUserId == userId)
.Where(p => p.ChapterId == chapterId)
.Include(p => p.AppUser)
.Select(p => new FullProgressDto()
{
AppUserId = p.AppUserId,
ChapterId = p.ChapterId,
PagesRead = p.PagesRead,
Id = p.Id,
Created = p.Created,
CreatedUtc = p.CreatedUtc,
LastModified = p.LastModified,
LastModifiedUtc = p.LastModifiedUtc,
UserName = p.AppUser.UserName
})
.ToListAsync();
}
#nullable enable
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
{

View file

@ -22,11 +22,16 @@ public enum ChapterIncludes
None = 1,
Volumes = 2,
Files = 4,
People = 8,
Genres = 16,
Tags = 32
}
public interface IChapterRepository
{
void Update(Chapter chapter);
void Remove(Chapter chapter);
void Remove(IList<Chapter> chapters);
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None);
Task<IChapterInfoDto?> GetChapterInfoDtoAsync(int chapterId);
Task<int> GetChapterTotalPagesAsync(int chapterId);
@ -34,7 +39,7 @@ public interface IChapterRepository
Task<ChapterDto?> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterMetadataDto?> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
Task<IList<Chapter>> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None);
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
Task<string?> GetChapterCoverImageAsync(int chapterId);
Task<IList<string>> GetAllCoverImagesAsync();
@ -42,6 +47,7 @@ public interface IChapterRepository
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
}
public class ChapterRepository : IChapterRepository
{
@ -59,6 +65,16 @@ public class ChapterRepository : IChapterRepository
_context.Entry(chapter).State = EntityState.Modified;
}
public void Remove(Chapter chapter)
{
_context.Chapter.Remove(chapter);
}
public void Remove(IList<Chapter> chapters)
{
_context.Chapter.RemoveRange(chapters);
}
public async Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None)
{
return await _context.Chapter
@ -78,7 +94,7 @@ public class ChapterRepository : IChapterRepository
.Where(c => c.Id == chapterId)
.Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new
{
ChapterNumber = chapter.Range,
ChapterNumber = chapter.MinNumber,
VolumeNumber = volume.Name,
VolumeId = volume.Id,
chapter.IsSpecial,
@ -102,8 +118,8 @@ public class ChapterRepository : IChapterRepository
})
.Select(data => new ChapterInfoDto()
{
ChapterNumber = data.ChapterNumber,
VolumeNumber = data.VolumeNumber + string.Empty,
ChapterNumber = data.ChapterNumber + string.Empty, // TODO: Fix this
VolumeNumber = data.VolumeNumber + string.Empty, // TODO: Fix this
VolumeId = data.VolumeId,
IsSpecial = data.IsSpecial,
SeriesId = data.SeriesId,
@ -175,6 +191,7 @@ public class ChapterRepository : IChapterRepository
{
return await _context.Chapter
.Includes(includes)
.OrderBy(c => c.SortOrder)
.FirstOrDefaultAsync(c => c.Id == chapterId);
}
@ -183,10 +200,12 @@ public class ChapterRepository : IChapterRepository
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId)
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None)
{
return await _context.Chapter
.Where(c => c.VolumeId == volumeId)
.Includes(includes)
.OrderBy(c => c.SortOrder)
.ToListAsync();
}
@ -267,11 +286,28 @@ public class ChapterRepository : IChapterRepository
return chapter;
}
/// <summary>
/// Includes Volumes
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public IEnumerable<Chapter> GetChaptersForSeries(int seriesId)
{
return _context.Chapter
.Where(c => c.Volume.SeriesId == seriesId)
.OrderBy(c => c.SortOrder)
.Include(c => c.Volume)
.AsEnumerable();
}
public async Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId)
{
return await _context.Chapter
.Where(c => c.Volume.SeriesId == seriesId)
.OrderBy(c => c.SortOrder)
.Include(c => c.Volume)
.Include(c => c.People)
.ThenInclude(cp => cp.Person)
.ToListAsync();
}
}

View file

@ -3,44 +3,64 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data.Misc;
using API.DTOs.CollectionTags;
using API.DTOs.Collection;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Extensions.QueryExtensions.Filtering;
using API.Services.Plus;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum CollectionTagIncludes
{
None = 1,
SeriesMetadata = 2,
SeriesMetadataWithSeries = 4
}
[Flags]
public enum CollectionIncludes
{
None = 1,
Series = 2,
}
public interface ICollectionTagRepository
{
void Add(CollectionTag tag);
void Remove(CollectionTag tag);
Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync();
Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId);
void Remove(AppUserCollection tag);
Task<string?> GetCoverImageAsync(int collectionTagId);
Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId);
Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None);
void Update(CollectionTag tag);
Task<int> RemoveTagsWithoutSeries();
Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None);
Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None);
void Update(AppUserCollection tag);
Task<int> RemoveCollectionsWithoutSeries();
Task<IEnumerable<AppUserCollection>> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None);
/// <summary>
/// Returns all of the user's collections with the option of other user's promoted
/// </summary>
/// <param name="userId"></param>
/// <param name="includePromoted"></param>
/// <returns></returns>
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false);
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false);
Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles,
CollectionTagIncludes includes = CollectionTagIncludes.None);
Task<IList<string>> GetAllCoverImagesAsync();
Task<bool> TagExists(string title);
Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
Task<bool> CollectionExists(string title, int userId);
Task<IList<AppUserCollection>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
Task<IList<string>> GetRandomCoverImagesAsync(int collectionId);
Task<IList<AppUserCollection>> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None);
Task UpdateCollectionAgeRating(AppUserCollection tag);
Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None);
Task<IList<AppUserCollection>> GetAllCollectionsForSyncing(DateTime expirationTime);
}
public class CollectionTagRepository : ICollectionTagRepository
{
private readonly DataContext _context;
@ -52,17 +72,12 @@ public class CollectionTagRepository : ICollectionTagRepository
_mapper = mapper;
}
public void Add(CollectionTag tag)
public void Remove(AppUserCollection tag)
{
_context.CollectionTag.Add(tag);
_context.AppUserCollection.Remove(tag);
}
public void Remove(CollectionTag tag)
{
_context.CollectionTag.Remove(tag);
}
public void Update(CollectionTag tag)
public void Update(AppUserCollection tag)
{
_context.Entry(tag).State = EntityState.Modified;
}
@ -70,38 +85,53 @@ public class CollectionTagRepository : ICollectionTagRepository
/// <summary>
/// Removes any collection tags without any series
/// </summary>
public async Task<int> RemoveTagsWithoutSeries()
public async Task<int> RemoveCollectionsWithoutSeries()
{
var tagsToDelete = await _context.CollectionTag
.Include(c => c.SeriesMetadatas)
.Where(c => c.SeriesMetadatas.Count == 0)
var tagsToDelete = await _context.AppUserCollection
.Include(c => c.Items)
.Where(c => c.Items.Count == 0)
.AsSplitQuery()
.ToListAsync();
_context.RemoveRange(tagsToDelete);
return await _context.SaveChangesAsync();
}
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None)
public async Task<IEnumerable<AppUserCollection>> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None)
{
return await _context.CollectionTag
return await _context.AppUserCollection
.OrderBy(c => c.NormalizedTitle)
.Includes(includes)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles, CollectionTagIncludes includes = CollectionTagIncludes.None)
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false)
{
return await _context.CollectionTag
.Where(c => normalizedTitles.Contains(c.NormalizedTitle))
.OrderBy(c => c.NormalizedTitle)
.Includes(includes)
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.AppUserCollection
.Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted))
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
.OrderBy(uc => uc.Title)
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.AppUserCollection
.Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted))
.Where(uc => uc.Items.Any(s => s.Id == seriesId))
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
.OrderBy(uc => uc.Title)
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<string?> GetCoverImageAsync(int collectionTagId)
{
return await _context.CollectionTag
return await _context.AppUserCollection
.Where(c => c.Id == collectionTagId)
.Select(c => c.CoverImage)
.SingleOrDefaultAsync();
@ -109,23 +139,30 @@ public class CollectionTagRepository : ICollectionTagRepository
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return (await _context.CollectionTag
return await _context.AppUserCollection
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.ToListAsync())!;
.ToListAsync();
}
public async Task<bool> TagExists(string title)
/// <summary>
/// If any tag exists for that given user's collections
/// </summary>
/// <param name="title"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> CollectionExists(string title, int userId)
{
var normalized = title.ToNormalized();
return await _context.CollectionTag
return await _context.AppUserCollection
.Where(uc => uc.AppUserId == userId)
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
}
public async Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
public async Task<IList<AppUserCollection>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
{
var extension = encodeFormat.GetExtension();
return await _context.CollectionTag
return await _context.AppUserCollection
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
.ToListAsync();
}
@ -133,44 +170,61 @@ public class CollectionTagRepository : ICollectionTagRepository
public async Task<IList<string>> GetRandomCoverImagesAsync(int collectionId)
{
var random = new Random();
var data = await _context.CollectionTag
var data = await _context.AppUserCollection
.Where(t => t.Id == collectionId)
.SelectMany(t => t.SeriesMetadatas)
.Select(sm => sm.Series.CoverImage)
.SelectMany(uc => uc.Items.Select(series => series.CoverImage))
.Where(t => !string.IsNullOrEmpty(t))
.ToListAsync();
return data
.OrderBy(_ => random.Next())
.Take(4)
.ToList();
}
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
public async Task<IList<AppUserCollection>> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None)
{
return await _context.CollectionTag
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
return await _context.AppUserCollection
.Where(c => c.AppUserId == userId)
.Includes(includes)
.ToListAsync();
}
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId)
public async Task UpdateCollectionAgeRating(AppUserCollection tag)
{
var userRating = await GetUserAgeRestriction(userId);
return await _context.CollectionTag
.Where(c => c.Promoted)
.RestrictAgainstAgeRestriction(userRating)
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
var maxAgeRating = await _context.AppUserCollection
.Where(t => t.Id == tag.Id)
.SelectMany(uc => uc.Items.Select(s => s.Metadata))
.Select(sm => sm.AgeRating)
.ToListAsync();
tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown;
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None)
{
return await _context.AppUserCollection
.Where(c => tags.Contains(c.Id))
.Includes(includes)
.AsSplitQuery()
.ToListAsync();
}
public async Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None)
public async Task<IList<AppUserCollection>> GetAllCollectionsForSyncing(DateTime expirationTime)
{
return await _context.CollectionTag
return await _context.AppUserCollection
.Where(c => c.Source == ScrobbleProvider.Mal)
.Where(c => c.LastSyncUtc <= expirationTime)
.Include(c => c.Items)
.AsSplitQuery()
.ToListAsync();
}
public async Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None)
{
return await _context.AppUserCollection
.Where(c => c.Id == tagId)
.Includes(includes)
.AsSplitQuery()
@ -190,16 +244,12 @@ public class CollectionTagRepository : ICollectionTagRepository
.SingleAsync();
}
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId)
public async Task<IEnumerable<AppUserCollectionDto>> SearchTagDtosAsync(string searchQuery, int userId)
{
var userRating = await GetUserAgeRestriction(userId);
return await _context.CollectionTag
.Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%"))
.RestrictAgainstAgeRestriction(userRating)
.OrderBy(s => s.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
return await _context.AppUserCollection
.Search(searchQuery, userId, userRating)
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using API.DTOs.CoverDb;
using API.Entities;
using API.Entities.Person;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace API.Data.Repositories;
#nullable enable
/// <summary>
/// This is a manual repository, not a DB repo
/// </summary>
public class CoverDbRepository
{
private readonly List<CoverDbAuthor> _authors;
public CoverDbRepository(string filePath)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
// Read and deserialize YAML file
var yamlContent = File.ReadAllText(filePath);
var peopleData = deserializer.Deserialize<CoverDbPeople>(yamlContent);
_authors = peopleData.People;
}
public CoverDbAuthor? FindAuthorByNameOrAlias(string name)
{
return _authors.Find(author =>
author.Name.Equals(name, StringComparison.OrdinalIgnoreCase) ||
author.Aliases.Contains(name, StringComparer.OrdinalIgnoreCase));
}
public CoverDbAuthor? FindBestAuthorMatch(Person person)
{
var aniListId = person.AniListId > 0 ? $"{person.AniListId}" : string.Empty;
var highestScore = 0;
CoverDbAuthor? bestMatch = null;
foreach (var author in _authors)
{
var score = 0;
// Check metadata IDs and add points if they match
if (!string.IsNullOrEmpty(author.Ids.AmazonId) && author.Ids.AmazonId == person.Asin)
{
score += 10;
}
if (!string.IsNullOrEmpty(author.Ids.AnilistId) && author.Ids.AnilistId == aniListId)
{
score += 10;
}
if (!string.IsNullOrEmpty(author.Ids.HardcoverId) && author.Ids.HardcoverId == person.HardcoverId)
{
score += 10;
}
// Check for exact name match
if (author.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase))
{
score += 7;
}
// Check for alias match
if (author.Aliases.Contains(person.Name, StringComparer.OrdinalIgnoreCase))
{
score += 5;
}
// Update the best match if current score is higher
if (score <= highestScore) continue;
highestScore = score;
bestMatch = author;
}
return bestMatch;
}
}

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Email;
using API.Entities;
using API.Helpers;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface IEmailHistoryRepository
{
Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams);
}
public class EmailHistoryRepository : IEmailHistoryRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public EmailHistoryRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams)
{
return await _context.EmailHistory
.OrderByDescending(h => h.SendDate)
.ProjectTo<EmailHistoryDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.DTOs;
using API.DTOs.KavitaPlus.Manage;
using API.DTOs.Recommendation;
using API.DTOs.Scrobbling;
using API.DTOs.SeriesDetail;
@ -31,13 +32,12 @@ public interface IExternalSeriesMetadataRepository
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
void Remove(ExternalSeriesMetadata metadata);
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
Task<bool> NeedsDataRefresh(int seriesId);
Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId);
Task LinkRecommendationsToSeries(Series series);
Task<bool> IsBlacklistedSeries(int seriesId);
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
Task RemoveFromBlacklist(int seriesId);
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
Task<IList<int>> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false);
Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
}
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
@ -106,7 +106,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.FirstOrDefaultAsync();
}
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
public async Task<bool> NeedsDataRefresh(int seriesId)
{
var row = await _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
@ -114,7 +114,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
}
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId)
{
var seriesDetailDto = await _context.ExternalSeriesMetadata
.Where(m => m.SeriesId == seriesId)
@ -157,8 +157,8 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.OrderByDescending(r => r.Score);
}
IEnumerable<RatingDto> ratings = new List<RatingDto>();
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any())
IEnumerable<RatingDto> ratings = [];
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0)
{
ratings = seriesDetailDto.ExternalRatings
.Select(r => _mapper.Map<RatingDto>(r));
@ -191,6 +191,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.Where(r => EF.Functions.Like(r.Name, series.Name) ||
EF.Functions.Like(r.Name, series.LocalizedName))
.ToListAsync();
foreach (var rec in recMatches)
{
rec.SeriesId = series.Id;
@ -201,55 +202,38 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
public Task<bool> IsBlacklistedSeries(int seriesId)
{
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
return _context.Series
.Where(s => s.Id == seriesId)
.Select(s => s.IsBlacklisted)
.FirstOrDefaultAsync();
}
/// <summary>
/// Creates a new instance against SeriesId and Saves to the DB
/// </summary>
/// <param name="seriesId"></param>
/// <param name="saveChanges"></param>
public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true)
{
if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return;
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
{
SeriesId = seriesId
});
if (saveChanges)
{
await _context.SaveChangesAsync();
}
}
/// <summary>
/// Removes the Series from Blacklist and Saves to the DB
/// </summary>
/// <param name="seriesId"></param>
public async Task RemoveFromBlacklist(int seriesId)
{
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
if (seriesBlacklist != null)
{
// Remove the SeriesBlacklist entity from the context
_context.SeriesBlacklist.Remove(seriesBlacklist);
// Save the changes to the database
await _context.SaveChangesAsync();
}
}
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
public async Task<IList<int>> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false)
{
return await _context.Series
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
.Where(s => s.Library.AllowMetadataMatching)
.WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0)
.Where(s => !s.IsBlacklisted && !s.DontMatch)
.OrderByDescending(s => s.Library.Type)
.ThenBy(s => s.NormalizedName)
.Select(s => s.Id)
.Take(limit)
.ToListAsync();
}
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
{
return await _context.Series
.Include(s => s.Library)
.Include(s => s.ExternalSeriesMetadata)
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.Where(s => s.Library.AllowMetadataMatching)
.FilterMatchState(filter.MatchStateOption)
.OrderBy(s => s.NormalizedName)
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

Some files were not shown because too many files have changed in this diff Show more