diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj
index 529f3cc93..d63d24ddc 100644
--- a/API.Benchmark/API.Benchmark.csproj
+++ b/API.Benchmark/API.Benchmark.csproj
@@ -15,4 +15,10 @@
+
+
+ Always
+
+
+
diff --git a/API.Benchmark/Data/SeriesNamesForNormalization.txt b/API.Benchmark/Data/SeriesNamesForNormalization.txt
new file mode 100644
index 000000000..99ae529fd
--- /dev/null
+++ b/API.Benchmark/Data/SeriesNamesForNormalization.txt
@@ -0,0 +1,573 @@
+Liar-Game
+Your Lie in April
+Love Hina
+Love Hina
+A Chronicle of the Last Pagans
+Otherworldly Munchkin - Let's Speedrun the Dungeon with Only 1 HP!
+Love Hina
+Rokka - Braves of the Six Flowers
+Real Account
+Bakekano
+Yancha Gal no Anjou-san
+Moshi Fanren
+The Devil Is a Part-Timer!
+My Home Hero
+Itoshi no Karin
+Claymore
+Dolls Fall
+Dragons Rioting
+Tokyo Ghoul - re
+Hajime no Ippo
+Mahoromatic
+DEATHTOPIA
+Negima! Neo - Magister Negi Magi
+Ichinensei ni Nacchattara
+How NOT to Summon a Demon Lord
+U12
+"Don't Toy With Me, Miss Nagatoro"
+Karakai Jouzu no Takagi-san
+UQ Holder!
+"Ore no Nounai Sentakushi ga, Gakuen Rabukome o Zenryoku de Jama Shite Iru"
+Do Chokkyuu Kareshi x Kanojo
+Ana Satsujin
+Deus Ex Machina
+Hidan no Aria
+Bokura wa Minna Kawaisou
+Epigraph of the Closed Curve
+Ibitsu
+Rave Master
+Lunar Legend Tsukihime
+Starving Anonymous
+High-Rise Invasion
+Fuuka
+Dai Dark
+Zero no Tsukaima Chevalier
+Cells at Work! CODE BLACK
+004 Cut Hero
+Renjoh Desperado
+Himegoto - Juukyuusai No Seifuku
+Shark Skin Man and Peach Hip Girl
+Tokyo Revengers
+Fire Punch
+Boarding School Juliet
+Mushihime
+Sankarea - Undying Love
+Hanako and the Terror of Allegory
+Mad Chimera World
+Kono Subarashii Sekai ni Bakuen wo!
+21st Century Boys
+Kono Subarashii Sekai ni Shukufuku wo! Megumin Anthology
+Konosuba
+Iinari
+Shimoneta - Manmaru Hen
+Ichiban Ushiro No Daimaou
+Yamada-kun and the Seven Witches
+Busou Shoujo Machiavellism
+Negative Happy Chainsaw Edge
+Stravaganza - Isai No Hime
+Seraph of the End - Vampire Reign 095
+Seraph of the End - Vampire Reign 098
+Kokkoku - Moment by Moment
+Magico
+Samurai Harem - Asu no Yoichi
+Change123
+Shomin Sample
+Eureka SeveN
+Kekkaishi
+Goblin Slayer Side Story - Year One
+Yomeiro Choice
+Okusama wa Shougakusei
+Monster No Goshujin-Sama
+Ase To Sekken
+How Do We Relationship
+Hantsu x Torasshu
+Magical Girl Apocalypse
+I Am a Hero
+Air Gear
+Dolly Kill Kill
+Blue Exorcist
+Kingdom of Z
+The Fable
+Mairimashita! Iruma-kun
+Spy x Family
+Goblin Slayer - Brand New Day
+Yesterday wo Utatte
+Mujaki No Rakuen
+Summer Time Rendering
+Eureka Seven Gravity Boys and Lifting Girl
+06
+Domestic Girlfriend
+Imperfect Girl
+Chrno Crusade
+Higurashi no Naku Koro ni Kai - Tsumihoroboshihen
+Nande koko ni sensei ga!
+Fukukaichou Ganbaru.
+Fraction
+Kono Subarashii Sekai ni Shukufuku wo! Megumin Anthology Aka
+Mouryou no Yurikago
+Ral Ω Grad
+Shomin Sample I Was Abducted by an Elite All-Girls School as a Sample Commoner
+City of Love Prison
+Tsugumomo
+Highschool of the Dead - Edition
+Cynthia The Mission
+Amano Megumi wa Suki Darake!
+Aria The Scarlet Ammo
+Noblesse
+Outlanders
+Bleach
+Kimi ni Todoke
+Corpse Party - Another Child
+The Heroic Legend of Arslan
+Fujiyama-San Wa Shishunki
+Let's Go Play
+Astra Lost in Space
+Mirai Nikki
+Doubt
+Again!!
+Gesellschaft Blume
+Momo The Blood Taker
+World's End Harem - Fantasia
+Tengoku Daimakyou
+Amaenaideyo MS
+Cage of Eden
+Arifureta - From Commonplace to World's Strongest
+"The 100 Girlfriends Who Really, Really, Really, Really, Really Love You"
+Frogman
+Chaika - The Coffin Princess
+Pandora Hearts
+I'm Not a Lolicon!
+Criminale!
+Drifting Net Cafe
+Kono Subarashii Sekai ni Nichijou wo!
+Tomodachi Game
+Accel World
+Sun-Ken Rock
+Parallel Paradise
+Otherworldly Munchkin - Let's Speedrun the Dungeon with Only 1 HP!
+Hentai Ouji to Warawanai Neko. Nya!
+Gokukoku no Brynhildr
+Rosario+Vampire Season 2
+Higurashi no Naku Koro ni - Tatarigoroshihen
+BEASTARS
+Grenadier
+The Duke of Death and His Black Maid
+Helck
+Ijousha no Ai
+Beelzebub
+Infection
+"Ota Tomo ga Kareshi ni Nattara, Saikou, Kamo Shirenai"
+Battle Vixens
+Kimi ha midara na Boku no Joou
+Immortal Hounds
+Battle Angel Alita
+My Monster Secret
+Blood Rain
+Kakegurui - Compulsive Gambler
+Combatants Will Be Dispatched!
+Tenjo Tenge - Digital Colored Comics
+Dorohedoro
+Tower Of God
+Toradora!
+Spice and Wolf
+Loose Relation Between Wizard and Apprentice
+Kaguya-sama - Love Is War - Digital Colored Comics
+RaW Hero
+Aiki
+Jagaaaaaan
+Gleipnir
+Darwin's Game
+I'm Standing on a Million Lives
+Battle Club
+School Rumble Z
+Wotakoi - Love Is Hard for Otaku
+Majimoji Rurumo
+Suisei no Gargantia
+Madan No Ou To Vanadis
+Full Metal Panic - Sigma
+Konosuba - An Explosion on This Wonderful World!
+Seraph of the End - Vampire Reign 096
+Higurashi no Naku Koro ni - Onikakushihen
+Corpse Party Cemetery 0 - Kaibyaku No Ars Moriendi
+World's End Harem
+Jack Frost
+The Men Who Created The Prison School Anime
+My Hero Academia
+Elfen Lied
+Berserk
+Witchcraft Works
+Chobits 20th Anniversary Edition
+Mx0
+Youkai Kyoushitsu
+Horimiya
+Mieruko-chan
+Drifters
+Suzuka
+The Iceblade Magician Rules Over the World
+Kaiju No. 8
+Yu-Gi-Oh!
+"A Story About Treating a Female Knight, Who Has Never Been Treated as a Woman, as a Woman"
+Mahoutsukai to Deshi no Futekisetsu na Kankei
+Battle Royale
+Mato Seihei no Slave
+One-Punch Man
+Boku No Kokoro No Yabai Yatsu
+Doku Mushi
+Kuzu no Honkai
+Hoshihimemura No Naishobanashi
+Knights of Sidonia
+Amaenaideyo
+Kono Subarashii Sekai ni Shukufuku wo! Spin-off Kono Kamen no Akuma ni Soudan wo!
+Killing Bites
+Fly Me to the Moon
+Tenjo Tenge
+D-Princess
+7thGARDEN
+Sumomomo Momomo
+Accel World Dural - Magisa Garden
+History's Strongest Disciple Kenichi
+Future Diary - Mosaic
+DEAD Tube
+Kaworu Watashiya - Kodomo no Jikan
+Undead Unluck
+Black Bullet
+Fureru To Kikoeru
+Konchuki
+Akuma no Riddle - Riddle Story of Devil
+Great Teacher Onizuka
+Scumbag Loser
+Jisatsutou
+Boku wa Mari no Naka
+Cherry x Cherry
+Seraph of the End - Vampire Reign 093
+Yumekui Merry - 4-Koma Anthology
+Love and Lies
+Nisekoi - False Love
+Another
+My Balls
+Akame ga KILL!
+Corpse Princess
+Needless 0
+My Charms Are Wasted On Kuroiwa Medaka
+Made in Abyss
+Hanako to Guuwa no Tera
+Yumekui Merry
+Miman Renai
+Sundome
+Gantz
+Accomplishments of the Duke's Daughter
+Grimgar of Fantasy and Ash
+Dansei Kyoufushou Datta Watashi Ga Av Jouyu Ni Naru Made No Hanashi
+Hour of the Zombie
+NOiSE
+Onani Master Kurosawa
+Sekirei
+Full Metal Panic
+Zero no Tsukaima
+Solo Leveling
+B Gata H Kei
+Shurabara!
+DEATH NOTE
+Terra Formars
+Goblin Slayer
+March Story
+Nozoki Ana
+Youkai Shoujo - Monsuga
+Maji de Watashi ni Koi Shinasai!!
+"Ore no Nounai Sentakushi ga, Gakuen Rabukome o Zenryoku de Jama Shite Iru H"
+Destruction Princess
+Mob Psycho 100
+Negima!
+Zero - The Illust collection of The Familiar of Zero
+20th Century Boys
+Girls of the Wild's
+Bleach - Digital Colored Comics
+Taboo Tattoo
+Let's Buy The Land And Cultivate In Different World
+Oroka na Tenshi wa Akuma to Odoru
+Future Diary
+Negima! Party Book!
+Buso Renkin
+Offal Island
+Mysterious Girlfriend X
+Getsurin ni Kiri Saku
+Magi
+Uzaki-chan Wants to Hang Out!
+A Town Where You Live
+WITCH WATCH
+Lord Marksman and Vanadis
+Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo
+Tonari No Furi-San Ga Tonikaku Kowai
+Hinowa ga CRUSH!
+Tsuredure Children
+Dance in the Vampire Bund
+Sperman
+The Rising Of The Shield Hero
+Triage X
+Kiruru Kill Me
+Hidan no Aria AA
+Origin
+Senran Kagura - Skirting Shadows
+Higurashi no Naku Koro ni - Himatsubushihen
+APOSIMZ
+Franken Fran
+Is This a Zombie
+School Rumble
+Darker than Black - Shikkoku no Hana
+Sweet X Trouble
+Close As Neighbors
+7SEEDS
+Dungeon Seeker
+Necromance
+Code Breaker
+Rokka Braves of the Six Flowers
+Prison School
+COPPELION
+Grand Blue Dreaming
+Libidors
+Skill of Lure
+Pluto - Urasawa x Tezuka
+Chibi Vampire
+Omamori Himari
+"Zoku, Kono Subarashii Sekai ni Bakuen wo!"
+"Please Go Home, Akutsu-San!"
+Mahoutsukai to Teishi no Futekisetsu na Kankei
+Chobits
+The Seven Deadly Sins
+Black Clover
+We Never Learn
+Tomogui Kyoushitsu
+Tokyo Ghoul
+Sweat and Soap
+Seraph of the End - Vampire Reign 097
+Higurashi no Naku Koro ni Kai - Meakashihen
+Children
+"Can You Just Die, My Darling"
+"Haganai, I Don't Have Many Friends"
+Heion Sedai no Idaten-tachi
+Baketeriya
+Magical Sempai
+Ajin - Demi-Human
+Kimi wa Midara na Boku no Joou
+DearS
+Pluto
+Lotte no Omocha!
+Love Hina
+Shoujo Kaitai
+El Cazador de la Bruja
+Akame ga KILL! ZERO
+"Beauty, Sage And The Devil's Sword"
+Higurashi no Naku Koro ni - Watanagashihen
+Corpse Party - Musume
+Getsuyoubi no Tawawa
+Trinity Seven
+"No Game, No Life"
+KanoKari Mythology
+Seraph of the End - Vampire Reign 094
+Uzumaki
+Darling in the FranXX
+The Blade Of Evolution-Walking Alone In The Dungeon
+BLAME! Master Edition
+Fire Force
+Toukyou Akazukin
+Darker than Black
+Karin
+Higurashi no Naku Koro ni Kai - Matsuribayashihen
+Akazukin
+Velvet Kiss
+"Kanojo, Okarishimasu"
+Teasing Master Takagi-san
+The Hentai Prince and the Stony Cat
+Corpse Party - Book of Shadows
+.hackxxxx
+Hachigatsu Kokonoka Boku wa Kimi ni Kuwareru.
+Corpse Party - Blood Covered
+King Of Thorn
+BTOOOM!
+Chimamire Sukeban Chainsaw
+Seraph of the End - Vampire Reign
+Juni Taisen Zodiac War
+Masamune-kun's Revenge
+How Many Light-Years to Babylon
+Midori no Hibi
+A Girl on the Shore
+Plunderer
+School Rumble - Pleasure File
+Green WorldZ
+Golden Boy
+Yuusha ga Shinda!
+Kodomo no Jikan
+unOrdinary
+My Wife is Wagatsuma-san
+VanDread
+Rosario+Vampire
+Kyochuu Rettou
+Deadman Wonderland
+KILL la KILL
+Mushoku Tensei - Jobless Reincarnation
+404 Case Manual 30 Seconds Till Apocalypse
+Iris Zero
+All You Need is Kill
+Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen
+High School DxD
+Needless
+Ichiban no Daimaou
+My Girlfriend Is A Zombie
+Hare-Kon
+Minamoto-kun Monogatari
+Batman Beyond 02
+Spawn
+iZombie
+Invincible 070.5 - Invincible Returns
+Invincible Presents - Atom Eve
+Invincible 033.5 - Marvel Team-Up
+Invincible 031.5 - Image - Future Shock
+Batman Wayne Family Adventures
+Batman Beyond 04
+Batman Beyond 2.0
+Batman Beyond 03
+Batman Beyond 05
+Chew
+Zombie Tramp vs. Vampblade TPB
+Free Scott Pilgrim
+Invincible Presents - Atom Eve & Rex Splode
+Scott Pilgrim 03 - Scott Pilgrim & The Infinite Sadness
+I Hate Fairyland
+Scott Pilgrim 06 - Scott Pilgrim's Finest Hour
+Scott Pilgrim 04 - Scott Pilgrim Gets It Together
+Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life
+Spawn - 25th Anniversary Director's Cut
+Zombie Tramp
+Invincible Universe
+The Official Handbook of the Invincible Universe
+Batman Beyond
+Saga
+Scott Pilgrim 05 - Scott Pilgrim vs. the Universe
+Batman Beyond 06
+Batman - Detective Comics - Rebirth Deluxe Edition Book
+Batman Beyond 01
+Batman - Catwoman
+Invincible 022.5 - Invincible
+Teen Titans - Raven
+Invincible 052
+Invincible 014.5 - Image Comics Summer
+Zombie Tramp v3 TPB
+Scott Pilgrim 02 - Scott Pilgrim vs. The World
+Invincible
+Spawn 220
+Y - The Last Man
+Kick-Ass - The Dave Lizewski Years
+Teen Titans
+Fables
+Book of Enoch
+To Love-Ru Darkness - Digital Colored Comics
+Medaka Box - Digital Colored Comics
+Magical P tissi re Kosaki-chan!!
+Pandora in the Crimson Shell - Ghost Urn
+Yuragi-sou no Yuuna-san - Digital Colored Comics
+Ziggurat
+Tsugumomo - Digital Colored Comics
+The War Poems Of Siegfried Sassoon
+Rokka - Braves of the Six Flowers
+Demon King Daimaou
+Blockade Billy
+Cujo
+The Magicians
+The Gunslinger
+Danse Macabre
+Christine
+Fool moon
+On Writing
+Roadwork
+Deep Learning with Python - A Hands-on Introduction
+If It Bleeds
+Night Shift
+Bag of Bones
+Dreamcatcher
+Desperation
+Duma Key
+Four Past Midnight
+Elevation
+The Colorado Kid
+The Eyes of the Dragon
+Consulting With This Masked Devil!
+Gifting the Wonderful World with Blessings!
+The Golden Harpoon / Lost Among the Floes
+Invaders of the Rokujouma
+Cell
+Uncollected Stories 2003
+Faithful
+"Full Dark, No Stars"
+Dolores Claiborne
+It
+Antonio's Tale
+Joyland
+konosuba
+CSHP19
+By the Grace of the Gods - LN
+EPUB 3 Collection
+Talisman
+Sword Art Online
+The Mist
+Insomnia
+Hearts In Atlantis
+11/22/63
+Kono Subarashii Sekai ni Bakuen wo!
+In the Tall Grass
+Nightmares and Dreamscapes
+Eloquent JavaScript
+The Bell Jar
+Six Stories
+Rose Madder
+The Stand
+The Devil Is a Part-Timer!
+Grimgar of Fantasy and Ash
+A Chronicle of the Last Pagans
+Cycle of the Werewolf
+Gifting this Wonderful World With Blessings!
+Unit 1. Operations on Numbers.
+Firestarter
+The Dark Half
+Accel World
+Love Hina - Volume
+Skeleton Crew
+Needful Things
+Kono Subarashii Sekai ni Syukufuku wo!
+Carrie
+Thinner
+Hentai Ouji to Warawanai Neko
+Blaze
+Saturn Run
+Throttle
+Just After Sunset
+Gerald's Game
+The Regulators
+Different Seasons
+The Dark Tower
+Pet Sematary
+The Girl Who Loved Tom Gordon
+Ano Orokamono ni mo Kyakkou wo!
+From A Buick 8
+The Green Mile
+"Celebration of Discipline, Special Anniversary Edition"
+Combatants Will Be Dispatched!
+Kore Wa Zombie Desu Ka
+The Shining
+The Tatami Galaxy
+Salem's Lot
+The Tommyknockers
+A Face in the Crowd
+UR
+この素晴らしい世界に祝福を! 9 紅の宿命 【電子特別版】
+Outsider
+Lisey's Story
+Everything's Eventual
+Dune
+The Dead Zone
+Mile 81
+Under the Dome
+The Long Walk
+The Running Man
+EPUB3 UNLEASHED 2012
+Gifting The Wonderful World With Explosions!
+Rage
diff --git a/API.Benchmark/ParseScannedFilesBenchmarks.cs b/API.Benchmark/ParseScannedFilesBenchmarks.cs
index d3fd19a4e..8681c1261 100644
--- a/API.Benchmark/ParseScannedFilesBenchmarks.cs
+++ b/API.Benchmark/ParseScannedFilesBenchmarks.cs
@@ -1,6 +1,4 @@
-using System;
-using System.IO;
-using API.Data;
+using System.IO;
using API.Entities.Enums;
using API.Interfaces.Services;
using API.Parser;
@@ -57,8 +55,8 @@ namespace API.Benchmark
Title = "A Town Where You Live",
Volumes = "1"
};
- var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath},
- out var totalFiles, out var scanElapsedTime);
+ _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath},
+ out _, out _);
_parseScannedFiles.MergeName(p1);
}
}
diff --git a/API.Benchmark/ParserBenchmarks.cs b/API.Benchmark/ParserBenchmarks.cs
new file mode 100644
index 000000000..ef12331cc
--- /dev/null
+++ b/API.Benchmark/ParserBenchmarks.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Order;
+
+namespace API.Benchmark
+{
+ [MemoryDiagnoser]
+ [Orderer(SummaryOrderPolicy.FastestToSlowest)]
+ [RankColumn]
+ public class ParserBenchmarks
+ {
+ private readonly IList _names;
+
+ private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9]",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled,
+ TimeSpan.FromMilliseconds(300));
+
+ private static readonly Regex IsEpub = new Regex(@"\.epub",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled,
+ TimeSpan.FromMilliseconds(300));
+
+ public ParserBenchmarks()
+ {
+ // Read all series from SeriesNamesForNormalization.txt
+ _names = File.ReadAllLines("Data/SeriesNamesForNormalization.txt");
+ Console.WriteLine($"Performing benchmark on {_names.Count} series");
+ }
+
+ private static void NormalizeOriginal(string name)
+ {
+ Regex.Replace(name.ToLower(), "[^a-zA-Z0-9]", string.Empty);
+ }
+
+ private static void NormalizeNew(string name)
+ {
+ // ReSharper disable once UnusedVariable
+ var ret = NormalizeRegex.Replace(name, string.Empty).ToLower();
+ }
+
+
+ [Benchmark]
+ public void TestNormalizeName()
+ {
+ foreach (var name in _names)
+ {
+ NormalizeOriginal(name);
+ }
+ }
+
+
+ [Benchmark]
+ public void TestNormalizeName_New()
+ {
+ foreach (var name in _names)
+ {
+ NormalizeNew(name);
+ }
+ }
+
+ [Benchmark]
+ public void TestIsEpub()
+ {
+ foreach (var name in _names)
+ {
+ if ((name + ".epub").ToLower() == ".epub")
+ {
+ /* No Operation */
+ }
+ }
+ }
+
+ [Benchmark]
+ public void TestIsEpub_New()
+ {
+ foreach (var name in _names)
+ {
+
+ if (IsEpub.IsMatch((name + ".epub")))
+ {
+ /* No Operation */
+ }
+ }
+ }
+
+
+ }
+}
diff --git a/API.Benchmark/Program.cs b/API.Benchmark/Program.cs
index b308a07b7..c3ef1b605 100644
--- a/API.Benchmark/Program.cs
+++ b/API.Benchmark/Program.cs
@@ -10,10 +10,12 @@ namespace API.Benchmark
///
public static class Program
{
- static void Main(string[] args)
+ private static void Main(string[] args)
{
- BenchmarkRunner.Run();
+ //BenchmarkRunner.Run();
//BenchmarkRunner.Run();
+ BenchmarkRunner.Run();
+
}
}
}
diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index e01bab216..59ecff406 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/API.Tests/Parser/BookParserTests.cs b/API.Tests/Parser/BookParserTests.cs
index 219f5d723..b33ef1f54 100644
--- a/API.Tests/Parser/BookParserTests.cs
+++ b/API.Tests/Parser/BookParserTests.cs
@@ -10,5 +10,12 @@ namespace API.Tests.Parser
{
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
}
+
+ [Theory]
+ [InlineData("Harrison, Kim - Dates from Hell - Hollows Vol 2.5.epub", "2.5")]
+ public void ParseVolumeTest(string filename, string expected)
+ {
+ Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
+ }
}
}
diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs
index 8d25661ff..5bd24f714 100644
--- a/API.Tests/Parser/ComicParserTests.cs
+++ b/API.Tests/Parser/ComicParserTests.cs
@@ -1,11 +1,22 @@
-using Xunit;
+using System;
+using System.Collections.Generic;
+using API.Entities.Enums;
+using API.Parser;
+using Xunit;
+using Xunit.Abstractions;
namespace API.Tests.Parser
{
public class ComicParserTests
{
+ private readonly ITestOutputHelper _testOutputHelper;
+
+ public ComicParserTests(ITestOutputHelper testOutputHelper)
+ {
+ _testOutputHelper = testOutputHelper;
+ }
+
[Theory]
- [InlineData("01 Spider-Man & Wolverine 01.cbr", "Spider-Man & Wolverine")]
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")]
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "The First Asterix Frieze")]
[InlineData("Batman & Catwoman - Trail of the Gun 01", "Batman & Catwoman - Trail of the Gun")]
@@ -28,7 +39,23 @@ namespace API.Tests.Parser
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "Invincible")]
[InlineData("Batman Wayne Family Adventures - Ep. 001 - Moving In", "Batman Wayne Family Adventures")]
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone).cbr", "Saga")]
+ [InlineData("spawn-123", "spawn")]
+ [InlineData("spawn-chapter-123", "spawn")]
+ [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "Spawn")]
[InlineData("Batman Beyond 04 (of 6) (1999)", "Batman Beyond")]
+ [InlineData("Batman Beyond 001 (2012)", "Batman Beyond")]
+ [InlineData("Batman Beyond 2.0 001 (2013)", "Batman Beyond 2.0")]
+ [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "Batman - Catwoman")]
+ [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "Chew")]
+ [InlineData("Chew Script Book (2011) (digital-Empire) SP04", "Chew Script Book")]
+ [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", "Batman - Detective Comics - Rebirth Deluxe Edition Book")]
+ [InlineData("Cyberpunk 2077 - Your Voice #01", "Cyberpunk 2077 - Your Voice")]
+ [InlineData("Cyberpunk 2077 #01", "Cyberpunk 2077")]
+ [InlineData("Cyberpunk 2077 - Trauma Team #04.cbz", "Cyberpunk 2077 - Trauma Team")]
+ [InlineData("Batgirl Vol.2000 #57 (December, 2004)", "Batgirl")]
+ [InlineData("Batgirl V2000 #57", "Batgirl")]
+ [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire)", "Fables")]
+ [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", "2000 AD")]
public void ParseComicSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename));
@@ -52,6 +79,20 @@ namespace API.Tests.Parser
[InlineData("Amazing Man Comics chapter 25", "0")]
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "0")]
[InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")]
+ [InlineData("spawn-123", "0")]
+ [InlineData("spawn-chapter-123", "0")]
+ [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "0")]
+ [InlineData("Batman Beyond 04 (of 6) (1999)", "0")]
+ [InlineData("Batman Beyond 001 (2012)", "0")]
+ [InlineData("Batman Beyond 2.0 001 (2013)", "0")]
+ [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "0")]
+ [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "1")]
+ [InlineData("Chew Script Book (2011) (digital-Empire) SP04", "0")]
+ [InlineData("Batgirl Vol.2000 #57 (December, 2004)", "2000")]
+ [InlineData("Batgirl V2000 #57", "2000")]
+ [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "0")]
+ [InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")]
+ [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", "0")]
public void ParseComicVolumeTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
@@ -77,12 +118,87 @@ namespace API.Tests.Parser
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "33.5")]
[InlineData("Batman Wayne Family Adventures - Ep. 014 - Moving In", "14")]
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone)", "1")]
+ [InlineData("spawn-123", "123")]
+ [InlineData("spawn-chapter-123", "123")]
+ [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "62")]
[InlineData("Batman Beyond 04 (of 6) (1999)", "4")]
[InlineData("Invincible 052 (c2c) (2008) (Minutemen-TheCouple)", "52")]
[InlineData("Y - The Last Man #001", "1")]
+ [InlineData("Batman Beyond 001 (2012)", "1")]
+ [InlineData("Batman Beyond 2.0 001 (2013)", "1")]
+ [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "1")]
+ [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "0")]
+ [InlineData("Chew Script Book (2011) (digital-Empire) SP04", "0")]
+ [InlineData("Batgirl Vol.2000 #57 (December, 2004)", "57")]
+ [InlineData("Batgirl V2000 #57", "57")]
+ [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "21")]
+ [InlineData("Cyberpunk 2077 - Trauma Team #04.cbz", "4")]
+ [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", "366")]
public void ParseComicChapterTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
}
+
+
+ [Theory]
+ [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", true)]
+ [InlineData("Zombie Tramp vs. Vampblade TPB (2016) (Digital) (TheArchivist-Empire)", true)]
+ [InlineData("Baldwin the Brave & Other Tales Special SP1.cbr", true)]
+ [InlineData("Mouse Guard Specials - Spring 1153 - Fraggle Rock FCBD 2010", true)]
+ public void ParseComicSpecialTest(string input, bool expected)
+ {
+ Assert.Equal(expected, !string.IsNullOrEmpty(API.Parser.Parser.ParseComicSpecial(input)));
+ }
+
+ [Fact]
+ public void ParseInfoTest()
+ {
+ const string rootPath = @"E:/Comics/";
+ var expected = new Dictionary();
+ var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
+ expected.Add(filepath, new ParserInfo
+ {
+ Series = "Teen Titans", Volumes = "0",
+ Chapters = "0", Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
+ FullFilePath = filepath
+ });
+
+ // Fallback test with bad naming
+ filepath = @"E:\Comics\Comics\Babe\Babe Vol.1 #1-4\Babe 01.cbr";
+ expected.Add(filepath, new ParserInfo
+ {
+ Series = "Babe", Volumes = "0", Edition = "",
+ Chapters = "1", Filename = "Babe 01.cbr", Format = MangaFormat.Archive,
+ FullFilePath = filepath, IsSpecial = false
+ });
+
+ foreach (var file in expected.Keys)
+ {
+ var expectedInfo = expected[file];
+ var actual = API.Parser.Parser.Parse(file, rootPath);
+ if (expectedInfo == null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+ Assert.NotNull(actual);
+ _testOutputHelper.WriteLine($"Validating {file}");
+ Assert.Equal(expectedInfo.Format, actual.Format);
+ _testOutputHelper.WriteLine("Format ✓");
+ Assert.Equal(expectedInfo.Series, actual.Series);
+ _testOutputHelper.WriteLine("Series ✓");
+ Assert.Equal(expectedInfo.Chapters, actual.Chapters);
+ _testOutputHelper.WriteLine("Chapters ✓");
+ Assert.Equal(expectedInfo.Volumes, actual.Volumes);
+ _testOutputHelper.WriteLine("Volumes ✓");
+ Assert.Equal(expectedInfo.Edition, actual.Edition);
+ _testOutputHelper.WriteLine("Edition ✓");
+ Assert.Equal(expectedInfo.Filename, actual.Filename);
+ _testOutputHelper.WriteLine("Filename ✓");
+ Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
+ _testOutputHelper.WriteLine("FullFilePath ✓");
+ }
+ }
+
}
}
diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs
index 917d1f467..9cb9d560a 100644
--- a/API.Tests/Parser/MangaParserTests.cs
+++ b/API.Tests/Parser/MangaParserTests.cs
@@ -67,6 +67,7 @@ namespace API.Tests.Parser
[InlineData("X-Men v1 #201 (September 2007).cbz", "1")]
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")]
+ [InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03.5 Ch. 023.5 - Volume 3 Extras.cbz", "3.5")]
public void ParseVolumeTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
@@ -238,6 +239,7 @@ namespace API.Tests.Parser
[InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")]
[InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")]
[InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")]
+ [InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")]
public void ParseChaptersTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
@@ -291,18 +293,6 @@ namespace API.Tests.Parser
Assert.Equal(expected, API.Parser.Parser.ParseMangaSpecial(inputFile));
}
-/*
- private static ParserInfo CreateParserInfo(string series, string chapter, string volume, bool isSpecial = false)
- {
- return new ParserInfo()
- {
- Chapters = chapter,
- Volumes = volume,
- IsSpecial = isSpecial,
- Series = series,
- };
- }
-*/
[Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
@@ -436,6 +426,14 @@ namespace API.Tests.Parser
FullFilePath = filepath, IsSpecial = false
});
+ filepath = @"E:\Manga\Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub";
+ expected.Add(filepath, new ParserInfo
+ {
+ Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "",
+ Chapters = "0", Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub,
+ FullFilePath = filepath, IsSpecial = false
+ });
+
// If an image is cover exclusively, ignore it
filepath = @"E:\Manga\Seraph of the End\cover.png";
expected.Add(filepath, null);
diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs
index 6830cde0d..8fdf0509d 100644
--- a/API.Tests/Parser/ParserTest.cs
+++ b/API.Tests/Parser/ParserTest.cs
@@ -11,6 +11,7 @@ namespace API.Tests.Parser
[InlineData("Beastars SP01", true)]
[InlineData("Beastars Special 01", false)]
[InlineData("Beastars Extra 01", false)]
+ [InlineData("Batman Beyond - Return of the Joker (2001) SP01", true)]
public void HasSpecialTest(string input, bool expected)
{
Assert.Equal(expected, HasSpecialMarker(input));
@@ -35,14 +36,15 @@ namespace API.Tests.Parser
}
[Theory]
- [InlineData("Hello_I_am_here", "Hello I am here")]
- [InlineData("Hello_I_am_here ", "Hello I am here")]
- [InlineData("[ReleaseGroup] The Title", "The Title")]
- [InlineData("[ReleaseGroup]_The_Title", "The Title")]
- [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", "Kasumi Otoko no Ko v1.1")]
- public void CleanTitleTest(string input, string expected)
+ [InlineData("Hello_I_am_here", false, "Hello I am here")]
+ [InlineData("Hello_I_am_here ", false, "Hello I am here")]
+ [InlineData("[ReleaseGroup] The Title", false, "The Title")]
+ [InlineData("[ReleaseGroup]_The_Title", false, "The Title")]
+ [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")]
+ [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")]
+ public void CleanTitleTest(string input, bool isComic, string expected)
{
- Assert.Equal(expected, CleanTitle(input));
+ Assert.Equal(expected, CleanTitle(input, isComic));
}
@@ -54,7 +56,7 @@ namespace API.Tests.Parser
// public void ReplaceStyleUrlTest(string input, string expected)
// {
// var replacementStr = "PaytoneOne.ttf";
- // // TODO: Use Match to validate since replace is weird
+ // // Use Match to validate since replace is weird
// //Assert.Equal(expected, FontSrcUrlRegex.Replace(input, "$1" + replacementStr + "$2" + "$3"));
// var match = FontSrcUrlRegex.Match(input);
// Assert.Equal(!string.IsNullOrEmpty(expected), FontSrcUrlRegex.Match(input).Success);
@@ -98,33 +100,6 @@ namespace API.Tests.Parser
Assert.Equal(expected, IsEpub(input));
}
- // [Theory]
- // [InlineData("Tenjou Tenge Omnibus", "Omnibus")]
- // [InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")]
- // [InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")]
- // [InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")]
- // [InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")]
- // [InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")]
- // [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "Digital Colored Comics")]
- // [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")]
- // public void ParseEditionTest(string input, string expected)
- // {
- // Assert.Equal(expected, ParseEdition(input));
- // }
-
- // [Theory]
- // [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", true)]
- // [InlineData("Beelzebub_Omake_June_2012_RHS", true)]
- // [InlineData("Beelzebub_Side_Story_02_RHS.zip", false)]
- // [InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", true)]
- // [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", true)]
- // [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)]
- // [InlineData("Ani-Hina Art Collection.cbz", true)]
- // public void ParseMangaSpecialTest(string input, bool expected)
- // {
- // Assert.Equal(expected, ParseMangaSpecial(input) != "");
- // }
-
[Theory]
[InlineData("12-14", 12)]
[InlineData("24", 24)]
@@ -142,6 +117,8 @@ namespace API.Tests.Parser
[InlineData("Darker Than Black", "darkerthanblack")]
[InlineData("Darker Than Black - Something", "darkerthanblacksomething")]
[InlineData("Darker Than_Black", "darkerthanblack")]
+ [InlineData("Citrus", "citrus")]
+ [InlineData("Citrus+", "citrus+")]
[InlineData("", "")]
public void NormalizeTest(string input, string expected)
{
diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs
index 80f09a144..fc3e21dd4 100644
--- a/API.Tests/Services/ArchiveServiceTests.cs
+++ b/API.Tests/Services/ArchiveServiceTests.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.IO.Compression;
using API.Archive;
+using API.Data.Metadata;
using API.Interfaces.Services;
using API.Services;
using Microsoft.Extensions.Logging;
@@ -216,8 +217,30 @@ namespace API.Tests.Services
var archive = Path.Join(testDirectory, "file in folder.zip");
var summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?";
- Assert.Equal(summaryInfo, _archiveService.GetSummaryInfo(archive));
+ Assert.Equal(summaryInfo, _archiveService.GetComicInfo(archive).Summary);
+ }
+ [Fact]
+ public void CanParseComicInfo()
+ {
+ var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
+ var archive = Path.Join(testDirectory, "ComicInfo.zip");
+ var actual = _archiveService.GetComicInfo(archive);
+ var expected = new ComicInfo()
+ {
+ Publisher = "Yen Press",
+ Genre = "Manga, Movies & TV",
+ Summary =
+ "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?",
+ PageCount = 194,
+ LanguageISO = "en",
+ Notes = "Scraped metadata from Comixology [CMXDB450184]",
+ Series = "BTOOOM!",
+ Title = "v01",
+ Web = "https://www.comixology.com/BTOOOM/digital-comic/450184"
+ };
+
+ Assert.NotStrictEqual(expected, actual);
}
}
}
diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs
index db756ebab..90cf1a217 100644
--- a/API.Tests/Services/DirectoryServiceTests.cs
+++ b/API.Tests/Services/DirectoryServiceTests.cs
@@ -90,7 +90,7 @@ namespace API.Tests.Services
}
[Theory]
- [InlineData(new string[] {"C:/Manga/"}, new string[] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")]
+ [InlineData(new [] {"C:/Manga/"}, new [] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")]
public void FindHighestDirectoriesFromFilesTest(string[] rootDirectories, string[] folders, string expectedDirectory)
{
var actual = DirectoryService.FindHighestDirectoriesFromFiles(rootDirectories, folders);
diff --git a/API.Tests/Services/MetadataServiceTests.cs b/API.Tests/Services/MetadataServiceTests.cs
index b921f74b7..5d61ee249 100644
--- a/API.Tests/Services/MetadataServiceTests.cs
+++ b/API.Tests/Services/MetadataServiceTests.cs
@@ -1,13 +1,7 @@
using System;
using System.IO;
using API.Entities;
-using API.Interfaces;
-using API.Interfaces.Services;
using API.Services;
-using API.SignalR;
-using Microsoft.AspNetCore.SignalR;
-using Microsoft.Extensions.Logging;
-using NSubstitute;
using Xunit;
namespace API.Tests.Services
diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs
index 93b254c8e..0253ccef6 100644
--- a/API.Tests/Services/ScannerServiceTests.cs
+++ b/API.Tests/Services/ScannerServiceTests.cs
@@ -111,7 +111,7 @@ namespace API.Tests.Services
- Assert.Empty(_scannerService.FindSeriesNotOnDisk(existingSeries, infos));
+ Assert.Empty(ScannerService.FindSeriesNotOnDisk(existingSeries, infos));
}
diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip b/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip
new file mode 100644
index 000000000..3ff536270
Binary files /dev/null and b/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip differ
diff --git a/API/API.csproj b/API/API.csproj
index fa1bcdfec..137a3a985 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -16,6 +16,10 @@
bin\Debug\API.xml
+
+ en
+
+
Kavita
@@ -33,39 +37,38 @@
-
+
-
-
+
+
-
+
-
-
-
+
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
-
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index 58478e2f8..86b1ac778 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -7,10 +7,10 @@ using API.Constants;
using API.DTOs;
using API.DTOs.Account;
using API.Entities;
-using API.Errors;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
+using API.Services;
using AutoMapper;
using Kavita.Common;
using Microsoft.AspNetCore.Identity;
@@ -31,13 +31,14 @@ namespace API.Controllers
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger _logger;
private readonly IMapper _mapper;
+ private readonly IAccountService _accountService;
///
public AccountController(UserManager userManager,
SignInManager signInManager,
ITokenService tokenService, IUnitOfWork unitOfWork,
ILogger logger,
- IMapper mapper)
+ IMapper mapper, IAccountService accountService)
{
_userManager = userManager;
_signInManager = signInManager;
@@ -45,6 +46,7 @@ namespace API.Controllers
_unitOfWork = unitOfWork;
_logger = logger;
_mapper = mapper;
+ _accountService = accountService;
}
///
@@ -61,30 +63,10 @@ namespace API.Controllers
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
return Unauthorized("You are not permitted to this operation.");
- // Validate Password
- foreach (var validator in _userManager.PasswordValidators)
+ var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
+ if (errors.Any())
{
- var validationResult = await validator.ValidateAsync(_userManager, user, resetPasswordDto.Password);
- if (!validationResult.Succeeded)
- {
- return BadRequest(
- validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
- }
- }
-
- var result = await _userManager.RemovePasswordAsync(user);
- if (!result.Succeeded)
- {
- _logger.LogError("Could not update password");
- return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
- }
-
-
- result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
- if (!result.Succeeded)
- {
- _logger.LogError("Could not update password");
- return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
+ return BadRequest(errors);
}
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
@@ -110,6 +92,13 @@ namespace API.Controllers
user.UserPreferences ??= new AppUserPreferences();
user.ApiKey = HashUtil.ApiKey();
+ var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ if (!settings.EnableAuthentication && !registerDto.IsAdmin)
+ {
+ _logger.LogInformation("User {UserName} is being registered as non-admin with no server authentication. Using default password.", registerDto.Username);
+ registerDto.Password = AccountService.DefaultPassword;
+ }
+
var result = await _userManager.CreateAsync(user, registerDto.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
@@ -166,6 +155,14 @@ namespace API.Controllers
if (user == null) return Unauthorized("Invalid username");
+ var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
+ var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ if (!settings.EnableAuthentication && !isAdmin)
+ {
+ _logger.LogDebug("User {UserName} is logging in with authentication disabled", loginDto.Username);
+ loginDto.Password = AccountService.DefaultPassword;
+ }
+
var result = await _signInManager
.CheckPasswordSignInAsync(user, loginDto.Password, false);
diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs
index 6081f7d58..049413388 100644
--- a/API/Controllers/CollectionController.cs
+++ b/API/Controllers/CollectionController.cs
@@ -2,13 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using API.Constants;
using API.DTOs;
using API.Entities;
using API.Extensions;
using API.Interfaces;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
@@ -19,13 +17,11 @@ namespace API.Controllers
public class CollectionController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
- private readonly UserManager _userManager;
///
- public CollectionController(IUnitOfWork unitOfWork, UserManager userManager)
+ public CollectionController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
- _userManager = userManager;
}
///
@@ -36,7 +32,7 @@ namespace API.Controllers
public async Task> GetAllTags()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
- var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
+ var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
if (isAdmin)
{
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs
index 3000e1f22..d5080846a 100644
--- a/API/Controllers/DownloadController.cs
+++ b/API/Controllers/DownloadController.cs
@@ -63,7 +63,7 @@ namespace API.Controllers
public async Task DownloadVolume(int volumeId)
{
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
- var volume = await _unitOfWork.SeriesRepository.GetVolumeByIdAsync(volumeId);
+ var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
{
@@ -92,7 +92,7 @@ namespace API.Controllers
{
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
- var volume = await _unitOfWork.SeriesRepository.GetVolumeByIdAsync(chapter.VolumeId);
+ var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
{
diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs
index f1ddb770e..88bafcff7 100644
--- a/API/Controllers/ImageController.cs
+++ b/API/Controllers/ImageController.cs
@@ -1,12 +1,9 @@
-using System;
-using System.IO;
-using System.Net;
+using System.IO;
using System.Threading.Tasks;
using API.Extensions;
using API.Interfaces;
using API.Services;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Net.Http.Headers;
namespace API.Controllers
{
diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs
index 25f224a28..07a4a3f97 100644
--- a/API/Controllers/LibraryController.cs
+++ b/API/Controllers/LibraryController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using API.Data.Repositories;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
@@ -179,7 +180,7 @@ namespace API.Controllers
try
{
- var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
+ var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
_unitOfWork.LibraryRepository.Delete(library);
await _unitOfWork.CommitAsync();
@@ -203,7 +204,7 @@ namespace API.Controllers
[HttpPost("update")]
public async Task UpdateLibrary(UpdateLibraryDto libraryForUserDto)
{
- var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id);
+ var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id, LibraryIncludes.Folders);
var originalFolders = library.Folders.Select(x => x.Path).ToList();
diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs
index ef83c2a69..c9e527e2c 100644
--- a/API/Controllers/OPDSController.cs
+++ b/API/Controllers/OPDSController.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using API.Comparators;
-using API.Constants;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.OPDS;
@@ -16,7 +15,6 @@ using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using Kavita.Common;
-using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
@@ -26,7 +24,6 @@ namespace API.Controllers
private readonly IUnitOfWork _unitOfWork;
private readonly IDownloadService _downloadService;
private readonly IDirectoryService _directoryService;
- private readonly UserManager _userManager;
private readonly ICacheService _cacheService;
private readonly IReaderService _readerService;
@@ -41,13 +38,12 @@ namespace API.Controllers
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
- IDirectoryService directoryService, UserManager userManager,
- ICacheService cacheService, IReaderService readerService)
+ IDirectoryService directoryService, ICacheService cacheService,
+ IReaderService readerService)
{
_unitOfWork = unitOfWork;
_downloadService = downloadService;
_directoryService = directoryService;
- _userManager = userManager;
_cacheService = cacheService;
_readerService = readerService;
@@ -170,16 +166,16 @@ namespace API.Controllers
return BadRequest("OPDS is not enabled on this server");
var userId = await GetUser(apiKey);
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
- var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
+ var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
- IEnumerable tags;
+ IList tags;
if (isAdmin)
{
- tags = await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
+ tags = (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()).ToList();
}
else
{
- tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
+ tags = (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync()).ToList();
}
@@ -201,6 +197,14 @@ namespace API.Controllers
});
}
+ if (tags.Count == 0)
+ {
+ feed.Entries.Add(new FeedEntry()
+ {
+ Title = "Nothing here",
+ });
+ }
+
return CreateXmlResult(SerializeXml(feed));
}
@@ -213,7 +217,7 @@ namespace API.Controllers
return BadRequest("OPDS is not enabled on this server");
var userId = await GetUser(apiKey);
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
- var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
+ var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
IEnumerable tags;
if (isAdmin)
@@ -300,13 +304,13 @@ namespace API.Controllers
var feed = CreateFeed(readingList.Title + " Reading List", $"{apiKey}/reading-list/{readingListId}", apiKey);
- var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
+ var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
foreach (var item in items)
{
feed.Entries.Add(new FeedEntry()
{
Id = item.ChapterId.ToString(),
- Title = "Chapter " + item.ChapterNumber,
+ Title = $"{item.SeriesName} Chapter {item.ChapterNumber}",
Links = new List()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
@@ -315,6 +319,14 @@ namespace API.Controllers
});
}
+ if (items.Count == 0)
+ {
+ feed.Entries.Add(new FeedEntry()
+ {
+ Title = "Nothing here",
+ });
+ }
+
return CreateXmlResult(SerializeXml(feed));
@@ -373,6 +385,14 @@ namespace API.Controllers
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
}
+ if (recentlyAdded.Count == 0)
+ {
+ feed.Entries.Add(new FeedEntry()
+ {
+ Title = "Nothing here",
+ });
+ }
+
return CreateXmlResult(SerializeXml(feed));
}
@@ -404,6 +424,14 @@ namespace API.Controllers
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
}
+ if (pagedList.Count == 0)
+ {
+ feed.Entries.Add(new FeedEntry()
+ {
+ Title = "Nothing here",
+ });
+ }
+
return CreateXmlResult(SerializeXml(feed));
}
@@ -467,7 +495,7 @@ namespace API.Controllers
return BadRequest("OPDS is not enabled on this server");
var userId = await GetUser(apiKey);
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
- var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId);
+ var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId);
var feed = CreateFeed(series.Name + " - Volumes", $"{apiKey}/series/{series.Id}", apiKey);
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesId}"));
foreach (var volumeDto in volumes)
@@ -486,7 +514,7 @@ namespace API.Controllers
return BadRequest("OPDS is not enabled on this server");
var userId = await GetUser(apiKey);
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
- var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
+ var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
var chapters =
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
_chapterSortComparer);
@@ -517,7 +545,7 @@ namespace API.Controllers
return BadRequest("OPDS is not enabled on this server");
var userId = await GetUser(apiKey);
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
- var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
+ var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs
index d1314674e..5b18cfb98 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -97,7 +97,7 @@ namespace API.Controllers
public async Task MarkRead(MarkReadDto markReadDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
- var volumes = await _unitOfWork.SeriesRepository.GetVolumes(markReadDto.SeriesId);
+ var volumes = await _unitOfWork.VolumeRepository.GetVolumes(markReadDto.SeriesId);
user.Progresses ??= new List();
foreach (var volume in volumes)
{
@@ -125,7 +125,7 @@ namespace API.Controllers
public async Task MarkUnread(MarkReadDto markReadDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
- var volumes = await _unitOfWork.SeriesRepository.GetVolumes(markReadDto.SeriesId);
+ var volumes = await _unitOfWork.VolumeRepository.GetVolumes(markReadDto.SeriesId);
user.Progresses ??= new List();
foreach (var volume in volumes)
{
@@ -267,7 +267,7 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
user.Progresses ??= new List();
- var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
+ var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
foreach (var volume in volumes)
{
_readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
@@ -294,7 +294,7 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
user.Progresses ??= new List();
- var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
+ var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
foreach (var volume in volumes)
{
_readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs
index 1f22263c7..19e4a4b49 100644
--- a/API/Controllers/ReadingListController.cs
+++ b/API/Controllers/ReadingListController.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
@@ -99,16 +100,20 @@ namespace API.Controllers
[HttpPost("delete-item")]
public async Task DeleteListItem(UpdateReadingListPosition dto)
{
- var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
- var item = items.Find(r => r.Id == dto.ReadingListItemId);
- items.Remove(item);
+ var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
+ readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList();
- for (var i = 0; i < items.Count; i++)
+
+ var index = 0;
+ foreach (var readingListItem in readingList.Items)
{
- items[i].Order = i;
+ readingListItem.Order = index;
+ index++;
}
- if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
+ if (!_unitOfWork.HasChanges()) return Ok();
+
+ if (await _unitOfWork.CommitAsync())
{
return Ok("Updated");
}
@@ -138,15 +143,10 @@ namespace API.Controllers
itemIdsToRemove.Contains(r.Id));
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
- if (_unitOfWork.HasChanges())
- {
- await _unitOfWork.CommitAsync();
- return Ok("Updated");
- }
- else
- {
- return Ok("Nothing to remove");
- }
+ if (!_unitOfWork.HasChanges()) return Ok("Nothing to remove");
+
+ await _unitOfWork.CommitAsync();
+ return Ok("Updated");
}
catch
{
@@ -437,7 +437,7 @@ namespace API.Controllers
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
- .OrderBy(c => int.Parse(c.Volume.Name))
+ .OrderBy(c => float.Parse(c.Volume.Name))
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
var index = lastOrder + 1;
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index ff0fa7587..cce0de2b5 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -10,9 +10,11 @@ using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
+using API.SignalR;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace API.Controllers
@@ -22,12 +24,14 @@ namespace API.Controllers
private readonly ILogger _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
+ private readonly IHubContext _messageHub;
- public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork)
+ public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IHubContext messageHub)
{
_logger = logger;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
+ _messageHub = messageHub;
}
[HttpPost]
@@ -97,14 +101,14 @@ namespace API.Controllers
public async Task>> GetVolumes(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
- return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId));
+ return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
}
[HttpGet("volume")]
public async Task> GetVolume(int volumeId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
- return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, userId));
+ return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
}
[HttpGet("chapter")]
@@ -217,7 +221,7 @@ namespace API.Controllers
[HttpPost("refresh-metadata")]
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
{
- _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId);
+ _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true);
return Ok();
}
@@ -296,6 +300,12 @@ namespace API.Controllers
if (await _unitOfWork.CommitAsync())
{
+ foreach (var tag in updateSeriesMetadataDto.Tags)
+ {
+ await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesAddedToCollection,
+ MessageFactory.SeriesAddedToCollection(tag.Id,
+ updateSeriesMetadataDto.SeriesMetadata.SeriesId));
+ }
return Ok("Successfully updated");
}
}
diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs
index acd1b61e8..ef3fe8997 100644
--- a/API/Controllers/SettingsController.cs
+++ b/API/Controllers/SettingsController.cs
@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using API.DTOs;
+using API.DTOs.Settings;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Converters;
using API.Interfaces;
+using API.Interfaces.Services;
+using API.Services;
using Kavita.Common;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Authorization;
@@ -21,12 +23,22 @@ namespace API.Controllers
private readonly ILogger _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ITaskScheduler _taskScheduler;
+ private readonly IAccountService _accountService;
- public SettingsController(ILogger logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler)
+ public SettingsController(ILogger logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IAccountService accountService)
{
_logger = logger;
_unitOfWork = unitOfWork;
_taskScheduler = taskScheduler;
+ _accountService = accountService;
+ }
+
+ [AllowAnonymous]
+ [HttpGet("base-url")]
+ public async Task> GetBaseUrl()
+ {
+ var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ return Ok(settingsDto.BaseUrl);
}
[Authorize(Policy = "RequireAdminRole")]
@@ -57,6 +69,7 @@ namespace API.Controllers
// We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
+ var updateAuthentication = false;
foreach (var setting in currentSettings)
{
@@ -80,6 +93,18 @@ namespace API.Controllers
_unitOfWork.SettingsRepository.Update(setting);
}
+ if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
+ {
+ var path = !updateSettingsDto.BaseUrl.StartsWith("/")
+ ? $"/{updateSettingsDto.BaseUrl}"
+ : updateSettingsDto.BaseUrl;
+ path = !path.EndsWith("/")
+ ? $"{path}/"
+ : path;
+ setting.Value = path;
+ _unitOfWork.SettingsRepository.Update(setting);
+ }
+
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
@@ -93,6 +118,13 @@ namespace API.Controllers
_unitOfWork.SettingsRepository.Update(setting);
}
+ if (setting.Key == ServerSettingKey.EnableAuthentication && updateSettingsDto.EnableAuthentication + string.Empty != setting.Value)
+ {
+ setting.Value = updateSettingsDto.EnableAuthentication + string.Empty;
+ _unitOfWork.SettingsRepository.Update(setting);
+ updateAuthentication = true;
+ }
+
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
@@ -110,12 +142,33 @@ namespace API.Controllers
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
- if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync())
+ try
{
+ await _unitOfWork.CommitAsync();
+
+ if (updateAuthentication)
+ {
+ var users = await _unitOfWork.UserRepository.GetNonAdminUsersAsync();
+ foreach (var user in users)
+ {
+ var errors = await _accountService.ChangeUserPassword(user, AccountService.DefaultPassword);
+ if (!errors.Any()) continue;
+
+ await _unitOfWork.RollbackAsync();
+ return BadRequest(errors);
+ }
+
+ _logger.LogInformation("Server authentication changed. Updated all non-admins to default password");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "There was an exception when updating server settings");
await _unitOfWork.RollbackAsync();
return BadRequest("There was a critical issue. Please try again.");
}
+
_logger.LogInformation("Server Settings updated");
_taskScheduler.ScheduleTasks();
return Ok(updateSettingsDto);
@@ -148,5 +201,12 @@ namespace API.Controllers
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto.EnableOpds);
}
+
+ [HttpGet("authentication-enabled")]
+ public async Task> GetAuthenticationEnabled()
+ {
+ var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ return Ok(settingsDto.EnableAuthentication);
+ }
}
}
diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs
index 4241a8bc6..43c9b8d09 100644
--- a/API/Controllers/UploadController.cs
+++ b/API/Controllers/UploadController.cs
@@ -148,7 +148,7 @@ namespace API.Controllers
chapter.CoverImage = filePath;
chapter.CoverImageLocked = true;
_unitOfWork.ChapterRepository.Update(chapter);
- var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
+ var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
volume.CoverImage = chapter.CoverImage;
_unitOfWork.VolumeRepository.Update(volume);
}
@@ -185,7 +185,7 @@ namespace API.Controllers
chapter.CoverImage = string.Empty;
chapter.CoverImageLocked = false;
_unitOfWork.ChapterRepository.Update(chapter);
- var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
+ var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
volume.CoverImage = chapter.CoverImage;
_unitOfWork.VolumeRepository.Update(volume);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs
index c35e368cc..f5171b819 100644
--- a/API/Controllers/UsersController.cs
+++ b/API/Controllers/UsersController.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using API.Data.Repositories;
using API.DTOs;
using API.Extensions;
using API.Interfaces;
@@ -38,11 +39,24 @@ namespace API.Controllers
return Ok(await _unitOfWork.UserRepository.GetMembersAsync());
}
+ [AllowAnonymous]
+ [HttpGet("names")]
+ public async Task>> GetUserNames()
+ {
+ var setting = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ if (setting.EnableAuthentication)
+ {
+ return Unauthorized("This API cannot be used given your server's configuration");
+ }
+ var members = await _unitOfWork.UserRepository.GetMembersAsync();
+ return Ok(members.Select(m => m.Username));
+ }
+
[HttpGet("has-reading-progress")]
public async Task> HasReadingProgress(int libraryId)
{
- var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
+ var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
}
diff --git a/API/DTOs/Account/LoginDto.cs b/API/DTOs/Account/LoginDto.cs
index 3da1841bf..a21e9868f 100644
--- a/API/DTOs/Account/LoginDto.cs
+++ b/API/DTOs/Account/LoginDto.cs
@@ -1,8 +1,8 @@
-namespace API.DTOs
+namespace API.DTOs.Account
{
public class LoginDto
{
public string Username { get; init; }
- public string Password { get; init; }
+ public string Password { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs
index fb08a53e8..9289cfa21 100644
--- a/API/DTOs/LibraryDto.cs
+++ b/API/DTOs/LibraryDto.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using API.Entities.Enums;
namespace API.DTOs
@@ -7,8 +8,11 @@ namespace API.DTOs
{
public int Id { get; init; }
public string Name { get; init; }
- public string CoverImage { get; init; }
+ ///
+ /// Last time Library was scanned
+ ///
+ public DateTime LastScanned { get; init; }
public LibraryType Type { get; init; }
public ICollection Folders { get; init; }
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/OPDS/FeedLink.cs b/API/DTOs/OPDS/FeedLink.cs
index 1dc3d5b1e..1589109ad 100644
--- a/API/DTOs/OPDS/FeedLink.cs
+++ b/API/DTOs/OPDS/FeedLink.cs
@@ -23,7 +23,7 @@ namespace API.DTOs.OPDS
public string Title { get; set; }
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
- public int TotalPages { get; set; } = 0;
+ public int TotalPages { get; set; }
public bool ShouldSerializeTotalPages()
{
diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/API/DTOs/Reader/IChapterInfoDto.cs
index 63b5c9a62..67aa6caf6 100644
--- a/API/DTOs/Reader/IChapterInfoDto.cs
+++ b/API/DTOs/Reader/IChapterInfoDto.cs
@@ -1,5 +1,4 @@
using API.Entities.Enums;
-using Newtonsoft.Json;
namespace API.DTOs.Reader
{
diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs
index d04c2a03e..1bf598f5d 100644
--- a/API/DTOs/RegisterDto.cs
+++ b/API/DTOs/RegisterDto.cs
@@ -8,7 +8,7 @@ namespace API.DTOs
public string Username { get; init; }
[Required]
[StringLength(32, MinimumLength = 6)]
- public string Password { get; init; }
+ public string Password { get; set; }
public bool IsAdmin { get; init; }
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs
index 271f7d7a6..aace57127 100644
--- a/API/DTOs/Settings/ServerSettingDTO.cs
+++ b/API/DTOs/Settings/ServerSettingDTO.cs
@@ -1,4 +1,4 @@
-namespace API.DTOs
+namespace API.DTOs.Settings
{
public class ServerSettingDto
{
@@ -21,5 +21,14 @@
/// Enables OPDS connections to be made to the server.
///
public bool EnableOpds { get; set; }
+
+ ///
+ /// Enables Authentication on the server. Defaults to true.
+ ///
+ public bool EnableAuthentication { get; set; }
+ ///
+ /// Base Url for the kavita. Requires restart to take effect.
+ ///
+ public string BaseUrl { get; set; }
}
}
diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs
new file mode 100644
index 000000000..9f846ea42
--- /dev/null
+++ b/API/Data/Metadata/ComicInfo.cs
@@ -0,0 +1,51 @@
+namespace API.Data.Metadata
+{
+ ///
+ /// A representation of a ComicInfo.xml file
+ ///
+ /// See reference of the loose spec here: https://github.com/Kussie/ComicInfoStandard/blob/main/ComicInfo.xsd
+ public class ComicInfo
+ {
+ public string Summary { get; set; }
+ public string Title { get; set; }
+ public string Series { get; set; }
+ public string Number { get; set; }
+ public string Volume { get; set; }
+ public string Notes { get; set; }
+ public string Genre { get; set; }
+ public int PageCount { get; set; }
+ // ReSharper disable once InconsistentNaming
+ public string LanguageISO { get; set; }
+ public string Web { get; set; }
+ public int Month { get; set; }
+ public int Year { get; set; }
+ ///
+ /// Rating based on the content. Think PG-13, R for movies
+ ///
+ public string AgeRating { get; set; }
+ ///
+ /// User's rating of the content
+ ///
+ public float UserRating { get; set; }
+
+ public string AlternateSeries { get; set; }
+ public string StoryArc { get; set; }
+ public string SeriesGroup { get; set; }
+ public string AlternativeSeries { get; set; }
+ public string AlternativeNumber { get; set; }
+
+
+ ///
+ /// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
+ ///
+ public string Writer { get; set; } // TODO: Validate if we should make this a list of writers
+ public string Penciller { get; set; }
+ public string Inker { get; set; }
+ public string Colorist { get; set; }
+ public string Letterer { get; set; }
+ public string CoverArtist { get; set; }
+ public string Editor { get; set; }
+ public string Publisher { get; set; }
+
+ }
+}
diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs b/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs
new file mode 100644
index 000000000..ad28c5839
--- /dev/null
+++ b/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs
@@ -0,0 +1,1045 @@
+//
+using System;
+using API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20211001113608_LastScannedLibrary")]
+ partial class LastScannedLibrary
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "5.0.8");
+
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Page")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserBookmark");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("SiteDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ b.ToTable("AppUserPreferences");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookScrollId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserProgresses");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("INTEGER");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserRating");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("Chapter");
+ });
+
+ modelBuilder.Entity("API.Entities.CollectionTag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id", "Promoted")
+ .IsUnique();
+
+ b.ToTable("CollectionTag");
+ });
+
+ modelBuilder.Entity("API.Entities.FolderPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("FolderPath");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.MangaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("MangaFile");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("ReadingList");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingListItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingListId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("ReadingListId");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("ReadingListItem");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalName")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
+ .IsUnique();
+
+ b.ToTable("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId")
+ .IsUnique();
+
+ b.HasIndex("Id", "SeriesId")
+ .IsUnique();
+
+ b.ToTable("SeriesMetadata");
+ });
+
+ modelBuilder.Entity("API.Entities.ServerSetting", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Key");
+
+ b.ToTable("ServerSetting");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("INTEGER");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("Volume");
+ });
+
+ modelBuilder.Entity("AppUserLibrary", b =>
+ {
+ b.Property("AppUsersId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibrariesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("AppUsersId", "LibrariesId");
+
+ b.HasIndex("LibrariesId");
+
+ b.ToTable("AppUserLibrary");
+ });
+
+ modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
+ {
+ b.Property("CollectionTagsId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("CollectionTagsId", "SeriesMetadatasId");
+
+ b.HasIndex("SeriesMetadatasId");
+
+ b.ToTable("CollectionTagSeriesMetadata");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("Bookmarks")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithOne("UserPreferences")
+ .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserProgress", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("Progresses")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("Ratings")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRole", b =>
+ {
+ b.HasOne("API.Entities.AppRole", "Role")
+ .WithMany("UserRoles")
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.AppUser", "User")
+ .WithMany("UserRoles")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Role");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.HasOne("API.Entities.Volume", "Volume")
+ .WithMany("Chapters")
+ .HasForeignKey("VolumeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Volume");
+ });
+
+ modelBuilder.Entity("API.Entities.FolderPath", b =>
+ {
+ b.HasOne("API.Entities.Library", "Library")
+ .WithMany("Folders")
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.MangaFile", b =>
+ {
+ b.HasOne("API.Entities.Chapter", "Chapter")
+ .WithMany("Files")
+ .HasForeignKey("ChapterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Chapter");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingList", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("ReadingLists")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingListItem", b =>
+ {
+ b.HasOne("API.Entities.Chapter", "Chapter")
+ .WithMany()
+ .HasForeignKey("ChapterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.ReadingList", "ReadingList")
+ .WithMany("Items")
+ .HasForeignKey("ReadingListId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.Series", "Series")
+ .WithMany()
+ .HasForeignKey("SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.Volume", "Volume")
+ .WithMany()
+ .HasForeignKey("VolumeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Chapter");
+
+ b.Navigation("ReadingList");
+
+ b.Navigation("Series");
+
+ b.Navigation("Volume");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.HasOne("API.Entities.Library", "Library")
+ .WithMany("Series")
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
+ {
+ b.HasOne("API.Entities.Series", "Series")
+ .WithOne("Metadata")
+ .HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.HasOne("API.Entities.Series", "Series")
+ .WithMany("Volumes")
+ .HasForeignKey("SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Series");
+ });
+
+ modelBuilder.Entity("AppUserLibrary", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("AppUsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.Library", null)
+ .WithMany()
+ .HasForeignKey("LibrariesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
+ {
+ b.HasOne("API.Entities.CollectionTag", null)
+ .WithMany()
+ .HasForeignKey("CollectionTagsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.SeriesMetadata", null)
+ .WithMany()
+ .HasForeignKey("SeriesMetadatasId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("API.Entities.AppRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Navigation("UserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Navigation("Bookmarks");
+
+ b.Navigation("Progresses");
+
+ b.Navigation("Ratings");
+
+ b.Navigation("ReadingLists");
+
+ b.Navigation("UserPreferences");
+
+ b.Navigation("UserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Navigation("Files");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Navigation("Folders");
+
+ b.Navigation("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingList", b =>
+ {
+ b.Navigation("Items");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.Navigation("Metadata");
+
+ b.Navigation("Volumes");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Navigation("Chapters");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.cs b/API/Data/Migrations/20211001113608_LastScannedLibrary.cs
new file mode 100644
index 000000000..eb1fdc5cb
--- /dev/null
+++ b/API/Data/Migrations/20211001113608_LastScannedLibrary.cs
@@ -0,0 +1,25 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace API.Data.Migrations
+{
+ public partial class LastScannedLibrary : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "LastScanned",
+ table: "Library",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "LastScanned",
+ table: "Library");
+ }
+ }
+}
diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs
index 38a09633e..21a9d930a 100644
--- a/API/Data/Migrations/DataContextModelSnapshot.cs
+++ b/API/Data/Migrations/DataContextModelSnapshot.cs
@@ -397,6 +397,9 @@ namespace API.Data.Migrations
b.Property("LastModified")
.HasColumnType("TEXT");
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
b.Property("Name")
.HasColumnType("TEXT");
diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs
index f1905eaa8..54c808d9c 100644
--- a/API/Data/Repositories/ChapterRepository.cs
+++ b/API/Data/Repositories/ChapterRepository.cs
@@ -1,7 +1,5 @@
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using API.DTOs;
using API.DTOs.Reader;
diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs
index 7f3544aee..caae93dd6 100644
--- a/API/Data/Repositories/LibraryRepository.cs
+++ b/API/Data/Repositories/LibraryRepository.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
@@ -11,6 +12,17 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories
{
+
+ [Flags]
+ public enum LibraryIncludes
+ {
+ None = 1,
+ Series = 2,
+ AppUser = 4,
+ Folders = 8,
+ // Ratings = 16
+ }
+
public class LibraryRepository : ILibraryRepository
{
private readonly DataContext _context;
@@ -58,7 +70,7 @@ namespace API.Data.Repositories
public async Task DeleteLibrary(int libraryId)
{
- var library = await GetLibraryForIdAsync(libraryId);
+ var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series);
_context.Library.Remove(library);
return await _context.SaveChangesAsync() > 0;
}
@@ -91,14 +103,37 @@ namespace API.Data.Repositories
.ToListAsync();
}
- public async Task GetLibraryForIdAsync(int libraryId)
+ public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes)
{
- return await _context.Library
- .Where(x => x.Id == libraryId)
- .Include(f => f.Folders)
- .Include(l => l.Series)
- .SingleAsync();
+
+ var query = _context.Library
+ .Where(x => x.Id == libraryId);
+
+ query = AddIncludesToQuery(query, includes);
+ return await query.SingleAsync();
}
+
+ private static IQueryable AddIncludesToQuery(IQueryable query, LibraryIncludes includeFlags)
+ {
+ if (includeFlags.HasFlag(LibraryIncludes.Folders))
+ {
+ query = query.Include(l => l.Folders);
+ }
+
+ if (includeFlags.HasFlag(LibraryIncludes.Series))
+ {
+ query = query.Include(l => l.Series);
+ }
+
+ if (includeFlags.HasFlag(LibraryIncludes.AppUser))
+ {
+ query = query.Include(l => l.AppUsers);
+ }
+
+ return query;
+ }
+
+
///
/// This returns a Library with all it's Series -> Volumes -> Chapters. This is expensive. Should only be called when needed.
///
@@ -106,7 +141,6 @@ namespace API.Data.Repositories
///
public async Task GetFullLibraryForIdAsync(int libraryId)
{
-
return await _context.Library
.Where(x => x.Id == libraryId)
.Include(f => f.Folders)
diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs
index 4f44bc943..fc9199ccb 100644
--- a/API/Data/Repositories/ReadingListRepository.cs
+++ b/API/Data/Repositories/ReadingListRepository.cs
@@ -53,7 +53,7 @@ namespace API.Data.Repositories
{
return await _context.ReadingList
.Where(r => r.Id == readingListId)
- .Include(r => r.Items)
+ .Include(r => r.Items.OrderBy(item => item.Order))
.SingleOrDefaultAsync();
}
diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs
index 3ed415859..67cd83276 100644
--- a/API/Data/Repositories/SeriesRepository.cs
+++ b/API/Data/Repositories/SeriesRepository.cs
@@ -1,15 +1,15 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using API.Comparators;
+using API.Data.Scanner;
using API.DTOs;
using API.DTOs.Filtering;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces.Repositories;
+using API.Services.Tasks;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
@@ -26,9 +26,9 @@ namespace API.Data.Repositories
_mapper = mapper;
}
- public void Add(Series series)
+ public void Attach(Series series)
{
- _context.Series.Add(series);
+ _context.Series.Attach(series);
}
public void Update(Series series)
@@ -36,19 +36,9 @@ namespace API.Data.Repositories
_context.Entry(series).State = EntityState.Modified;
}
- public async Task SaveAllAsync()
+ public void Remove(Series series)
{
- return await _context.SaveChangesAsync() > 0;
- }
-
- public bool SaveAll()
- {
- return _context.SaveChanges() > 0;
- }
-
- public async Task GetSeriesByNameAsync(string name)
- {
- return await _context.Series.SingleOrDefaultAsync(x => x.Name == name);
+ _context.Series.Remove(series);
}
public async Task DoesSeriesNameExistInLibrary(string name)
@@ -64,11 +54,6 @@ namespace API.Data.Repositories
.CountAsync() > 1;
}
- public Series GetSeriesByName(string name)
- {
- return _context.Series.SingleOrDefault(x => x.Name == name);
- }
-
public async Task> GetSeriesForLibraryIdAsync(int libraryId)
{
return await _context.Series
@@ -77,6 +62,43 @@ namespace API.Data.Repositories
.ToListAsync();
}
+ ///
+ /// Used for to
+ ///
+ ///
+ ///
+ public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams)
+ {
+ var query = _context.Series
+ .Where(s => s.LibraryId == libraryId)
+ .Include(s => s.Metadata)
+ .Include(s => s.Volumes)
+ .ThenInclude(v => v.Chapters)
+ .ThenInclude(c => c.Files)
+ .AsSplitQuery()
+ .OrderBy(s => s.SortName);
+
+ return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
+ }
+
+ ///
+ /// This is a heavy call. Returns all entities down to Files and Library and Series Metadata.
+ ///
+ ///
+ ///
+ public async Task GetFullSeriesForSeriesIdAsync(int seriesId)
+ {
+ return await _context.Series
+ .Where(s => s.Id == seriesId)
+ .Include(s => s.Metadata)
+ .Include(s => s.Library)
+ .Include(s => s.Volumes)
+ .ThenInclude(v => v.Chapters)
+ .ThenInclude(c => c.Files)
+ .AsSplitQuery()
+ .SingleOrDefaultAsync();
+ }
+
public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
{
var formats = filter.GetSqlFilter();
@@ -103,41 +125,12 @@ namespace API.Data.Repositories
.ToListAsync();
}
- public async Task> GetVolumesDtoAsync(int seriesId, int userId)
- {
- var volumes = await _context.Volume
- .Where(vol => vol.SeriesId == seriesId)
- .Include(vol => vol.Chapters)
- .OrderBy(volume => volume.Number)
- .ProjectTo(_mapper.ConfigurationProvider)
- .AsNoTracking()
- .ToListAsync();
-
- await AddVolumeModifiers(userId, volumes);
- SortSpecialChapters(volumes);
-
- return volumes;
- }
-
- private static void SortSpecialChapters(IEnumerable volumes)
- {
- var sorter = new NaturalSortComparer();
- foreach (var v in volumes.Where(vDto => vDto.Number == 0))
- {
- v.Chapters = v.Chapters.OrderBy(x => x.Range, sorter).ToList();
- }
- }
- public async Task> GetVolumes(int seriesId)
- {
- return await _context.Volume
- .Where(vol => vol.SeriesId == seriesId)
- .Include(vol => vol.Chapters)
- .ThenInclude(c => c.Files)
- .OrderBy(vol => vol.Number)
- .ToListAsync();
- }
+
+
+
+
public async Task GetSeriesDtoByIdAsync(int seriesId, int userId)
{
@@ -151,55 +144,8 @@ namespace API.Data.Repositories
return seriesList[0];
}
- public async Task GetVolumeAsync(int volumeId)
- {
- return await _context.Volume
- .Include(vol => vol.Chapters)
- .ThenInclude(c => c.Files)
- .SingleOrDefaultAsync(vol => vol.Id == volumeId);
- }
- public async Task GetVolumeDtoAsync(int volumeId)
- {
- return await _context.Volume
- .Where(vol => vol.Id == volumeId)
- .AsNoTracking()
- .ProjectTo(_mapper.ConfigurationProvider)
- .SingleAsync();
- }
-
- public async Task GetVolumeDtoAsync(int volumeId, int userId)
- {
- var volume = await _context.Volume
- .Where(vol => vol.Id == volumeId)
- .Include(vol => vol.Chapters)
- .ThenInclude(c => c.Files)
- .ProjectTo(_mapper.ConfigurationProvider)
- .SingleAsync(vol => vol.Id == volumeId);
-
- var volumeList = new List() {volume};
- await AddVolumeModifiers(userId, volumeList);
-
- return volumeList[0];
- }
-
- ///
- /// Returns all volumes that contain a seriesId in passed array.
- ///
- ///
- ///
- public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false)
- {
- var query = _context.Volume
- .Where(v => seriesIds.Contains(v.SeriesId));
-
- if (includeChapters)
- {
- query = query.Include(v => v.Chapters);
- }
- return await query.ToListAsync();
- }
public async Task DeleteSeriesAsync(int seriesId)
{
@@ -209,11 +155,12 @@ namespace API.Data.Repositories
return await _context.SaveChangesAsync() > 0;
}
- public async Task GetVolumeByIdAsync(int volumeId)
- {
- return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
- }
+ ///
+ /// Returns Volumes, Metadata, and Collection Tags
+ ///
+ ///
+ ///
public async Task GetSeriesByIdAsync(int seriesId)
{
return await _context.Series
@@ -244,7 +191,7 @@ namespace API.Data.Repositories
}
///
- /// This returns a list of tuples back for each series id passed
+ /// This returns a dictonary mapping seriesId -> list of chapters back for each series id passed
///
///
///
@@ -301,24 +248,7 @@ namespace API.Data.Repositories
.SingleOrDefaultAsync();
}
- private async Task AddVolumeModifiers(int userId, IReadOnlyCollection volumes)
- {
- var volIds = volumes.Select(s => s.Id);
- var userProgress = await _context.AppUserProgresses
- .Where(p => p.AppUserId == userId && volIds.Contains(p.VolumeId))
- .AsNoTracking()
- .ToListAsync();
- foreach (var v in volumes)
- {
- foreach (var c in v.Chapters)
- {
- c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
- }
-
- v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
- }
- }
///
/// Returns a list of Series that were added, ordered by Created desc
@@ -497,5 +427,63 @@ namespace API.Data.Repositories
.AsNoTracking()
.ToListAsync();
}
+
+ ///
+ /// Returns the number of series for a given library (or all libraries if libraryId is 0)
+ ///
+ /// Defaults to 0, library to restrict count to
+ ///
+ private async Task GetSeriesCount(int libraryId = 0)
+ {
+ if (libraryId > 0)
+ {
+ return await _context.Series
+ .Where(s => s.LibraryId == libraryId)
+ .CountAsync();
+ }
+ return await _context.Series.CountAsync();
+ }
+
+ ///
+ /// Returns the number of series that should be processed in parallel to optimize speed and memory. Minimum of 50
+ ///
+ /// Defaults to 0 meaning no library
+ ///
+ private async Task> GetChunkSize(int libraryId = 0)
+ {
+ // TODO: Think about making this bigger depending on number of files a user has in said library
+ // and number of cores and amount of memory. We can then make an optimal choice
+ var totalSeries = await GetSeriesCount(libraryId);
+ var procCount = Math.Max(Environment.ProcessorCount - 1, 1);
+
+ if (totalSeries < procCount * 2 || totalSeries < 50)
+ {
+ return new Tuple(totalSeries, totalSeries);
+ }
+
+
+ return new Tuple(totalSeries, Math.Max(totalSeries / procCount, 50));
+ }
+
+ public async Task GetChunkInfo(int libraryId = 0)
+ {
+ var (totalSeries, chunkSize) = await GetChunkSize(libraryId);
+
+ if (totalSeries == 0) return new Chunk()
+ {
+ TotalChunks = 0,
+ TotalSize = 0,
+ ChunkSize = 0
+ };
+
+ var totalChunks = Math.Max((int) Math.Ceiling((totalSeries * 1.0) / chunkSize), 1);
+
+ return new Chunk()
+ {
+ TotalSize = totalSeries,
+ ChunkSize = chunkSize,
+ TotalChunks = totalChunks
+ };
+ }
}
}
diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs
index 1eb0165bb..168b5a21e 100644
--- a/API/Data/Repositories/SettingsRepository.cs
+++ b/API/Data/Repositories/SettingsRepository.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using API.DTOs;
+using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces.Repositories;
@@ -35,6 +35,15 @@ namespace API.Data.Repositories
return _mapper.Map(settings);
}
+ public ServerSettingDto GetSettingsDto()
+ {
+ var settings = _context.ServerSetting
+ .Select(x => x)
+ .AsNoTracking()
+ .ToList();
+ return _mapper.Map(settings);
+ }
+
public Task GetSettingAsync(ServerSettingKey key)
{
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);
diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs
index 4e20039c7..ece1356fd 100644
--- a/API/Data/Repositories/UserRepository.cs
+++ b/API/Data/Repositories/UserRepository.cs
@@ -153,6 +153,16 @@ namespace API.Data.Repositories
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
}
+ public async Task> GetNonAdminUsersAsync()
+ {
+ return await _userManager.GetUsersInRoleAsync(PolicyConstants.PlebRole);
+ }
+
+ public async Task IsUserAdmin(AppUser user)
+ {
+ return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
+ }
+
public async Task GetUserRating(int seriesId, int userId)
{
return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
@@ -237,8 +247,8 @@ namespace API.Data.Repositories
Libraries = u.Libraries.Select(l => new LibraryDto
{
Name = l.Name,
- CoverImage = l.CoverImage,
Type = l.Type,
+ LastScanned = l.LastScanned,
Folders = l.Folders.Select(x => x.Path).ToList()
}).ToList()
})
diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs
index d991a928c..339da798d 100644
--- a/API/Data/Repositories/VolumeRepository.cs
+++ b/API/Data/Repositories/VolumeRepository.cs
@@ -1,9 +1,8 @@
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using API.Comparators;
using API.DTOs;
-using API.DTOs.Reader;
using API.Entities;
using API.Interfaces.Repositories;
using AutoMapper;
@@ -15,10 +14,17 @@ namespace API.Data.Repositories
public class VolumeRepository : IVolumeRepository
{
private readonly DataContext _context;
+ private readonly IMapper _mapper;
- public VolumeRepository(DataContext context)
+ public VolumeRepository(DataContext context, IMapper mapper)
{
_context = context;
+ _mapper = mapper;
+ }
+
+ public void Add(Volume volume)
+ {
+ _context.Volume.Add(volume);
}
public void Update(Volume volume)
@@ -26,6 +32,16 @@ namespace API.Data.Repositories
_context.Entry(volume).State = EntityState.Modified;
}
+ public void Remove(Volume volume)
+ {
+ _context.Volume.Remove(volume);
+ }
+
+ ///
+ /// Returns a list of non-tracked files for a given volume.
+ ///
+ ///
+ ///
public async Task> GetFilesForVolume(int volumeId)
{
return await _context.Chapter
@@ -36,6 +52,11 @@ namespace API.Data.Repositories
.ToListAsync();
}
+ ///
+ /// Returns the cover image file for the given volume
+ ///
+ ///
+ ///
public async Task GetVolumeCoverImageAsync(int volumeId)
{
return await _context.Volume
@@ -45,6 +66,11 @@ namespace API.Data.Repositories
.SingleOrDefaultAsync();
}
+ ///
+ /// Returns all chapter Ids belonging to a list of Volume Ids
+ ///
+ ///
+ ///
public async Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds)
{
return await _context.Chapter
@@ -52,5 +78,131 @@ namespace API.Data.Repositories
.Select(c => c.Id)
.ToListAsync();
}
+
+ ///
+ /// Returns all volumes that contain a seriesId in passed array.
+ ///
+ ///
+ ///
+ public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false)
+ {
+ var query = _context.Volume
+ .Where(v => seriesIds.Contains(v.SeriesId));
+
+ if (includeChapters)
+ {
+ query = query.Include(v => v.Chapters);
+ }
+ return await query.ToListAsync();
+ }
+
+ ///
+ /// Returns an individual Volume including Chapters and Files and Reading Progress for a given volumeId
+ ///
+ ///
+ ///
+ ///
+ public async Task GetVolumeDtoAsync(int volumeId, int userId)
+ {
+ var volume = await _context.Volume
+ .Where(vol => vol.Id == volumeId)
+ .Include(vol => vol.Chapters)
+ .ThenInclude(c => c.Files)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .SingleAsync(vol => vol.Id == volumeId);
+
+ var volumeList = new List() {volume};
+ await AddVolumeModifiers(userId, volumeList);
+
+ return volumeList[0];
+ }
+
+ ///
+ /// Returns the full Volumes including Chapters and Files for a given series
+ ///
+ ///
+ ///
+ public async Task> GetVolumes(int seriesId)
+ {
+ return await _context.Volume
+ .Where(vol => vol.SeriesId == seriesId)
+ .Include(vol => vol.Chapters)
+ .ThenInclude(c => c.Files)
+ .OrderBy(vol => vol.Number)
+ .ToListAsync();
+ }
+
+ ///
+ /// Returns a single volume with Chapter and Files
+ ///
+ ///
+ ///
+ public async Task GetVolumeAsync(int volumeId)
+ {
+ return await _context.Volume
+ .Include(vol => vol.Chapters)
+ .ThenInclude(c => c.Files)
+ .SingleOrDefaultAsync(vol => vol.Id == volumeId);
+ }
+
+
+ ///
+ /// Returns all volumes for a given series with progress information attached. Includes all Chapters as well.
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetVolumesDtoAsync(int seriesId, int userId)
+ {
+ var volumes = await _context.Volume
+ .Where(vol => vol.SeriesId == seriesId)
+ .Include(vol => vol.Chapters)
+ .OrderBy(volume => volume.Number)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .AsNoTracking()
+ .ToListAsync();
+
+ await AddVolumeModifiers(userId, volumes);
+ SortSpecialChapters(volumes);
+
+ return volumes;
+ }
+
+ public async Task GetVolumeByIdAsync(int volumeId)
+ {
+ return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
+ }
+
+
+ private static void SortSpecialChapters(IEnumerable volumes)
+ {
+ var sorter = new NaturalSortComparer();
+ foreach (var v in volumes.Where(vDto => vDto.Number == 0))
+ {
+ v.Chapters = v.Chapters.OrderBy(x => x.Range, sorter).ToList();
+ }
+ }
+
+
+ private async Task AddVolumeModifiers(int userId, IReadOnlyCollection volumes)
+ {
+ var volIds = volumes.Select(s => s.Id);
+ var userProgress = await _context.AppUserProgresses
+ .Where(p => p.AppUserId == userId && volIds.Contains(p.VolumeId))
+ .AsNoTracking()
+ .ToListAsync();
+
+ foreach (var v in volumes)
+ {
+ foreach (var c in v.Chapters)
+ {
+ c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
+ }
+
+ v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
+ }
+ }
+
+
}
}
diff --git a/API/Data/Scanner/Chunk.cs b/API/Data/Scanner/Chunk.cs
new file mode 100644
index 000000000..9a9e04f5c
--- /dev/null
+++ b/API/Data/Scanner/Chunk.cs
@@ -0,0 +1,21 @@
+namespace API.Data.Scanner
+{
+ ///
+ /// Represents a set of Entities which is broken up and iterated on
+ ///
+ public class Chunk
+ {
+ ///
+ /// Total number of entities
+ ///
+ public int TotalSize { get; set; }
+ ///
+ /// Size of each chunk to iterate over
+ ///
+ public int ChunkSize { get; set; }
+ ///
+ /// Total chunks to iterate over
+ ///
+ public int TotalChunks { get; set; }
+ }
+}
diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs
index f264168ae..6b62089d0 100644
--- a/API/Data/Seed.cs
+++ b/API/Data/Seed.cs
@@ -49,6 +49,8 @@ namespace API.Data
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
+ new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
+ new () {Key = ServerSettingKey.BaseUrl, Value = "/"},
};
foreach (var defaultSetting in defaultSettings)
diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs
index 08fffa540..b3e0a5dfd 100644
--- a/API/Entities/AppUserProgress.cs
+++ b/API/Entities/AppUserProgress.cs
@@ -1,6 +1,5 @@
using System;
-using System.ComponentModel.DataAnnotations;
using API.Entities.Interfaces;
namespace API.Entities
diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs
index fab4a7cba..997d0a33e 100644
--- a/API/Entities/Enums/ServerSettingKey.cs
+++ b/API/Entities/Enums/ServerSettingKey.cs
@@ -20,6 +20,10 @@ namespace API.Entities.Enums
AllowStatCollection = 6,
[Description("EnableOpds")]
EnableOpds = 7,
+ [Description("EnableAuthentication")]
+ EnableAuthentication = 8,
+ [Description("BaseUrl")]
+ BaseUrl = 9
}
}
diff --git a/API/Entities/FolderPath.cs b/API/Entities/FolderPath.cs
index dab3d86cd..267564fe8 100644
--- a/API/Entities/FolderPath.cs
+++ b/API/Entities/FolderPath.cs
@@ -8,12 +8,12 @@ namespace API.Entities
public int Id { get; set; }
public string Path { get; set; }
///
- /// Used when scanning to see if we can skip if nothing has changed.
+ /// Used when scanning to see if we can skip if nothing has changed. (not implemented)
///
public DateTime LastScanned { get; set; }
-
+
// Relationship
public Library Library { get; set; }
public int LibraryId { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs
index faf95e149..c77fb68dd 100644
--- a/API/Entities/Library.cs
+++ b/API/Entities/Library.cs
@@ -13,9 +13,13 @@ namespace API.Entities
public LibraryType Type { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
+ ///
+ /// Last time Library was scanned
+ ///
+ public DateTime LastScanned { get; set; }
public ICollection Folders { get; set; }
public ICollection AppUsers { get; set; }
public ICollection Series { get; set; }
-
+
}
-}
\ No newline at end of file
+}
diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs
index 72c620ce9..2865178c7 100644
--- a/API/Entities/MangaFile.cs
+++ b/API/Entities/MangaFile.cs
@@ -38,5 +38,13 @@ namespace API.Entities
{
return File.GetLastWriteTime(FilePath) > LastModified;
}
+
+ ///
+ /// Updates the Last Modified time of the underlying file
+ ///
+ public void UpdateLastModified()
+ {
+ LastModified = File.GetLastWriteTime(FilePath);
+ }
}
}
diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs
index 899e52bfd..5b7bc86bd 100644
--- a/API/Entities/Series.cs
+++ b/API/Entities/Series.cs
@@ -33,7 +33,7 @@ namespace API.Entities
///
/// Summary information related to the Series
///
- public string Summary { get; set; } // TODO: Migrate into SeriesMetdata (with Metadata update)
+ public string Summary { get; set; } // NOTE: Migrate into SeriesMetdata (with Metadata update)
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
///
diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs
index 3be7a4d6a..f4f0076db 100644
--- a/API/Entities/Volume.cs
+++ b/API/Entities/Volume.cs
@@ -8,6 +8,9 @@ namespace API.Entities
public class Volume : IEntityDate
{
public int Id { get; set; }
+ ///
+ /// A String representation of the volume number. Allows for floats
+ ///
public string Name { get; set; }
public int Number { get; set; }
public IList Chapters { get; set; }
diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs
index c08555c69..cd5a621fc 100644
--- a/API/Extensions/ApplicationServiceExtensions.cs
+++ b/API/Extensions/ApplicationServiceExtensions.cs
@@ -36,12 +36,13 @@ namespace API.Extensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddSqLite(config, env);
services.AddLogging(config);
- services.AddSignalR();
+ services.AddSignalR(opt => opt.EnableDetailedErrors = true);
}
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
diff --git a/API/Extensions/DirectoryInfoExtensions.cs b/API/Extensions/DirectoryInfoExtensions.cs
index 892c690b3..b92901046 100644
--- a/API/Extensions/DirectoryInfoExtensions.cs
+++ b/API/Extensions/DirectoryInfoExtensions.cs
@@ -76,7 +76,8 @@ namespace API.Extensions
directoryIndex++;
}
- foreach (var subDirectory in directory.EnumerateDirectories())
+ var sort = new NaturalSortComparer();
+ foreach (var subDirectory in directory.EnumerateDirectories().OrderBy(d => d.FullName, sort))
{
FlattenDirectory(root, subDirectory, ref directoryIndex);
}
diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs
index 80b52f18f..975cbde5f 100644
--- a/API/Extensions/HttpExtensions.cs
+++ b/API/Extensions/HttpExtensions.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System.IO;
+using System.Linq;
using System.Text;
using System.Text.Json;
using API.Helpers;
@@ -41,8 +42,9 @@ namespace API.Extensions
public static void AddCacheHeader(this HttpResponse response, string filename)
{
if (filename == null || filename.Length <= 0) return;
+ var hashContent = filename + File.GetLastWriteTimeUtc(filename);
using var sha1 = new System.Security.Cryptography.SHA256CryptoServiceProvider();
- response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(filename)).Select(x => x.ToString("X2"))));
+ response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
}
}
diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs
index 03445ccb2..ff1a30e34 100644
--- a/API/Helpers/AutoMapperProfiles.cs
+++ b/API/Helpers/AutoMapperProfiles.cs
@@ -3,6 +3,7 @@ using System.Linq;
using API.DTOs;
using API.DTOs.Reader;
using API.DTOs.ReadingLists;
+using API.DTOs.Settings;
using API.Entities;
using API.Helpers.Converters;
using AutoMapper;
diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs
index dbd13ab9e..86ed6235e 100644
--- a/API/Helpers/Converters/ServerSettingConverter.cs
+++ b/API/Helpers/Converters/ServerSettingConverter.cs
@@ -1,5 +1,5 @@
using System.Collections.Generic;
-using API.DTOs;
+using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using AutoMapper;
@@ -36,6 +36,12 @@ namespace API.Helpers.Converters
case ServerSettingKey.EnableOpds:
destination.EnableOpds = bool.Parse(row.Value);
break;
+ case ServerSettingKey.EnableAuthentication:
+ destination.EnableAuthentication = bool.Parse(row.Value);
+ break;
+ case ServerSettingKey.BaseUrl:
+ destination.BaseUrl = row.Value;
+ break;
}
}
diff --git a/API/Interfaces/Repositories/ILibraryRepository.cs b/API/Interfaces/Repositories/ILibraryRepository.cs
index 4d9b03fe4..1ba6ac910 100644
--- a/API/Interfaces/Repositories/ILibraryRepository.cs
+++ b/API/Interfaces/Repositories/ILibraryRepository.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+using API.Data.Repositories;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
@@ -13,7 +14,7 @@ namespace API.Interfaces.Repositories
void Delete(Library library);
Task