diff --git a/.github/workflows/nightly-docker.yml b/.github/workflows/nightly-docker.yml
new file mode 100644
index 000000000..5129ba8b2
--- /dev/null
+++ b/.github/workflows/nightly-docker.yml
@@ -0,0 +1,33 @@
+name: ci
+
+on:
+ push:
+ branches:
+ - 'master'
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ -
+ name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
+ -
+ name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ -
+ name: Build and push
+ id: docker_build
+ uses: docker/build-push-action@v2
+ with:
+ push: true
+ tags: user/app:latest
+ -
+ name: Image digest
+ run: echo ${{ steps.docker_build.outputs.digest }}
\ No newline at end of file
diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs
index fa932dfeb..39437a2a1 100644
--- a/API.Tests/Parser/MangaParserTests.cs
+++ b/API.Tests/Parser/MangaParserTests.cs
@@ -61,7 +61,6 @@ namespace API.Tests.Parser
[InlineData("[Hidoi]_Amaenaideyo_MS_vol01_chp02.rar", "1")]
[InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")]
[InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")]
-
public void ParseVolumeTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
@@ -137,6 +136,7 @@ namespace API.Tests.Parser
[InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "Okusama wa Shougakusei")]
[InlineData("VanDread-v01-c001[MD].zip", "VanDread")]
[InlineData("Momo The Blood Taker - Chapter 027 Violent Emotion.cbz", "Momo The Blood Taker")]
+ [InlineData("Green Worldz - Chapter 112 Final Chapter (End).cbz", "Green Worldz")]
public void ParseSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
@@ -225,6 +225,7 @@ namespace API.Tests.Parser
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)]
[InlineData("Ani-Hina Art Collection.cbz", true)]
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", true)]
+ [InlineData("Yuki Merry - 4-Komga Anthology", true)]
public void ParseMangaSpecialTest(string input, bool expected)
{
Assert.Equal(expected, !string.IsNullOrEmpty(API.Parser.Parser.ParseMangaSpecial(input)));
diff --git a/API/API.csproj b/API/API.csproj
index 8c96cb129..458830ca1 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -12,6 +12,23 @@
../favicon.ico
+
+
+ Kavita
+ kareadita.github.io
+ Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kareadita.github.io (GNU General Public v3)
+
+
+ 0.4.1
+ $(Configuration)-dev
+
+ false
+ false
+ false
+
+ False
+
+
@@ -33,6 +50,7 @@
+
all
@@ -65,4 +83,8 @@
<_ContentIncludedByDefault Remove="logs\kavita.json" />
+
+
+
+
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index 0654f7d70..cde3d9c0f 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
@@ -148,6 +149,7 @@ namespace API.Controllers
public async Task>> GetRecentlyAdded(int libraryId = 0, int limit = 20)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+ if (user == null) return Ok(Array.Empty());
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAdded(user.Id, libraryId, limit));
}
@@ -155,6 +157,7 @@ namespace API.Controllers
public async Task>> GetInProgress(int libraryId = 0, int limit = 20)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+ if (user == null) return Ok(Array.Empty());
return Ok(await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, limit));
}
diff --git a/API/Dockerfile b/API/Dockerfile
index d813139f8..4289aaa3d 100644
--- a/API/Dockerfile
+++ b/API/Dockerfile
@@ -1,20 +1,40 @@
-FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
-WORKDIR /app
-EXPOSE 80
-EXPOSE 443
+#This Dockerfile pulls the latest git commit and builds Kavita from source
+FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS builder
-FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
-WORKDIR /src
-COPY ["API/API.csproj", "API/"]
-RUN dotnet restore "API/API.csproj"
-COPY . .
-WORKDIR "/src/API"
-RUN dotnet build "API.csproj" -c Release -o /app/build
+ENV DEBIAN_FRONTEND=noninteractive
+ARG TARGETPLATFORM
-FROM build AS publish
-RUN dotnet publish "API.csproj" -c Release -o /app/publish
+#Installs nodejs and npm
+RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
+ && apt-get install -y nodejs \
+ && rm -rf /var/lib/apt/lists/*
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "API.dll"]
+#Builds app based on platform
+COPY build_target.sh /build_target.sh
+RUN /build_target.sh
+
+#Production image
+FROM ubuntu:focal
+
+#Move the output files to where they need to be
+COPY --from=builder /Projects/Kavita/_output/build/Kavita /kavita
+
+#Installs program dependencies
+RUN apt-get update \
+ && apt-get install -y libicu-dev libssl1.1 pwgen \
+ && rm -rf /var/lib/apt/lists/*
+
+#Creates the manga storage directory
+RUN mkdir /manga /kavita/data
+
+RUN cp /kavita/appsettings.Development.json /kavita/appsettings.json \
+ && sed -i 's/Data source=kavita.db/Data source=data\/kavita.db/g' /kavita/appsettings.json
+
+COPY entrypoint.sh /entrypoint.sh
+
+EXPOSE 5000
+
+WORKDIR /kavita
+
+ENTRYPOINT ["/bin/bash"]
+CMD ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs
index 29d584954..71e01785c 100644
--- a/API/Parser/Parser.cs
+++ b/API/Parser/Parser.cs
@@ -92,7 +92,7 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Momo The Blood Taker - Chapter 027 Violent Emotion.cbz
new Regex(
- @"(?.*) (\b|_|-)(?:chapter)",
+ @"(?.*)(\b|_|-|\s)(?:chapter)(\b|_|-|\s)\d",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
new Regex(
@@ -101,7 +101,7 @@ namespace API.Parser
//Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
// due to duplicate version identifiers in file.
new Regex(
- @"(?.*)(v|s)\d+(-\d+)?(_| )",
+ @"(?.*)(v|s)\d+(-\d+)?(_|\s)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex(
@@ -121,7 +121,7 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Tonikaku Kawaii (Ch 59-67) (Ongoing)
new Regex(
- @"(?.*)( |_)\((c |ch |chapter )",
+ @"(?.*)(\s|_)\((c\s|ch\s|chapter\s)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Black Bullet (This is very loose, keep towards bottom)
new Regex(
@@ -364,7 +364,7 @@ namespace API.Parser
{
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
new Regex(
- @"(?Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories)",
+ @"(?Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories|(?
+ {
+ options.Dsn = "https://40f4e7b49c094172a6f99d61efb2740f@o641015.ingest.sentry.io/5757423";
+ options.MaxBreadcrumbs = 200;
+ options.AttachStacktrace = true;
+ options.Debug = false;
+ options.SendDefaultPii = false;
+ options.DiagnosticLevel = SentryLevel.Debug;
+ options.ShutdownTimeout = TimeSpan.FromSeconds(5);
+ options.Release = BuildInfo.Version.ToString();
+ options.AddExceptionFilterForType();
+ options.AddExceptionFilterForType();
+ options.AddExceptionFilterForType();
+ options.ConfigureScope(scope =>
+ {
+ scope.User = new User()
+ {
+ Id = HashUtil.AnonymousToken()
+ };
+ scope.Contexts.App.Name = BuildInfo.AppName;
+ scope.Contexts.App.Version = BuildInfo.Version.ToString();
+ scope.Contexts.App.StartTime = DateTime.UtcNow;
+ scope.Contexts.App.Hash = HashUtil.AnonymousToken();
+ scope.Contexts.App.Build = BuildInfo.Release;
+ scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
+ scope.SetTag("branch", BuildInfo.Branch);
+ });
+
+ });
webBuilder.UseStartup();
});
}
diff --git a/API/Startup.cs b/API/Startup.cs
index 07aeb09d5..f030fda92 100644
--- a/API/Startup.cs
+++ b/API/Startup.cs
@@ -1,12 +1,14 @@
using System;
using System.IO.Compression;
using System.Linq;
+using System.Reflection;
using API.Extensions;
using API.Interfaces;
using API.Middleware;
using API.Services;
using Hangfire;
using Hangfire.MemoryStorage;
+using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
@@ -131,7 +133,7 @@ namespace API
applicationLifetime.ApplicationStopping.Register(OnShutdown);
applicationLifetime.ApplicationStarted.Register(() =>
{
- Console.WriteLine("Kavita - v0.4.1");
+ Console.WriteLine("Kavita - v" + BuildInfo.Version);
});
// Any services that should be bootstrapped go here
@@ -140,10 +142,10 @@ namespace API
private void OnShutdown()
{
- Console.WriteLine("Server is shutting down. Going to dispose Hangfire");
- //this code is called when the application stops
+ Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs...");
TaskScheduler.Client.Dispose();
System.Threading.Thread.Sleep(1000);
+ Console.WriteLine("You may now close the application window.");
}
}
}
diff --git a/Kavita.Common/EnvironmentInfo/BuildInfo.cs b/Kavita.Common/EnvironmentInfo/BuildInfo.cs
new file mode 100644
index 000000000..32b3e60d8
--- /dev/null
+++ b/Kavita.Common/EnvironmentInfo/BuildInfo.cs
@@ -0,0 +1,56 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace Kavita.Common.EnvironmentInfo
+{
+ public static class BuildInfo
+ {
+ static BuildInfo()
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+
+ Version = assembly.GetName().Version;
+
+ var attributes = assembly.GetCustomAttributes(true);
+
+ Branch = "unknown";
+
+ var config = attributes.OfType().FirstOrDefault();
+ if (config != null)
+ {
+ Branch = config.Configuration;
+ }
+
+ Release = $"{Version}-{Branch}";
+ }
+
+ public static string AppName { get; } = "Kavita";
+
+ public static Version Version { get; }
+ public static string Branch { get; }
+ public static string Release { get; }
+
+ public static DateTime BuildDateTime
+ {
+ get
+ {
+ var fileLocation = Assembly.GetCallingAssembly().Location;
+ return new FileInfo(fileLocation).LastWriteTimeUtc;
+ }
+ }
+
+ public static bool IsDebug
+ {
+ get
+ {
+#if DEBUG
+ return true;
+#else
+ return false;
+#endif
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs
new file mode 100644
index 000000000..f93e4781c
--- /dev/null
+++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace Kavita.Common.EnvironmentInfo
+{
+ public class OsInfo : IOsInfo
+ {
+ public static Os Os { get; }
+
+ public static bool IsNotWindows => !IsWindows;
+ public static bool IsLinux => Os == Os.Linux || Os == Os.LinuxMusl || Os == Os.Bsd;
+ public static bool IsOsx => Os == Os.Osx;
+ public static bool IsWindows => Os == Os.Windows;
+
+ // this needs to not be static so we can mock it
+ public bool IsDocker { get; }
+
+ public string Version { get; }
+ public string Name { get; }
+ public string FullName { get; }
+
+ static OsInfo()
+ {
+ var platform = Environment.OSVersion.Platform;
+
+ switch (platform)
+ {
+ case PlatformID.Win32NT:
+ {
+ Os = Os.Windows;
+ break;
+ }
+
+ case PlatformID.MacOSX:
+ case PlatformID.Unix:
+ {
+ Os = GetPosixFlavour();
+ break;
+ }
+ }
+ }
+
+ public OsInfo(IEnumerable versionAdapters)
+ {
+ OsVersionModel osInfo = null;
+
+ foreach (var osVersionAdapter in versionAdapters.Where(c => c.Enabled))
+ {
+ try
+ {
+ osInfo = osVersionAdapter.Read();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("Couldn't get OS Version info: " + e.Message);
+ }
+
+ if (osInfo != null)
+ {
+ break;
+ }
+ }
+
+ if (osInfo != null)
+ {
+ Name = osInfo.Name;
+ Version = osInfo.Version;
+ FullName = osInfo.FullName;
+ }
+ else
+ {
+ Name = Os.ToString();
+ FullName = Name;
+ }
+
+ if (IsLinux && File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/"))
+ {
+ IsDocker = true;
+ }
+ }
+
+ private static Os GetPosixFlavour()
+ {
+ var output = RunAndCapture("uname", "-s");
+
+ if (output.StartsWith("Darwin"))
+ {
+ return Os.Osx;
+ }
+ else if (output.Contains("BSD"))
+ {
+ return Os.Bsd;
+ }
+ else
+ {
+#if ISMUSL
+ return Os.LinuxMusl;
+#else
+ return Os.Linux;
+#endif
+ }
+ }
+
+ private static string RunAndCapture(string filename, string args)
+ {
+ var p = new Process
+ {
+ StartInfo =
+ {
+ FileName = filename,
+ Arguments = args,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true
+ }
+ };
+
+ p.Start();
+
+ // To avoid deadlocks, always read the output stream first and then wait.
+ var output = p.StandardOutput.ReadToEnd();
+ p.WaitForExit(1000);
+
+ return output;
+ }
+ }
+
+ public interface IOsInfo
+ {
+ string Version { get; }
+ string Name { get; }
+ string FullName { get; }
+
+ bool IsDocker { get; }
+ }
+
+ public enum Os
+ {
+ Windows,
+ Linux,
+ Osx,
+ LinuxMusl,
+ Bsd
+ }
+}
\ No newline at end of file
diff --git a/Kavita.Common/EnvironmentInfo/IOsVersionAdapter.cs b/Kavita.Common/EnvironmentInfo/IOsVersionAdapter.cs
new file mode 100644
index 000000000..fbf4403d3
--- /dev/null
+++ b/Kavita.Common/EnvironmentInfo/IOsVersionAdapter.cs
@@ -0,0 +1,8 @@
+namespace Kavita.Common.EnvironmentInfo
+{
+ public interface IOsVersionAdapter
+ {
+ bool Enabled { get; }
+ OsVersionModel Read();
+ }
+}
\ No newline at end of file
diff --git a/Kavita.Common/EnvironmentInfo/OsVersionModel.cs b/Kavita.Common/EnvironmentInfo/OsVersionModel.cs
new file mode 100644
index 000000000..9e91daa18
--- /dev/null
+++ b/Kavita.Common/EnvironmentInfo/OsVersionModel.cs
@@ -0,0 +1,27 @@
+namespace Kavita.Common.EnvironmentInfo
+{
+ public class OsVersionModel
+ {
+ public OsVersionModel(string name, string version, string fullName = null)
+ {
+ Name = Trim(name);
+ Version = Trim(version);
+
+ if (string.IsNullOrWhiteSpace(fullName))
+ {
+ fullName = $"{Name} {Version}";
+ }
+
+ FullName = Trim(fullName);
+ }
+
+ private static string Trim(string source)
+ {
+ return source.Trim().Trim('"', '\'');
+ }
+
+ public string Name { get; }
+ public string FullName { get; }
+ public string Version { get; }
+ }
+}
\ No newline at end of file
diff --git a/Kavita.Common/HashUtil.cs b/Kavita.Common/HashUtil.cs
new file mode 100644
index 000000000..ff02d7d32
--- /dev/null
+++ b/Kavita.Common/HashUtil.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Text;
+
+namespace Kavita.Common
+{
+ public static class HashUtil
+ {
+ public static string CalculateCrc(string input)
+ {
+ uint mCrc = 0xffffffff;
+ byte[] bytes = Encoding.UTF8.GetBytes(input);
+ foreach (byte myByte in bytes)
+ {
+ mCrc ^= (uint)myByte << 24;
+ for (var i = 0; i < 8; i++)
+ {
+ if ((Convert.ToUInt32(mCrc) & 0x80000000) == 0x80000000)
+ {
+ mCrc = (mCrc << 1) ^ 0x04C11DB7;
+ }
+ else
+ {
+ mCrc <<= 1;
+ }
+ }
+ }
+
+ return $"{mCrc:x8}";
+ }
+
+ public static string AnonymousToken()
+ {
+ var seed = $"{Environment.ProcessorCount}_{Environment.OSVersion.Platform}_{Environment.MachineName}_{Environment.UserName}";
+ return HashUtil.CalculateCrc(seed);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj
new file mode 100644
index 000000000..736bcdb7a
--- /dev/null
+++ b/Kavita.Common/Kavita.Common.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net5.0
+ kareadita.github.io
+ Kavita
+ 0.4.1
+ en
+
+
+
+
+
+
+
+
+ D:\Program Files\JetBrains\JetBrains Rider 2020.3.2\lib\ReSharperHost\TestRunner\netcoreapp2.0\JetBrains.ReSharper.TestRunner.Merged.dll
+
+
+ ..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.5\Microsoft.Win32.Registry.dll
+
+
+
+
diff --git a/Kavita.sln b/Kavita.sln
index 74927a34f..e49484b02 100644
--- a/Kavita.sln
+++ b/Kavita.sln
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{1
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "API.Tests\API.Tests.csproj", "{6F7910F2-1B95-4570-A490-519C8935B9D1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common", "Kavita.Common\Kavita.Common.csproj", "{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -44,5 +46,17 @@ Global
{6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x64.Build.0 = Release|Any CPU
{6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x86.ActiveCfg = Release|Any CPU
{6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x86.Build.0 = Release|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|x64.Build.0 = Debug|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|x86.Build.0 = Debug|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x64.ActiveCfg = Release|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x64.Build.0 = Release|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.ActiveCfg = Release|Any CPU
+ {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 021232415..83229889c 100644
--- a/README.md
+++ b/README.md
@@ -10,12 +10,13 @@ your manga collection with your friends and family!
## Goals:
-* Serve up Manga (cbr, cbz, zip/rar, raw images) and Books (epub, mobi, azw, djvu, pdf)
-* Provide Reader for Manga and Books (Light Novels) via web app that is responsive
-* Provide customization themes (server installed) for web app
+* Serve up Manga/Webtoons/Comics (cbr, cbz, zip/rar, raw images) and Books (epub, mobi, azw, djvu, pdf)
+* Provide Readers via web app that is responsive
+* Provide a dark theme for web app
* Provide hooks into metadata providers to fetch Manga data
* Metadata should allow for collections, want to read integration from 3rd party services, genres.
* Ability to manage users, access, and ratings
+* Ability to sync ratings and reviews to external services
## How to Build
- Ensure you've cloned Kavita-webui. You should have Projects/Kavita and Projects/Kavita-webui