diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index 8ab7b5dc..71ec1993 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -109,18 +109,18 @@ namespace Kyoo.Models if (!ep.Show.IsMovie) { if (ep.EpisodeNumber > 1) - previous = await library.Get(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); + previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); else if (ep.SeasonNumber > 1) { int count = await library.GetCount(x => x.ShowID == ep.ShowID && x.SeasonNumber == ep.SeasonNumber - 1); - previous = await library.Get(ep.ShowID, ep.SeasonNumber - 1, count); + previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber - 1, count); } if (ep.EpisodeNumber >= await library.GetCount(x => x.SeasonID == ep.SeasonID)) - next = await library.Get(ep.ShowID, ep.SeasonNumber + 1, 1); + next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber + 1, 1); else - next = await library.Get(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); + next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); } return new WatchItem(ep.ID, diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index fc384904..032dff38 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -86,7 +86,7 @@ namespace Kyoo.Controllers /// public override async Task GetOrDefault(int id) { - Episode ret = await base.Get(id); + Episode ret = await base.GetOrDefault(id); if (ret != null) ret.ShowSlug = await _shows.GetSlug(ret.ShowID); return ret; @@ -111,9 +111,9 @@ namespace Kyoo.Controllers } /// - public override async Task GetOrDefault(Expression> predicate) + public override async Task GetOrDefault(Expression> where) { - Episode ret = await base.Get(predicate); + Episode ret = await base.GetOrDefault(where); if (ret != null) ret.ShowSlug = await _shows.GetSlug(ret.ShowID); return ret; diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 9d96b23f..44fd81d3 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -92,15 +92,15 @@ namespace Kyoo.Controllers } /// - public override async Task Get(int id) + public override async Task GetOrDefault(int id) { return id > 0 - ? new LibraryItem(await _shows.Value.Get(id)) - : new LibraryItem(await _collections.Value.Get(-id)); + ? new LibraryItem(await _shows.Value.GetOrDefault(id)) + : new LibraryItem(await _collections.Value.GetOrDefault(-id)); } /// - public override Task Get(string slug) + public override Task GetOrDefault(string slug) { throw new InvalidOperationException("You can't get a library item by a slug."); } @@ -189,7 +189,7 @@ namespace Kyoo.Controllers where, sort, limit); - if (!items.Any() && await _libraries.Value.Get(id) == null) + if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null) throw new ItemNotFound(); return items; } @@ -204,7 +204,7 @@ namespace Kyoo.Controllers where, sort, limit); - if (!items.Any() && await _libraries.Value.Get(slug) == null) + if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null) throw new ItemNotFound(); return items; } diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 22ee9a64..a2dce413 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -98,9 +98,9 @@ namespace Kyoo.Controllers } /// - public override async Task Get(Expression> predicate) + public override async Task Get(Expression> where) { - Season ret = await base.Get(predicate); + Season ret = await base.Get(where); ret.ShowSlug = await _shows.GetSlug(ret.ShowID); return ret; } diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 4d3c725d..55ddb427 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -36,13 +36,13 @@ namespace Kyoo.Controllers /// - public override Task Get(string slug) + Task IRepository.Get(string slug) { - return Get(slug, StreamType.Unknown); + return Get(slug); } /// - public async Task Get(string slug, StreamType type) + public async Task Get(string slug, StreamType type = StreamType.Unknown) { Track ret = await GetOrDefault(slug, type); if (ret == null) @@ -51,7 +51,7 @@ namespace Kyoo.Controllers } /// - public Task GetOrDefault(string slug, StreamType type) + public Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) { Match match = Regex.Match(slug, @"(?.*)-s(?\d+)e(?\d+)(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..*)?"); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 713d807c..2d624b9c 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -113,10 +113,8 @@ - - + + diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index 7d76a3d4..577e9903 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; @@ -10,28 +11,71 @@ using Npgsql; namespace Kyoo { + /// + /// The database handle used for all local repositories. + /// + /// + /// It should not be used directly, to access the database use a or repositories. + /// public class DatabaseContext : DbContext { - public DatabaseContext(DbContextOptions options) : base(options) - { - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - ChangeTracker.LazyLoadingEnabled = false; - } - + /// + /// All libraries of Kyoo. See . + /// public DbSet Libraries { get; set; } + /// + /// All collections of Kyoo. See . + /// public DbSet Collections { get; set; } + /// + /// All shows of Kyoo. See . + /// public DbSet Shows { get; set; } + /// + /// All seasons of Kyoo. See . + /// public DbSet Seasons { get; set; } + /// + /// All episodes of Kyoo. See . + /// public DbSet Episodes { get; set; } + /// + /// All tracks of Kyoo. See . + /// public DbSet Tracks { get; set; } + /// + /// All genres of Kyoo. See . + /// public DbSet Genres { get; set; } + /// + /// All people of Kyoo. See . + /// public DbSet People { get; set; } + /// + /// All studios of Kyoo. See . + /// public DbSet Studios { get; set; } + /// + /// All providers of Kyoo. See . + /// public DbSet Providers { get; set; } + /// + /// All metadataIDs (ExternalIDs) of Kyoo. See . + /// public DbSet MetadataIds { get; set; } + /// + /// All people's role. See . + /// public DbSet PeopleRoles { get; set; } + /// + /// Get a generic link between two resource types. + /// + /// Types are order dependant. You can't inverse the order. Please always put the owner first. + /// The first resource type of the relation. It is the owner of the second + /// The second resource type of the relation. It is the contained resource. + /// All links between the two types. public DbSet> Links() where T1 : class, IResource where T2 : class, IResource @@ -39,7 +83,10 @@ namespace Kyoo return Set>(); } - + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// public DatabaseContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); @@ -50,6 +97,21 @@ namespace Kyoo ChangeTracker.LazyLoadingEnabled = false; } + /// + /// Create a new . + /// + /// Connection options to use (witch databse provider to use, connection strings...) + public DatabaseContext(DbContextOptions options) + : base(options) + { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.LazyLoadingEnabled = false; + } + + /// + /// Set database parameters to support every types of Kyoo. + /// + /// The database's model builder. protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -58,14 +120,6 @@ namespace Kyoo modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); - // modelBuilder.Entity() - // .Property(x => x.Paths) - // .HasColumnType("text[]"); - // - // modelBuilder.Entity() - // .Property(x => x.Aliases) - // .HasColumnType("text[]"); - modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); @@ -196,6 +250,13 @@ namespace Kyoo .IsUnique(); } + /// + /// Return a new or an in cache temporary object wih the same ID as the one given + /// + /// If a resource with the same ID is found in the database, it will be used. + /// will be used overwise + /// The type of the resource + /// A resource that is now tracked by this context. public T GetTemporaryObject(T model) where T : class, IResource { @@ -206,6 +267,11 @@ namespace Kyoo return model; } + /// + /// Save changes that are applied to this context. + /// + /// A duplicated item has been found. + /// The number of state entries written to the database. public override int SaveChanges() { try @@ -221,6 +287,13 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// Indicates whether AcceptAllChanges() is called after the changes + /// have been sent successfully to the database. + /// A duplicated item has been found. + /// The number of state entries written to the database. public override int SaveChanges(bool acceptAllChangesOnSuccess) { try @@ -236,6 +309,13 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// The message that will have the + /// (if a duplicate is found). + /// A duplicated item has been found. + /// The number of state entries written to the database. public int SaveChanges(string duplicateMessage) { try @@ -251,6 +331,14 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// Indicates whether AcceptAllChanges() is called after the changes + /// have been sent successfully to the database. + /// A to observe while waiting for the task to complete + /// A duplicated item has been found. + /// The number of state entries written to the database. public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new()) { @@ -267,6 +355,12 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// A to observe while waiting for the task to complete + /// A duplicated item has been found. + /// The number of state entries written to the database. public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { try @@ -282,6 +376,14 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// The message that will have the + /// (if a duplicate is found). + /// A to observe while waiting for the task to complete + /// A duplicated item has been found. + /// The number of state entries written to the database. public async Task SaveChangesAsync(string duplicateMessage, CancellationToken cancellationToken = new()) { @@ -298,6 +400,12 @@ namespace Kyoo } } + /// + /// Save changes if no duplicates are found. If one is found, no change are saved but the current changes are no discarded. + /// The current context will still hold those invalid changes. + /// + /// A to observe while waiting for the task to complete + /// The number of state entries written to the database or -1 if a duplicate exist. public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new()) { try @@ -310,12 +418,31 @@ namespace Kyoo } } + /// + /// Save items or retry with a custom method if a duplicate is found. + /// + /// The item to save (other changes of this context will also be saved) + /// A function to run on fail, the param wil be mapped. + /// The second parameter is the current retry number. + /// A to observe while waiting for the task to complete + /// The type of the item to save + /// The number of state entries written to the database. public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new()) { return SaveOrRetry(obj, onFail, 0, cancellationToken); } - public async Task SaveOrRetry(T obj, + /// + /// Save items or retry with a custom method if a duplicate is found. + /// + /// The item to save (other changes of this context will also be saved) + /// A function to run on fail, the param wil be mapped. + /// The second parameter is the current retry number. + /// The current retry number. + /// A to observe while waiting for the task to complete + /// The type of the item to save + /// The number of state entries written to the database. + private async Task SaveOrRetry(T obj, Func onFail, int recurse, CancellationToken cancellationToken = new()) @@ -337,11 +464,20 @@ namespace Kyoo } } + /// + /// Check if the exception is a duplicated exception. + /// + /// WARNING: this only works for PostgreSQL + /// The exception to check + /// True if the exception is a duplicate exception. False otherwise private static bool IsDuplicateException(Exception ex) { return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; } + /// + /// Delete every changes that are on this context. + /// private void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 81e73def..1fa2b7c3 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -3,7 +3,11 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.VisualBasic.FileIO; +using Microsoft.AspNetCore.Hosting.StaticWebAssets; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Kyoo { @@ -18,12 +22,9 @@ namespace Kyoo /// Command line arguments public static async Task Main(string[] args) { - if (args.Length > 0) - FileSystem.CurrentDirectory = args[0]; - if (!File.Exists("./appsettings.json")) - File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"), "appsettings.json"); - - + if (!File.Exists("./settings.json")) + File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "settings.json"), "settings.json"); + bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch { "d" => true, @@ -49,15 +50,50 @@ namespace Kyoo await host.Build().RunAsync(); } + /// + /// Register settings.json, environment variables and command lines arguments as configuration. + /// + /// The configuration builder to use + /// The command line arguments + /// The modified configuration builder + private static IConfigurationBuilder SetupConfig(IConfigurationBuilder builder, string[] args) + { + return builder.AddJsonFile("./settings.json", false, true) + .AddEnvironmentVariables() + .AddCommandLine(args); + } + /// /// Createa a web host /// /// Command line parameters that can be handled by kestrel /// A new web host instance - private static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseKestrel(config => { config.AddServerHeader = false; }) - .UseUrls("http://*:5000") + private static IWebHostBuilder CreateWebHostBuilder(string[] args) + { + WebHost.CreateDefaultBuilder(args); + + return new WebHostBuilder() + .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) + .UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build()) + .ConfigureAppConfiguration(x => SetupConfig(x, args)) + .ConfigureLogging((context, builder) => + { + builder.AddConfiguration(context.Configuration.GetSection("logging")) + .AddConsole() + .AddDebug() + .AddEventSourceLogger(); + }) + .UseDefaultServiceProvider((context, options) => + { + options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); + if (context.HostingEnvironment.IsDevelopment()) + StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration); + }) + .ConfigureServices(x => x.AddRouting()) + .UseKestrel(options => { options.AddServerHeader = false; }) + .UseIIS() + .UseIISIntegration() .UseStartup(); + } } } diff --git a/Kyoo/Views/EpisodeApi.cs b/Kyoo/Views/EpisodeApi.cs index 9d240e55..8dd28956 100644 --- a/Kyoo/Views/EpisodeApi.cs +++ b/Kyoo/Views/EpisodeApi.cs @@ -64,14 +64,28 @@ namespace Kyoo.Api [Authorize(Policy = "Read")] public async Task> GetSeason(string showSlug, int seasonNumber, int episodeNumber) { - return await _libraryManager.Get(showSlug, seasonNumber); + try + { + return await _libraryManager.Get(showSlug, seasonNumber); + } + catch (ItemNotFound) + { + return NotFound(); + } } [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")] [Authorize(Policy = "Read")] public async Task> GetSeason(int showID, int seasonNumber, int episodeNumber) { - return await _libraryManager.Get(showID, seasonNumber); + try + { + return await _libraryManager.Get(showID, seasonNumber); + } + catch (ItemNotFound) + { + return NotFound(); + } } [HttpGet("{episodeID:int}/track")] @@ -120,7 +134,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID, seasonNumber, episodeNumber) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber, episodeNumber) == null) return NotFound(); return Page(resources, limit); } @@ -130,10 +144,10 @@ namespace Kyoo.Api } } - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/track")] - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] + [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/track")] + [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] [Authorize(Policy = "Read")] - public async Task>> GetEpisode(string showSlug, + public async Task>> GetEpisode(string slug, int seasonNumber, int episodeNumber, [FromQuery] string sortBy, @@ -144,13 +158,13 @@ namespace Kyoo.Api try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == showSlug + ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == slug && x.Episode.SeasonNumber == seasonNumber && x.Episode.EpisodeNumber == episodeNumber), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showSlug, seasonNumber, episodeNumber) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug, seasonNumber, episodeNumber) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/SeasonApi.cs b/Kyoo/Views/SeasonApi.cs index 4ca19c29..9803f956 100644 --- a/Kyoo/Views/SeasonApi.cs +++ b/Kyoo/Views/SeasonApi.cs @@ -75,7 +75,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showSlug, seasonNumber) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showSlug, seasonNumber) == null) return NotFound(); return Page(resources, limit); } @@ -102,7 +102,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID, seasonNumber) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index f1a38ff3..4ae053de 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -30,14 +30,14 @@ namespace Kyoo.Api Track subtitle; try { - subtitle = await _libraryManager.Get(slug, StreamType.Subtitle); + subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle); } catch (ArgumentException ex) { return BadRequest(new {error = ex.Message}); } - if (subtitle == null || subtitle.Type != StreamType.Subtitle) + if (subtitle is not {Type: StreamType.Subtitle}) return NotFound(); if (subtitle.Codec == "subrip" && extension == "vtt") diff --git a/Kyoo/appsettings.json b/Kyoo/settings.json similarity index 56% rename from Kyoo/appsettings.json rename to Kyoo/settings.json index c49df121..bdd2f362 100644 --- a/Kyoo/appsettings.json +++ b/Kyoo/settings.json @@ -1,31 +1,25 @@ { - "server.urls": "http://0.0.0.0:5000", + "server.urls": "http://*:5000", "public_url": "http://localhost:5000/", - "http_port": 5000, - "https_port": 44300, - "Database": { - "Server": "127.0.0.1", - "Port": "5432", - "Database": "kyooDB", - "User Id": "kyoo", - "Password": "kyooPassword", - "Pooling": "true", - "MaxPoolSize": "95", - "Timeout": "30" + "database": { + "server": "127.0.0.1", + "port": "5432", + "database": "kyooDB", + "user ID": "kyoo", + "password": "kyooPassword", + "pooling": "true", + "maxPoolSize": "95", + "timeout": "30" }, - "Logging": { - "LogLevel": { - "Default": "Warning", + "logging": { + "logLevel": { + "default": "Warning", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore.DbUpdateException": "None", - "Microsoft.EntityFrameworkCore.Update": "None", - "Microsoft.EntityFrameworkCore.Database.Command": "None" + "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*", "parallelTasks": "1", diff --git a/deployment/kyoo.service b/deployment/kyoo.service index 0eceef42..277c26ca 100644 --- a/deployment/kyoo.service +++ b/deployment/kyoo.service @@ -5,7 +5,8 @@ After=network.target [Service] User=kyoo -ExecStart=/usr/lib/kyoo/Kyoo /var/lib/kyoo +WorkingDirectory=/var/lib/kyoo +ExecStart=/usr/lib/kyoo/Kyoo Restart=on-abort TimeoutSec=20 diff --git a/settings.json b/settings.json new file mode 100644 index 00000000..bdd2f362 --- /dev/null +++ b/settings.json @@ -0,0 +1,42 @@ +{ + "server.urls": "http://*:5000", + "public_url": "http://localhost:5000/", + + "database": { + "server": "127.0.0.1", + "port": "5432", + "database": "kyooDB", + "user ID": "kyoo", + "password": "kyooPassword", + "pooling": "true", + "maxPoolSize": "95", + "timeout": "30" + }, + + "logging": { + "logLevel": { + "default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + + "parallelTasks": "1", + + "scheduledTasks": { + "scan": "24:00:00" + }, + + "certificatePassword": "passphrase", + + "transmuxTempPath": "cached/kyoo/transmux", + "transcodeTempPath": "cached/kyoo/transcode", + "peoplePath": "people", + "providerPath": "providers", + "profilePicturePath": "users/", + "plugins": "plugins/", + "defaultPermissions": "read,play,write,admin", + "newUserPermissions": "read,play,write,admin", + "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$", + "subtitleRegex": "^(?.*)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$" +}