diff --git a/Shoko.CLI/Program.cs b/Shoko.CLI/Program.cs index 3f6365fa3..d345b54cd 100644 --- a/Shoko.CLI/Program.cs +++ b/Shoko.CLI/Program.cs @@ -1,9 +1,14 @@ #region using System; using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using Shoko.Server.Commands; +using Shoko.Server.Filters; +using Shoko.Server.Repositories; using Shoko.Server.Server; using Shoko.Server.Settings; using Shoko.Server.Utilities; @@ -13,6 +18,7 @@ namespace Shoko.CLI; public static class Program { + private static ILogger _logger; public static void Main() { try @@ -26,7 +32,7 @@ public static void Main() Utils.SetInstance(); Utils.InitLogger(); var logFactory = new LoggerFactory().AddNLog(); - var logger = logFactory.CreateLogger("Main"); + _logger = logFactory.CreateLogger("Main"); try { @@ -40,7 +46,7 @@ public static void Main() } catch (Exception e) { - logger.LogCritical(e, "The server failed to start"); + _logger.LogCritical(e, "The server failed to start"); } } diff --git a/Shoko.Commons b/Shoko.Commons index 9e08c74a3..dbfe0d9f5 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 9e08c74a395fb3cbf4b8d99c6a67f911b28b5a81 +Subproject commit dbfe0d9f5b00b421349c1177f4ad37ead5c4c846 diff --git a/Shoko.Server/API/APIExtensions.cs b/Shoko.Server/API/APIExtensions.cs index 283f0c3ea..05a7c7095 100644 --- a/Shoko.Server/API/APIExtensions.cs +++ b/Shoko.Server/API/APIExtensions.cs @@ -21,6 +21,7 @@ using Shoko.Server.API.SignalR.Aggregate; using Shoko.Server.API.SignalR.Legacy; using Shoko.Server.API.Swagger; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.API.WebUI; using Shoko.Server.Plugin; @@ -50,6 +51,9 @@ public static IServiceCollection AddAPI(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddAuthentication(options => { diff --git a/Shoko.Server/API/AuthenticationController.cs b/Shoko.Server/API/AuthenticationController.cs index e327a7792..aab916a8e 100644 --- a/Shoko.Server/API/AuthenticationController.cs +++ b/Shoko.Server/API/AuthenticationController.cs @@ -24,7 +24,7 @@ public class AuthenticationController : BaseController [ProducesResponseType(400)] [ProducesResponseType(401)] [ProducesResponseType(200)] - public ActionResult Login(AuthUser auth) + public ActionResult Login(AuthUser auth) { if (!ModelState.IsValid || string.IsNullOrEmpty(auth.user?.Trim())) { @@ -58,7 +58,7 @@ public ActionResult ChangePassword([FromBody] string newPassword) try { User.Password = Digest.Hash(newPassword.Trim()); - RepoFactory.JMMUser.Save(User, false); + RepoFactory.JMMUser.Save(User); RepoFactory.AuthTokens.DeleteAllWithUserID(User.JMMUserID); return Ok(); } diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs index 23fd7154e..7b05bcd9b 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs @@ -18,6 +18,8 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Legacy; using Shoko.Server.Models; using Shoko.Server.Plex; using Shoko.Server.Providers.AniDB.Interfaces; @@ -175,16 +177,17 @@ public CL_MainChanges GetAllChanges(DateTime date, int userID) var changes = ChangeTracker.GetChainedChanges( new List> { - RepoFactory.GroupFilter.GetChangeTracker(), + RepoFactory.FilterPreset.GetChangeTracker(), RepoFactory.AnimeGroup.GetChangeTracker(), RepoFactory.AnimeGroup_User.GetChangeTracker(userID), RepoFactory.AnimeSeries.GetChangeTracker(), RepoFactory.AnimeSeries_User.GetChangeTracker(userID) }, date); + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); c.Filters = new CL_Changes { - ChangedItems = changes[0] - .ChangedItems.Select(a => RepoFactory.GroupFilter.GetByID(a)?.ToClient()) + ChangedItems = legacyConverter.ToClient(changes[0].ChangedItems.Select(a => RepoFactory.FilterPreset.GetByID(a)).ToList()) + .Select(a => a.Value) .Where(a => a != null) .ToList(), RemovedItems = changes[0].RemovedItems.ToList(), @@ -203,8 +206,7 @@ public CL_MainChanges GetAllChanges(DateTime date, int userID) if (!c.Filters.ChangedItems.Any(a => a.GroupFilterID == ag.ParentGroupFilterID.Value)) { end = false; - var cag = RepoFactory.GroupFilter.GetByID(ag.ParentGroupFilterID.Value)? - .ToClient(); + var cag = legacyConverter.ToClient(RepoFactory.FilterPreset.GetByID(ag.ParentGroupFilterID.Value)); if (cag != null) { c.Filters.ChangedItems.Add(cag); @@ -270,8 +272,10 @@ public CL_Changes GetGroupFilterChanges(DateTime date) var c = new CL_Changes(); try { - var changes = RepoFactory.GroupFilter.GetChangeTracker().GetChanges(date); - c.ChangedItems = changes.ChangedItems.Select(a => RepoFactory.GroupFilter.GetByID(a).ToClient()) + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + var changes = RepoFactory.FilterPreset.GetChangeTracker().GetChanges(date); + c.ChangedItems = legacyConverter.ToClient(changes.ChangedItems.Select(a => RepoFactory.FilterPreset.GetByID(a)).ToList()) + .Select(a => a.Value) .Where(a => a != null) .ToList(); c.RemovedItems = changes.RemovedItems.ToList(); diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index 523a01d9b..400d6bd48 100755 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Shoko.Commons.Extensions; using Shoko.Models.Client; using Shoko.Models.Enums; @@ -10,6 +11,8 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Legacy; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Tasks; @@ -159,40 +162,17 @@ public List GetContinueWatchingFilter(int userID, int maxR try { var user = RepoFactory.JMMUser.GetByID(userID); - if (user == null) - { - return retEps; - } + if (user == null) return retEps; // find the locked Continue Watching Filter - SVR_GroupFilter gf = null; - var lockedGFs = RepoFactory.GroupFilter.GetLockedGroupFilters(); - if (lockedGFs != null) - { - // if it already exists we can leave - foreach (var gfTemp in lockedGFs) - { - if (gfTemp.FilterType == (int)GroupFilterType.ContinueWatching) - { - gf = gfTemp; - break; - } - } - } - - if (gf == null || !gf.GroupsIds.ContainsKey(userID)) - { - return retEps; - } + var lockedGFs = RepoFactory.FilterPreset.GetLockedGroupFilters(); + var gf = lockedGFs?.FirstOrDefault(a => a.Name == "Continue Watching"); + if (gf == null) return retEps; - var comboGroups = gf.GroupsIds[userID].Select(a => RepoFactory.AnimeGroup.GetByID(a)).Where(a => a != null) + var evaluator = HttpContext.RequestServices.GetRequiredService(); + var comboGroups = evaluator.EvaluateFilter(gf, userID).Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null) .Select(a => a.GetUserContract(userID)); - - // apply sorting - comboGroups = GroupFilterHelper.Sort(comboGroups, gf); - - foreach (var grp in comboGroups) { var sers = RepoFactory.AnimeSeries.GetByGroupID(grp.AnimeGroupID).OrderBy(a => a.AirDate).ToList(); @@ -201,51 +181,22 @@ public List GetContinueWatchingFilter(int userID, int maxR foreach (var ser in sers) { - if (!user.AllowedSeries(ser)) - { - continue; - } - - var useSeries = true; - - if (seriesWatching.Count > 0) - { - if (ser.GetAnime().AnimeType == (int)AnimeType.TVSeries) - { - // make sure this series is not a sequel to an existing series we have already added - foreach (AniDB_Anime_Relation rel in ser.GetAnime().GetRelatedAnime()) - { - if (rel.RelationType.ToLower().Trim().Equals("sequel") || - rel.RelationType.ToLower().Trim().Equals("prequel")) - { - useSeries = false; - } - } - } - } - - if (!useSeries) - { - continue; - } + if (!user.AllowedSeries(ser)) continue; + var anime = ser.GetAnime(); + var useSeries = seriesWatching.Count == 0 || anime.AnimeType != (int)AnimeType.TVSeries || !anime.GetRelatedAnime().Any(a => + a.RelationType.ToLower().Trim().Equals("sequel") || a.RelationType.ToLower().Trim().Equals("prequel")); + if (!useSeries) continue; var ep = GetNextUnwatchedEpisode(ser.AnimeSeriesID, userID); - if (ep != null) - { - retEps.Add(ep); + if (ep == null) continue; - // Lets only return the specified amount - if (retEps.Count == maxRecords) - { - return retEps; - } + retEps.Add(ep); - if (ser.GetAnime().AnimeType == (int)AnimeType.TVSeries) - { - seriesWatching.Add(ser.AniDB_ID); - } - } + // Lets only return the specified amount + if (retEps.Count == maxRecords) return retEps; + + if (anime.AnimeType == (int)AnimeType.TVSeries) seriesWatching.Add(ser.AniDB_ID); } } } @@ -397,30 +348,20 @@ public List GetEpisodesRecentlyAdded(int maxRecords, int u } // We will deal with a large list, don't perform ops on the whole thing! - var vids = RepoFactory.VideoLocal.GetMostRecentlyAdded(maxRecords, userID); + var vids = RepoFactory.VideoLocal.GetMostRecentlyAdded(maxRecords*5, userID); foreach (var vid in vids) { - if (string.IsNullOrEmpty(vid.Hash)) - { - continue; - } + if (string.IsNullOrEmpty(vid.Hash)) continue; foreach (var ep in vid.GetAnimeEpisodes()) { var epContract = ep.GetUserContract(userID); - if (user.AllowedSeries(ep.GetAnimeSeries())) - { - if (epContract != null) - { - retEps.Add(epContract); + if (!user.AllowedSeries(ep.GetAnimeSeries()) || epContract == null) continue; + retEps.Add(epContract); - // Lets only return the specified amount - if (retEps.Count >= maxRecords) - { - return retEps; - } - } - } + // Lets only return the specified amount + if (retEps.Count < maxRecords) continue; + return retEps; } } } @@ -2084,48 +2025,39 @@ public List GetAnimeGroupsForFilter(int groupFilterID, int u try { var user = RepoFactory.JMMUser.GetByID(userID); - if (user == null) - { - return retGroups; - } + if (user == null) return retGroups; - SVR_GroupFilter gf; - gf = RepoFactory.GroupFilter.GetByID(groupFilterID); - if (gf != null && gf.GroupsIds.ContainsKey(userID)) + var gf = RepoFactory.FilterPreset.GetByID(groupFilterID); + + if (gf != null) { - retGroups = gf.GroupsIds[userID].Select(a => RepoFactory.AnimeGroup.GetByID(a)) - .Where(a => a != null).Select(a => a.GetUserContract(userID)).ToList(); + var evaluator = HttpContext.RequestServices.GetRequiredService(); + var results = evaluator.EvaluateFilter(gf, userID); + retGroups = results.Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null).Select(a => a.GetUserContract(userID)).ToList(); } - if (getSingleSeriesGroups) + if (!getSingleSeriesGroups) return retGroups; + + var nGroups = new List(); + foreach (var cag in retGroups) { - var nGroups = new List(); - foreach (var cag in retGroups) + var ng = cag.DeepCopy(); + if (cag.Stat_SeriesCount == 1) { - var ng = cag.DeepCopy(); - if (cag.Stat_SeriesCount == 1) + if (cag.DefaultAnimeSeriesID.HasValue) { - if (cag.DefaultAnimeSeriesID.HasValue) - { - ng.SeriesForNameOverride = RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID) - .FirstOrDefault(a => a.AnimeSeriesID == cag.DefaultAnimeSeriesID.Value) - ?.GetUserContract(userID); - } - - if (ng.SeriesForNameOverride == null) - { - ng.SeriesForNameOverride = RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID) - .FirstOrDefault()?.GetUserContract(userID); - } + ng.SeriesForNameOverride = RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID) + .FirstOrDefault(a => a.AnimeSeriesID == cag.DefaultAnimeSeriesID.Value) + ?.GetUserContract(userID); } - nGroups.Add(ng); + ng.SeriesForNameOverride ??= RepoFactory.AnimeSeries.GetByGroupID(ng.AnimeGroupID).FirstOrDefault()?.GetUserContract(userID); } - retGroups = nGroups; + nGroups.Add(ng); } - return retGroups; + return nGroups; } catch (Exception ex) { @@ -3091,12 +3023,11 @@ public CL_Response SaveGroupFilter(CL_GroupFilter contract) { var response = new CL_Response { ErrorMessage = string.Empty, Result = null }; - // Process the group - SVR_GroupFilter gf; + FilterPreset gf = null; if (contract.GroupFilterID != 0) { - gf = RepoFactory.GroupFilter.GetByID(contract.GroupFilterID); + gf = RepoFactory.FilterPreset.GetByID(contract.GroupFilterID); if (gf == null) { response.ErrorMessage = "Could not find existing Group Filter with ID: " + @@ -3105,11 +3036,24 @@ public CL_Response SaveGroupFilter(CL_GroupFilter contract) } } - gf = SVR_GroupFilter.FromClient(contract); + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + var newFilter = legacyConverter.FromClient(contract); + if (gf == null) + { + gf = newFilter; + } + else + { + gf.Name = newFilter.Name; + gf.Hidden = newFilter.Hidden; + gf.ApplyAtSeriesLevel = newFilter.ApplyAtSeriesLevel; + gf.Expression = newFilter.Expression; + gf.SortingExpression = newFilter.SortingExpression; + } + + RepoFactory.FilterPreset.Save(gf); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - response.Result = gf.ToClient(); + response.Result = legacyConverter.ToClient(gf); return response; } @@ -3118,13 +3062,10 @@ public string DeleteGroupFilter(int groupFilterID) { try { - var gf = RepoFactory.GroupFilter.GetByID(groupFilterID); - if (gf == null) - { - return "Group Filter not found"; - } + var gf = RepoFactory.FilterPreset.GetByID(groupFilterID); + if (gf == null) return "Group Filter not found"; - RepoFactory.GroupFilter.Delete(groupFilterID); + RepoFactory.FilterPreset.Delete(groupFilterID); return string.Empty; } @@ -3140,7 +3081,7 @@ public CL_GroupFilterExtended GetGroupFilterExtended(int groupFilterID, int user { try { - var gf = RepoFactory.GroupFilter.GetByID(groupFilterID); + var gf = RepoFactory.FilterPreset.GetByID(groupFilterID); if (gf == null) { return null; @@ -3152,9 +3093,14 @@ public CL_GroupFilterExtended GetGroupFilterExtended(int groupFilterID, int user return null; } - var contract = gf.ToClientExtended(user); - - return contract; + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + var model = legacyConverter.ToClient(gf); + return new CL_GroupFilterExtended + { + GroupFilter = model, + GroupCount = model.Groups[userID].Count, + SeriesCount = model.Series[userID].Count + }; } catch (Exception ex) { @@ -3176,21 +3122,12 @@ public List GetAllGroupFiltersExtended(int userID) return gfs; } - var allGfs = RepoFactory.GroupFilter.GetAll(); - foreach (var gf in allGfs) + var allGfs = RepoFactory.FilterPreset.GetAll(); + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + gfs = legacyConverter.ToClient(allGfs).Select(a => new CL_GroupFilterExtended { - var gfContract = gf.ToClient(); - var gfeContract = new CL_GroupFilterExtended - { - GroupFilter = gfContract, GroupCount = 0, SeriesCount = 0 - }; - if (gf.GroupsIds.ContainsKey(user.JMMUserID)) - { - gfeContract.GroupCount = gf.GroupsIds.Count; - } - - gfs.Add(gfeContract); - } + GroupFilter = a.Value, GroupCount = a.Value.Groups[userID].Count + }).ToList(); } catch (Exception ex) { @@ -3213,22 +3150,13 @@ public List GetGroupFiltersExtended(int userID, int gfpa } var allGfs = gfparentid == 0 - ? RepoFactory.GroupFilter.GetTopLevel() - : RepoFactory.GroupFilter.GetByParentID(gfparentid); - foreach (var gf in allGfs) + ? RepoFactory.FilterPreset.GetTopLevel() + : RepoFactory.FilterPreset.GetByParentID(gfparentid); + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + gfs = legacyConverter.ToClient(allGfs).Select(a => new CL_GroupFilterExtended { - var gfContract = gf.ToClient(); - var gfeContract = new CL_GroupFilterExtended - { - GroupFilter = gfContract, GroupCount = 0, SeriesCount = 0 - }; - if (gf.GroupsIds.ContainsKey(user.JMMUserID)) - { - gfeContract.GroupCount = gf.GroupsIds.Count; - } - - gfs.Add(gfeContract); - } + GroupFilter = a.Value, GroupCount = a.Value.Groups.FirstOrDefault().Value.Count + }).ToList(); } catch (Exception ex) { @@ -3246,15 +3174,15 @@ public List GetAllGroupFilters() { var start = DateTime.Now; - var allGfs = RepoFactory.GroupFilter.GetAll(); + var allGfs = RepoFactory.FilterPreset.GetAll(); var ts = DateTime.Now - start; logger.Info("GetAllGroupFilters (Database) in {0} ms", ts.TotalMilliseconds); - start = DateTime.Now; - foreach (var gf in allGfs) - { - gfs.Add(gf.ToClient()); - } + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + gfs = legacyConverter.ToClient(allGfs) + .Select(a => a.Value) + .Where(a => a != null) + .ToList(); } catch (Exception ex) { @@ -3272,17 +3200,14 @@ public List GetGroupFilters(int gfparentid = 0) { var start = DateTime.Now; - var allGfs = gfparentid == 0 - ? RepoFactory.GroupFilter.GetTopLevel() - : RepoFactory.GroupFilter.GetByParentID(gfparentid); + var allGfs = gfparentid == 0 ? RepoFactory.FilterPreset.GetTopLevel() : RepoFactory.FilterPreset.GetByParentID(gfparentid); var ts = DateTime.Now - start; logger.Info("GetAllGroupFilters (Database) in {0} ms", ts.TotalMilliseconds); - - start = DateTime.Now; - foreach (var gf in allGfs) - { - gfs.Add(gf.ToClient()); - } + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + gfs = legacyConverter.ToClient(allGfs) + .Select(a => a.Value) + .Where(a => a != null) + .ToList(); } catch (Exception ex) { @@ -3297,7 +3222,8 @@ public CL_GroupFilter GetGroupFilter(int gf) { try { - return RepoFactory.GroupFilter.GetByID(gf)?.ToClient(); + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + return legacyConverter.ToClient(RepoFactory.FilterPreset.GetByID(gf)); } catch (Exception ex) { @@ -3312,7 +3238,10 @@ public CL_GroupFilter EvaluateGroupFilter(CL_GroupFilter contract) { try { - return SVR_GroupFilter.EvaluateContract(contract); + var legacyConverter = HttpContext.RequestServices.GetRequiredService(); + var filter = legacyConverter.FromClient(contract); + var model = legacyConverter.ToClient(filter); + return model; } catch (Exception ex) { @@ -3672,7 +3601,7 @@ public string ChangePassword(int userID, string newPassword, bool revokeapikey) } jmmUser.Password = Digest.Hash(newPassword); - RepoFactory.JMMUser.Save(jmmUser, false); + RepoFactory.JMMUser.Save(jmmUser); if (revokeapikey) { RepoFactory.AuthTokens.DeleteAllWithUserID(jmmUser.JMMUserID); @@ -3780,7 +3709,7 @@ public string SaveUser(JMMUser user) } } - RepoFactory.JMMUser.Save(jmmUser, updateGf); + RepoFactory.JMMUser.Save(jmmUser); // update stats if (updateStats) diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs deleted file mode 100644 index b37f4b5cb..000000000 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationKodi.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Quartz; -using Shoko.Models.Interfaces; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.Commands; -using Shoko.Server.PlexAndKodi; -using Shoko.Server.PlexAndKodi.Kodi; -using Shoko.Server.Settings; -using Stream = System.IO.Stream; - -namespace Shoko.Server.API.v1.Implementations; - -[ApiController] -[Route("/api/Kodi")] -[ApiVersion("1.0", Deprecated = true)] -public class ShokoServiceImplementationKodi : IShokoServerKodi, IHttpContextAccessor -{ - private readonly ShokoServiceImplementation _service; - public HttpContext HttpContext { get; set; } - - private readonly CommonImplementation _impl; - - private readonly ILogger _logger; - private readonly ISettingsProvider _settingsProvider; - - public ShokoServiceImplementationKodi(ICommandRequestFactory commandFactory, - ILogger logger, ISchedulerFactory schedulerFactory, ISettingsProvider settingsProvider, CommonImplementation impl) - { - _settingsProvider = settingsProvider; - _service = new ShokoServiceImplementation(null, null, null, commandFactory, schedulerFactory, settingsProvider); - _logger = logger; - _impl = impl; - } - - - [HttpGet("Image/Support/{name}")] - public Stream GetSupportImage(string name) - { - return _impl.GetSupportImage(name); - } - - [HttpGet("Filters/{userId}")] - public MediaContainer GetFilters(string userId) - { - return _impl.GetFilters(new KodiProvider { HttpContext = HttpContext }, userId); - } - - [HttpGet("Metadata/{userId}/{type}/{id}/{filterid?}")] - public MediaContainer GetMetadata(string userId, int type, string id, int? filterid) - { - return _impl.GetMetadata(new KodiProvider { HttpContext = HttpContext }, userId, type, id, null, false, - filterid); - } - - [HttpGet("User")] - public PlexContract_Users GetUsers() - { - return _impl.GetUsers(new KodiProvider { HttpContext = HttpContext }); - } - - [HttpGet("Version")] - public Response Version() - { - return _impl.GetVersion(); - } - - [HttpGet("Search/{userId}/{limit}/{query}")] - public MediaContainer Search(string userId, int limit, string query) - { - return _impl.Search(new KodiProvider { HttpContext = HttpContext }, userId, limit, query, false); - } - - [HttpGet("SearchTag/{userId}/{limit}/{query}")] - public MediaContainer SearchTag(string userId, int limit, string query) - { - return _impl.Search(new KodiProvider { HttpContext = HttpContext }, userId, limit, query, true); - } - - [HttpGet("Group/Watch/{userId}/{groupid}/{status}")] - public Response ToggleWatchedStatusOnGroup(string userId, int groupid, bool status) - { - return _impl.ToggleWatchedStatusOnGroup(new KodiProvider { HttpContext = HttpContext }, userId, - groupid, status); - } - - [HttpGet("Serie/Watch/{userId}/{serieid}/{status}")] - public Response ToggleWatchedStatusOnSeries(string userId, int serieid, bool status) - { - return _impl.ToggleWatchedStatusOnSeries(new KodiProvider { HttpContext = HttpContext }, userId, - serieid, status); - } - - [HttpGet("Serie/Watch/{userId}/{epid}/{status}")] - public Response ToggleWatchedStatusOnEpisode(string userId, int epid, bool status) - { - return _impl.ToggleWatchedStatusOnEpisode(new KodiProvider { HttpContext = HttpContext }, userId, epid, - status); - } - - [HttpGet("Vote/{userId}/{id}/{votevalue}/{votetype}")] - public Response Vote(string userId, int id, float votevalue, int votetype) - { - return _impl.VoteAnime(new KodiProvider { HttpContext = HttpContext }, userId, id, votevalue, - votetype); - } - - [HttpGet("Trakt/Scrobble/{animeId}/{type}/{progress}/{status}")] - public Response TraktScrobble(string animeId, int type, float progress, int status) - { - return _impl.TraktScrobble(new KodiProvider { HttpContext = HttpContext }, animeId, type, progress, - status); - } - - [HttpGet("Video/Rescan/{vlid}")] - public Response Rescan(int vlid) - { - var r = new Response(); - try - { - var output = _service.RescanFile(vlid); - if (!string.IsNullOrEmpty(output)) - { - r.Code = HttpStatusCode.BadRequest.ToString(); - r.Message = output; - return r; - } - - r.Code = HttpStatusCode.OK.ToString(); - } - catch (Exception ex) - { - r.Code = "500"; - r.Message = "Internal Error : " + ex; - _logger.LogError(ex, "{Ex}", ex.ToString()); - } - - return r; - } - - - [HttpGet("Video/Rehash/{vlid}")] - public Response Rehash(int vlid) - { - var r = new Response(); - try - { - _service.RehashFile(vlid); - r.Code = HttpStatusCode.OK.ToString(); - } - catch (Exception ex) - { - r.Code = "500"; - r.Message = "Internal Error : " + ex; - _logger.LogError(ex, "{Ex}", ex.ToString()); - } - - return r; - } -} diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs index 2356115d8..4ed4df31d 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs @@ -17,6 +17,7 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB.Interfaces; using Shoko.Server.Providers.TraktTV; @@ -518,43 +519,30 @@ public List GetAnimeContinueWatching(int maxRecords, int jm } // find the locked Continue Watching Filter - SVR_GroupFilter gf = null; - var lockedGFs = RepoFactory.GroupFilter.GetLockedGroupFilters(); + FilterPreset gf = null; + var lockedGFs = RepoFactory.FilterPreset.GetLockedGroupFilters(); if (lockedGFs != null) { // if it already exists we can leave - foreach (var gfTemp in lockedGFs) + foreach (var gfTemp in lockedGFs.Where(gfTemp => gfTemp.Name == "Continue Watching")) { - if (gfTemp.FilterType == (int)GroupFilterType.ContinueWatching) - { - gf = gfTemp; - break; - } + gf = gfTemp; + break; } } - if (gf == null || !gf.GroupsIds.ContainsKey(jmmuserID)) - { - return retAnime; - } + if (gf == null) return retAnime; - var comboGroups = - gf.GroupsIds[jmmuserID] - .Select(a => RepoFactory.AnimeGroup.GetByID(a)) - .Where(a => a != null) - .Select(a => a.GetUserContract(jmmuserID)); + var evaluator = HttpContext.RequestServices.GetRequiredService(); + var results = evaluator.EvaluateFilter(gf, user.JMMUserID); - // apply sorting - comboGroups = GroupFilterHelper.Sort(comboGroups, gf); + var comboGroups = results.Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null).Select(a => a.GetUserContract(jmmuserID)); foreach (var grp in comboGroups) { foreach (var ser in RepoFactory.AnimeSeries.GetByGroupID(grp.AnimeGroupID)) { - if (!user.AllowedSeries(ser)) - { - continue; - } + if (!user.AllowedSeries(ser)) continue; var serUser = ser.GetUserRecord(jmmuserID); @@ -588,7 +576,6 @@ public List GetAnimeContinueWatching(int maxRecords, int jm retAnime.Add(summ); - // Lets only return the specified amount if (retAnime.Count == maxRecords) { diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs index 07fe98de5..86eca15b5 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationPlex.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Shoko.Models.Interfaces; using Shoko.Models.Plex.Connections; using Shoko.Models.PlexAndKodi; -using Shoko.Server.PlexAndKodi; -using Shoko.Server.PlexAndKodi.Plex; +using Shoko.Server.Plex; +using Shoko.Server.Repositories; +using Shoko.Server.Settings; using Directory = Shoko.Models.Plex.Libraries.Directory; -using MediaContainer = Shoko.Models.PlexAndKodi.MediaContainer; -using Stream = System.IO.Stream; namespace Shoko.Server.API.v1.Implementations; @@ -18,92 +18,64 @@ namespace Shoko.Server.API.v1.Implementations; public class ShokoServiceImplementationPlex : IShokoServerPlex, IHttpContextAccessor { public HttpContext HttpContext { get; set; } - private readonly CommonImplementation _impl; + private readonly ISettingsProvider _settingsProvider; - public ShokoServiceImplementationPlex(CommonImplementation impl) + public ShokoServiceImplementationPlex(ISettingsProvider settingsProvider) { - _impl = impl; - } - - [HttpGet("Image/Support/{name}")] - public Stream GetSupportImage(string name) - { - return _impl.GetSupportImage(name); - } - - [HttpGet("Filters/{userId}")] - public MediaContainer GetFilters(string userId) - { - return _impl.GetFilters(new PlexProvider { HttpContext = HttpContext }, userId); - } - - [HttpGet("Metadata/{userId}/{type}/{id}/{historyinfo}/{filterid?}")] - public MediaContainer GetMetadata(string userId, int type, string id, string historyinfo, int? filterid) - { - return _impl.GetMetadata(new PlexProvider { HttpContext = HttpContext }, userId, type, id, historyinfo, - false, filterid); + _settingsProvider = settingsProvider; } [HttpGet("User")] public PlexContract_Users GetUsers() { - return _impl.GetUsers(new PlexProvider { HttpContext = HttpContext }); - } - - [HttpGet("Search/{userId}/{limit}/{query}")] - public MediaContainer Search(string userId, int limit, string query) - { - return _impl.Search(new PlexProvider { HttpContext = HttpContext }, userId, limit, query, false); - } - - [HttpGet("Serie/Watch/{userId}/{epid}/{status}")] - public Response ToggleWatchedStatusOnEpisode(string userId, int epid, bool status) - { - return _impl.ToggleWatchedStatusOnEpisode(new PlexProvider { HttpContext = HttpContext }, userId, epid, - status); - } - - [HttpGet("Vote/{userId}/{id}/{votevalue}/{votetype}")] - public Response Vote(string userId, int id, float votevalue, int votetype) - { - return _impl.VoteAnime(new PlexProvider { HttpContext = HttpContext }, userId, id, votevalue, - votetype); + var gfs = new PlexContract_Users + { + Users = new List() + }; + foreach (var us in RepoFactory.JMMUser.GetAll()) + { + var p = new PlexContract_User { id = us.JMMUserID.ToString(), name = us.Username }; + gfs.Users.Add(p); + } + + return gfs; } [HttpGet("Linking/Devices/Current/{userId}")] public MediaDevice CurrentDevice(int userId) { - return _impl.CurrentDevice(userId); + return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).ServerCache; } [HttpPost("Linking/Directories/{userId}")] public void UseDirectories(int userId, List directories) { - _impl.UseDirectories(userId, directories); + var settings = _settingsProvider.GetSettings(); + if (directories == null) + { + settings.Plex.Libraries = new List(); + return; + } + + settings.Plex.Libraries = directories.Select(s => s.Key).ToList(); + _settingsProvider.SaveSettings(); } [HttpGet("Linking/Directories/{userId}")] public Directory[] Directories(int userId) { - return _impl.Directories(userId); + return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetDirectories(); } [HttpPost("Linking/Servers/{userId}")] public void UseDevice(int userId, MediaDevice server) { - _impl.UseDevice(userId, server); + PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).UseServer(server); } [HttpGet("Linking/Devices/{userId}")] public MediaDevice[] AvailableDevices(int userId) { - return _impl.AvailableDevices(userId) ?? new MediaDevice[0]; - } - - - [HttpGet("Metadata/{userId}/{type}/{id}")] - public MediaContainer GetMetadataWithoutHistory(string userId, int type, string id) - { - return GetMetadata(userId, type, id, null, null); + return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetPlexServers().ToArray(); } } diff --git a/Shoko.Server/API/v1/Modules/ImageModule.cs b/Shoko.Server/API/v1/Modules/ImageModule.cs deleted file mode 100644 index 714d07759..000000000 --- a/Shoko.Server/API/v1/Modules/ImageModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -#if false -using Nancy.Rest.Module; - -namespace Shoko.Server.API.v1 -{ - public class ImageModule : RestModule - { - public ImageModule() - { - SetRestImplementation(new ShokoServiceImplementationImage()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/KodiModule.cs b/Shoko.Server/API/v1/Modules/KodiModule.cs deleted file mode 100644 index 182f070f6..000000000 --- a/Shoko.Server/API/v1/Modules/KodiModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if false -using Nancy.Rest.Module; -using Shoko.Server.API.v1.Implementations; - -namespace Shoko.Server.API.v1.Modules -{ - public class KodiModule : RestModule - { - public KodiModule() - { - SetRestImplementation(new ShokoServiceImplementationKodi()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/MainModule.cs b/Shoko.Server/API/v1/Modules/MainModule.cs deleted file mode 100644 index d9f1c0707..000000000 --- a/Shoko.Server/API/v1/Modules/MainModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -#if false -using Nancy.Rest.Module; - -namespace Shoko.Server.API.v1 -{ - public class MainModule : RestModule - { - public MainModule() - { - SetRestImplementation(new ShokoServiceImplementation()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/MetroModule.cs b/Shoko.Server/API/v1/Modules/MetroModule.cs deleted file mode 100644 index a73ecaef4..000000000 --- a/Shoko.Server/API/v1/Modules/MetroModule.cs +++ /dev/null @@ -1,19 +0,0 @@ -#if false -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Nancy.Rest.Module; - -namespace Shoko.Server.API.v1 -{ - public class MetroModule : RestModule - { - public MetroModule() - { - SetRestImplementation(new ShokoServiceImplementationMetro()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/PlexModule.cs b/Shoko.Server/API/v1/Modules/PlexModule.cs deleted file mode 100644 index 50f481de8..000000000 --- a/Shoko.Server/API/v1/Modules/PlexModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if false -using Nancy.Rest.Module; -using Shoko.Server.API.v1.Implementations; - -namespace Shoko.Server.API.v1.Modules -{ - public class PlexModule : RestModule - { - public PlexModule() - { - SetRestImplementation(new ShokoServiceImplementationPlex()); - } - } -} -#endif diff --git a/Shoko.Server/API/v1/Modules/StreamModule.cs b/Shoko.Server/API/v1/Modules/StreamModule.cs deleted file mode 100644 index e2028b4e1..000000000 --- a/Shoko.Server/API/v1/Modules/StreamModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if false -using Nancy.Rest.Module; -using Shoko.Server.API.v1.Implementations; - -namespace Shoko.Server.API.v1.Modules -{ - public class StreamModule : RestModule - { - public StreamModule() - { - SetRestImplementation(new ShokoServiceImplementationStream()); - } - } -} -#endif diff --git a/Shoko.Server/API/v2/APIV2Helper.cs b/Shoko.Server/API/v2/APIV2Helper.cs index 45cd90160..8fefe4356 100644 --- a/Shoko.Server/API/v2/APIV2Helper.cs +++ b/Shoko.Server/API/v2/APIV2Helper.cs @@ -1,41 +1,14 @@ -using System; -using System.Net; using Microsoft.AspNetCore.Http; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.API.v2.Models.common; -using Shoko.Server.Models; -using Shoko.Server.PlexAndKodi; -using Shoko.Server.Repositories; namespace Shoko.Server.API.v2; public class APIV2Helper { - #region Contructors - public static string ConstructUnsortUrl(HttpContext ctx, bool short_url = false) { return APIHelper.ProperURL(ctx, "/api/file/unsort", short_url); } - [Obsolete] - public static string ConstructGroupIdUrl(HttpContext ctx, string gid, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__" + (int)JMMType.Group + "/" + gid, short_url); - } - - [Obsolete] - public static string ConstructSerieIdUrl(HttpContext ctx, string sid, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__" + (int)JMMType.Serie + " / " + sid, short_url); - } - - [Obsolete] - public static string ConstructVideoUrl(HttpContext ctx, string vid, JMMType type, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__" + (int)type + "/" + vid, short_url); - } - public static string ConstructFilterIdUrl(HttpContext ctx, int groupfilter_id, bool short_url = false) { return APIHelper.ProperURL(ctx, "/api/filter?id=" + groupfilter_id, short_url); @@ -46,39 +19,6 @@ public static string ConstructFilterUrl(HttpContext ctx, bool short_url = false) return APIHelper.ProperURL(ctx, "/api/filter", short_url); } - [Obsolete] - public static string ConstructFiltersUrl(HttpContext ctx, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "__TEST__", short_url); - } - - [Obsolete] - public static string ConstructSearchUrl(HttpContext ctx, string limit, string query, bool searchTag, - bool short_url = false) - { - if (searchTag) - { - return APIHelper.ProperURL(ctx, "/api/searchTag/" + limit + "/" + WebUtility.UrlEncode(query), - short_url); - } - - return APIHelper.ProperURL(ctx, "/api/search/" + limit + "/" + WebUtility.UrlEncode(query), - short_url); - } - - [Obsolete] - public static string ConstructPlaylistUrl(HttpContext ctx, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "/api/metadata/" + (int)JMMType.Playlist + "/0", short_url); - } - - [Obsolete] - public static string ConstructPlaylistIdUrl(HttpContext ctx, int pid, bool short_url = false) - { - return APIHelper.ProperURL(ctx, "/api/metadata/" + (int)JMMType.Playlist + "/" + pid, short_url); - } - - public static string ConstructVideoLocalStream(HttpContext ctx, int userid, string vid, string name, bool autowatch) { return APIHelper.ProperURL(ctx, "/Stream/" + vid + "/" + userid + "/" + autowatch + "/" + name); @@ -88,123 +28,4 @@ public static string ConstructSupportImageLink(HttpContext ctx, string name, boo { return APIHelper.ProperURL(ctx, "/api/v2/image/support/" + name, short_url); } - - public static string ConstructImageLinkFromRest(HttpContext ctx, string path, bool short_url = true) - { - return ConvertRestImageToNonRestUrl(ctx, path, short_url); - } - - private static string ConvertRestImageToNonRestUrl(HttpContext ctx, string url, bool short_url) - { - // Rest URLs should always end in either type/id or type/id/ratio - // Regardless of ',' or '.', ratio will not parse as int - if (string.IsNullOrEmpty(url)) - { - return null; - } - - var link = url.ToLower(); - var split = link.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - int type; - if (int.TryParse(split[split.Length - 1], out var id)) // no ratio - { - if (int.TryParse(split[split.Length - 2], out type)) - { - return APIHelper.ConstructImageLinkFromTypeAndId(ctx, type, id, short_url); - } - } - else if (int.TryParse(split[split.Length - 2], out id)) // ratio - { - if (int.TryParse(split[split.Length - 3], out type)) - { - return APIHelper.ConstructImageLinkFromTypeAndId(ctx, type, id, short_url); - } - } - - return null; // invalid url, which did not end in type/id[/ratio] - } - - #endregion - - public static Filter FilterFromGroupFilter(HttpContext ctx, SVR_GroupFilter gg, int uid) - { - var ob = new Filter - { - name = gg.GroupFilterName, id = gg.GroupFilterID, url = ConstructFilterIdUrl(ctx, gg.GroupFilterID) - }; - if (gg.GroupsIds.ContainsKey(uid)) - { - var groups = gg.GroupsIds[uid]; - if (groups.Count != 0) - { - ob.size = groups.Count; - ob.viewed = 0; - - foreach (var grp in groups) - { - var ag = RepoFactory.AnimeGroup.GetByID(grp); - var v = ag.GetPlexContract(uid); - if (v?.Art != null && v.Thumb != null) - { - ob.art.fanart.Add(new Art { url = ConstructImageLinkFromRest(ctx, v.Art), index = 0 }); - ob.art.thumb.Add(new Art { url = ConstructImageLinkFromRest(ctx, v.Thumb), index = 0 }); - break; - } - } - } - } - - return ob; - } - - public static Filter FilterFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup grp, int uid) - { - var ob = new Filter - { - name = grp.GroupName, - id = grp.AnimeGroupID, - url = ConstructFilterIdUrl(ctx, grp.AnimeGroupID), - size = -1, - viewed = -1 - }; - foreach (var ser in grp.GetSeries().Randomize()) - { - var anim = ser.GetAnime(); - if (anim != null) - { - var fanart = anim.GetDefaultFanartDetailsNoBlanks(); - var banner = anim.GetDefaultWideBannerDetailsNoBlanks(); - - if (fanart != null) - { - ob.art.fanart.Add(new Art - { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)fanart.ImageType, fanart.ImageID), - index = ob.art.fanart.Count - }); - ob.art.thumb.Add(new Art - { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)fanart.ImageType, fanart.ImageID), - index = ob.art.thumb.Count - }); - } - - if (banner != null) - { - ob.art.banner.Add(new Art - { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)banner.ImageType, banner.ImageID), - index = ob.art.banner.Count - }); - } - - if (ob.art.fanart.Count > 0) - { - break; - } - } - } - - return ob; - } } diff --git a/Shoko.Server/API/v2/Models/common/Filter.cs b/Shoko.Server/API/v2/Models/common/Filter.cs index 6b0db010c..431b6e77b 100644 --- a/Shoko.Server/API/v2/Models/common/Filter.cs +++ b/Shoko.Server/API/v2/Models/common/Filter.cs @@ -3,9 +3,10 @@ using System.Linq; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; -using Shoko.Models.Client; +using Microsoft.Extensions.DependencyInjection; using Shoko.Models.Enums; using Shoko.Models.Server; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -33,107 +34,88 @@ public Filter() groups = new List(); } - internal new static Filter GenerateFromGroupFilter(HttpContext ctx, SVR_GroupFilter gf, int uid, bool nocast, + internal static Filter GenerateFromGroupFilter(HttpContext ctx, FilterPreset gf, int uid, bool nocast, bool notag, int level, - bool all, bool allpic, int pic, TagFilter.Filter tagfilter) + bool all, bool allpic, int pic, TagFilter.Filter tagfilter, List> evaluatedResults = null) { var groups = new List(); - var filter = new Filter { name = gf.GroupFilterName, id = gf.GroupFilterID, size = 0 }; - if (gf.GroupsIds.ContainsKey(uid)) + var filter = new Filter { name = gf.Name, id = gf.FilterPresetID, size = 0 }; + if (evaluatedResults == null) { - var groupsh = gf.GroupsIds[uid]; - if (groupsh.Count != 0) - { - filter.size = groupsh.Count; + var evaluator = ctx.RequestServices.GetRequiredService(); + evaluatedResults = evaluator.EvaluateFilter(gf, ctx.GetUser().JMMUserID).ToList(); + } + + if (evaluatedResults.Count != 0) + { + filter.size = evaluatedResults.Count; - // Populate Random Art - List groupsList; + // Populate Random Art - List arts = null; - if (gf.SeriesIds.ContainsKey(uid)) + List arts = null; + var seriesList = evaluatedResults.SelectMany(a => a).Select(RepoFactory.AnimeSeries.GetByID).ToList(); + var groupsList = evaluatedResults.Select(r => RepoFactory.AnimeGroup.GetByID(r.Key)).ToList(); + if (pic == 1) + { + arts = seriesList.Where(SeriesHasCompleteArt).Where(a => a != null).ToList(); + if (arts.Count == 0) { - var seriesList = gf.SeriesIds[uid].Select(RepoFactory.AnimeSeries.GetByID).ToList(); - groupsList = seriesList.Select(a => a.AnimeGroupID).Distinct() - .Select(RepoFactory.AnimeGroup.GetByID).ToList(); - if (pic == 1) - { - arts = seriesList.Where(SeriesHasCompleteArt).Where(a => a != null).ToList(); - if (arts.Count == 0) - { - arts = seriesList.Where(SeriesHasMostlyCompleteArt).Where(a => a != null).ToList(); - } - - if (arts.Count == 0) - { - arts = seriesList; - } - } + arts = seriesList.Where(SeriesHasMostlyCompleteArt).Where(a => a != null).ToList(); } - else + + if (arts.Count == 0) { - groupsList = new List(); + arts = seriesList; } + } - if (arts?.Count > 0) - { - var rand = new Random(); - var anime = arts[rand.Next(arts.Count)]; + if (arts?.Count > 0) + { + var rand = new Random(); + var anime = arts[rand.Next(arts.Count)]; - var fanarts = GetFanartFromSeries(anime); - if (fanarts.Any()) - { - var fanart = fanarts[rand.Next(fanarts.Count)]; - filter.art.fanart.Add(new Art - { - index = 0, - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_FanArt, - fanart.TvDB_ImageFanartID) - }); - } - - var banners = GetBannersFromSeries(anime); - if (banners.Any()) - { - var banner = banners[rand.Next(banners.Count)]; - filter.art.banner.Add(new Art - { - index = 0, - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_Banner, - banner.TvDB_ImageWideBannerID) - }); - } - - filter.art.thumb.Add(new Art + var fanarts = GetFanartFromSeries(anime); + if (fanarts.Any()) + { + var fanart = fanarts[rand.Next(fanarts.Count)]; + filter.art.fanart.Add(new Art { - url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.AniDB_Cover, - anime.AniDB_ID), - index = 0 + index = 0, + url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_FanArt, + fanart.TvDB_ImageFanartID) }); } - var order = new Dictionary(); - if (level > 0) + var banners = GetBannersFromSeries(anime); + if (banners.Any()) { - foreach (var ag in groupsList) + var banner = banners[rand.Next(banners.Count)]; + filter.art.banner.Add(new Art { - var group = - Group.GenerateFromAnimeGroup(ctx, ag, uid, nocast, notag, level - 1, all, - filter.id, allpic, pic, tagfilter); - groups.Add(group); - order.Add(ag.GetUserContract(uid), group); - } + index = 0, + url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.TvDB_Banner, + banner.TvDB_ImageWideBannerID) + }); } - if (groups.Count > 0) + filter.art.thumb.Add(new Art { - // Proper Sorting! - IEnumerable grps = order.Keys; - grps = gf.SortCriteriaList.Count != 0 - ? GroupFilterHelper.Sort(grps, gf) - : grps.OrderBy(a => a.GroupName); - groups = grps.Select(a => order[a]).ToList(); - filter.groups = groups; - } + url = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.AniDB_Cover, + anime.AniDB_ID), + index = 0 + }); + } + + if (level > 0) + { + groups.AddRange(groupsList.Select(ag => + Group.GenerateFromAnimeGroup(ctx, ag, uid, nocast, notag, level - 1, all, filter.id, allpic, pic, tagfilter, + evaluatedResults.FirstOrDefault(a => a.Key == ag.AnimeGroupID)?.ToList()))); + } + + if (groups.Count > 0) + { + filter.groups = groups; } } diff --git a/Shoko.Server/API/v2/Models/common/Filters.cs b/Shoko.Server/API/v2/Models/common/Filters.cs index a10659efd..d6aee9315 100644 --- a/Shoko.Server/API/v2/Models/common/Filters.cs +++ b/Shoko.Server/API/v2/Models/common/Filters.cs @@ -2,9 +2,11 @@ using System.Linq; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Shoko.Commons.Extensions; using Shoko.Models; using Shoko.Models.Enums; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -24,43 +26,38 @@ public Filters() filters = new List(); } - internal static Filters GenerateFromGroupFilter(HttpContext ctx, SVR_GroupFilter gf, int uid, bool nocast, + internal static Filters GenerateFromGroupFilter(HttpContext ctx, FilterPreset gf, int uid, bool nocast, bool notag, int level, - bool all, bool allpic, int pic, TagFilter.Filter tagfilter) + bool all, bool allpic, int pic, TagFilter.Filter tagfilter, Dictionary>> evaluatedResults = null) { - var f = new Filters { id = gf.GroupFilterID, name = gf.GroupFilterName }; + var f = new Filters { id = gf.FilterPresetID, name = gf.Name }; + var hideCategories = ctx.GetUser().GetHideCategories(); + var gfs = RepoFactory.FilterPreset.GetByParentID(f.id).AsParallel().Where(a => + !a.Hidden && !((a.FilterType & GroupFilterType.Tag) != 0 && + (!hideCategories.Contains(a.Name) || TagFilter.IsTagBlackListed(a.Name, tagfilter)))) + .ToList(); - var _ = new List(); - var gfs = RepoFactory.GroupFilter.GetByParentID(f.id).AsParallel() - // Not invisible in clients - .Where(a => !a.IsHidden && - // and Has groups or is a directory - ((a.GroupsIds.ContainsKey(uid) && a.GroupsIds[uid].Count > 0) || - a.IsDirectory) && - // and is not a blacklisted tag - !((a.FilterType & (int)GroupFilterType.Tag) != 0 && - TagFilter.IsTagBlackListed(a.GroupFilterName, tagfilter))); + if (evaluatedResults == null) + { + var evaluator = ctx.RequestServices.GetRequiredService(); + evaluatedResults = evaluator.BatchEvaluateFilters(gfs, ctx.GetUser().JMMUserID); + gfs = gfs.Where(a => evaluatedResults[a].Any()).ToList(); + } if (level > 0) { var filters = gfs.Select(cgf => - Filter.GenerateFromGroupFilter(ctx, cgf, uid, nocast, notag, level - 1, all, allpic, pic, - tagfilter)).ToList(); + Filter.GenerateFromGroupFilter(ctx, cgf, uid, nocast, notag, level - 1, all, allpic, pic, tagfilter, evaluatedResults[cgf].ToList())).ToList(); - if (gf.FilterType == ((int)GroupFilterType.Season | (int)GroupFilterType.Directory)) - { - f.filters = filters.OrderBy(a => a.name, new SeasonComparator()).Cast().ToList(); - } - else - { - f.filters = filters.OrderByNatural(a => a.name).Cast().ToList(); - } + f.filters = gf.FilterType == (GroupFilterType.Season | GroupFilterType.Directory) + ? filters.OrderBy(a => a.name, new SeasonComparator()).Cast().ToList() + : filters.OrderByNatural(a => a.name).Cast().ToList(); f.size = filters.Count; } else { - f.size = gfs.Count(); + f.size = gfs.Count; } f.url = APIV2Helper.ConstructFilterIdUrl(ctx, f.id); diff --git a/Shoko.Server/API/v2/Models/common/Group.cs b/Shoko.Server/API/v2/Models/common/Group.cs index 59eaefdb9..6e5fc74f2 100644 --- a/Shoko.Server/API/v2/Models/common/Group.cs +++ b/Shoko.Server/API/v2/Models/common/Group.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Shoko.Models.Enums; using Shoko.Models.PlexAndKodi; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.PlexAndKodi; using Shoko.Server.Repositories; @@ -31,7 +33,7 @@ public Group() public static Group GenerateFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup ag, int uid, bool nocast, bool notag, int level, - bool all, int filterid, bool allpic, int pic, TagFilter.Filter tagfilter) + bool all, int filterid, bool allpic, int pic, TagFilter.Filter tagfilter, List evaluatedSeriesIDs = null) { var g = new Group { @@ -43,124 +45,106 @@ public static Group GenerateFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup ag, i edited = ag.DateTimeUpdated }; - SVR_GroupFilter filter = null; - if (filterid > 0) + if (filterid > 0 && evaluatedSeriesIDs == null) { - filter = RepoFactory.GroupFilter.GetByID(filterid); - if (filter?.ApplyToSeries == 0) - { - filter = null; - } + var filter = RepoFactory.FilterPreset.GetByID(filterid); + var evaluator = ctx.RequestServices.GetRequiredService(); + evaluatedSeriesIDs = evaluator.EvaluateFilter(filter, ctx.GetUser().JMMUserID).FirstOrDefault(a => a.Key == ag.AnimeGroupID)?.ToList(); } - List animes; - if (filter != null) + var animes = evaluatedSeriesIDs != null + ? evaluatedSeriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)).Select(ser => ser.GetAnime()).Where(a => a != null).ToList() + : ag.Anime?.OrderBy(a => a.BeginYear).ThenBy(a => a.AirDate ?? DateTime.MaxValue).ToList(); + + if (animes is not { Count: > 0 }) return g; + + var anime = animes.FirstOrDefault(); + if (anime == null) return g; + + PopulateArtFromAniDBAnime(ctx, animes, g, allpic, pic); + + List ael; + if (evaluatedSeriesIDs != null) { - animes = filter.SeriesIds[uid].Select(id => RepoFactory.AnimeSeries.GetByID(id)) - .Where(ser => ser?.AnimeGroupID == ag.AnimeGroupID).Select(ser => ser.GetAnime()) - .Where(a => a != null).OrderBy(a => a.BeginYear).ThenBy(a => a.AirDate ?? DateTime.MaxValue) - .ToList(); + var series = evaluatedSeriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)).ToList(); + ael = series.SelectMany(ser => ser?.GetAnimeEpisodes()).Where(a => a != null).ToList(); + g.size = series.Count; } else { - animes = ag.Anime?.OrderBy(a => a.BeginYear).ThenBy(a => a.AirDate ?? DateTime.MaxValue).ToList(); + var series = ag.GetAllSeries(); + ael = series.SelectMany(a => a?.GetAnimeEpisodes()).Where(a => a != null).ToList(); + g.size = series.Count; } - if (animes != null && animes.Count > 0) - { - var anime = animes.FirstOrDefault(a => a != null); - if (anime == null) - { - return g; - } + GenerateSizes(g, ael, uid); - PopulateArtFromAniDBAnime(ctx, animes, g, allpic, pic); + g.air = anime.AirDate?.ToPlexDate() ?? string.Empty; - List ael; - if (filter != null && filter.SeriesIds.ContainsKey(uid)) - { - var series = filter.SeriesIds[uid].Select(id => RepoFactory.AnimeSeries.GetByID(id)) - .Where(ser => (ser?.AnimeGroupID ?? 0) == ag.AnimeGroupID).ToList(); - ael = series.SelectMany(ser => ser?.GetAnimeEpisodes()).Where(a => a != null) - .ToList(); - g.size = series.Count; - } - else - { - var series = ag.GetAllSeries(); - ael = series.SelectMany(a => a?.GetAnimeEpisodes()).Where(a => a != null).ToList(); - g.size = series.Count; - } - - GenerateSizes(g, ael, uid); - - g.air = anime.AirDate?.ToPlexDate() ?? string.Empty; - - g.rating = Math.Round(ag.AniDBRating / 100, 1).ToString(CultureInfo.InvariantCulture); - g.summary = anime.Description ?? string.Empty; - g.titles = anime.GetTitles().Select(s => new AnimeTitle - { - Type = s.TitleType.ToString().ToLower(), Language = s.LanguageCode, Title = s.Title - }).ToList(); - g.year = anime.BeginYear.ToString(); + g.rating = Math.Round(ag.AniDBRating / 100, 1).ToString(CultureInfo.InvariantCulture); + g.summary = anime.Description ?? string.Empty; + g.titles = anime.GetTitles().Select(s => new AnimeTitle + { + Type = s.TitleType.ToString().ToLower(), Language = s.LanguageCode, Title = s.Title + }).ToList(); + g.year = anime.BeginYear.ToString(); - if (!notag && ag.Contract.Stat_AllTags != null) - { - g.tags = TagFilter.String.ProcessTags(tagfilter, ag.Contract.Stat_AllTags.ToList()); - } + if (!notag && ag.Contract.Stat_AllTags != null) + { + g.tags = TagFilter.String.ProcessTags(tagfilter, ag.Contract.Stat_AllTags.ToList()); + } - if (!nocast) + if (!nocast) + { + var xref_animestaff = + RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(anime.AnimeID, StaffRoleType.Seiyuu); + foreach (var xref in xref_animestaff) { - var xref_animestaff = - RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(anime.AnimeID, StaffRoleType.Seiyuu); - foreach (var xref in xref_animestaff) + if (xref.RoleID == null) { - if (xref.RoleID == null) - { - continue; - } - - var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); - if (character == null) - { - continue; - } + continue; + } - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); - if (staff == null) - { - continue; - } + var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); + if (character == null) + { + continue; + } - var role = new Role - { - character = character.Name, - character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Character, - xref.RoleID.Value), - staff = staff.Name, - staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Staff, - xref.StaffID), - role = xref.Role, - type = ((StaffRoleType)xref.RoleType).ToString() - }; - if (g.roles == null) - { - g.roles = new List(); - } + var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); + if (staff == null) + { + continue; + } - g.roles.Add(role); + var role = new Role + { + character = character.Name, + character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Character, + xref.RoleID.Value), + staff = staff.Name, + staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, (int)ImageEntityType.Staff, + xref.StaffID), + role = xref.Role, + type = ((StaffRoleType)xref.RoleType).ToString() + }; + if (g.roles == null) + { + g.roles = new List(); } + + g.roles.Add(role); } + } - if (level > 0) + if (level > 0) + { + foreach (var ada in animes.Select(a => RepoFactory.AnimeSeries.GetByAnimeID(a.AnimeID))) { - foreach (var ada in animes.Select(a => RepoFactory.AnimeSeries.GetByAnimeID(a.AnimeID))) - { - g.series.Add(Serie.GenerateFromAnimeSeries(ctx, ada, uid, nocast, notag, level - 1, all, allpic, - pic, tagfilter)); - } - // we already sorted animes, so no need to sort + g.series.Add(Serie.GenerateFromAnimeSeries(ctx, ada, uid, nocast, notag, level - 1, all, allpic, + pic, tagfilter)); } + // we already sorted animes, so no need to sort } return g; diff --git a/Shoko.Server/API/v2/Modules/Common.cs b/Shoko.Server/API/v2/Modules/Common.cs index a63f68675..2305aaba6 100644 --- a/Shoko.Server/API/v2/Modules/Common.cs +++ b/Shoko.Server/API/v2/Modules/Common.cs @@ -10,12 +10,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using NLog; using Quartz; using QuartzJobFactory; using Shoko.Commons.Extensions; -using Shoko.Commons.Utils; using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Server.API.v2.Models.common; @@ -23,12 +23,14 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Scheduling.Jobs; using Shoko.Server.Server; using Shoko.Server.Settings; using Shoko.Server.Utilities; +using APIFilters = Shoko.Server.API.v2.Models.common.Filters; namespace Shoko.Server.API.v2.Modules; @@ -2657,28 +2659,24 @@ internal ActionResult SerieVote(int id, int score, int uid) [HttpGet("cloud/list")] public ActionResult GetCloudAccounts() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } [HttpGet("cloud/count")] public ActionResult GetCloudAccountsCount() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } [HttpPost("cloud/add")] public ActionResult AddCloudAccount() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } [HttpPost("cloud/delete")] public ActionResult DeleteCloudAccount() { - // TODO APIv2: Cloud return StatusCode(StatusCodes.Status501NotImplemented); } @@ -2698,7 +2696,7 @@ public async Task RunCloudImport() /// Handle /api/filter /// Using if without ?id consider using ?level as it will scan resursive for object from Filter to RawFile /// - /// Filter or List + /// or [HttpGet("filter")] public object GetFilters([FromQuery] API_Call_Parameters para) { @@ -2727,29 +2725,31 @@ public object GetFilters([FromQuery] API_Call_Parameters para) internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool all, bool allpic, int pic, TagFilter.Filter tagfilter) { - var filters = new Filters + var filters = new APIFilters { id = 0, name = "Filters", viewed = 0, url = APIV2Helper.ConstructFilterUrl(HttpContext) }; - var allGfs = RepoFactory.GroupFilter.GetTopLevel() - .Where(a => !a.IsHidden && - ((a.GroupsIds.ContainsKey(uid) && a.GroupsIds[uid].Count > 0) || - a.IsDirectory)) - .ToList(); - var _filters = new List(); + var allGfs = RepoFactory.FilterPreset.GetTopLevel().Where(a => !a.Hidden).ToList(); + var _filters = new List(); + var evaluator = HttpContext.RequestServices.GetRequiredService(); + var user = HttpContext.GetUser(); + var hideCategories = user.GetHideCategories(); + var filtersToEvaluate = level > 1 + ? RepoFactory.FilterPreset.GetAll().Where(a => (a.FilterType & GroupFilterType.Tag) == 0 || !hideCategories.Contains(a.Name)).ToList() + : allGfs; + var result = evaluator.BatchEvaluateFilters(filtersToEvaluate, user.JMMUserID); + allGfs = allGfs.Where(a => result[a].Any()).ToList(); foreach (var gf in allGfs) { - Filters filter; - if (!gf.IsDirectory) + APIFilters filter; + if (!gf.IsDirectory()) { - filter = Filter.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, - tagfilter); + filter = Filter.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter, result[gf].ToList()); } else { - filter = Filters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, - tagfilter); + filter = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter, result); } _filters.Add(filter); @@ -2791,12 +2791,12 @@ internal object GetAllFilters(int uid, bool nocast, bool notag, int level, bool internal object GetFilter(int id, int uid, bool nocast, bool notag, int level, bool all, bool allpic, int pic, TagFilter.Filter tagfilter) { - var gf = RepoFactory.GroupFilter.GetByID(id); + var gf = RepoFactory.FilterPreset.GetByID(id); - if (gf.IsDirectory) + if (gf.IsDirectory()) { // if it's a directory, it IS a filter-inception; - var fgs = Filters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, + var fgs = APIFilters.GenerateFromGroupFilter(HttpContext, gf, uid, nocast, notag, level, all, allpic, pic, tagfilter); return fgs; } diff --git a/Shoko.Server/API/v2/Modules/Core.cs b/Shoko.Server/API/v2/Modules/Core.cs index 8716bda5c..8778ff8e3 100644 --- a/Shoko.Server/API/v2/Modules/Core.cs +++ b/Shoko.Server/API/v2/Modules/Core.cs @@ -241,7 +241,7 @@ public Credentials GetAniDB() [HttpGet("anidb/votes/sync")] public ActionResult SyncAniDBVotes() { - //TODO APIv2: Command should be split into AniDb/MAL sepereate + //TODO APIv2: Command should be split into AniDb/MAL separate _commandFactory.CreateAndSave(); return APIStatus.OK(); } @@ -610,8 +610,20 @@ public ActionResult ScanMovieDB() [HttpGet("user/list")] public ActionResult> GetUsers() { - var common = HttpContext.RequestServices.GetRequiredService(); - return common.GetUsers(); + var users = new Dictionary(); + try + { + foreach (var us in RepoFactory.JMMUser.GetAll()) + { + users.Add(us.JMMUserID, us.Username); + } + + return users; + } + catch + { + return null; + } } /// diff --git a/Shoko.Server/API/v3/Controllers/ActionController.cs b/Shoko.Server/API/v3/Controllers/ActionController.cs index b4c8aaf34..3aa344344 100644 --- a/Shoko.Server/API/v3/Controllers/ActionController.cs +++ b/Shoko.Server/API/v3/Controllers/ActionController.cs @@ -9,6 +9,7 @@ using Quartz; using QuartzJobFactory; using Shoko.Server.API.Annotations; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; @@ -255,7 +256,7 @@ public ActionResult UpdateMissingAniDBXML() if (rawXml != null) continue; - Series.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, true, false, false); + SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, true, false, false); queuedAnimeSet.Add(animeID); } @@ -275,7 +276,7 @@ public ActionResult UpdateMissingAniDBXML() if (++index % 10 == 1) _logger.LogInformation("Queueing {MissingAnimeCount} anime that needs an update — {CurrentCount}/{MissingAnimeCount}", missingAnimeSet.Count, index + 1, missingAnimeSet.Count); - Series.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, false, true, true); + SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, animeID, false, true, true); queuedAnimeSet.Add(animeID); } diff --git a/Shoko.Server/API/v3/Controllers/DashboardController.cs b/Shoko.Server/API/v3/Controllers/DashboardController.cs index d77b20535..ee20ea8e2 100644 --- a/Shoko.Server/API/v3/Controllers/DashboardController.cs +++ b/Shoko.Server/API/v3/Controllers/DashboardController.cs @@ -7,6 +7,7 @@ using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Server.API.Annotations; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Models; @@ -23,6 +24,8 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class DashboardController : BaseController { + private readonly SeriesFactory _seriesFactory; + /// /// Get the counters of various collection stats /// @@ -327,14 +330,14 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool includeRestr if (pageSize <= 0) { return seriesList - .Select(a => new Series(HttpContext, a)) + .Select(a => _seriesFactory.GetSeries(a)) .ToList(); } return seriesList .Skip(pageSize * (page - 1)) .Take(pageSize) - .Select(a => new Series(HttpContext, a)) + .Select(a => _seriesFactory.GetSeries(a)) .ToList(); } @@ -499,7 +502,8 @@ public Dashboard.EpisodeDetails GetEpisodeDetailsForSeriesAndEpisode(SVR_JMMUser .ToList(); } - public DashboardController(ISettingsProvider settingsProvider) : base(settingsProvider) + public DashboardController(ISettingsProvider settingsProvider, SeriesFactory seriesFactory) : base(settingsProvider) { + _seriesFactory = seriesFactory; } } diff --git a/Shoko.Server/API/v3/Controllers/FilterController.cs b/Shoko.Server/API/v3/Controllers/FilterController.cs index f3463a80e..4470a99c1 100644 --- a/Shoko.Server/API/v3/Controllers/FilterController.cs +++ b/Shoko.Server/API/v3/Controllers/FilterController.cs @@ -3,10 +3,8 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Caching.Memory; using Shoko.Models.Enums; using Shoko.Server.API.Annotations; @@ -14,6 +12,7 @@ using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Settings; @@ -26,12 +25,16 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class FilterController : BaseController { - private static IMemoryCache PreviewCache = new MemoryCache(new MemoryCacheOptions() { + private static readonly IMemoryCache PreviewCache = new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromMinutes(50), }); internal const string FilterNotFound = "No Filter entry for the given filterID"; + private readonly FilterFactory _factory; + private readonly SeriesFactory _seriesFactory; + private readonly FilterEvaluator _filterEvaluator; + #region Existing Filters /// @@ -49,25 +52,22 @@ public ActionResult> GetAllFilters([FromQuery] bool includeEm [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool withConditions = false) { var user = User; - return RepoFactory.GroupFilter.GetTopLevel() - .Where(filter => + + return _filterEvaluator.BatchEvaluateFilters(RepoFactory.FilterPreset.GetTopLevel(), user.JMMUserID, true) + .Where(kv => { - if (!showHidden && filter.IsHidden) + var filter = kv.Key; + if (!showHidden && filter.Hidden) return false; - if (includeEmpty || (filter.IsDirectory ? ( - // Check if the directory filter have any sub-directories - RepoFactory.GroupFilter.GetByParentID(filter.GroupFilterID).Count > 0 - ) : ( - // Check if the filter have any groups for the current user. - filter.GroupsIds.ContainsKey(user.JMMUserID) && filter.GroupsIds[user.JMMUserID].Count > 0 - ))) + if (includeEmpty || (filter.IsDirectory() ? RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Count > 0 : kv.Value.Any())) return true; return false; }) - .OrderBy(filter => filter.GroupFilterName) - .ToListResult(filter => new Filter(HttpContext, filter, withConditions), page, pageSize); + .Select(a => a.Key) + .OrderBy(filter => filter.Name) + .ToListResult(filter => _factory.GetFilter(filter, withConditions), page, pageSize); } /// @@ -79,8 +79,8 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool withConditio [HttpPost] public ActionResult AddNewFilter(Filter.Input.CreateOrUpdateFilterBody body) { - var groupFilter = new SVR_GroupFilter { FilterType = (int)GroupFilterType.UserDefined }; - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState); + var filterPreset = new FilterPreset { FilterType = GroupFilterType.UserDefined }; + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -96,11 +96,11 @@ public ActionResult AddNewFilter(Filter.Input.CreateOrUpdateFilterBody b [HttpGet("{filterID}")] public ActionResult GetFilter([FromRoute] int filterID, [FromQuery] bool withConditions = false) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - return new Filter(HttpContext, groupFilter, withConditions); + return _factory.GetFilter(filterPreset, withConditions); } /// @@ -114,16 +114,16 @@ public ActionResult GetFilter([FromRoute] int filterID, [FromQuery] bool [HttpPatch("{filterID}")] public ActionResult PatchFilter([FromRoute] int filterID, JsonPatchDocument document) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - var body = new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + var body = _factory.CreateOrUpdateFilterBody(filterPreset); document.ApplyTo(body, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState); + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -140,11 +140,11 @@ public ActionResult PatchFilter([FromRoute] int filterID, JsonPatchDocum [HttpPut("{filterID}")] public ActionResult PutFilter([FromRoute] int filterID, Filter.Input.CreateOrUpdateFilterBody body) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState); + var filter = _factory.MergeWithExisting(body, filterPreset, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); @@ -160,11 +160,11 @@ public ActionResult PutFilter([FromRoute] int filterID, Filter.Input.Cre [HttpDelete("{filterID}")] public ActionResult DeleteFilter(int filterID) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterNotFound); - RepoFactory.GroupFilter.Delete(groupFilter); + RepoFactory.FilterPreset.Delete(filterPreset); return NoContent(); } @@ -175,34 +175,27 @@ public ActionResult DeleteFilter(int filterID) #region Preview/On-the-fly Filter [NonAction] - internal static SVR_GroupFilter GetDefaultFilterForUser(SVR_JMMUser user) + internal static FilterPreset GetDefaultFilterForUser(SVR_JMMUser user) { - var groupFilter = new SVR_GroupFilter + var filterPreset = new FilterPreset { - FilterType = (int)GroupFilterType.UserDefined, - GroupFilterName = "Live Filtering", - InvisibleInClients = 0, - ApplyToSeries = 0, - BaseCondition = (int)GroupFilterBaseCondition.Include, - Conditions = new(), - SortCriteriaList = new(), + FilterType = GroupFilterType.UserDefined, + Name = "Live Filtering", }; - // TODO: Update default filter for user here. - - return groupFilter; + return filterPreset; } [NonAction] - internal static SVR_GroupFilter GetPreviewFilterForUser(SVR_JMMUser user) + internal static FilterPreset GetPreviewFilterForUser(SVR_JMMUser user) { var userId = user.JMMUserID; var key = $"User={userId}"; - if (!PreviewCache.TryGetValue(key, out SVR_GroupFilter groupFilter)) - groupFilter = PreviewCache.Set(key, GetDefaultFilterForUser(user), new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromHours(1) }); + if (!PreviewCache.TryGetValue(key, out FilterPreset filterPreset)) + filterPreset = PreviewCache.Set(key, GetDefaultFilterForUser(user), new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromHours(1) }); - return groupFilter; + return filterPreset; } [NonAction] @@ -210,7 +203,7 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) { var userId = user.JMMUserID; var key = $"User={userId}"; - if (!PreviewCache.TryGetValue(key, out SVR_GroupFilter groupFilter)) + if (!PreviewCache.TryGetValue(key, out FilterPreset _)) return false; PreviewCache.Remove(key); @@ -225,8 +218,8 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) [HttpGet("Preview")] public ActionResult GetPreviewFilter() { - var groupFilter = GetPreviewFilterForUser(User); - return new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + var filterPreset = GetPreviewFilterForUser(User); + return _factory.CreateOrUpdateFilterBody(filterPreset); } /// @@ -238,18 +231,18 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) [HttpPatch("Preview")] public ActionResult PatchPreviewFilter([FromBody] JsonPatchDocument document) { - var groupFilter = GetPreviewFilterForUser(User); + var filterPreset = GetPreviewFilterForUser(User); - var body = new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + var body = _factory.CreateOrUpdateFilterBody(filterPreset); document.ApplyTo(body, ModelState); if (!ModelState.IsValid) return ValidationProblem(ModelState); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState, true); + _factory.MergeWithExisting(body, filterPreset, ModelState, true); if (!ModelState.IsValid) return ValidationProblem(ModelState); - return new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + return _factory.CreateOrUpdateFilterBody(filterPreset); } /// @@ -260,12 +253,12 @@ internal static bool ResetPreviewFilterForUser(SVR_JMMUser user) [HttpPut("Preview")] public ActionResult PutPreviewFilter([FromBody] Filter.Input.CreateOrUpdateFilterBody body) { - var groupFilter = GetPreviewFilterForUser(User); - var filter = body.MergeWithExisting(HttpContext, groupFilter, ModelState, true); + var filterPreset = GetPreviewFilterForUser(User); + _factory.MergeWithExisting(body, filterPreset, ModelState, true); if (!ModelState.IsValid) return ValidationProblem(ModelState); - return new Filter.Input.CreateOrUpdateFilterBody(groupFilter); + return _factory.CreateOrUpdateFilterBody(filterPreset); } /// @@ -294,30 +287,25 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu [FromQuery] bool includeEmpty = false, [FromQuery] bool randomImages = false, [FromQuery] bool orderByName = false) { // Directories should only contain sub-filters, not groups and series. - var groupFilter = GetPreviewFilterForUser(User); - if (groupFilter.IsDirectory) + var filterPreset = GetPreviewFilterForUser(User); + if (filterPreset.IsDirectory()) return new ListResult(); // Fast path when user is not in the filter. - if (!groupFilter.GroupsIds.TryGetValue(User.JMMUserID, out var groupIds)) - return new ListResult(); + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID); + if (!results.Any()) return new ListResult(); - var groups = groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + var groups = results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { + // not top level groups if (group == null || group.AnimeGroupParentID.HasValue) - { return false; - } return includeEmpty || group.GetAllSeries() .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0)); }); - - groups = orderByName ? groups.OrderBy(group => group.GetSortName()) : - groups.OrderByGroupFilter(groupFilter); - return groups .ToListResult(group => new Group(HttpContext, group, randomImages), page, pageSize); } @@ -334,19 +322,20 @@ public ActionResult> GetPreviewGroupNameLettersInFilter([F { // Directories should only contain sub-filters, not groups and series. var user = User; - var groupFilter = GetPreviewFilterForUser(user); - if (groupFilter.IsDirectory) + var filterPreset = GetPreviewFilterForUser(user); + if (filterPreset.IsDirectory()) return new Dictionary(); // Fast path when user is not in the filter - if (!groupFilter.GroupsIds.TryGetValue(user.JMMUserID, out var groupIds)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new Dictionary(); - return groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + return results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { - if (group == null || group.AnimeGroupParentID.HasValue) + if (group is not { AnimeGroupParentID: null }) return false; return includeEmpty || group.GetAllSeries() @@ -373,25 +362,20 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu { // Directories should only contain sub-filters, not groups and series. var user = User; - var groupFilter = GetPreviewFilterForUser(user); - if (groupFilter.IsDirectory) + var filterPreset = GetPreviewFilterForUser(user); + if (filterPreset.IsDirectory()) return new ListResult(); - // Return all series if group filter is not applied to series. - if (groupFilter.ApplyToSeries != 1) - return RepoFactory.AnimeSeries.GetAll() - .Where(series => user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) - .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); - // Return early if every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new ListResult(); - return seriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)) - .Where(series => series != null && user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) + // We don't need separate logic for ApplyAtSeriesLevel, as the FilterEvaluator handles that + return results.SelectMany(a => a.Select(id => RepoFactory.AnimeSeries.GetByID(id))) + .Where(series => series != null && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); } /// @@ -406,7 +390,7 @@ public ActionResult> GetPreviewFilteredSubGroups([FromRoute] int gro [FromQuery] bool randomImages = false, [FromQuery] bool includeEmpty = false) { var user = User; - var groupFilter = GetPreviewFilterForUser(user); + var filterPreset = GetPreviewFilterForUser(user); // Check if the group exists. var group = RepoFactory.AnimeGroup.GetByID(groupID); @@ -417,13 +401,19 @@ public ActionResult> GetPreviewFilteredSubGroups([FromRoute] int gro return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); // Just return early because the every group will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - + + // Subgroups are weird. We'll take the group, build a set of all subgroup IDs, and use that to determine if a group should be included + // This should maintain the order of results, but have every group in the tree for those results + var orderedGroups = results.SelectMany(a => RepoFactory.AnimeGroup.GetByID(a.Key).TopLevelAnimeGroup.GetAllChildGroups().Select(b => b.AnimeGroupID)).ToArray(); + var groups = orderedGroups.ToHashSet(); + return group.GetChildGroups() .Where(subGroup => { @@ -437,13 +427,10 @@ public ActionResult> GetPreviewFilteredSubGroups([FromRoute] int gro .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0))) return false; - if (groupFilter.ApplyToSeries != 1) - return true; - - return subGroup.GetAllSeries().Any(series => seriesIDs.Contains(series.AnimeSeriesID)); + return groups.Contains(subGroup.AnimeGroupID); }) - .OrderByGroupFilter(groupFilter) - .Select(group => new Group(HttpContext, group, randomImages)) + .OrderBy(a => Array.IndexOf(orderedGroups, a.AnimeGroupID)) + .Select(g => new Group(HttpContext, g, randomImages)) .ToList(); } @@ -462,7 +449,7 @@ public ActionResult> GetPreviewSeriesInFilteredGroup([FromRoute] in [FromQuery] bool randomImages = false, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null) { var user = User; - var groupFilter = GetPreviewFilterForUser(user); + var filterPreset = GetPreviewFilterForUser(user); // Check if the group exists. var group = RepoFactory.AnimeGroup.GetByID(groupID); @@ -473,32 +460,40 @@ public ActionResult> GetPreviewSeriesInFilteredGroup([FromRoute] in return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); - if (groupFilter.ApplyToSeries != 1) + if (!filterPreset.ApplyAtSeriesLevel) return (recursive ? group.GetAllSeries() : group.GetSeries()) .Where(a => user.AllowedSeries(a)) .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) + .Select(series => _seriesFactory.GetSeries(series, randomImages, includeDataFrom)) .Where(series => series.Size > 0 || includeMissing) .ToList(); // Just return early because the every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - return (recursive ? group.GetAllSeries() : group.GetSeries()) - .Where(series => seriesIDs.Contains(series.AnimeSeriesID)) - .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) - .Where(series => series.Size > 0 || includeMissing) + var seriesIDs = recursive + ? group.GetAllChildGroups().SelectMany(a => results.FirstOrDefault(b => b.Key == a.AnimeGroupID)) + : results.FirstOrDefault(a => a.Key == groupID); + + var series = seriesIDs?.Select(a => RepoFactory.AnimeSeries.GetByID(a)).Where(a => a.GetVideoLocals().Any() || includeMissing) ?? + Array.Empty(); + + return series + .Select(a => _seriesFactory.GetSeries(a, randomImages, includeDataFrom)) .ToList(); } #endregion - public FilterController(ISettingsProvider settingsProvider) : base(settingsProvider) + public FilterController(ISettingsProvider settingsProvider, FilterFactory factory, SeriesFactory seriesFactory, FilterEvaluator filterEvaluator) : base(settingsProvider) { + _factory = factory; + _seriesFactory = seriesFactory; + _filterEvaluator = filterEvaluator; } } diff --git a/Shoko.Server/API/v3/Controllers/GroupController.cs b/Shoko.Server/API/v3/Controllers/GroupController.cs index d711392c3..c9564d3ae 100644 --- a/Shoko.Server/API/v3/Controllers/GroupController.cs +++ b/Shoko.Server/API/v3/Controllers/GroupController.cs @@ -280,7 +280,7 @@ public ActionResult> GetShokoRelationsBySeriesID([FromRoute .Select(series => series.AniDB_ID) .ToHashSet(); - // TODO: Replace with a more generic implementation capable of suplying relations from more than just AniDB. + // TODO: Replace with a more generic implementation capable of supplying relations from more than just AniDB. return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(animeIds) .Select(relation => (relation, relatedSeries: RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID))) diff --git a/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs b/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs index 0c85d4234..3e5ce6cc8 100644 --- a/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs +++ b/Shoko.Server/API/v3/Controllers/ReverseTreeController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Shoko.Server.API.Annotations; using Shoko.Server.API.ModelBinders; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Repositories; @@ -21,6 +22,9 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class ReverseTreeController : BaseController { + private readonly FilterFactory _filterFactory; + private readonly SeriesFactory _seriesFactory; + /// /// Get the parent for the with the given . /// @@ -35,26 +39,26 @@ public class ReverseTreeController : BaseController /// Always get the top-level /// [HttpGet("Filter/{filterID}/Parent")] - public ActionResult GetFilterFromFilter([FromRoute] int filterID, [FromQuery] bool topLevel = false) + public ActionResult GetParentFromFilter([FromRoute] int filterID, [FromQuery] bool topLevel = false) { - var filter = RepoFactory.GroupFilter.GetByID(filterID); + var filter = RepoFactory.FilterPreset.GetByID(filterID); if (filter == null) { return NotFound(FilterController.FilterNotFound); } - if (!filter.ParentGroupFilterID.HasValue || filter.ParentGroupFilterID.Value == 0) + if (filter.ParentFilterPresetID is null or 0) { return ValidationProblem("Unable to get parent Filter for a top-level Filter", "filterID"); } - var parentGroup = topLevel ? filter.TopLevelGroupFilter : filter.Parent; + var parentGroup = topLevel ? RepoFactory.FilterPreset.GetTopLevelFilter(filter.ParentFilterPresetID.Value) : RepoFactory.FilterPreset.GetByID(filter.ParentFilterPresetID.Value); if (parentGroup == null) { return InternalError("No parent Filter entry for the given filterID"); } - return new Filter(HttpContext, parentGroup); + return _filterFactory.GetFilter(parentGroup); } /// @@ -71,7 +75,7 @@ public ActionResult GetFilterFromFilter([FromRoute] int filterID, [FromQ /// Always get the top-level /// [HttpGet("Group/{groupID}/Parent")] - public ActionResult GetGroupFromGroup([FromRoute] int groupID, [FromQuery] bool topLevel = false) + public ActionResult GetParentFromGroup([FromRoute] int groupID, [FromQuery] bool topLevel = false) { var group = RepoFactory.AnimeGroup.GetByID(groupID); if (group == null) @@ -160,7 +164,7 @@ public ActionResult GetSeriesFromEpisode([FromRoute] int episodeID, [Fro return Forbid(EpisodeController.EpisodeForbiddenForUser); } - return new Series(HttpContext, series, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(series, randomImages, includeDataFrom); } /// @@ -190,7 +194,9 @@ public ActionResult> GetEpisodeFromFile([FromRoute] int fileID, .ToList(); } - public ReverseTreeController(ISettingsProvider settingsProvider) : base(settingsProvider) + public ReverseTreeController(ISettingsProvider settingsProvider, FilterFactory filterFactory, SeriesFactory seriesFactory) : base(settingsProvider) { + _filterFactory = filterFactory; + _seriesFactory = seriesFactory; } } diff --git a/Shoko.Server/API/v3/Controllers/SeriesController.cs b/Shoko.Server/API/v3/Controllers/SeriesController.cs index d5ad4a4d7..60dd2480f 100644 --- a/Shoko.Server/API/v3/Controllers/SeriesController.cs +++ b/Shoko.Server/API/v3/Controllers/SeriesController.cs @@ -37,12 +37,14 @@ namespace Shoko.Server.API.v3.Controllers; public class SeriesController : BaseController { private readonly ICommandRequestFactory _commandFactory; + private readonly SeriesFactory _seriesFactory; private readonly IHttpConnectionHandler _httpHandler; - public SeriesController(ICommandRequestFactory commandFactory, IHttpConnectionHandler httpHandler, ISettingsProvider settingsProvider) : base(settingsProvider) + public SeriesController(ICommandRequestFactory commandFactory, IHttpConnectionHandler httpHandler, ISettingsProvider settingsProvider, SeriesFactory seriesFactory) : base(settingsProvider) { _commandFactory = commandFactory; _httpHandler = httpHandler; + _seriesFactory = seriesFactory; } #region Return messages @@ -97,7 +99,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] string startsWith return user.AllowedSeries(series); }) .OrderBy(a => a.seriesName) - .ToListResult(tuple => new Series(HttpContext, tuple.series), page, pageSize); + .ToListResult(tuple => _seriesFactory.GetSeries(tuple.series), page, pageSize); } /// @@ -122,7 +124,7 @@ public ActionResult GetSeries([FromRoute] int seriesID, [FromQuery] bool return Forbid(SeriesForbiddenForUser); } - return new Series(HttpContext, series, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(series, randomImages, includeDataFrom); } /// @@ -234,7 +236,7 @@ public ActionResult> GetShokoRelationsBySeriesID([FromRoute return Forbid(SeriesForbiddenForUser); } - // TODO: Replace with a more generic implementation capable of suplying relations from more than just AniDB. + // TODO: Replace with a more generic implementation capable of supplying relations from more than just AniDB. return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(series.AniDB_ID) .Select(relation => (relation, relatedSeries: RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID))) @@ -257,7 +259,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return RepoFactory.AnimeSeries.GetAll() .Where(series => user.AllowedSeries(series) && series.GetVideoLocals().Count == 0) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series), page, pageSize); } /// @@ -274,7 +276,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return RepoFactory.AnimeSeries.GetAll() .Where(series => user.AllowedSeries(series) && series.GetVideoLocals(CrossRefSource.User).Count() != 0) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series), page, pageSize); } #endregion @@ -289,7 +291,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) /// Search only for anime with a main title that start with the given query. /// [HttpGet("AniDB")] - public ActionResult> GetAllAnime([FromQuery] [Range(0, 100)] int pageSize = 50, + public ActionResult> GetAllAnime([FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] string startsWith = "") { startsWith = startsWith.ToLowerInvariant(); @@ -307,7 +309,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] string startsWith return user.AllowedAnime(anime); }) .OrderBy(a => a.animeTitle) - .ToListResult(tuple => new Series.AniDBWithDate(tuple.anime), page, pageSize); + .ToListResult(tuple => _seriesFactory.GetAniDB(tuple.anime), page, pageSize); } /// @@ -333,7 +335,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) /// Shoko ID /// [HttpGet("{seriesID}/AniDB")] - public ActionResult GetSeriesAnidbBySeriesID([FromRoute] int seriesID) + public ActionResult GetSeriesAnidbBySeriesID([FromRoute] int seriesID) { var series = RepoFactory.AnimeSeries.GetByID(seriesID); if (series == null) @@ -352,7 +354,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return InternalError(AnidbNotFoundForSeriesID); } - return new Series.AniDBWithDate(anidb, series); + return _seriesFactory.GetAniDB(anidb, series); } /// @@ -381,7 +383,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) } return RepoFactory.AniDB_Anime_Similar.GetByAnimeID(anidb.AnimeID) - .Select(similar => new Series.AniDB(similar)) + .Select(similar => _seriesFactory.GetAniDB(similar)) .ToList(); } @@ -411,7 +413,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) } return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(anidb.AnimeID) - .Select(relation => new Series.AniDB(relation)) + .Select(relation => _seriesFactory.GetAniDB(relation)) .ToList(); } @@ -511,7 +513,7 @@ [FromQuery] [Range(0, 1)] double? approval = null var similarToCount = similarTo.Count(); return new Series.AniDBRecommendedForYou() { - Anime = new Series.AniDBWithDate(anime, series), SimilarTo = similarToCount + Anime = _seriesFactory.GetAniDB(anime, series), SimilarTo = similarToCount }; }) .OrderByDescending(e => e.SimilarTo) @@ -598,7 +600,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim /// AniDB ID /// [HttpGet("AniDB/{anidbID}")] - public ActionResult GetSeriesAnidbByAnidbID([FromRoute] int anidbID) + public ActionResult GetSeriesAnidbByAnidbID([FromRoute] int anidbID) { var anidb = RepoFactory.AniDB_Anime.GetByAnimeID(anidbID); if (anidb == null) @@ -611,7 +613,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim return Forbid(AnidbForbiddenForUser); } - return new Series.AniDBWithDate(anidb); + return _seriesFactory.GetAniDB(anidb); } /// @@ -634,7 +636,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim } return RepoFactory.AniDB_Anime_Similar.GetByAnimeID(anidbID) - .Select(similar => new Series.AniDB(similar)) + .Select(similar => _seriesFactory.GetAniDB(similar)) .ToList(); } @@ -658,7 +660,7 @@ private List GetWatchedAnimeForPeriod(SVR_JMMUser user, DateTim } return RepoFactory.AniDB_Anime_Relation.GetByAnimeID(anidbID) - .Select(relation => new Series.AniDB(relation)) + .Select(relation => _seriesFactory.GetAniDB(relation)) .ToList(); } @@ -708,7 +710,7 @@ public ActionResult GetSeriesByAnidbID([FromRoute] int anidbID, [FromQue return Forbid(SeriesForbiddenForUser); } - return new Series(HttpContext, series, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(series, randomImages, includeDataFrom); } /// @@ -736,7 +738,8 @@ public ActionResult RefreshAniDBByAniDBID([FromRoute] int anidbID, [FromQu createSeriesEntry = settings.AniDb.AutomaticallyImportSeries; } - return Series.QueueAniDBRefresh(_commandFactory, _httpHandler, anidbID, force, downloadRelations, + // TODO No + return SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, anidbID, force, downloadRelations, createSeriesEntry.Value, immediate, cacheOnly); } @@ -782,7 +785,8 @@ public ActionResult RefreshAniDBBySeriesID([FromRoute] int seriesID, [From return InternalError(AnidbNotFoundForSeriesID); } - return Series.QueueAniDBRefresh(_commandFactory, _httpHandler, anidb.AnimeID, force, downloadRelations, + // TODO No + return SeriesFactory.QueueAniDBRefresh(_commandFactory, _httpHandler, anidb.AnimeID, force, downloadRelations, createSeriesEntry.Value, immediate, cacheOnly); } @@ -819,7 +823,7 @@ public ActionResult RefreshAniDBFromXML([FromRoute] int seriesID) return Forbid(TvdbForbiddenForUser); } - return Series.GetTvDBInfo(HttpContext, series); + return _seriesFactory.GetTvDBInfo(series); } /// @@ -894,8 +898,9 @@ public ActionResult RefreshSeriesTvdbBySeriesID([FromRoute] int seriesID, [FromQ } var tvSeriesList = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID); + // TODO No foreach (var crossRef in tvSeriesList) - Series.QueueTvDBRefresh(_commandFactory, crossRef.TvDBID, force); + SeriesFactory.QueueTvDBRefresh(_commandFactory, crossRef.TvDBID, force); return Ok(); } @@ -931,7 +936,7 @@ public ActionResult RefreshSeriesTvdbBySeriesID([FromRoute] int seriesID, [FromQ return Forbid(TvdbForbiddenForUser); } - return new Series.TvDB(HttpContext, tvdb, series); + return _seriesFactory.GetTvDB(tvdb, series); } /// @@ -945,7 +950,8 @@ public ActionResult RefreshSeriesTvdbBySeriesID([FromRoute] int seriesID, [FromQ [HttpPost("TvDB/{tvdbID}/Refresh")] public ActionResult RefreshSeriesTvdbByTvdbId([FromRoute] int tvdbID, [FromQuery] bool force = false, [FromQuery] bool immediate = false) { - return Series.QueueTvDBRefresh(_commandFactory, tvdbID,force, immediate); + // TODO No + return SeriesFactory.QueueTvDBRefresh(_commandFactory, tvdbID,force, immediate); } /// @@ -974,7 +980,7 @@ public ActionResult> GetSeriesByTvdbID([FromRoute] int tvdbID) } return seriesList - .Select(series => new Series(HttpContext, series)) + .Select(series => _seriesFactory.GetSeries(series)) .ToList(); } @@ -1071,7 +1077,7 @@ public ActionResult PostSeriesUserVote([FromRoute] int seriesID, [FromBody] Vote if (vote.Value > vote.MaxValue) return ValidationProblem($"Value must be less than or equal to the set max value ({vote.MaxValue}).", nameof(vote.Value)); - Series.AddSeriesVote(_commandFactory, series, User.JMMUserID, vote); + SeriesFactory.AddSeriesVote(_commandFactory, series, User.JMMUserID, vote); return NoContent(); } @@ -1111,7 +1117,7 @@ public ActionResult GetSeriesImages([FromRoute] int seriesID, [FromQuery return Forbid(SeriesForbiddenForUser); } - return Series.GetArt(HttpContext, series.AniDB_ID, includeDisabled); + return SeriesFactory.GetArt(series.AniDB_ID, includeDisabled); } #endregion @@ -1139,13 +1145,13 @@ public ActionResult GetSeriesDefaultImageForType([FromRoute] int seriesID return Forbid(SeriesForbiddenForUser); var imageSizeType = Image.GetImageSizeTypeFromType(imageType); - var defaultBanner = Series.GetDefaultImage(series.AniDB_ID, imageSizeType); + var defaultBanner = SeriesFactory.GetDefaultImage(series.AniDB_ID, imageSizeType); if (defaultBanner != null) { return defaultBanner; } - var images = Series.GetArt(HttpContext, series.AniDB_ID); + var images = SeriesFactory.GetArt(series.AniDB_ID); return imageSizeType switch { ImageSizeType.Poster => images.Posters.FirstOrDefault(), @@ -1364,7 +1370,7 @@ public ActionResult> GetSeriesTags([FromRoute] int seriesID, [FromQuer return new List(); } - return Series.GetTags(anidb, filter, excludeDescriptions, orderByName, onlyVerified); + return _seriesFactory.GetTags(anidb, filter, excludeDescriptions, orderByName, onlyVerified); } /// @@ -1408,7 +1414,7 @@ public ActionResult> GetSeriesCast([FromRoute] int seriesID, return Forbid(SeriesForbiddenForUser); } - return Series.GetCast(series.AniDB_ID, roleType); + return _seriesFactory.GetCast(series.AniDB_ID, roleType); } #endregion @@ -1482,7 +1488,7 @@ internal ActionResult> SearchInternal(string que flags |= SeriesSearch.SearchFlags.Fuzzy; return SeriesSearch.SearchSeries(User, query, limit, flags) - .Select(result => new SeriesSearchResult(HttpContext, result)) + .Select(result => _seriesFactory.GetSeriesSearchResult(result)) .ToList(); } @@ -1532,7 +1538,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) if (anime != null) { return new ListResult(1, - new List { new Series.AniDB(anime, includeTitles) }); + new List { _seriesFactory.GetAniDB(anime, includeTitles: includeTitles) }); } // Check the title cache for a match. @@ -1540,7 +1546,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) if (result != null) { return new ListResult(1, - new List { new Series.AniDB(result, includeTitles) }); + new List { _seriesFactory.GetAniDB(result, includeTitles: includeTitles) }); } return new ListResult(); @@ -1558,7 +1564,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return null; } - return new Series.AniDB(result, series, includeTitles); + return _seriesFactory.GetAniDB(result, series, includeTitles); }) .Where(result => result != null) .ToListResult(page, pageSize); @@ -1573,7 +1579,7 @@ [FromQuery] [Range(1, int.MaxValue)] int page = 1) return null; } - return new Series.AniDB(result, series, includeTitles); + return _seriesFactory.GetAniDB(result, series, includeTitles); }) .Where(result => result != null) .ToListResult(page, pageSize); @@ -1606,7 +1612,7 @@ public ActionResult> StartsWith([FromRoute] string quer foreach (var (ser, match) in series) { - seriesList.Add(new SeriesSearchResult(HttpContext, new() { Result = ser, Match = match })); + seriesList.Add(_seriesFactory.GetSeriesSearchResult(new() { Result = ser, Match = match })); if (seriesList.Count >= limit) { break; @@ -1649,7 +1655,7 @@ public ActionResult> PathEndsWith([FromRoute] string path) }) .SelectMany(a => a.VideoLocal?.GetAnimeEpisodes() ?? Enumerable.Empty()).Select(a => a.GetAnimeSeries()) .Distinct() - .Where(ser => ser != null && user.AllowedSeries(ser)).Select(a => new Series(HttpContext, a)).ToList(); + .Where(ser => ser != null && user.AllowedSeries(ser)).Select(a => _seriesFactory.GetSeries(a)).ToList(); } #region Helpers diff --git a/Shoko.Server/API/v3/Controllers/TreeController.cs b/Shoko.Server/API/v3/Controllers/TreeController.cs index ac67cd249..f89fddb52 100644 --- a/Shoko.Server/API/v3/Controllers/TreeController.cs +++ b/Shoko.Server/API/v3/Controllers/TreeController.cs @@ -4,8 +4,8 @@ using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Shoko.Commons.Extensions; using Shoko.Models.Enums; -using Shoko.Models.Server; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; using Shoko.Server.API.Annotations; @@ -13,6 +13,7 @@ using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Filters; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Settings; @@ -34,6 +35,9 @@ namespace Shoko.Server.API.v3.Controllers; [Authorize] public class TreeController : BaseController { + private readonly FilterFactory _filterFactory; + private readonly SeriesFactory _seriesFactory; + private readonly FilterEvaluator _filterEvaluator; #region Import Folder /// @@ -84,17 +88,18 @@ public ActionResult> GetSubFilters([FromRoute] int filterID, [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValue)] int page = 1, [FromQuery] bool showHidden = false) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); - if (!groupFilter.IsDirectory) + if (!filterPreset.IsDirectory()) return new ListResult(); - return RepoFactory.GroupFilter.GetByParentID(filterID) - .Where(filter => showHidden || !filter.IsHidden) - .OrderBy(filter => filter.GroupFilterName) - .ToListResult(filter => new Filter(HttpContext, filter), page, pageSize); + var hideCategories = HttpContext.GetUser().GetHideCategories(); + + return _filterFactory.GetFilters(RepoFactory.FilterPreset.GetByParentID(filterID) + .Where(filter => (showHidden || !filter.Hidden) && !hideCategories.Contains(filter.Name)).OrderBy(a => a.Name).ToList()) + .ToListResult(page, pageSize); } /// @@ -137,30 +142,29 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu } else { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new ListResult(); - // Fast path when user is not in the filter - if (!groupFilter.GroupsIds.TryGetValue(User.JMMUserID, out var groupIds)) - return new ListResult(); + // Gets Group and Series IDs in a filter, already sorted by the filter + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID); + if (!results.Any()) return new ListResult(); - groups = groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + groups = results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { + // not top level groups if (group == null || group.AnimeGroupParentID.HasValue) return false; return includeEmpty || group.GetAllSeries() .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0)); }); - groups = orderByName ? groups.OrderBy(group => group.GetSortName()) : - groups.OrderByGroupFilter(groupFilter); } return groups @@ -186,23 +190,24 @@ public ActionResult> GetGroupNameLettersInFilter([FromRout var user = User; if (filterID.HasValue && filterID > 0) { - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID.Value); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID.Value); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new Dictionary(); // Fast path when user is not in the filter - if (!groupFilter.GroupsIds.TryGetValue(user.JMMUserID, out var groupIds)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new Dictionary(); - return groupIds - .Select(group => RepoFactory.AnimeGroup.GetByID(group)) + return results + .Select(group => RepoFactory.AnimeGroup.GetByID(group.Key)) .Where(group => { - if (group == null || group.AnimeGroupParentID.HasValue) + if (group is not { AnimeGroupParentID: null }) return false; return includeEmpty || group.GetAllSeries() @@ -257,32 +262,26 @@ [FromQuery] [Range(0, 100)] int pageSize = 50, [FromQuery] [Range(1, int.MaxValu return RepoFactory.AnimeSeries.GetAll() .Where(series => user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); // Check if the group filter exists. - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new ListResult(); - // Return all series if group filter is not applied to series. - if (groupFilter.ApplyToSeries != 1) - return RepoFactory.AnimeSeries.GetAll() - .Where(series => user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) - .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); - - // Return early if every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new ListResult(); - return seriesIDs.Select(id => RepoFactory.AnimeSeries.GetByID(id)) - .Where(series => series != null && user.AllowedSeries(series) && (includeMissing || series.GetVideoLocals().Count > 0)) + // We don't need separate logic for ApplyAtSeriesLevel, as the FilterEvaluator handles that + return results.SelectMany(a => a.Select(id => RepoFactory.AnimeSeries.GetByID(id))) + .Where(series => series != null && (includeMissing || series.GetVideoLocals().Count > 0)) .OrderBy(series => series.GetSeriesName().ToLowerInvariant()) - .ToListResult(series => new Series(HttpContext, series, randomImages), page, pageSize); + .ToListResult(series => _seriesFactory.GetSeries(series, randomImages), page, pageSize); } /// @@ -305,8 +304,8 @@ public ActionResult> GetFilteredSubGroups([FromRoute] int filterID, if (filterID == 0) return GetSubGroups(groupID, randomImages, includeEmpty); - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); // Check if the group exists. @@ -319,13 +318,19 @@ public ActionResult> GetFilteredSubGroups([FromRoute] int filterID, return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); // Just return early because the every group will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - + + // Subgroups are weird. We'll take the group, build a set of all subgroup IDs, and use that to determine if a group should be included + // This should maintain the order of results, but have every group in the tree for those results + var orderedGroups = results.SelectMany(a => RepoFactory.AnimeGroup.GetByID(a.Key).TopLevelAnimeGroup.GetAllChildGroups().Select(b => b.AnimeGroupID)).ToArray(); + var groups = orderedGroups.ToHashSet(); + return group.GetChildGroups() .Where(subGroup => { @@ -339,13 +344,10 @@ public ActionResult> GetFilteredSubGroups([FromRoute] int filterID, .Any(s => s.GetAnimeEpisodes().Any(e => e.GetVideoLocals().Count > 0))) return false; - if (groupFilter.ApplyToSeries != 1) - return true; - - return subGroup.GetAllSeries().Any(series => seriesIDs.Contains(series.AnimeSeriesID)); + return groups.Contains(subGroup.AnimeGroupID); }) - .OrderByGroupFilter(groupFilter) - .Select(group => new Group(HttpContext, group, randomImages)) + .OrderBy(a => Array.IndexOf(orderedGroups, a.AnimeGroupID)) + .Select(g => new Group(HttpContext, g, randomImages)) .ToList(); } @@ -375,11 +377,11 @@ public ActionResult> GetSeriesInFilteredGroup([FromRoute] int filte return GetSeriesInGroup(groupID, recursive, includeMissing, randomImages, includeDataFrom); // Check if the group filter exists. - var groupFilter = RepoFactory.GroupFilter.GetByID(filterID); - if (groupFilter == null) + var filterPreset = RepoFactory.FilterPreset.GetByID(filterID); + if (filterPreset == null) return NotFound(FilterController.FilterNotFound); - if (groupFilter.ApplyToSeries != 1) + if (!filterPreset.ApplyAtSeriesLevel) return GetSeriesInGroup(groupID, recursive, includeMissing, randomImages); // Check if the group exists. @@ -392,18 +394,23 @@ public ActionResult> GetSeriesInFilteredGroup([FromRoute] int filte return Forbid(GroupController.GroupForbiddenForUser); // Directories should only contain sub-filters, not groups and series. - if (groupFilter.IsDirectory) + if (filterPreset.IsDirectory()) return new List(); // Just return early because the every series will be filtered out. - if (!groupFilter.SeriesIds.TryGetValue(user.JMMUserID, out var seriesIDs)) + var results = _filterEvaluator.EvaluateFilter(filterPreset, User.JMMUserID).ToArray(); + if (results.Length == 0) return new List(); - return (recursive ? group.GetAllSeries() : group.GetSeries()) - .Where(series => seriesIDs.Contains(series.AnimeSeriesID)) - .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) - .Where(series => series.Size > 0 || includeMissing) + var seriesIDs = recursive + ? group.GetAllChildGroups().SelectMany(a => results.FirstOrDefault(b => b.Key == a.AnimeGroupID)) + : results.FirstOrDefault(a => a.Key == groupID); + + var series = seriesIDs?.Select(a => RepoFactory.AnimeSeries.GetByID(a)).Where(a => a.GetVideoLocals().Any() || includeMissing) ?? + Array.Empty(); + + return series + .Select(a => _seriesFactory.GetSeries(a, randomImages, includeDataFrom)) .ToList(); } @@ -485,7 +492,7 @@ public ActionResult> GetSeriesInGroup([FromRoute] int groupID, [Fro return (recursive ? group.GetAllSeries() : group.GetSeries()) .Where(a => user.AllowedSeries(a)) .OrderBy(series => series.GetAnime()?.AirDate ?? DateTime.MaxValue) - .Select(series => new Series(HttpContext, series, randomImages, includeDataFrom)) + .Select(series => _seriesFactory.GetSeries(series, randomImages, includeDataFrom)) .Where(series => series.Size > 0 || includeMissing) .ToList(); } @@ -525,7 +532,7 @@ public ActionResult GetMainSeriesInGroup([FromRoute] int groupID, [FromQ return InternalError("Unable to find main series for group."); } - return new Series(HttpContext, mainSeries, randomImages, includeDataFrom); + return _seriesFactory.GetSeries(mainSeries, randomImages, includeDataFrom); } #endregion @@ -905,7 +912,10 @@ public ActionResult> GetFilesForEpisode([FromRoute] int episodeID, [F #endregion - public TreeController(ISettingsProvider settingsProvider) : base(settingsProvider) + public TreeController(ISettingsProvider settingsProvider, FilterFactory filterFactory, FilterEvaluator filterEvaluator, SeriesFactory seriesFactory) : base(settingsProvider) { + _filterFactory = filterFactory; + _filterEvaluator = filterEvaluator; + _seriesFactory = seriesFactory; } } diff --git a/Shoko.Server/API/v3/Controllers/UserController.cs b/Shoko.Server/API/v3/Controllers/UserController.cs index 3b7f61074..2e38e4b09 100644 --- a/Shoko.Server/API/v3/Controllers/UserController.cs +++ b/Shoko.Server/API/v3/Controllers/UserController.cs @@ -230,7 +230,7 @@ private ActionResult ChangePassword(SVR_JMMUser user, User.Input.ChangePasswordB return Forbid("User must be admin to change other's password."); user.Password = string.IsNullOrEmpty(body.Password) ? "" : Digest.Hash(body.Password); - RepoFactory.JMMUser.Save(user, false); + RepoFactory.JMMUser.Save(user); if (body.RevokeAPIKeys) RepoFactory.AuthTokens.DeleteAllWithUserID(user.JMMUserID); diff --git a/Shoko.Server/API/v3/Controllers/WebUIController.cs b/Shoko.Server/API/v3/Controllers/WebUIController.cs index f75a3b667..0e1dfcfc7 100644 --- a/Shoko.Server/API/v3/Controllers/WebUIController.cs +++ b/Shoko.Server/API/v3/Controllers/WebUIController.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Caching.Memory; using Shoko.Server.API.Annotations; using Shoko.Server.API.ModelBinders; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.API.WebUI; @@ -42,6 +43,7 @@ public class WebUIController : BaseController }); private static readonly TimeSpan CacheTTL = TimeSpan.FromHours(1); + private readonly WebUIFactory _webUIFactory; /// /// Retrieves the list of available themes. @@ -208,7 +210,7 @@ public ActionResult> GetGroupView([FromBody] Input.WebUIGr return null; } - return new WebUIGroupExtra(group, series, anime, body.TagFilter, body.OrderByName, + return _webUIFactory.GetWebUIGroupExtra(group, series, anime, body.TagFilter, body.OrderByName, body.TagLimit); }) .ToList(); @@ -234,7 +236,7 @@ public ActionResult GetSeries([FromRoute] int seriesID) return Forbid(SeriesController.SeriesForbiddenForUser); } - return new WebUISeriesExtra(HttpContext, series); + return _webUIFactory.GetWebUISeriesExtra(series); } /// @@ -562,7 +564,8 @@ public ActionResult LatestServerWebUIVersion([FromQuery] Relea } - public WebUIController(ISettingsProvider settingsProvider) : base(settingsProvider) + public WebUIController(ISettingsProvider settingsProvider, WebUIFactory webUIFactory) : base(settingsProvider) { + _webUIFactory = webUIFactory; } } diff --git a/Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs b/Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs deleted file mode 100644 index 82e37e141..000000000 --- a/Shoko.Server/API/v3/Helpers/APIGroupFilterSortingHelper.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Shoko.Models.Enums; -using Shoko.Server.Models; - -namespace Shoko.Server.API.v3.Helpers; - -public static class APIGroupFilterSortingHelper -{ - public static IEnumerable OrderByGroupFilter(this IEnumerable groups, - SVR_GroupFilter gf) - { - var isFirst = true; - var query = groups; - foreach (var gfsc in gf.SortCriteriaList) - { - query = Order(query, gfsc, isFirst); - isFirst = false; - } - - return query; - } - - public static IEnumerable OrderByGroupFilter(this IEnumerable series, - SVR_GroupFilter gf) - { - var isFirst = true; - var result = series; - foreach (var gfsc in gf.SortCriteriaList) - { - result = Order(result, gfsc, isFirst); - isFirst = false; - } - - return result; - } - - private static IOrderedEnumerable Order(IEnumerable groups, - GroupFilterSortingCriteria gfsc, bool isFirst) - { - var desc = gfsc.SortDirection == GroupFilterSortDirection.Desc; - switch (gfsc.SortType) - { - case GroupFilterSorting.Year: - return !desc - ? Order(groups, a => a.Contract.Stat_AirDate_Min, false, isFirst) - : Order(groups, a => a.Contract.Stat_AirDate_Max, true, isFirst); - case GroupFilterSorting.AniDBRating: - return Order(groups, a => a.Contract.Stat_AniDBRating, desc, isFirst); - case GroupFilterSorting.EpisodeAddedDate: - return Order(groups, a => a.EpisodeAddedDate, desc, isFirst); - case GroupFilterSorting.EpisodeAirDate: - return Order(groups, a => a.LatestEpisodeAirDate, desc, isFirst); - case GroupFilterSorting.EpisodeWatchedDate: - return Order(groups, a => a.Contract.WatchedDate, desc, isFirst); - case GroupFilterSorting.MissingEpisodeCount: - return Order(groups, a => a.MissingEpisodeCount, desc, isFirst); - case GroupFilterSorting.SeriesAddedDate: - return Order(groups, a => a.Contract.Stat_SeriesCreatedDate, desc, isFirst); - case GroupFilterSorting.SeriesCount: - return Order(groups, a => a.Contract.Stat_SeriesCount, desc, isFirst); - case GroupFilterSorting.SortName: - return Order(groups, a => a.SortName, desc, isFirst); - case GroupFilterSorting.UnwatchedEpisodeCount: - return Order(groups, a => a.Contract.UnwatchedEpisodeCount, desc, isFirst); - case GroupFilterSorting.UserRating: - return Order(groups, a => a.Contract.Stat_UserVoteOverall, desc, isFirst); - case GroupFilterSorting.GroupName: - case GroupFilterSorting.GroupFilterName: - return Order(groups, a => a.GroupName, desc, isFirst); - default: - return Order(groups, a => a.GroupName, desc, isFirst); - } - } - - private static IOrderedEnumerable Order(IEnumerable groups, - GroupFilterSortingCriteria gfsc, bool isFirst) - { - var desc = gfsc.SortDirection == GroupFilterSortDirection.Desc; - switch (gfsc.SortType) - { - case GroupFilterSorting.Year: - return Order(groups, a => a.Contract.AniDBAnime.AniDBAnime.AirDate, desc, isFirst); - case GroupFilterSorting.AniDBRating: - return Order(groups, a => a.Contract.AniDBAnime.AniDBAnime.Rating, desc, isFirst); - case GroupFilterSorting.EpisodeAddedDate: - return Order(groups, a => a.EpisodeAddedDate, desc, isFirst); - case GroupFilterSorting.EpisodeAirDate: - return Order(groups, a => a.LatestEpisodeAirDate, desc, isFirst); - case GroupFilterSorting.EpisodeWatchedDate: - return Order(groups, a => a.Contract.WatchedDate, desc, isFirst); - case GroupFilterSorting.MissingEpisodeCount: - return Order(groups, a => a.MissingEpisodeCount, desc, isFirst); - case GroupFilterSorting.SeriesAddedDate: - return Order(groups, a => a.Contract.DateTimeCreated, desc, isFirst); - case GroupFilterSorting.SeriesCount: - return Order(groups, a => 1, desc, isFirst); - case GroupFilterSorting.SortName: - return Order(groups, a => a.GetSeriesName(), desc, isFirst); - case GroupFilterSorting.UnwatchedEpisodeCount: - return Order(groups, a => a.Contract.UnwatchedEpisodeCount, desc, isFirst); - case GroupFilterSorting.UserRating: - return Order(groups, a => a.Contract.AniDBAnime.UserVote.VoteValue, desc, isFirst); - case GroupFilterSorting.GroupName: - case GroupFilterSorting.GroupFilterName: - return Order(groups, a => a.GetSeriesName(), desc, isFirst); - default: - return Order(groups, a => a.GetSeriesName(), desc, isFirst); - } - } - - private static IOrderedEnumerable Order(IEnumerable groups, - Func o, - bool descending, bool isFirst) - { - if (isFirst) - { - return descending - ? groups.OrderByDescending(o) - : groups.OrderBy(o); - } - - return descending - ? ((IOrderedEnumerable)groups).ThenByDescending(o) - : ((IOrderedEnumerable)groups).ThenBy(o); - } -} diff --git a/Shoko.Server/API/v3/Helpers/FilterFactory.cs b/Shoko.Server/API/v3/Helpers/FilterFactory.cs new file mode 100644 index 000000000..c4611ad78 --- /dev/null +++ b/Shoko.Server/API/v3/Helpers/FilterFactory.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Shoko.Commons.Extensions; +using Shoko.Models.Enums; +using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Models; +using Shoko.Server.Repositories; + +namespace Shoko.Server.API.v3.Helpers; + +public class FilterFactory +{ + private readonly HttpContext _context; + private readonly FilterEvaluator _evaluator; + + public FilterFactory(IHttpContextAccessor context, FilterEvaluator evaluator) + { + _context = context.HttpContext; + _evaluator = evaluator; + } + + public Filter GetFilter(FilterPreset groupFilter, bool fullModel = false) + { + var user = _context.GetUser(); + var filter = new Filter + { + IDs = new Filter.FilterIDs + { + ID = groupFilter.FilterPresetID, ParentFilter = groupFilter.ParentFilterPresetID + }, + Name = groupFilter.Name, + IsLocked = groupFilter.Locked, + IsDirectory = groupFilter.IsDirectory(), + IsHidden = groupFilter.Hidden, + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel, + }; + + if (fullModel) + { + filter.Expression = GetExpressionTree(groupFilter.Expression); + filter.Sorting = GetSortingCriteria(groupFilter.SortingExpression); + } + + filter.Size = filter.IsDirectory + ? RepoFactory.FilterPreset.GetByParentID(groupFilter.FilterPresetID).Count + : _evaluator.EvaluateFilter(groupFilter, user?.JMMUserID).Count(); + return filter; + } + + public IEnumerable GetFilters(List groupFilters, bool fullModel = false) + { + var user = _context.GetUser(); + var evaluate = groupFilters.Any(a => !a.IsDirectory()); + var results = evaluate ? _evaluator.BatchEvaluateFilters(groupFilters, user.JMMUserID, true) : null; + var filters = groupFilters.Select(groupFilter => + { + var filter = new Filter + { + IDs = new Filter.FilterIDs + { + ID = groupFilter.FilterPresetID, ParentFilter = groupFilter.ParentFilterPresetID + }, + Name = groupFilter.Name, + IsLocked = groupFilter.Locked, + IsDirectory = groupFilter.IsDirectory(), + IsHidden = groupFilter.Hidden, + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel, + }; + + if (fullModel) + { + filter.Expression = GetExpressionTree(groupFilter.Expression); + filter.Sorting = GetSortingCriteria(groupFilter.SortingExpression); + } + + filter.Size = filter.IsDirectory ? RepoFactory.FilterPreset.GetByParentID(groupFilter.FilterPresetID).Count : results?[groupFilter].Count() ?? 0; + return filter; + }); + + return filters; + } + + public static Filter.FilterCondition GetExpressionTree(FilterExpression expression) + { + if (expression is null) return null; + var result = new Filter.FilterCondition + { + Type = expression.GetType().Name.Replace("Expression", "") + }; + + // Left/First + switch (expression) + { + case IWithExpressionParameter left: + result.Left = GetExpressionTree(left.Left); + break; + case IWithDateSelectorParameter left: + result.Left = GetExpressionTree(left.Left); + break; + case IWithNumberSelectorParameter left: + result.Left = GetExpressionTree(left.Left); + break; + case IWithStringSelectorParameter left: + result.Left = GetExpressionTree(left.Left); + break; + } + + // Parameters + switch (expression) + { + case IWithStringParameter parameter: + result.Parameter = parameter.Parameter; + break; + case IWithNumberParameter parameter: + result.Parameter = parameter.Parameter.ToString(); + break; + case IWithDateParameter parameter: + result.Parameter = parameter.Parameter.ToString("yyyy-MM-dd"); + break; + case IWithTimeSpanParameter parameter: + result.Parameter = parameter.Parameter.ToString("G"); + break; + } + + // Right/Second + switch (expression) + { + case IWithSecondExpressionParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondDateSelectorParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondStringSelectorParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondNumberSelectorParameter right: + result.Right = GetExpressionTree(right.Right); + break; + case IWithSecondStringParameter right: + result.SecondParameter = right.SecondParameter; + break; + } + + return result; + } + + public FilterExpression GetExpressionTree(Filter.FilterCondition condition) + { + var type = Type.GetType(condition.Type + "Expression"); + if (type == null) throw new ArgumentException($"FilterCondition type {condition.Type}Expression was not found"); + var result = (FilterExpression)Activator.CreateInstance(type); + + // Left/First + switch (result) + { + case IWithExpressionParameter left: + left.Left = GetExpressionTree(condition.Left); + break; + case IWithDateSelectorParameter left: + left.Left = GetExpressionTree(condition.Left); + break; + case IWithNumberSelectorParameter left: + left.Left = GetExpressionTree(condition.Left); + break; + case IWithStringSelectorParameter left: + left.Left = GetExpressionTree(condition.Left); + break; + } + + // Parameters + switch (result) + { + case IWithStringParameter parameter: + parameter.Parameter = parameter.Parameter; + break; + case IWithNumberParameter parameter: + parameter.Parameter = double.Parse(condition.Parameter!); + break; + case IWithDateParameter parameter: + parameter.Parameter = DateTime.ParseExact(condition.Parameter!, "yyyy-MM-dd", CultureInfo.InvariantCulture.DateTimeFormat); + break; + case IWithTimeSpanParameter parameter: + parameter.Parameter = TimeSpan.ParseExact(condition.Parameter!, "G", CultureInfo.InvariantCulture.DateTimeFormat); + break; + } + + // Right/Second + switch (result) + { + case IWithSecondExpressionParameter right: + right.Right = GetExpressionTree(condition.Right); + break; + case IWithSecondDateSelectorParameter right: + right.Right = GetExpressionTree(condition.Right); + break; + case IWithSecondStringSelectorParameter right: + right.Right = GetExpressionTree(condition.Right); + break; + case IWithSecondNumberSelectorParameter right: + right.Right = GetExpressionTree(condition.Right); + break; + case IWithSecondStringParameter right: + right.SecondParameter = condition.SecondParameter; + break; + } + + return result; + } + + public static Filter.SortingCriteria GetSortingCriteria(SortingExpression expression) + { + var result = new Filter.SortingCriteria + { + Type = expression.GetType().Name.Replace("Selector", ""), + IsInverted = expression.Descending + }; + + var currentCriteria = result; + var currentExpression = expression; + while (currentExpression.Next != null) + { + currentCriteria.Next = new Filter.SortingCriteria + { + Type = currentExpression.GetType().Name.Replace("Selector", ""), + IsInverted = currentExpression.Descending + }; + currentCriteria = currentCriteria.Next; + currentExpression = currentExpression.Next; + } + + return result; + } + + public static SortingExpression GetSortingCriteria(Filter.SortingCriteria criteria) + { + var type = Type.GetType(criteria.Type + "Selector"); + if (type == null) throw new ArgumentException($"SortingExpression type {criteria.Type}Selector was not found"); + var result = (SortingExpression)Activator.CreateInstance(type)!; + result.Descending = criteria.IsInverted; + + if (criteria.Next != null) result.Next = GetSortingCriteria(criteria.Next); + + return result; + } + + public Filter.Input.CreateOrUpdateFilterBody CreateOrUpdateFilterBody(FilterPreset groupFilter) + { + var result = new Filter.Input.CreateOrUpdateFilterBody + { + Name = groupFilter.Name, + ParentID = groupFilter.ParentFilterPresetID, + IsDirectory = groupFilter.IsDirectory(), + IsHidden = groupFilter.Hidden, + ApplyAtSeriesLevel = groupFilter.ApplyAtSeriesLevel + }; + + if (!result.IsDirectory) + { + result.Expression = GetExpressionTree(groupFilter.Expression); + result.Sorting = GetSortingCriteria(groupFilter.SortingExpression); + } + + return result; + } + + public Filter MergeWithExisting(Filter.Input.CreateOrUpdateFilterBody body, FilterPreset groupFilter, ModelStateDictionary modelState, bool skipSave = false) + { + if (groupFilter.Locked) + modelState.AddModelError("IsLocked", "Filter is locked."); + + // Defer to `null` if the id is `0`. + if (body.ParentID is 0) + body.ParentID = null; + + if (body.ParentID.HasValue) + { + var parentFilter = RepoFactory.FilterPreset.GetByID(body.ParentID.Value); + if (parentFilter == null) + { + modelState.AddModelError(nameof(body.ParentID), $"Unable to find parent filter with id {body.ParentID.Value}"); + } + else + { + if (parentFilter.Locked) + modelState.AddModelError(nameof(body.ParentID), $"Unable to add a sub-filter to a filter that is locked."); + + if (!parentFilter.IsDirectory()) + modelState.AddModelError(nameof(body.ParentID), $"Unable to add a sub-filter to a filter that is not a directorty filter."); + } + } + + if (body.IsDirectory) + { + if (body.Expression != null) + modelState.AddModelError(nameof(body.Expression), "Directory filters cannot have any conditions applied to them."); + + if (body.Sorting != null) + modelState.AddModelError(nameof(body.Sorting), "Directory filters cannot have custom sorting applied to them."); + } + else + { + var subFilters = groupFilter.FilterPresetID != 0 ? RepoFactory.FilterPreset.GetByParentID(groupFilter.FilterPresetID) : new(); + if (subFilters.Count > 0) + modelState.AddModelError(nameof(body.IsDirectory), "Cannot turn a directory filter with sub-filters into a normal filter without first removing the sub-filters"); + } + + // Return now if we encountered any validation errors. + if (!modelState.IsValid) + return null; + + groupFilter.ParentFilterPresetID = body.ParentID; + groupFilter.FilterType = body.IsDirectory ? GroupFilterType.UserDefined | GroupFilterType.Directory : GroupFilterType.UserDefined; + groupFilter.Name = body.Name; + groupFilter.Hidden = body.IsHidden; + groupFilter.ApplyAtSeriesLevel = body.ApplyAtSeriesLevel; + if (!body.IsDirectory) + { + if (body.Expression != null) groupFilter.Expression = GetExpressionTree(body.Expression); + if (body.Sorting != null) groupFilter.SortingExpression = GetSortingCriteria(body.Sorting); + } + + // Skip saving if we're just going to preview a group filter. + if (!skipSave) + RepoFactory.FilterPreset.Save(groupFilter); + + return GetFilter(groupFilter, true); + } + + public Filter GetFirstAiringSeasonGroupFilter(SVR_AniDB_Anime anime) + { + var type = (AnimeType)anime.AnimeType; + if (type != AnimeType.TVSeries && type != AnimeType.Web) + return null; + + var (year, season) = anime.GetSeasons() + .FirstOrDefault(); + if (year == 0) + return null; + + var seasonName = $"{season} {year}"; + var seasonsFilterID = RepoFactory.FilterPreset.GetTopLevel() + .FirstOrDefault(f => f.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))?.FilterPresetID; + if (seasonsFilterID == null) return null; + var firstAirSeason = RepoFactory.FilterPreset.GetByParentID(seasonsFilterID.Value) + .FirstOrDefault(f => f.Name == seasonName); + if (firstAirSeason == null) + return null; + + return GetFilter(firstAirSeason); + } +} diff --git a/Shoko.Server/API/v3/Helpers/ModelHelper.cs b/Shoko.Server/API/v3/Helpers/ModelHelper.cs index a360470f2..479b61496 100644 --- a/Shoko.Server/API/v3/Helpers/ModelHelper.cs +++ b/Shoko.Server/API/v3/Helpers/ModelHelper.cs @@ -339,7 +339,7 @@ public static GroupSizes GenerateGroupSizes(List seriesList, Li foreach (var series in seriesList) { var anime = series.GetAnime(); - switch (Series.GetAniDBSeriesType(anime?.AnimeType)) + switch (SeriesFactory.GetAniDBSeriesType(anime?.AnimeType)) { case SeriesType.Unknown: sizes.SeriesTypes.Unknown++; diff --git a/Shoko.Server/API/v3/Helpers/SeriesFactory.cs b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs new file mode 100644 index 000000000..176abb37d --- /dev/null +++ b/Shoko.Server/API/v3/Helpers/SeriesFactory.cs @@ -0,0 +1,869 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Shoko.Commons.Extensions; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Plugin.Abstractions.DataModels; +using Shoko.Server.API.v3.Models.Common; +using Shoko.Server.API.v3.Models.Shoko; +using Shoko.Server.Commands; +using Shoko.Server.Commands.AniDB; +using Shoko.Server.Models; +using Shoko.Server.Providers.AniDB.Interfaces; +using Shoko.Server.Providers.AniDB.Titles; +using Shoko.Server.Repositories; +using Shoko.Server.Repositories.Cached; +using Shoko.Server.Utilities; +using AniDBAnimeType = Shoko.Models.Enums.AnimeType; +using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType; +using Image = Shoko.Server.API.v3.Models.Common.Image; +using Series = Shoko.Server.API.v3.Models.Shoko.Series; + +namespace Shoko.Server.API.v3.Helpers; + +public class SeriesFactory +{ + private readonly HttpContext _context; + private readonly CrossRef_Anime_StaffRepository _crossRefAnimeStaffRepository; + private readonly AnimeCharacterRepository _animeCharacterRepository; + private readonly AnimeStaffRepository _animeStaffRepository; + private readonly CustomTagRepository _customTagRepository; + private readonly AniDB_TagRepository _aniDBTagRepository; + private readonly AniDB_Anime_TagRepository _aniDBAnimeTagRepository; + + public SeriesFactory(IHttpContextAccessor context) + { + _context = context.HttpContext; + _crossRefAnimeStaffRepository = RepoFactory.CrossRef_Anime_Staff; + _animeCharacterRepository = RepoFactory.AnimeCharacter; + _animeStaffRepository = RepoFactory.AnimeStaff; + _customTagRepository = RepoFactory.CustomTag; + _aniDBTagRepository = RepoFactory.AniDB_Tag; + _aniDBAnimeTagRepository = RepoFactory.AniDB_Anime_Tag; + } + + public Series GetSeries(SVR_AnimeSeries ser, bool randomiseImages = false, HashSet includeDataFrom = null) + { + var uid = _context.GetUser()?.JMMUserID ?? 0; + var anime = ser.GetAnime(); + var animeType = (AniDBAnimeType)anime.AnimeType; + + var result = new Series(); + AddBasicAniDBInfo(result, ser, anime); + + var ael = ser.GetAnimeEpisodes(); + var contract = ser.Contract; + if (contract == null) + { + ser.UpdateContract(); + } + + result.IDs = GetIDs(ser); + result.Images = GetDefaultImages(ser, randomiseImages); + result.AirsOn = animeType == AniDBAnimeType.TVSeries || animeType == AniDBAnimeType.Web ? ser.GetAirsOnDaysOfWeek(ael) : new(); + + result.Name = ser.GetSeriesName(); + result.Sizes = ModelHelper.GenerateSeriesSizes(ael, uid); + result.Size = result.Sizes.Local.Credits + result.Sizes.Local.Episodes + result.Sizes.Local.Others + result.Sizes.Local.Parodies + + result.Sizes.Local.Specials + result.Sizes.Local.Trailers; + + result.Created = ser.DateTimeCreated.ToUniversalTime(); + result.Updated = ser.DateTimeUpdated.ToUniversalTime(); + + if (includeDataFrom?.Contains(DataSource.AniDB) ?? false) + { + result._AniDB = GetAniDB(anime, ser); + } + if (includeDataFrom?.Contains(DataSource.TvDB) ?? false) + result._TvDB = GetTvDBInfo(ser); + + return result; + } + + private void AddBasicAniDBInfo(Series result, SVR_AnimeSeries series, SVR_AniDB_Anime anime) + { + if (anime == null) + { + return; + } + + result.Links = new(); + if (!string.IsNullOrEmpty(anime.Site_EN)) + foreach (var site in anime.Site_EN.Split('|')) + result.Links.Add(new() { Type = "source", Name = "Official Site (EN)", URL = site }); + + if (!string.IsNullOrEmpty(anime.Site_JP)) + foreach (var site in anime.Site_JP.Split('|')) + result.Links.Add(new() { Type = "source", Name = "Official Site (JP)", URL = site }); + + if (!string.IsNullOrEmpty(anime.Wikipedia_ID)) + result.Links.Add(new() { Type = "wiki", Name = "Wikipedia (EN)", URL = $"https://en.wikipedia.org/{anime.Wikipedia_ID}" }); + + if (!string.IsNullOrEmpty(anime.WikipediaJP_ID)) + result.Links.Add(new() { Type = "wiki", Name = "Wikipedia (JP)", URL = $"https://en.wikipedia.org/{anime.WikipediaJP_ID}" }); + + if (!string.IsNullOrEmpty(anime.CrunchyrollID)) + result.Links.Add(new() { Type = "streaming", Name = "Crunchyroll", URL = $"https://crunchyroll.com/anime/{anime.CrunchyrollID}" }); + + if (!string.IsNullOrEmpty(anime.FunimationID)) + result.Links.Add(new() { Type = "streaming", Name = "Funimation", URL = anime.FunimationID }); + + if (!string.IsNullOrEmpty(anime.HiDiveID)) + result.Links.Add(new() { Type = "streaming", Name = "HiDive", URL = $"https://www.hidive.com/{anime.HiDiveID}" }); + + if (anime.AllCinemaID.HasValue && anime.AllCinemaID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "allcinema", URL = $"https://allcinema.net/cinema/{anime.AllCinemaID.Value}" }); + + if (anime.AnisonID.HasValue && anime.AnisonID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "Anison", URL = $"https://anison.info/data/program/{anime.AnisonID.Value}.html" }); + + if (anime.SyoboiID.HasValue && anime.SyoboiID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "syoboi", URL = $"https://cal.syoboi.jp/tid/{anime.SyoboiID.Value}/time" }); + + if (anime.BangumiID.HasValue && anime.BangumiID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = "bangumi", URL = $"https://bgm.tv/subject/{anime.BangumiID.Value}" }); + + if (anime.LainID.HasValue && anime.LainID.Value > 0) + result.Links.Add(new() { Type = "foreign-metadata", Name = ".lain", URL = $"http://lain.gr.jp/mediadb/media/{anime.LainID.Value}" }); + + if (anime.ANNID.HasValue && anime.ANNID.Value > 0) + result.Links.Add(new() { Type = "english-metadata", Name = "AnimeNewsNetwork", URL = $"https://www.animenewsnetwork.com/encyclopedia/anime.php?id={anime.ANNID.Value}" }); + + if (anime.VNDBID.HasValue && anime.VNDBID.Value > 0) + result.Links.Add(new() { Type = "english-metadata", Name = "VNDB", URL = $"https://vndb.org/v{anime.VNDBID.Value}" }); + + var vote = RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.Anime) ?? + RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.AnimeTemp); + if (vote != null) + { + var voteType = (AniDBVoteType)vote.VoteType == AniDBVoteType.Anime ? "Permanent" : "Temporary"; + result.UserRating = new Rating + { + Value = (decimal)Math.Round(vote.VoteValue / 100D, 1), + MaxValue = 10, + Type = voteType, + Source = "User" + }; + } + } + + public static bool QueueAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, + int animeID, bool force, bool downloadRelations, bool createSeriesEntry, bool immediate = false, + bool cacheOnly = false) + { + if (force) + return QueueForcedAniDBRefresh(commandFactory, handler, animeID, downloadRelations, createSeriesEntry, immediate); + + var command = commandFactory.Create(c => + { + c.AnimeID = animeID; + c.DownloadRelations = downloadRelations; + c.ForceRefresh = force; + c.CacheOnly = !force && cacheOnly; + c.CreateSeriesEntry = createSeriesEntry; + c.BubbleExceptions = immediate; + }); + if (immediate) + { + try + { + command.ProcessCommand(); + } + catch + { + return false; + } + + return command.Result != null; + } + + commandFactory.Save(command); + return false; + } + + private static bool QueueForcedAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, + int animeID, bool downloadRelations, bool createSeriesEntry, bool immediate = false) + { + var command = commandFactory.Create(c => + { + c.AnimeID = animeID; + c.DownloadRelations = downloadRelations; + c.CreateSeriesEntry = createSeriesEntry; + c.BubbleExceptions = immediate; + }); + if (immediate && !handler.IsBanned) + { + try + { + command.ProcessCommand(); + } + catch + { + return false; + } + + return command.Result != null; + } + + commandFactory.Save(command); + return false; + } + + public static bool QueueTvDBRefresh(ICommandRequestFactory commandFactory, int tvdbID, bool force, bool immediate = false) + { + var command = commandFactory.Create(c => + { + c.TvDBSeriesID = tvdbID; + c.ForceRefresh = force; + c.BubbleExceptions = immediate; + }); + if (immediate) + { + try + { + command.ProcessCommand(); + } + catch + { + return false; + } + + return command.Result != null; + } + + commandFactory.Save(command); + return false; + } + + public static SeriesIDs GetIDs(SVR_AnimeSeries ser) + { + // Shoko + var ids = new SeriesIDs + { + ID = ser.AnimeSeriesID, + ParentGroup = ser.AnimeGroupID, + TopLevelGroup = ser.TopLevelAnimeGroup.AnimeGroupID + }; + + // AniDB + var anidbId = ser.GetAnime()?.AnimeID; + if (anidbId.HasValue) + { + ids.AniDB = anidbId.Value; + } + + // TvDB + var tvdbIds = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID); + if (tvdbIds.Any()) + { + ids.TvDB.AddRange(tvdbIds.Select(a => a.TvDBID).Distinct()); + } + + // TODO: Cache the rest of these, so that they don't severely slow down the API + + // TMDB + // TODO: make this able to support more than one, in fact move it to its own and remove CrossRef_Other + var tmdbId = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(ser.AniDB_ID, CrossRefType.MovieDB); + if (tmdbId != null && int.TryParse(tmdbId.CrossRefID, out var movieID)) + { + ids.TMDB.Add(movieID); + } + + // Trakt + // var traktIds = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(ser.AniDB_ID).Select(a => a.TraktID) + // .Distinct().ToList(); + // if (traktIds.Any()) ids.TraktTv.AddRange(traktIds); + + // MAL + var malIds = RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(ser.AniDB_ID).Select(a => a.MALID).Distinct() + .ToList(); + if (malIds.Any()) + { + ids.MAL.AddRange(malIds); + } + + // TODO: AniList later + return ids; + } + + public static Image GetDefaultImage(int anidbId, ImageSizeType imageSizeType, + ImageEntityType? imageEntityType = null) + { + var defaultImage = imageEntityType.HasValue + ? RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(anidbId, + imageSizeType, imageEntityType.Value) + : RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(anidbId, imageSizeType); + return defaultImage != null + ? new Image(defaultImage.ImageParentID, (ImageEntityType)defaultImage.ImageParentType, true) + : null; + } + + public Images GetDefaultImages(SVR_AnimeSeries ser, bool randomiseImages = false) + { + var images = new Images(); + var random = _context.Items["Random"] as Random; + var allImages = GetArt(ser.AniDB_ID); + + var poster = randomiseImages + ? allImages.Posters.GetRandomElement(random) + : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Poster) ?? allImages.Posters.FirstOrDefault(); + if (poster != null) + { + images.Posters.Add(poster); + } + + var fanart = randomiseImages + ? allImages.Fanarts.GetRandomElement(random) + : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Fanart) ?? allImages.Fanarts.FirstOrDefault(); + if (fanart != null) + { + images.Fanarts.Add(fanart); + } + + var banner = randomiseImages + ? allImages.Banners.GetRandomElement(random) + : GetDefaultImage(ser.AniDB_ID, ImageSizeType.WideBanner) ?? allImages.Banners.FirstOrDefault(); + if (banner != null) + { + images.Banners.Add(banner); + } + + return images; + } + + public List GetTvDBInfo(SVR_AnimeSeries ser) + { + var ael = ser.GetAnimeEpisodes(true); + return RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID) + .Select(xref => RepoFactory.TvDB_Series.GetByTvDBID(xref.TvDBID)) + .Select(tvdbSer => GetTvDB(tvdbSer, ser, ael)) + .ToList(); + } + + public static void AddSeriesVote(ICommandRequestFactory commandFactory, SVR_AnimeSeries ser, int userID, Vote vote) + { + var voteType = (vote.Type?.ToLowerInvariant() ?? "") switch + { + "temporary" => (int)AniDBVoteType.AnimeTemp, + "permanent" => (int)AniDBVoteType.Anime, + _ => ser.GetAnime()?.GetFinishedAiring() ?? false ? (int)AniDBVoteType.Anime : (int)AniDBVoteType.AnimeTemp + }; + + var dbVote = RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.AnimeTemp) ?? + RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.Anime); + + if (dbVote == null) + { + dbVote = new AniDB_Vote { EntityID = ser.AniDB_ID }; + } + + dbVote.VoteValue = (int)Math.Floor(vote.GetRating(1000)); + dbVote.VoteType = voteType; + + RepoFactory.AniDB_Vote.Save(dbVote); + + commandFactory.CreateAndSave( + c => + { + c.AnimeID = ser.AniDB_ID; + c.VoteType = voteType; + c.VoteValue = vote.GetRating(); + } + ); + } + + public static Images GetArt(int animeID, bool includeDisabled = false) + { + var images = new Images(); + AddAniDBPoster(images, animeID); + AddTvDBImages(images, animeID, includeDisabled); + // AddMovieDBImages(ctx, images, animeID, includeDisabled); + return images; + } + + private static void AddAniDBPoster(Images images, int animeID) + { + images.Posters.Add(GetAniDBPoster(animeID)); + } + + public static Image GetAniDBPoster(int animeID) + { + var defaultImage = RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(animeID, + ImageSizeType.Poster); + var preferred = defaultImage != null && defaultImage.ImageParentType == (int)ImageEntityType.AniDB_Cover; + return new Image(animeID, ImageEntityType.AniDB_Cover, preferred); + } + + private static void AddTvDBImages(Images images, int animeID, bool includeDisabled = false) + { + var tvdbIDs = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(animeID).ToList(); + + var defaultFanart = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Fanart, ImageEntityType.TvDB_FanArt); + var fanarts = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageFanart.GetBySeriesID(a.TvDBID)).ToList(); + images.Fanarts.AddRange(fanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.TvDB_ImageFanartID; + return new Image(a.TvDB_ImageFanartID, ImageEntityType.TvDB_FanArt, preferred, a.Enabled == 0); + })); + + var defaultBanner = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.WideBanner, ImageEntityType.TvDB_Banner); + var banners = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageWideBanner.GetBySeriesID(a.TvDBID)).ToList(); + images.Banners.AddRange(banners.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultBanner != null && defaultBanner.ImageParentID == a.TvDB_ImageWideBannerID; + return new Image(a.TvDB_ImageWideBannerID, ImageEntityType.TvDB_Banner, preferred, a.Enabled == 0); + })); + + var defaultPoster = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Poster, ImageEntityType.TvDB_Cover); + var posters = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImagePoster.GetBySeriesID(a.TvDBID)).ToList(); + images.Posters.AddRange(posters.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.TvDB_ImagePosterID; + return new Image(a.TvDB_ImagePosterID, ImageEntityType.TvDB_Cover, preferred, a.Enabled == 0); + })); + } + + private static void AddMovieDBImages(Images images, int animeID, bool includeDisabled = false) + { + var moviedb = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(animeID, CrossRefType.MovieDB); + + var moviedbPosters = moviedb == null + ? new List() + : RepoFactory.MovieDB_Poster.GetByMovieID(int.Parse(moviedb.CrossRefID)); + var defaultPoster = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Poster, ImageEntityType.MovieDB_Poster); + images.Posters.AddRange(moviedbPosters.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.MovieDB_PosterID; + return new Image(a.MovieDB_PosterID, ImageEntityType.MovieDB_Poster, preferred, a.Enabled == 1); + })); + + var moviedbFanarts = moviedb == null + ? new List() + : RepoFactory.MovieDB_Fanart.GetByMovieID(int.Parse(moviedb.CrossRefID)); + var defaultFanart = + RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, + ImageSizeType.Fanart, ImageEntityType.MovieDB_FanArt); + images.Fanarts.AddRange(moviedbFanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => + { + var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.MovieDB_FanartID; + return new Image(a.MovieDB_FanartID, ImageEntityType.MovieDB_FanArt, preferred, a.Enabled == 1); + })); + } + + public Series.AniDB GetAniDB(SVR_AniDB_Anime anime, SVR_AnimeSeries series = null, bool includeTitles = true) + { + series ??= RepoFactory.AnimeSeries.GetByAnimeID(anime.AnimeID); + var seriesTitle = series?.GetSeriesName() ?? anime.PreferredTitle; + var result = new Series.AniDB + { + ID = anime.AnimeID, + ShokoID = series?.AnimeSeriesID, + Type = GetAniDBSeriesType(anime.AnimeType), + Title = seriesTitle, + Titles = includeTitles + ? anime.GetTitles().Select(title => new Title + { + Name = title.Title, + Language = title.LanguageCode, + Type = title.TitleType, + Default = string.Equals(title.Title, seriesTitle), + Source = "AniDB" + } + ).ToList() + : null, + Description = anime.Description, + Restricted = anime.Restricted == 1, + Poster = GetAniDBPoster(anime.AnimeID), + EpisodeCount = anime.EpisodeCountNormal, + Rating = new Rating + { + Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount + }, + UserApproval = null, + Relation = null, + }; + + if (anime.AirDate.HasValue) result.AirDate = anime.AirDate.Value; + if (anime.EndDate.HasValue) result.EndDate = anime.EndDate.Value; + + return result; + } + + public Series.AniDB GetAniDB(ResponseAniDBTitles.Anime result, SVR_AnimeSeries series = null, bool includeTitles = false) + { + if (series == null) + { + series = RepoFactory.AnimeSeries.GetByAnimeID(result.AnimeID); + } + + var anime = series != null ? series.GetAnime() : RepoFactory.AniDB_Anime.GetByAnimeID(result.AnimeID); + + var animeTitle = series?.GetSeriesName() ?? anime?.PreferredTitle ?? result.MainTitle; + var anidb = new Series.AniDB + { + ID = result.AnimeID, + ShokoID = series?.AnimeSeriesID, + Type = GetAniDBSeriesType(anime?.AnimeType), + Title = animeTitle, + Titles = includeTitles + ? result.Titles.Select(title => new Title + { + Language = title.LanguageCode, + Name = title.Title, + Type = title.TitleType, + Default = string.Equals(title.Title, animeTitle), + Source = "AniDB" + } + ).ToList() + : null, + Description = anime?.Description, + Restricted = anime is { Restricted: 1 }, + EpisodeCount = anime?.EpisodeCount, + Poster = GetAniDBPoster(result.AnimeID), + }; + if (anime?.AirDate != null) anidb.AirDate = anime.AirDate.Value; + if (anime?.EndDate != null) anidb.EndDate = anime.EndDate.Value; + return anidb; + } + + public Series.AniDB GetAniDB(SVR_AniDB_Anime_Relation relation, SVR_AnimeSeries series = null, bool includeTitles = true) + { + series ??= RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID); + var result = new Series.AniDB + { + ID = relation.RelatedAnimeID, + ShokoID = series?.AnimeSeriesID, + Poster = GetAniDBPoster(relation.RelatedAnimeID), + Rating = null, + UserApproval = null, + Relation = ((IRelatedAnime)relation).RelationType, + }; + SetAniDBTitles(result, relation, series, includeTitles); + return result; + } + + private void SetAniDBTitles(Series.AniDB aniDB, AniDB_Anime_Relation relation, SVR_AnimeSeries series, bool includeTitles) + { + var anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.RelatedAnimeID); + if (anime is not null) + { + aniDB.Type = GetAniDBSeriesType(anime.AnimeType); + aniDB.Title = series?.GetSeriesName() ?? anime.PreferredTitle; + aniDB.Titles = includeTitles + ? anime.GetTitles().Select( + title => new Title + { + Name = title.Title, + Language = title.LanguageCode, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = anime.Description; + aniDB.Restricted = anime.Restricted == 1; + aniDB.EpisodeCount = anime.EpisodeCountNormal; + return; + } + + var result = Utils.AniDBTitleHelper.SearchAnimeID(relation.RelatedAnimeID); + if (result != null) + { + aniDB.Type = SeriesType.Unknown; + aniDB.Title = result.PreferredTitle; + aniDB.Titles = includeTitles + ? result.Titles.Select( + title => new Title + { + Language = title.LanguageCode, + Name = title.Title, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = null; + // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. + anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.AnimeID); + aniDB.Restricted = anime is not null && anime.Restricted == 1; + return; + } + + aniDB.Type = SeriesType.Unknown; + aniDB.Titles = includeTitles ? new List() : null; + aniDB.Restricted = false; + } + + public Series.AniDB GetAniDB(AniDB_Anime_Similar similar, SVR_AnimeSeries series = null, bool includeTitles = true) + { + series ??= RepoFactory.AnimeSeries.GetByAnimeID(similar.SimilarAnimeID); + var result = new Series.AniDB + { + ID = similar.SimilarAnimeID, + ShokoID = series?.AnimeSeriesID, + Poster = GetAniDBPoster(similar.SimilarAnimeID), + Rating = null, + UserApproval = new Rating + { + Value = new Vote(similar.Approval, similar.Total).GetRating(100), + MaxValue = 100, + Votes = similar.Total, + Source = "AniDB", + Type = "User Approval" + }, + Relation = null, + Restricted = false, + }; + SetAniDBTitles(result, similar, series, includeTitles); + return result; + } + + private void SetAniDBTitles(Series.AniDB aniDB, AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool includeTitles) + { + var anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.SimilarAnimeID); + if (anime is not null) + { + aniDB.Type = GetAniDBSeriesType(anime.AnimeType); + aniDB.Title = series?.GetSeriesName() ?? anime.PreferredTitle; + aniDB.Titles = includeTitles + ? anime.GetTitles().Select( + title => new Title + { + Name = title.Title, + Language = title.LanguageCode, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = anime.Description; + aniDB.Restricted = anime.Restricted == 1; + return; + } + + var result = Utils.AniDBTitleHelper.SearchAnimeID(similar.SimilarAnimeID); + if (result != null) + { + aniDB.Type = SeriesType.Unknown; + aniDB.Title = result.PreferredTitle; + aniDB.Titles = includeTitles + ? result.Titles.Select( + title => new Title + { + Language = title.LanguageCode, + Name = title.Title, + Type = title.TitleType, + Default = string.Equals(title.Title, aniDB.Title), + Source = "AniDB" + } + ).ToList() + : null; + aniDB.Description = null; + // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. + anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.AnimeID); + aniDB.Restricted = anime is not null && anime.Restricted == 1; + return; + } + + aniDB.Type = SeriesType.Unknown; + aniDB.Title = null; + aniDB.Titles = includeTitles ? new List<Title>() : null; + aniDB.Description = null; + aniDB.Restricted = false; + } + + /// <summary> + /// Cast is aggregated, and therefore not in each provider + /// </summary> + /// <param name="animeID"></param> + /// <param name="roleTypes"></param> + /// <returns></returns> + public List<Role> GetCast(int animeID, HashSet<Role.CreatorRoleType> roleTypes = null) + { + var roles = new List<Role>(); + var xrefAnimeStaff = _crossRefAnimeStaffRepository.GetByAnimeID(animeID); + foreach (var xref in xrefAnimeStaff) + { + // Filter out any roles that are not of the desired type. + if (roleTypes != null && !roleTypes.Contains((Role.CreatorRoleType)xref.RoleType)) + continue; + + var character = xref.RoleID.HasValue ? _animeCharacterRepository.GetByID(xref.RoleID.Value) : null; + var staff = _animeStaffRepository.GetByID(xref.StaffID); + if (staff == null) + continue; + + var role = new Role + { + Character = + character != null + ? new Role.Person + { + Name = character.Name, + AlternateName = character.AlternateName, + Image = new Image(character.CharacterID, ImageEntityType.Character), + Description = character.Description + } + : null, + Staff = new Role.Person + { + Name = staff.Name, + AlternateName = staff.AlternateName, + Description = staff.Description, + Image = staff.ImagePath != null ? new Image(staff.StaffID, ImageEntityType.Staff) : null + }, + RoleName = (Role.CreatorRoleType)xref.RoleType, + RoleDetails = xref.Role + }; + roles.Add(role); + } + + return roles; + } + + public List<Tag> GetTags(SVR_AniDB_Anime anime, TagFilter.Filter filter, + bool excludeDescriptions = false, bool orderByName = false, bool onlyVerified = true) + { + // Only get the user tags if we don't exclude it (false == false), or if we invert the logic and want to include it (true == true). + IEnumerable<Tag> userTags = new List<Tag>(); + if (filter.HasFlag(TagFilter.Filter.User) == filter.HasFlag(TagFilter.Filter.Invert)) + { + userTags = _customTagRepository.GetByAnimeID(anime.AnimeID) + .Select(tag => new Tag(tag, excludeDescriptions)); + } + + var selectedTags = anime.GetAniDBTags(onlyVerified) + .DistinctBy(a => a.TagName) + .ToList(); + var tagFilter = new TagFilter<AniDB_Tag>(name => _aniDBTagRepository.GetByName(name).FirstOrDefault(), tag => tag.TagName, + name => new AniDB_Tag { TagNameSource = name }); + var anidbTags = tagFilter + .ProcessTags(filter, selectedTags) + .Select(tag => + { + var xref = _aniDBAnimeTagRepository.GetByTagID(tag.TagID).FirstOrDefault(xref => xref.AnimeID == anime.AnimeID); + return new Tag(tag, excludeDescriptions) { Weight = xref?.Weight ?? 0, IsLocalSpoiler = xref?.LocalSpoiler }; + }); + + if (orderByName) + return userTags.Concat(anidbTags) + .OrderByDescending(tag => tag.Source) + .ThenBy(tag => tag.Name) + .ToList(); + + return userTags.Concat(anidbTags) + .OrderByDescending(tag => tag.Source) + .ThenByDescending(tag => tag.Weight) + .ThenBy(tag => tag.Name) + .ToList(); + } + + public static SeriesType GetAniDBSeriesType(int? animeType) + { + return animeType.HasValue ? GetAniDBSeriesType((AniDBAnimeType)animeType.Value) : SeriesType.Unknown; + } + + public static SeriesType GetAniDBSeriesType(AniDBAnimeType animeType) + { + switch (animeType) + { + default: + case AniDBAnimeType.None: + return SeriesType.Unknown; + case AniDBAnimeType.TVSeries: + return SeriesType.TV; + case AniDBAnimeType.Movie: + return SeriesType.Movie; + case AniDBAnimeType.OVA: + return SeriesType.OVA; + case AniDBAnimeType.TVSpecial: + return SeriesType.TVSpecial; + case AniDBAnimeType.Web: + return SeriesType.Web; + case AniDBAnimeType.Other: + return SeriesType.Other; + } + } + + public Series.TvDB GetTvDB(TvDB_Series tbdbSeries, SVR_AnimeSeries series, + List<SVR_AnimeEpisode> episodeList = null) + { + if (episodeList == null) + { + episodeList = series.GetAnimeEpisodes(true); + } + + var images = new Images(); + AddTvDBImages(images, series.AniDB_ID); + + // Aggregate stuff + var firstEp = episodeList.FirstOrDefault(a => + a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode && + a.AniDB_Episode.EpisodeNumber == 1) + ?.TvDBEpisode; + + var lastEp = episodeList + .Where(a => a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode) + .OrderBy(a => a.AniDB_Episode.EpisodeType) + .ThenBy(a => a.AniDB_Episode.EpisodeNumber).LastOrDefault() + ?.TvDBEpisode; + + var result = new Series.TvDB + { + ID = tbdbSeries.SeriesID, + Description = tbdbSeries.Overview, + Title = tbdbSeries.SeriesName, + Posters = images.Posters, + Fanarts = images.Fanarts, + Banners = images.Banners, + Season = firstEp?.SeasonNumber, + AirDate = firstEp?.AirDate, + EndDate = lastEp?.AirDate, + }; + if (tbdbSeries.Rating != null) + { + result.Rating = new Rating { Source = "TvDB", Value = tbdbSeries.Rating.Value, MaxValue = 10 }; + } + + return result; + } + + public SeriesSearchResult GetSeriesSearchResult(SeriesSearch.SearchResult<SVR_AnimeSeries> result) + { + var series = GetSeries(result.Result); + var searchResult = new SeriesSearchResult + { + Name = series.Name, + IDs = series.IDs, + Size = series.Size, + Sizes = series.Sizes, + Created = series.Created, + Updated = series.Updated, + AirsOn = series.AirsOn, + UserRating = series.UserRating, + Images = series.Images, + Links = series.Links, + _AniDB = series._AniDB, + _TvDB = series._TvDB, + ExactMatch = result.ExactMatch, + Index = result.Index, + Distance = result.Distance, + LengthDifference = result.LengthDifference, + Match = result.Match, + }; + return searchResult; + } +} diff --git a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs new file mode 100644 index 000000000..39e890e5d --- /dev/null +++ b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using Shoko.Server.API.v3.Models.Common; +using Shoko.Server.Models; + +namespace Shoko.Server.API.v3.Helpers; + +public class WebUIFactory +{ + private readonly FilterFactory _filterFactory; + private readonly SeriesFactory _seriesFactory; + + public WebUIFactory(FilterFactory filterFactory, SeriesFactory seriesFactory) + { + _filterFactory = filterFactory; + _seriesFactory = seriesFactory; + } + + public Models.Shoko.WebUI.WebUISeriesExtra GetWebUISeriesExtra(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + var cast = _seriesFactory.GetCast(anime.AnimeID, new () { Role.CreatorRoleType.Studio, Role.CreatorRoleType.Producer }); + + var result = new Models.Shoko.WebUI.WebUISeriesExtra + { + FirstAirSeason = _filterFactory.GetFirstAiringSeasonGroupFilter(anime), + Studios = cast.Where(role => role.RoleName == Role.CreatorRoleType.Studio).Select(role => role.Staff).ToList(), + Producers = cast.Where(role => role.RoleName == Role.CreatorRoleType.Producer).Select(role => role.Staff).ToList(), + SourceMaterial = _seriesFactory.GetTags(anime, TagFilter.Filter.Invert | TagFilter.Filter.Source, excludeDescriptions: true).FirstOrDefault()?.Name ?? "Original Work", + }; + return result; + } + + public Models.Shoko.WebUI.WebUIGroupExtra GetWebUIGroupExtra(SVR_AnimeGroup group, SVR_AnimeSeries series, SVR_AniDB_Anime anime, + TagFilter.Filter filter = TagFilter.Filter.None, bool orderByName = false, int tagLimit = 30) + { + var result = new Models.Shoko.WebUI.WebUIGroupExtra{ + ID = group.AnimeGroupID, + Type = SeriesFactory.GetAniDBSeriesType(anime.AnimeType), + Rating = new Rating { Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount } + }; + if (anime.AirDate != null) + { + var airdate = anime.AirDate.Value; + if (airdate != DateTime.MinValue) + { + result.AirDate = airdate; + } + } + + if (anime.EndDate != null) + { + var enddate = anime.EndDate.Value; + if (enddate != DateTime.MinValue) + { + result.EndDate = enddate; + } + } + + result.Tags = _seriesFactory.GetTags(anime, filter, excludeDescriptions: true, orderByName) + .Take(tagLimit) + .ToList(); + + return result; + } +} diff --git a/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs b/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs index 887e339f2..3e4271aec 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Dashboard.cs @@ -5,6 +5,7 @@ using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Server.API.Converters; +using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -150,8 +151,8 @@ public EpisodeDetails(AniDB_Episode episode, SVR_AniDB_Anime anime, SVR_AnimeSer ResumePosition = userRecord?.ResumePositionTimeSpan; Watched = userRecord?.WatchedDate?.ToUniversalTime(); SeriesTitle = series?.GetSeriesName() ?? anime.PreferredTitle; - SeriesPoster = Series.GetDefaultImage(anime.AnimeID, ImageSizeType.Poster) ?? - Series.GetAniDBPoster(anime.AnimeID); + SeriesPoster = SeriesFactory.GetDefaultImage(anime.AnimeID, ImageSizeType.Poster) ?? + SeriesFactory.GetAniDBPoster(anime.AnimeID); } /// <summary> diff --git a/Shoko.Server/API/v3/Models/Shoko/Filter.cs b/Shoko.Server/API/v3/Models/Shoko/Filter.cs index aa545dcc8..d73055bb8 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Filter.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Filter.cs @@ -1,16 +1,6 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Shoko.Models.Enums; -using Shoko.Models.Server; using Shoko.Server.API.v3.Models.Common; -using Shoko.Server.Models; -using Shoko.Server.Repositories; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -44,67 +34,27 @@ public class Filter : BaseModel public bool IsDirectory { get; set; } /// <summary> - /// Indicates that the filter is inverted and all conditions applied - /// to it will be used to exclude groups and series instead of - /// include them. - /// </summary> - public bool IsInverted { get; set; } - - /// <summary> - /// Indicates the filter should be hidden unless explictly requested. This will hide the filter from the normal UIs. + /// Indicates the filter should be hidden unless explicitly requested. This will hide the filter from the normal UIs. /// </summary> public bool IsHidden { get; set; } /// <summary> - /// Inidcates the filter should be applied at the series level. + /// Indicates the filter should be applied at the series level. /// Filter conditions like like Seasons, Years, Tags, etc only count series individually, rather than by group. /// </summary> public bool ApplyAtSeriesLevel { get; set; } /// <summary> - /// List of Conditions. Order doesn't matter. + /// The FilterExpression tree /// </summary> [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public List<FilterCondition>? Conditions { get; set; } + public FilterCondition? Expression { get; set; } /// <summary> - /// The sorting criteria. Order matters. + /// The sorting criteria /// </summary> [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public List<SortingCriteria>? Sorting { get; set; } - - public Filter(HttpContext ctx, SVR_GroupFilter groupFilter, bool fullModel = false) - { - var user = ctx.GetUser(); - - IDs = new FilterIDs { ID = groupFilter.GroupFilterID, ParentFilter = groupFilter.ParentGroupFilterID }; - Name = groupFilter.GroupFilterName; - IsLocked = groupFilter.IsLocked; - IsDirectory = groupFilter.IsDirectory; - IsInverted = groupFilter.BaseCondition == (int)GroupFilterBaseCondition.Exclude; - IsHidden = groupFilter.IsHidden; - ApplyAtSeriesLevel = groupFilter.ApplyToSeries == 1; - if (fullModel) - { - Conditions = groupFilter.Conditions.Select(condition => new FilterCondition(condition)).ToList(); - Sorting = groupFilter.SortCriteriaList.Select(sort => new SortingCriteria(sort)).ToList(); - } - Size = IsDirectory ? ( - RepoFactory.GroupFilter.GetByParentID(groupFilter.GroupFilterID).Count - ) : ( - groupFilter.GroupsIds.TryGetValue(user.JMMUserID, out var groupSet) ? groupSet.Count : 0 - ); - } - - /// <summary> - /// Get the Sorting Criteria for the Group Filter. ORDER DOES MATTER - /// </summary> - /// <param name="gf"></param> - /// <returns></returns> - public static List<SortingCriteria> GetSortingCriteria(SVR_GroupFilter gf) - { - return gf.SortCriteriaList.Select(a => new SortingCriteria(a)).ToList(); - } + public SortingCriteria? Sorting { get; set; } public class FilterIDs : IDs { @@ -117,32 +67,43 @@ public class FilterIDs : IDs public class FilterCondition { /// <summary> - /// Condition Type. What it does + /// Condition Type. What it does. + /// This is not the GroupFilterConditionType, but the type of the FilterExpression, with 'Expression' removed. + /// ex. And, Or, Not, HasAudioLanguage /// </summary> [Required] - [JsonConverter(typeof(StringEnumConverter))] - public GroupFilterConditionType Type { get; set; } + public string Type { get; set; } /// <summary> - /// Condition Operator, how it applies + /// The first, or left, child expression. + /// This might be another logic operator like And, a selector for data like Today's Date, or an expression like HasAudioLanguage. + /// Whether this is included depends on the expression. /// </summary> - [Required] - [JsonConverter(typeof(StringEnumConverter))] - public GroupFilterOperator Operator { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public FilterCondition? Left { get; set; } /// <summary> - /// The actual value to compare + /// The second, or right, child expression. + /// This might be another logic operator like And, a selector for data like Today's Date, or an expression like HasAudioLanguage. + /// Whether this is included depends on the expression. /// </summary> - public string Parameter { get; set; } = string.Empty; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public FilterCondition? Right { get; set; } - public FilterCondition() { } + /// <summary> + /// The actual value to compare. Dependent on the expression type. + /// Coerced this to string to make things easier. + /// </summary> + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? Parameter { get; set; } - public FilterCondition(GroupFilterCondition condition) - { - Type = (GroupFilterConditionType)condition.ConditionType; - Operator = (GroupFilterOperator)condition.ConditionOperator; - Parameter = condition.ConditionParameter ?? string.Empty; - } + /// <summary> + /// The actual value to compare. Dependent on the expression type. + /// Very few things have a second parameter. Seasons are one of them + /// Coerced this to string to make things easier. + /// </summary> + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? SecondParameter { get; set; } } /// <summary> @@ -153,25 +114,23 @@ public FilterCondition(GroupFilterCondition condition) public class SortingCriteria { /// <summary> - /// The sorting type. What it is sorted on + /// The sorting type. What it is sorted on. + /// This is not the GroupFilterSorting, but the type of the SortingExpression, with 'Expression' removed. + /// ex. And, Or, Not, HasAudioLanguage /// </summary> [Required] - [JsonConverter(typeof(StringEnumConverter))] - public GroupFilterSorting Type { get; set; } + public string Type { get; set; } + + /// <summary> + /// The next expression to fall back on when the SortingExpression is equal or invalid, for example, sort by Episode Count descending then by Name + /// </summary> + public SortingCriteria? Next { get; set; } /// <summary> /// Assumed Ascending unless this is specified. You must set this if you want highest rating, for example /// </summary> [Required] public bool IsInverted { get; set; } - - public SortingCriteria() { } - - public SortingCriteria(GroupFilterSortingCriteria criteria) - { - Type = criteria.SortType; - IsInverted = criteria.SortDirection == GroupFilterSortDirection.Desc; - } } public class Input @@ -203,13 +162,6 @@ public class CreateOrUpdateFilterBody /// </remarks> public bool IsDirectory { get; set; } - /// <summary> - /// Indicates that the filter is inverted and all conditions applied - /// to it will be used to exclude groups and series instead of - /// include them. - /// </summary> - public bool IsInverted { get; set; } - /// <summary> /// Indicates the filter should be hidden unless explictly requested. This will hide the filter from the normal UIs. /// </summary> @@ -222,133 +174,16 @@ public class CreateOrUpdateFilterBody public bool ApplyAtSeriesLevel { get; set; } /// <summary> - /// List of Conditions. Order doesn't matter. + /// The FilterExpression tree /// </summary> - public List<FilterCondition>? Conditions { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public FilterCondition? Expression { get; set; } /// <summary> - /// The sorting criteria. Order matters. + /// The sorting criteria /// </summary> - public List<SortingCriteria>? Sorting { get; set; } - - public CreateOrUpdateFilterBody() { } - - public CreateOrUpdateFilterBody(SVR_GroupFilter groupFilter) - { - Name = groupFilter.GroupFilterName; - ParentID = groupFilter.ParentGroupFilterID; - IsDirectory = groupFilter.IsDirectory; - IsInverted = groupFilter.BaseCondition == (int)GroupFilterBaseCondition.Exclude; - IsHidden = groupFilter.IsHidden; - ApplyAtSeriesLevel = groupFilter.ApplyToSeries == 1; - if (!IsDirectory) - { - Conditions = groupFilter.Conditions.Select(condition => new FilterCondition(condition)).ToList(); - Sorting = groupFilter.SortCriteriaList.Select(sort => new SortingCriteria(sort)).ToList(); - } - } - - public Filter? MergeWithExisting(HttpContext ctx, SVR_GroupFilter groupFilter, ModelStateDictionary modelState, bool skipSave = false) - { - if (groupFilter.IsLocked) - modelState.AddModelError("IsLocked", "Filter is locked."); - - // Defer to `null` if the id is `0`. - if (ParentID.HasValue && ParentID.Value == 0) - ParentID = null; - - if (ParentID.HasValue) - { - var parentFilter = RepoFactory.GroupFilter.GetByID(ParentID.Value); - if (parentFilter == null) - { - modelState.AddModelError(nameof(ParentID), $"Unable to find parent filter with id {ParentID.Value}"); - } - else - { - if (parentFilter.IsLocked) - modelState.AddModelError(nameof(ParentID), $"Unable to add a sub-filter to a filter that is locked."); - - if (!parentFilter.IsDirectory) - modelState.AddModelError(nameof(ParentID), $"Unable to add a sub-filter to a filter that is not a directorty filter."); - } - } - - if (IsDirectory) - { - if (IsInverted) - modelState.AddModelError(nameof(IsInverted), "Cannot invert the filter conditions for a directory filter."); - - if (Conditions != null && Conditions.Count > 0) - modelState.AddModelError(nameof(Conditions), "Directory filters cannot have any conditions applied to them."); - - if (Sorting != null && Sorting.Count > 0) - modelState.AddModelError(nameof(Sorting), "Directory filters cannot have custom sorting applied to them."); - } - else - { - var subFilters = groupFilter.GroupFilterID != 0 ? RepoFactory.GroupFilter.GetByParentID(groupFilter.GroupFilterID) : new(); - if (subFilters.Count > 0) - modelState.AddModelError(nameof(IsDirectory), "Cannot turn a directory filter with sub-filters into a normal filter without first removing the sub-filters"); - } - - // Return now if we encountered any validation errors. - if (!modelState.IsValid) - return null; - - groupFilter.ParentGroupFilterID = ParentID; - groupFilter.FilterType = (int)(IsDirectory ? GroupFilterType.UserDefined | GroupFilterType.Directory : GroupFilterType.UserDefined); - groupFilter.GroupFilterName = Name ?? string.Empty; - groupFilter.IsHidden = IsHidden; - groupFilter.ApplyToSeries = ApplyAtSeriesLevel ? 1 : 0; - if (IsDirectory) - { - groupFilter.BaseCondition = (int)GroupFilterBaseCondition.Include; - groupFilter.Conditions = new(); - groupFilter.SortCriteriaList = new() - { - new GroupFilterSortingCriteria() - { - SortType = GroupFilterSorting.GroupFilterName, - SortDirection = GroupFilterSortDirection.Asc, - }, - }; - } - else - { - groupFilter.BaseCondition = (int)(IsInverted ? GroupFilterBaseCondition.Exclude : GroupFilterBaseCondition.Include); - if (Conditions != null) - { - groupFilter.Conditions = Conditions - .Select(c => new GroupFilterCondition() - { - ConditionOperator = (int)c.Operator, - ConditionParameter = c.Parameter, - ConditionType = (int)c.Type, - }) - .ToList(); - } - if (Sorting != null) - { - groupFilter.SortCriteriaList = Sorting - .Select(s => new GroupFilterSortingCriteria - { - SortType = s.Type, - SortDirection = s.IsInverted ? GroupFilterSortDirection.Desc : GroupFilterSortDirection.Asc - }) - .ToList(); - } - } - - // Re-calculate the groups and series that belong to the group filter. - groupFilter.CalculateGroupsAndSeries(); - - // Skip saving if we're just going to preview a group filter. - if (!skipSave) - RepoFactory.GroupFilter.Save(groupFilter); - - return new Filter(ctx, groupFilter, true); - } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public SortingCriteria? Sorting { get; set; } } } } diff --git a/Shoko.Server/API/v3/Models/Shoko/Group.cs b/Shoko.Server/API/v3/Models/Shoko/Group.cs index 202ff6252..0e9c7c92a 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Group.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Group.cs @@ -4,10 +4,12 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; using Shoko.Server.Models; using Shoko.Server.Repositories; +using Shoko.Server.Utilities; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -92,11 +94,13 @@ public Group(HttpContext ctx, SVR_AnimeGroup group, bool randomiseImages = false SortName = group.SortName; Description = group.Description; Sizes = ModelHelper.GenerateGroupSizes(allSeries, episodes, subGroupCount, userID); - Size = allSeries.Where(series => series.AnimeGroupID == group.AnimeGroupID).Count(); + Size = allSeries.Count(series => series.AnimeGroupID == group.AnimeGroupID); HasCustomName = group.IsManuallyNamed == 1; HasCustomDescription = group.OverrideDescription == 1; - Images = mainSeries == null ? new Images() : Series.GetDefaultImages(ctx, mainSeries, randomiseImages); + // TODO make a factory for this file. Not feeling it rn + var factory = ctx.RequestServices.GetRequiredService<SeriesFactory>(); + Images = mainSeries == null ? new Images() : factory.GetDefaultImages(mainSeries, randomiseImages); } #endregion diff --git a/Shoko.Server/API/v3/Models/Shoko/Series.cs b/Shoko.Server/API/v3/Models/Shoko/Series.cs index 95c4d3d6c..ad40007ad 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Series.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Series.cs @@ -1,24 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Shoko.Commons.Extensions; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Plugin.Abstractions.DataModels; using Shoko.Server.API.Converters; -using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; -using Shoko.Server.Commands; -using Shoko.Server.Commands.AniDB; using Shoko.Server.Models; -using Shoko.Server.Providers.AniDB.Interfaces; -using Shoko.Server.Providers.AniDB.Titles; using Shoko.Server.Repositories; -using Shoko.Server.Utilities; using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType; using AniDBAnimeType = Shoko.Models.Enums.AnimeType; @@ -86,7 +74,7 @@ public class Series : BaseModel /// included in the data to add. /// </summary> [JsonProperty("AniDB", NullValueHandling = NullValueHandling.Ignore)] - public AniDBWithDate _AniDB { get; set; } + public AniDB _AniDB { get; set; } /// <summary> /// The <see cref="Series.TvDB"/> entries, if <see cref="DataSource.TvDB"/> @@ -95,534 +83,6 @@ public class Series : BaseModel [JsonProperty("TvDB", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable<TvDB> _TvDB { get; set; } - #region Constructors and Helper Methods - - public Series() { } - - public Series(HttpContext ctx, SVR_AnimeSeries ser, bool randomiseImages = false, HashSet<DataSource> includeDataFrom = null) - { - var uid = ctx.GetUser()?.JMMUserID ?? 0; - var anime = ser.GetAnime(); - var animeType = (AniDBAnimeType)anime.AnimeType; - - AddBasicAniDBInfo(ctx, ser, anime); - - var ael = ser.GetAnimeEpisodes(); - var contract = ser.Contract; - if (contract == null) - { - ser.UpdateContract(); - } - - IDs = GetIDs(ser); - Images = GetDefaultImages(ctx, ser, randomiseImages); - AirsOn = animeType == AniDBAnimeType.TVSeries || animeType == AniDBAnimeType.Web ? ser.GetAirsOnDaysOfWeek(ael) : new(); - - Name = ser.GetSeriesName(); - Sizes = ModelHelper.GenerateSeriesSizes(ael, uid); - Size = Sizes.Local.Credits + Sizes.Local.Episodes + Sizes.Local.Others + Sizes.Local.Parodies + - Sizes.Local.Specials + Sizes.Local.Trailers; - - Created = ser.DateTimeCreated.ToUniversalTime(); - Updated = ser.DateTimeUpdated.ToUniversalTime(); - - if (includeDataFrom?.Contains(DataSource.AniDB) ?? false) - this._AniDB = new Series.AniDBWithDate(anime, ser); - if (includeDataFrom?.Contains(DataSource.TvDB) ?? false) - this._TvDB = GetTvDBInfo(ctx, ser); - } - - private void AddBasicAniDBInfo(HttpContext ctx, SVR_AnimeSeries series, SVR_AniDB_Anime anime) - { - if (anime == null) - { - return; - } - - Links = new(); - if (!string.IsNullOrEmpty(anime.Site_EN)) - foreach (var site in anime.Site_EN.Split('|')) - Links.Add(new() { Type = "source", Name = "Official Site (EN)", URL = site }); - - if (!string.IsNullOrEmpty(anime.Site_JP)) - foreach (var site in anime.Site_JP.Split('|')) - Links.Add(new() { Type = "source", Name = "Official Site (JP)", URL = site }); - - if (!string.IsNullOrEmpty(anime.Wikipedia_ID)) - Links.Add(new() { Type = "wiki", Name = "Wikipedia (EN)", URL = $"https://en.wikipedia.org/{anime.Wikipedia_ID}" }); - - if (!string.IsNullOrEmpty(anime.WikipediaJP_ID)) - Links.Add(new() { Type = "wiki", Name = "Wikipedia (JP)", URL = $"https://en.wikipedia.org/{anime.WikipediaJP_ID}" }); - - if (!string.IsNullOrEmpty(anime.CrunchyrollID)) - Links.Add(new() { Type = "streaming", Name = "Crunchyroll", URL = $"https://crunchyroll.com/anime/{anime.CrunchyrollID}" }); - - if (!string.IsNullOrEmpty(anime.FunimationID)) - Links.Add(new() { Type = "streaming", Name = "Funimation", URL = anime.FunimationID }); - - if (!string.IsNullOrEmpty(anime.HiDiveID)) - Links.Add(new() { Type = "streaming", Name = "HiDive", URL = $"https://www.hidive.com/{anime.HiDiveID}" }); - - if (anime.AllCinemaID.HasValue && anime.AllCinemaID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "allcinema", URL = $"https://allcinema.net/cinema/{anime.AllCinemaID.Value}" }); - - if (anime.AnisonID.HasValue && anime.AnisonID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "Anison", URL = $"https://anison.info/data/program/{anime.AnisonID.Value}.html" }); - - if (anime.SyoboiID.HasValue && anime.SyoboiID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "syoboi", URL = $"https://cal.syoboi.jp/tid/{anime.SyoboiID.Value}/time" }); - - if (anime.BangumiID.HasValue && anime.BangumiID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = "bangumi", URL = $"https://bgm.tv/subject/{anime.BangumiID.Value}" }); - - if (anime.LainID.HasValue && anime.LainID.Value > 0) - Links.Add(new() { Type = "foreign-metadata", Name = ".lain", URL = $"http://lain.gr.jp/mediadb/media/{anime.LainID.Value}" }); - - if (anime.ANNID.HasValue && anime.ANNID.Value > 0) - Links.Add(new() { Type = "english-metadata", Name = "AnimeNewsNetwork", URL = $"https://www.animenewsnetwork.com/encyclopedia/anime.php?id={anime.ANNID.Value}" }); - - if (anime.VNDBID.HasValue && anime.VNDBID.Value > 0) - Links.Add(new() { Type = "english-metadata", Name = "VNDB", URL = $"https://vndb.org/v{anime.VNDBID.Value}" }); - - var vote = RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.Anime) ?? - RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.AnimeTemp); - if (vote != null) - { - var voteType = (AniDBVoteType)vote.VoteType == AniDBVoteType.Anime ? "Permanent" : "Temporary"; - UserRating = new Rating - { - Value = (decimal)Math.Round(vote.VoteValue / 100D, 1), - MaxValue = 10, - Type = voteType, - Source = "User" - }; - } - } - - public static bool QueueAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, - int animeID, bool force, bool downloadRelations, bool createSeriesEntry, bool immediate = false, - bool cacheOnly = false) - { - if (force) - return QueueForcedAniDBRefresh(commandFactory, handler, animeID, downloadRelations, createSeriesEntry, immediate); - - var command = commandFactory.Create<CommandRequest_GetAnimeHTTP>(c => - { - c.AnimeID = animeID; - c.DownloadRelations = downloadRelations; - c.ForceRefresh = force; - c.CacheOnly = !force && cacheOnly; - c.CreateSeriesEntry = createSeriesEntry; - c.BubbleExceptions = immediate; - }); - if (immediate) - { - try - { - command.ProcessCommand(); - } - catch - { - return false; - } - - return command.Result != null; - } - - commandFactory.Save(command); - return false; - } - - private static bool QueueForcedAniDBRefresh(ICommandRequestFactory commandFactory, IHttpConnectionHandler handler, - int animeID, bool downloadRelations, bool createSeriesEntry, bool immediate = false) - { - var command = commandFactory.Create<CommandRequest_GetAnimeHTTP_Force>(c => - { - c.AnimeID = animeID; - c.DownloadRelations = downloadRelations; - c.CreateSeriesEntry = createSeriesEntry; - c.BubbleExceptions = immediate; - }); - if (immediate && !handler.IsBanned) - { - try - { - command.ProcessCommand(); - } - catch - { - return false; - } - - return command.Result != null; - } - - commandFactory.Save(command); - return false; - } - - public static bool QueueTvDBRefresh(ICommandRequestFactory commandFactory, int tvdbID, bool force, bool immediate = false) - { - var command = commandFactory.Create<CommandRequest_TvDBUpdateSeries>(c => - { - c.TvDBSeriesID = tvdbID; - c.ForceRefresh = force; - c.BubbleExceptions = immediate; - }); - if (immediate) - { - try - { - command.ProcessCommand(); - } - catch - { - return false; - } - - return command.Result != null; - } - - commandFactory.Save(command); - return false; - } - - public static SeriesIDs GetIDs(SVR_AnimeSeries ser) - { - // Shoko - var ids = new SeriesIDs - { - ID = ser.AnimeSeriesID, - ParentGroup = ser.AnimeGroupID, - TopLevelGroup = ser.TopLevelAnimeGroup.AnimeGroupID - }; - - // AniDB - var anidbId = ser.GetAnime()?.AnimeID; - if (anidbId.HasValue) - { - ids.AniDB = anidbId.Value; - } - - // TvDB - var tvdbIds = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID); - if (tvdbIds.Any()) - { - ids.TvDB.AddRange(tvdbIds.Select(a => a.TvDBID).Distinct()); - } - - // TODO: Cache the rest of these, so that they don't severely slow down the API - - // TMDB - // TODO: make this able to support more than one, in fact move it to its own and remove CrossRef_Other - var tmdbId = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(ser.AniDB_ID, CrossRefType.MovieDB); - if (tmdbId != null && int.TryParse(tmdbId.CrossRefID, out var movieID)) - { - ids.TMDB.Add(movieID); - } - - // Trakt - // var traktIds = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(ser.AniDB_ID).Select(a => a.TraktID) - // .Distinct().ToList(); - // if (traktIds.Any()) ids.TraktTv.AddRange(traktIds); - - // MAL - var malIds = RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(ser.AniDB_ID).Select(a => a.MALID).Distinct() - .ToList(); - if (malIds.Any()) - { - ids.MAL.AddRange(malIds); - } - - // TODO: AniList later - return ids; - } - - public static Image GetDefaultImage(int anidbId, ImageSizeType imageSizeType, - ImageEntityType? imageEntityType = null) - { - var defaultImage = imageEntityType.HasValue - ? RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(anidbId, - imageSizeType, imageEntityType.Value) - : RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(anidbId, imageSizeType); - return defaultImage != null - ? new Image(defaultImage.ImageParentID, (ImageEntityType)defaultImage.ImageParentType, true) - : null; - } - - public static Images GetDefaultImages(HttpContext ctx, SVR_AnimeSeries ser, bool randomiseImages = false) - { - var images = new Images(); - var random = ctx.Items["Random"] as Random; - var allImages = GetArt(ctx, ser.AniDB_ID); - - var poster = randomiseImages - ? allImages.Posters.GetRandomElement(random) - : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Poster) ?? allImages.Posters.FirstOrDefault(); - if (poster != null) - { - images.Posters.Add(poster); - } - - var fanart = randomiseImages - ? allImages.Fanarts.GetRandomElement(random) - : GetDefaultImage(ser.AniDB_ID, ImageSizeType.Fanart) ?? allImages.Fanarts.FirstOrDefault(); - if (fanart != null) - { - images.Fanarts.Add(fanart); - } - - var banner = randomiseImages - ? allImages.Banners.GetRandomElement(random) - : GetDefaultImage(ser.AniDB_ID, ImageSizeType.WideBanner) ?? allImages.Banners.FirstOrDefault(); - if (banner != null) - { - images.Banners.Add(banner); - } - - return images; - } - - /// <summary> - /// Cast is aggregated, and therefore not in each provider - /// </summary> - /// <param name="animeID"></param> - /// <param name="roleTypes"></param> - /// <returns></returns> - public static List<Role> GetCast(int animeID, HashSet<Role.CreatorRoleType> roleTypes = null) - { - var roles = new List<Role>(); - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByAnimeID(animeID); - foreach (var xref in xrefAnimeStaff) - { - // Filter out any roles that are not of the desired type. - if (roleTypes != null && !roleTypes.Contains((Role.CreatorRoleType)xref.RoleType)) - continue; - - var character = xref.RoleID.HasValue ? RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value) : null; - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); - if (staff == null) - continue; - - var role = new Role - { - Character = - character != null - ? new Role.Person - { - Name = character.Name, - AlternateName = character.AlternateName, - Image = new Image(character.CharacterID, ImageEntityType.Character), - Description = character.Description - } - : null, - Staff = new Role.Person - { - Name = staff.Name, - AlternateName = staff.AlternateName, - Description = staff.Description, - Image = staff.ImagePath != null ? new Image(staff.StaffID, ImageEntityType.Staff) : null - }, - RoleName = (Role.CreatorRoleType)xref.RoleType, - RoleDetails = xref.Role - }; - roles.Add(role); - } - - return roles; - } - - public static List<Tag> GetTags(SVR_AniDB_Anime anime, TagFilter.Filter filter, - bool excludeDescriptions = false, bool orderByName = false, bool onlyVerified = true) - { - // Only get the user tags if we don't exclude it (false == false), or if we invert the logic and want to include it (true == true). - IEnumerable<Tag> userTags = new List<Tag>(); - if (filter.HasFlag(TagFilter.Filter.User) == filter.HasFlag(TagFilter.Filter.Invert)) - userTags = RepoFactory.CustomTag.GetByAnimeID(anime.AnimeID) - .Select(tag => new Tag(tag, excludeDescriptions)); - - var selectedTags = anime.GetAniDBTags(onlyVerified) - .DistinctBy(a => a.TagName) - .ToList(); - var tagFilter = new TagFilter<AniDB_Tag>(name => RepoFactory.AniDB_Tag.GetByName(name).FirstOrDefault(), tag => tag.TagName, - name => new AniDB_Tag { TagNameSource = name }); - var anidbTags = tagFilter - .ProcessTags(filter, selectedTags) - .Select(tag => - { - var xref = RepoFactory.AniDB_Anime_Tag.GetByTagID(tag.TagID).FirstOrDefault(xref => xref.AnimeID == anime.AnimeID); - return new Tag(tag, excludeDescriptions) { Weight = xref?.Weight ?? 0, IsLocalSpoiler = xref?.LocalSpoiler }; - }); - - if (orderByName) - return userTags.Concat(anidbTags) - .OrderByDescending(tag => tag.Source) - .ThenBy(tag => tag.Name) - .ToList(); - - return userTags.Concat(anidbTags) - .OrderByDescending(tag => tag.Source) - .ThenByDescending(tag => tag.Weight) - .ThenBy(tag => tag.Name) - .ToList(); - } - - public static SeriesType GetAniDBSeriesType(int? animeType) - { - return animeType.HasValue ? GetAniDBSeriesType((AniDBAnimeType)animeType.Value) : SeriesType.Unknown; - } - - public static SeriesType GetAniDBSeriesType(AniDBAnimeType animeType) - { - switch (animeType) - { - default: - case AniDBAnimeType.None: - return SeriesType.Unknown; - case AniDBAnimeType.TVSeries: - return SeriesType.TV; - case AniDBAnimeType.Movie: - return SeriesType.Movie; - case AniDBAnimeType.OVA: - return SeriesType.OVA; - case AniDBAnimeType.TVSpecial: - return SeriesType.TVSpecial; - case AniDBAnimeType.Web: - return SeriesType.Web; - case AniDBAnimeType.Other: - return SeriesType.Other; - } - } - - public static List<TvDB> GetTvDBInfo(HttpContext ctx, SVR_AnimeSeries ser) - { - var ael = ser.GetAnimeEpisodes(true); - return RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(ser.AniDB_ID) - .Select(xref => RepoFactory.TvDB_Series.GetByTvDBID(xref.TvDBID)) - .Select(tvdbSer => new TvDB(ctx, tvdbSer, ser, ael)) - .ToList(); - } - - public static void AddSeriesVote(ICommandRequestFactory commandFactory, SVR_AnimeSeries ser, int userID, Vote vote) - { - var voteType = (vote.Type?.ToLowerInvariant() ?? "") switch - { - "temporary" => (int)AniDBVoteType.AnimeTemp, - "permanent" => (int)AniDBVoteType.Anime, - _ => ser.GetAnime()?.GetFinishedAiring() ?? false ? (int)AniDBVoteType.Anime : (int)AniDBVoteType.AnimeTemp - }; - - var dbVote = RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.AnimeTemp) ?? - RepoFactory.AniDB_Vote.GetByEntityAndType(ser.AniDB_ID, AniDBVoteType.Anime); - - if (dbVote == null) - { - dbVote = new AniDB_Vote { EntityID = ser.AniDB_ID }; - } - - dbVote.VoteValue = (int)Math.Floor(vote.GetRating(1000)); - dbVote.VoteType = voteType; - - RepoFactory.AniDB_Vote.Save(dbVote); - - commandFactory.CreateAndSave<CommandRequest_VoteAnime>( - c => - { - c.AnimeID = ser.AniDB_ID; - c.VoteType = voteType; - c.VoteValue = vote.GetRating(); - } - ); - } - - public static Images GetArt(HttpContext ctx, int animeID, bool includeDisabled = false) - { - var images = new Images(); - AddAniDBPoster(ctx, images, animeID); - AddTvDBImages(ctx, images, animeID, includeDisabled); - // AddMovieDBImages(ctx, images, animeID, includeDisabled); - return images; - } - - private static void AddAniDBPoster(HttpContext ctx, Images images, int animeID) - { - images.Posters.Add(GetAniDBPoster(animeID)); - } - - public static Image GetAniDBPoster(int animeID) - { - var defaultImage = RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(animeID, - ImageSizeType.Poster); - var preferred = defaultImage != null && defaultImage.ImageParentType == (int)ImageEntityType.AniDB_Cover; - return new Image(animeID, ImageEntityType.AniDB_Cover, preferred); - } - - private static void AddTvDBImages(HttpContext ctx, Images images, int animeID, bool includeDisabled = false) - { - var tvdbIDs = RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(animeID).ToList(); - - var defaultFanart = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Fanart, ImageEntityType.TvDB_FanArt); - var fanarts = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageFanart.GetBySeriesID(a.TvDBID)).ToList(); - images.Fanarts.AddRange(fanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.TvDB_ImageFanartID; - return new Image(a.TvDB_ImageFanartID, ImageEntityType.TvDB_FanArt, preferred, a.Enabled == 0); - })); - - var defaultBanner = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.WideBanner, ImageEntityType.TvDB_Banner); - var banners = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImageWideBanner.GetBySeriesID(a.TvDBID)).ToList(); - images.Banners.AddRange(banners.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultBanner != null && defaultBanner.ImageParentID == a.TvDB_ImageWideBannerID; - return new Image(a.TvDB_ImageWideBannerID, ImageEntityType.TvDB_Banner, preferred, a.Enabled == 0); - })); - - var defaultPoster = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Poster, ImageEntityType.TvDB_Cover); - var posters = tvdbIDs.SelectMany(a => RepoFactory.TvDB_ImagePoster.GetBySeriesID(a.TvDBID)).ToList(); - images.Posters.AddRange(posters.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.TvDB_ImagePosterID; - return new Image(a.TvDB_ImagePosterID, ImageEntityType.TvDB_Cover, preferred, a.Enabled == 0); - })); - } - - private static void AddMovieDBImages(HttpContext ctx, Images images, int animeID, bool includeDisabled = false) - { - var moviedb = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(animeID, CrossRefType.MovieDB); - - var moviedbPosters = moviedb == null - ? new List<MovieDB_Poster>() - : RepoFactory.MovieDB_Poster.GetByMovieID(int.Parse(moviedb.CrossRefID)); - var defaultPoster = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Poster, ImageEntityType.MovieDB_Poster); - images.Posters.AddRange(moviedbPosters.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultPoster != null && defaultPoster.ImageParentID == a.MovieDB_PosterID; - return new Image(a.MovieDB_PosterID, ImageEntityType.MovieDB_Poster, preferred, a.Enabled == 1); - })); - - var moviedbFanarts = moviedb == null - ? new List<MovieDB_Fanart>() - : RepoFactory.MovieDB_Fanart.GetByMovieID(int.Parse(moviedb.CrossRefID)); - var defaultFanart = - RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeTypeAndImageEntityType(animeID, - ImageSizeType.Fanart, ImageEntityType.MovieDB_FanArt); - images.Fanarts.AddRange(moviedbFanarts.Where(a => includeDisabled || a.Enabled != 0).Select(a => - { - var preferred = defaultFanart != null && defaultFanart.ImageParentID == a.MovieDB_FanartID; - return new Image(a.MovieDB_FanartID, ImageEntityType.MovieDB_FanArt, preferred, a.Enabled == 1); - })); - } - - #endregion - /// <summary> /// Auto-matching settings for the series. /// </summary> @@ -713,214 +173,6 @@ public AutoMatchSettings MergeWithExisting(SVR_AnimeSeries series) /// </summary> public class AniDB { - public AniDB() { } - - public AniDB(SVR_AniDB_Anime anime, bool includeTitles) : this(anime, null, includeTitles) { } - - public AniDB(SVR_AniDB_Anime anime, SVR_AnimeSeries series = null, bool includeTitles = true) - { - series ??= RepoFactory.AnimeSeries.GetByAnimeID(anime.AnimeID); - ID = anime.AnimeID; - ShokoID = series?.AnimeSeriesID; - Type = GetAniDBSeriesType(anime.AnimeType); - Title = series?.GetSeriesName() ?? anime.PreferredTitle; - Titles = includeTitles - ? anime.GetTitles().Select(title => new Title - { - Name = title.Title, - Language = title.LanguageCode, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime.Description; - Restricted = anime.Restricted == 1; - Poster = GetAniDBPoster(anime.AnimeID); - EpisodeCount = anime.EpisodeCountNormal; - Rating = new Rating { Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount }; - UserApproval = null; - Relation = null; - } - - public AniDB(ResponseAniDBTitles.Anime result, bool includeTitles) : this(result, null, includeTitles) { } - - public AniDB(ResponseAniDBTitles.Anime result, SVR_AnimeSeries series = null, bool includeTitles = false) - { - if (series == null) - { - series = RepoFactory.AnimeSeries.GetByAnimeID(result.AnimeID); - } - - var anime = series != null ? series.GetAnime() : RepoFactory.AniDB_Anime.GetByAnimeID(result.AnimeID); - - ID = result.AnimeID; - ShokoID = series?.AnimeSeriesID; - Type = GetAniDBSeriesType(anime?.AnimeType); - Title = series?.GetSeriesName() ?? anime?.PreferredTitle ?? result.MainTitle; - Titles = includeTitles - ? result.Titles.Select(title => new Title - { - Language = title.LanguageCode, - Name = title.Title, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime?.Description; - Restricted = anime is { Restricted: 1 }; - EpisodeCount = anime?.EpisodeCount; - Poster = GetAniDBPoster(result.AnimeID); - } - - public AniDB(SVR_AniDB_Anime_Relation relation, bool includeTitles) : this(relation, null, includeTitles) { } - - public AniDB(SVR_AniDB_Anime_Relation relation, SVR_AnimeSeries series = null, bool includeTitles = true) - { - series ??= RepoFactory.AnimeSeries.GetByAnimeID(relation.RelatedAnimeID); - ID = relation.RelatedAnimeID; - ShokoID = series?.AnimeSeriesID; - SetTitles(relation, series, includeTitles); - Poster = GetAniDBPoster(relation.RelatedAnimeID); - Rating = null; - UserApproval = null; - Relation = ((IRelatedAnime)relation).RelationType; - } - - private void SetTitles(AniDB_Anime_Relation relation, SVR_AnimeSeries series, bool includeTitles) - { - var anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.RelatedAnimeID); - if (anime is not null) - { - Type = GetAniDBSeriesType(anime.AnimeType); - Title = series?.GetSeriesName() ?? anime.PreferredTitle; - Titles = includeTitles - ? anime.GetTitles().Select( - title => new Title - { - Name = title.Title, - Language = title.LanguageCode, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime.Description; - Restricted = anime.Restricted == 1; - EpisodeCount = anime.EpisodeCountNormal; - return; - } - - var result = Utils.AniDBTitleHelper.SearchAnimeID(relation.RelatedAnimeID); - if (result != null) - { - Type = SeriesType.Unknown; - Title = result.PreferredTitle; - Titles = includeTitles - ? result.Titles.Select( - title => new Title - { - Language = title.LanguageCode, - Name = title.Title, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = null; - // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. - anime = RepoFactory.AniDB_Anime.GetByAnimeID(relation.AnimeID); - Restricted = anime is not null && anime.Restricted == 1; - return; - } - - Type = SeriesType.Unknown; - Titles = includeTitles ? new List<Title>() : null; - Restricted = false; - } - - public AniDB(AniDB_Anime_Similar similar, bool includeTitles) : this(similar, null, includeTitles) { } - - public AniDB(AniDB_Anime_Similar similar, SVR_AnimeSeries series = null, bool includeTitles = true) - { - series ??= RepoFactory.AnimeSeries.GetByAnimeID(similar.SimilarAnimeID); - ID = similar.SimilarAnimeID; - ShokoID = series?.AnimeSeriesID; - SetTitles(similar, series, includeTitles); - Poster = GetAniDBPoster(similar.SimilarAnimeID); - Rating = null; - UserApproval = new Rating - { - Value = new Vote(similar.Approval, similar.Total).GetRating(100), - MaxValue = 100, - Votes = similar.Total, - Source = "AniDB", - Type = "User Approval" - }; - Relation = null; - Restricted = false; - } - - private void SetTitles(AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool includeTitles) - { - var anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.SimilarAnimeID); - if (anime is not null) - { - Type = GetAniDBSeriesType(anime.AnimeType); - Title = series?.GetSeriesName() ?? anime.PreferredTitle; - Titles = includeTitles - ? anime.GetTitles().Select( - title => new Title - { - Name = title.Title, - Language = title.LanguageCode, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = anime.Description; - Restricted = anime.Restricted == 1; - return; - } - - var result = Utils.AniDBTitleHelper.SearchAnimeID(similar.SimilarAnimeID); - if (result != null) - { - Type = SeriesType.Unknown; - Title = result.PreferredTitle; - Titles = includeTitles - ? result.Titles.Select( - title => new Title - { - Language = title.LanguageCode, - Name = title.Title, - Type = title.TitleType, - Default = string.Equals(title.Title, Title), - Source = "AniDB" - } - ).ToList() - : null; - Description = null; - // If the other anime is present we assume they're of the same kind. Be it restricted or unrestricted. - anime = RepoFactory.AniDB_Anime.GetByAnimeID(similar.AnimeID); - Restricted = anime is not null && anime.Restricted == 1; - return; - } - - Type = SeriesType.Unknown; - Title = null; - Titles = includeTitles ? new List<Title>() : null; - Description = null; - Restricted = false; - } - /// <summary> /// AniDB ID /// </summary> @@ -959,6 +211,19 @@ private void SetTitles(AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } + /// <summary> + /// Air date (2013-02-27, shut up avael). Anything without an air date is going to be missing a lot of info. + /// </summary> + [Required] + [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] + public DateTime? AirDate { get; set; } + + /// <summary> + /// End date, can be omitted. Omitted means that it's still airing (2013-02-27) + /// </summary> + [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] + public DateTime? EndDate { get; set; } + /// <summary> /// Restricted content. Mainly porn. /// </summary> @@ -995,39 +260,6 @@ private void SetTitles(AniDB_Anime_Similar similar, SVR_AnimeSeries series, bool public RelationType? Relation { get; set; } } - /// <summary> - /// The AniDB Data model for series - /// </summary> - public class AniDBWithDate : AniDB - { - public AniDBWithDate(SVR_AniDB_Anime anime, SVR_AnimeSeries series = null) : base(anime, - series) - { - if (anime.AirDate.HasValue) - { - AirDate = anime.AirDate.Value; - } - - if (anime.EndDate.HasValue) - { - EndDate = anime.EndDate.Value; - } - } - - /// <summary> - /// Air date (2013-02-27, shut up avael). Anything without an air date is going to be missing a lot of info. - /// </summary> - [Required] - [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] - public DateTime? AirDate { get; set; } - - /// <summary> - /// End date, can be omitted. Omitted means that it's still airing (2013-02-27) - /// </summary> - [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")] - public DateTime? EndDate { get; set; } - } - /// <summary> /// The result entries for the "Recommended For You" algorithm. /// </summary> @@ -1036,7 +268,7 @@ public class AniDBRecommendedForYou /// <summary> /// The recommended AniDB entry. /// </summary> - public AniDBWithDate Anime; + public AniDB Anime; /// <summary> /// Number of similar anime that resulted in this recommendation. @@ -1049,45 +281,6 @@ public class AniDBRecommendedForYou /// </summary> public class TvDB { - public TvDB(HttpContext ctx, TvDB_Series tbdbSeries, SVR_AnimeSeries series, - List<SVR_AnimeEpisode> episodeList = null) - { - if (episodeList == null) - { - episodeList = series.GetAnimeEpisodes(true); - } - - ID = tbdbSeries.SeriesID; - Description = tbdbSeries.Overview; - Title = tbdbSeries.SeriesName; - if (tbdbSeries.Rating != null) - { - Rating = new Rating { Source = "TvDB", Value = tbdbSeries.Rating.Value, MaxValue = 10 }; - } - - var images = new Images(); - AddTvDBImages(ctx, images, series.AniDB_ID); - Posters = images.Posters; - Fanarts = images.Fanarts; - Banners = images.Banners; - - // Aggregate stuff - var firstEp = episodeList.FirstOrDefault(a => - a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode && - a.AniDB_Episode.EpisodeNumber == 1) - ?.TvDBEpisode; - - var lastEp = episodeList - .Where(a => a.AniDB_Episode != null && a.AniDB_Episode.EpisodeType == (int)AniDBEpisodeType.Episode) - .OrderBy(a => a.AniDB_Episode.EpisodeType) - .ThenBy(a => a.AniDB_Episode.EpisodeNumber).LastOrDefault() - ?.TvDBEpisode; - - Season = firstEp?.SeasonNumber; - AirDate = firstEp?.AirDate; - EndDate = lastEp?.AirDate; - } - /// <summary> /// TvDB ID /// </summary> @@ -1286,15 +479,6 @@ public class SeriesSearchResult : Series /// Contains the original matched substring from the original string. /// </summary> public string Match { get; set; } = string.Empty; - - public SeriesSearchResult(HttpContext ctx, SeriesSearch.SearchResult<SVR_AnimeSeries> result) : base(ctx, result.Result) - { - ExactMatch = result.ExactMatch; - Index = result.Index; - Distance = result.Distance; - LengthDifference = result.LengthDifference; - Match = result.Match; - } } public enum SeriesType diff --git a/Shoko.Server/API/v3/Models/Shoko/User.cs b/Shoko.Server/API/v3/Models/Shoko/User.cs index eb448bafc..c3dfef4b6 100644 --- a/Shoko.Server/API/v3/Models/Shoko/User.cs +++ b/Shoko.Server/API/v3/Models/Shoko/User.cs @@ -231,7 +231,7 @@ public CreateOrUpdateUserBody() { } } // Save the model now. - RepoFactory.JMMUser.Save(user, false); + RepoFactory.JMMUser.Save(user); return new User(user); } diff --git a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs index 62355ef9e..acaac74fa 100644 --- a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs +++ b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs @@ -94,35 +94,6 @@ public WebUITheme(WebUIThemeProvider.ThemeDefinition definition, bool withCSS = public class WebUIGroupExtra { - public WebUIGroupExtra(SVR_AnimeGroup group, SVR_AnimeSeries series, SVR_AniDB_Anime anime, - TagFilter.Filter filter = TagFilter.Filter.None, bool orderByName = false, int tagLimit = 30) - { - ID = group.AnimeGroupID; - Type = Series.GetAniDBSeriesType(anime.AnimeType); - Rating = new Rating { Source = "AniDB", Value = anime.Rating, MaxValue = 1000, Votes = anime.VoteCount }; - if (anime.AirDate != null) - { - var airdate = anime.AirDate.Value; - if (airdate != DateTime.MinValue) - { - AirDate = airdate; - } - } - - if (anime.EndDate != null) - { - var enddate = anime.EndDate.Value; - if (enddate != DateTime.MinValue) - { - EndDate = enddate; - } - } - - Tags = Series.GetTags(anime, filter, excludeDescriptions: true, orderByName) - .Take(tagLimit) - .ToList(); - } - /// <summary> /// Shoko Group ID. /// </summary> @@ -183,46 +154,6 @@ public class WebUISeriesExtra /// The inferred source material for the series. /// </summary> public string SourceMaterial { get; set; } - - public WebUISeriesExtra(HttpContext ctx, SVR_AnimeSeries series) - { - var anime = series.GetAnime(); - var cast = Series.GetCast(anime.AnimeID, new () { Role.CreatorRoleType.Studio, Role.CreatorRoleType.Producer }); - - FirstAirSeason = GetFirstAiringSeasonGroupFilter(ctx, anime); - Studios = cast - .Where(role => role.RoleName == Role.CreatorRoleType.Studio) - .Select(role => role.Staff) - .ToList(); - Producers = cast - .Where(role => role.RoleName == Role.CreatorRoleType.Producer) - .Select(role => role.Staff) - .ToList(); - SourceMaterial = Series.GetTags(anime, TagFilter.Filter.Invert | TagFilter.Filter.Source, excludeDescriptions: true) - .FirstOrDefault()?.Name ?? "Original Work"; - } - - private Filter GetFirstAiringSeasonGroupFilter(HttpContext ctx, SVR_AniDB_Anime anime) - { - var type = (AnimeType)anime.AnimeType; - if (type != AnimeType.TVSeries && type != AnimeType.Web) - return null; - - var (year, season) = anime.GetSeasons() - .FirstOrDefault(); - if (year == 0) - return null; - - var seasonName = $"{season} {year}"; - var seasonsFilterID = RepoFactory.GroupFilter.GetTopLevel() - .FirstOrDefault(f => f.GroupFilterName == "Seasons").GroupFilterID; - var firstAirSeason = RepoFactory.GroupFilter.GetByParentID(seasonsFilterID) - .FirstOrDefault(f => f.GroupFilterName == seasonName); - if (firstAirSeason == null) - return null; - - return new Filter(ctx, firstAirSeason); - } } public class WebUISeriesFileSummary diff --git a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs index 7db5db0a7..3a1dc66bd 100644 --- a/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs +++ b/Shoko.Server/Commands/Actions/CommandRequest_RefreshGroupFilter.cs @@ -24,20 +24,8 @@ public class CommandRequest_RefreshGroupFilter : CommandRequestImplementation protected override void Process() { - if (GroupFilterID == 0) - { - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - return; - } - - var gf = RepoFactory.GroupFilter.GetByID(GroupFilterID); - if (gf == null) - { - return; - } - - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); + // TODO Remove this + RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); } public override void GenerateCommandID() diff --git a/Shoko.Server/Commands/CommandStartup.cs b/Shoko.Server/Commands/CommandStartup.cs index 093f3c68e..5d4cf1833 100644 --- a/Shoko.Server/Commands/CommandStartup.cs +++ b/Shoko.Server/Commands/CommandStartup.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using Shoko.Server.Databases; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; namespace Shoko.Server.Commands; diff --git a/Shoko.Server/Databases/BaseDatabase.cs b/Shoko.Server/Databases/BaseDatabase.cs index 7abb991d8..f45d63a3f 100644 --- a/Shoko.Server/Databases/BaseDatabase.cs +++ b/Shoko.Server/Databases/BaseDatabase.cs @@ -280,167 +280,12 @@ public void PopulateInitialData() public void CreateOrVerifyLockedFilters() { - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); + RepoFactory.FilterPreset.CreateOrVerifyLockedFilters(); } private void CreateInitialGroupFilters() { - // group filters - // Do to DatabaseFixes, some filters may be made, namely directory filters - // All, Continue Watching, Years, Seasons, Tags... 6 seems to be enough to tell for now - // We can't just check the existence of anything specific, as the user can delete most of these - if (RepoFactory.GroupFilter.GetTopLevel().Count() > 6) - { - return; - } - - // Favorites - var gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Favorites, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Favourite, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Missing Episodes - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_MissingEpisodes, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - - // Newly Added Series - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Added, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.SeriesCreatedDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "10" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Newly Airing Series - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Airing, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.AirDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "30" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Votes Needed - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Votes, - ApplyToSeries = 1, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.CompletedSeries, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.UserVotedAny, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // Recently Watched - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_RecentlyWatched, - ApplyToSeries = 0, - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.EpisodeWatchedDate, - ConditionOperator = (int)GroupFilterOperator.LastXDays, - ConditionParameter = "10" - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - - // TvDB/MovieDB Link Missing - gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_LinkMissing, - ApplyToSeries = 1, // This makes far more sense as applied to series - BaseCondition = 1, - Locked = 0, - FilterType = (int)GroupFilterType.UserDefined - }; - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo, - ConditionOperator = (int)GroupFilterOperator.Exclude, - ConditionParameter = string.Empty - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); + RepoFactory.FilterPreset.CreateInitialFilters(); } private void CreateInitialUsers() @@ -465,7 +310,7 @@ private void CreateInitialUsers() Password = defaultPassword, Username = settings.Database.DefaultUserUsername }; - RepoFactory.JMMUser.Save(defaultUser, true); + RepoFactory.JMMUser.Save(defaultUser); var familyUser = new SVR_JMMUser { @@ -477,7 +322,7 @@ private void CreateInitialUsers() Password = string.Empty, Username = "Family Friendly" }; - RepoFactory.JMMUser.Save(familyUser, true); + RepoFactory.JMMUser.Save(familyUser); } private void CreateInitialRenameScript() diff --git a/Shoko.Server/Databases/DatabaseFixes.cs b/Shoko.Server/Databases/DatabaseFixes.cs index 2cec4674c..eb50ce61c 100644 --- a/Shoko.Server/Databases/DatabaseFixes.cs +++ b/Shoko.Server/Databases/DatabaseFixes.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using NHibernate; using NLog; using Shoko.Commons.Properties; @@ -12,6 +13,7 @@ using Shoko.Server.Commands; using Shoko.Server.Commands.AniDB; using Shoko.Server.Extensions; +using Shoko.Server.Filters.Legacy; using Shoko.Server.ImageDownload; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB; @@ -27,6 +29,85 @@ public class DatabaseFixes { private static Logger logger = LogManager.GetCurrentClassLogger(); + public static void MigrateGroupFilterToFilterPreset() + { + var legacyConverter = Utils.ServiceContainer.GetRequiredService<LegacyFilterConverter>(); + using var session = DatabaseFactory.SessionFactory.OpenSession(); + var groupFilters = session.CreateSQLQuery( + "SELECT GroupFilterID, " + + "ParentGroupFilterID, " + + "GroupFilterName, " + + "ApplyToSeries, " + + "BaseCondition, " + + "Locked, " + + "FilterType, " + + "InvisibleInClients, " + + "GroupConditions, " + + "SortingCriteria " + + "FROM GroupFilter") + .AddScalar("GroupFilterID", NHibernateUtil.Int32) + .AddScalar("ParentGroupFilterID", NHibernateUtil.Int32) + .AddScalar("GroupFilterName", NHibernateUtil.String) + .AddScalar("ApplyToSeries", NHibernateUtil.Int32) + .AddScalar("BaseCondition", NHibernateUtil.Int32) + .AddScalar("Locked", NHibernateUtil.Int32) + .AddScalar("FilterType", NHibernateUtil.Int32) + .AddScalar("InvisibleInClients", NHibernateUtil.Int32) + .AddScalar("GroupConditions", NHibernateUtil.StringClob) + .AddScalar("SortingCriteria", NHibernateUtil.String) + .List(); + + var filters = new Dictionary<GroupFilter, List<GroupFilterCondition>>(); + foreach (var item in groupFilters) + { + var fields = (object[])item; + var filter = new GroupFilter + { + GroupFilterID = (int)fields[0], + ParentGroupFilterID = (int?)fields[1], + GroupFilterName = (string)fields[2], + ApplyToSeries = (int)fields[3], + BaseCondition = (int)fields[4], + Locked = (int)fields[5], + FilterType = (int)fields[6], + InvisibleInClients = (int)fields[7] + }; + var conditions = JsonConvert.DeserializeObject<List<GroupFilterCondition>>((string)fields[8]); + filters[filter] = conditions; + } + + var idMappings = new Dictionary<int, int>(); + // first, do the ones with no parent + foreach (var key in filters.Keys.Where(a => a.ParentGroupFilterID == null).OrderBy(a => a.GroupFilterID)) + { + var filter = legacyConverter.FromLegacy(key, filters[key]); + RepoFactory.FilterPreset.Save(filter); + idMappings[key.GroupFilterID] = filter.FilterPresetID; + } + + var filtersToProcess = filters.Keys.Where(a => !idMappings.ContainsKey(a.GroupFilterID) && idMappings.ContainsKey(a.ParentGroupFilterID.Value)) + .ToList(); + while (filtersToProcess.Count > 0) + { + foreach (var key in filtersToProcess) + { + var filter = legacyConverter.FromLegacy(key, filters[key]); + filter.ParentFilterPresetID = idMappings[key.ParentGroupFilterID.Value]; + RepoFactory.FilterPreset.Save(filter); + idMappings[key.GroupFilterID] = filter.FilterPresetID; + } + + filtersToProcess = filters.Keys.Where(a => !idMappings.ContainsKey(a.GroupFilterID) && idMappings.ContainsKey(a.ParentGroupFilterID.Value)) + .ToList(); + } + } + + public static void DropGroupFilter() + { + using var session = DatabaseFactory.SessionFactory.OpenSession(); + session.CreateSQLQuery("DROP TABLE GroupFilter; DROP TABLE GroupFilterCondition").ExecuteUpdate(); + } + public static void MigrateAniDBToNet() { var settings = Utils.SettingsProvider.GetSettings(); @@ -121,26 +202,7 @@ public static void RemoveOldMovieDBImageRecords() } - public static void FixContinueWatchingGroupFilter_20160406() - { - // group filters - - // check if it already exists - var lockedGFs = RepoFactory.GroupFilter.GetLockedGroupFilters(); - - if (lockedGFs != null) - { - foreach (var gf in lockedGFs) - { - if (gf.GroupFilterName.Equals(Constants.GroupFilterName.ContinueWatching, - StringComparison.InvariantCultureIgnoreCase)) - { - gf.FilterType = (int)GroupFilterType.ContinueWatching; - RepoFactory.GroupFilter.Save(gf); - } - } - } - } + public static void FixContinueWatchingGroupFilter_20160406() { } public static void MigrateTraktLinks_V1_to_V2() { @@ -523,59 +585,9 @@ public static void MigrateAniDB_FileUpdates() RepoFactory.AniDB_FileUpdate.Save(tosave); } - public static void FixDuplicateTagFiltersAndUpdateSeasons() - { - var filters = RepoFactory.GroupFilter.GetAll(); - var seasons = filters.Where(a => a.FilterType == (int)GroupFilterType.Season).ToList(); - var tags = filters.Where(a => a.FilterType == (int)GroupFilterType.Tag).ToList(); - - var tagsGrouping = tags.GroupBy(a => a.GroupFilterName).SelectMany(a => a.Skip(1)).ToList(); - - tagsGrouping.ForEach(RepoFactory.GroupFilter.Delete); - - tags = filters.Where(a => a.FilterType == (int)GroupFilterType.Tag).ToList(); - - foreach (var filter in tags.Where(a => a.GroupConditions.Contains("`"))) - { - filter.GroupConditions = filter.GroupConditions.Replace("`", "'"); - RepoFactory.GroupFilter.Save(filter); - } - - foreach (var seasonFilter in seasons) - { - seasonFilter.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(seasonFilter); - } - } - - public static void RecalculateYears() - { - try - { - var filters = RepoFactory.GroupFilter.GetAll(); - if (filters.Count == 0) - { - return; - } - - foreach (var gf in filters) - { - if (gf.FilterType != (int)GroupFilterType.Year) - { - continue; - } + public static void FixDuplicateTagFiltersAndUpdateSeasons() { } - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - } - catch (Exception e) - { - logger.Error(e); - } - } + public static void RecalculateYears() { } public static void PopulateResourceLinks() { @@ -598,107 +610,11 @@ public static void PopulateTagWeight() } } - public static void FixTagsWithInclude() - { - try - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - if (gf.FilterType != (int)GroupFilterType.Tag) - { - continue; - } - - foreach (var gfc in gf.Conditions) - { - if (gfc.ConditionType != (int)GroupFilterConditionType.Tag) - { - continue; - } - - if (gfc.ConditionOperator == (int)GroupFilterOperator.Include) - { - gfc.ConditionOperator = (int)GroupFilterOperator.In; - RepoFactory.GroupFilterCondition.Save(gfc); - continue; - } + public static void FixTagsWithInclude() { } - if (gfc.ConditionOperator == (int)GroupFilterOperator.Exclude) - { - gfc.ConditionOperator = (int)GroupFilterOperator.NotIn; - RepoFactory.GroupFilterCondition.Save(gfc); - } - } - - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - } - catch (Exception e) - { - logger.Error(e); - } - } - - public static void MakeTagsApplyToSeries() - { - try - { - var filters = RepoFactory.GroupFilter.GetAll(); - if (filters.Count == 0) - { - return; - } + public static void MakeTagsApplyToSeries() { } - foreach (var gf in filters) - { - if (gf.FilterType != (int)GroupFilterType.Tag) - { - continue; - } - - gf.ApplyToSeries = 1; - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - } - catch (Exception e) - { - logger.Error(e); - } - } - - public static void MakeYearsApplyToSeries() - { - try - { - var filters = RepoFactory.GroupFilter.GetAll(); - if (filters.Count == 0) - { - return; - } - - foreach (var gf in filters) - { - if (gf.FilterType != (int)GroupFilterType.Year) - { - continue; - } - - gf.ApplyToSeries = 1; - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - - RepoFactory.GroupFilter.CreateOrVerifyLockedFilters(); - } - catch (Exception e) - { - logger.Error(e); - } - } + public static void MakeYearsApplyToSeries() { } public static void UpdateAllTvDBSeries() { diff --git a/Shoko.Server/Databases/MySQL.cs b/Shoko.Server/Databases/MySQL.cs index 388adc848..24054f5a2 100644 --- a/Shoko.Server/Databases/MySQL.cs +++ b/Shoko.Server/Databases/MySQL.cs @@ -8,6 +8,7 @@ using NHibernate; using NHibernate.Driver.MySqlConnector; using Shoko.Commons.Properties; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Repositories; using Shoko.Server.Server; using Shoko.Server.Utilities; @@ -20,7 +21,7 @@ namespace Shoko.Server.Databases; public class MySQL : BaseDatabase<MySqlConnection> { public override string Name { get; } = "MySQL"; - public override int RequiredVersion { get; } = 118; + public override int RequiredVersion { get; } = 119; private List<DatabaseCommand> createVersionTable = new() @@ -737,6 +738,13 @@ public class MySQL : BaseDatabase<MySqlConnection> new(117, 2, "ALTER TABLE VideoLocal ADD LastAVDumpVersion nvarchar(128);"), new(118, 1, DatabaseFixes.FixAnimeSourceLinks), new(118, 2, DatabaseFixes.FixOrphanedShokoEpisodes), + new DatabaseCommand(119, 1, + "CREATE TABLE FilterPreset( FilterPresetID INT NOT NULL AUTO_INCREMENT, ParentFilterPresetID int, Name text NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression longtext, SortingExpression longtext, PRIMARY KEY (`FilterID`) ); "), + new DatabaseCommand(119, 2, + "ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_ParentFilterPresetID (ParentFilterPresetID); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_Name (Name); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_FilterType (FilterType); ALTER TABLE FilterPreset ADD INDEX IX_FilterPreset_LockedHidden (Locked, Hidden);"), + new DatabaseCommand(119, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), + new DatabaseCommand(119, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), + new DatabaseCommand(119, 5, DatabaseFixes.DropGroupFilter), }; private DatabaseCommand linuxTableVersionsFix = new("RENAME TABLE versions TO Versions;"); diff --git a/Shoko.Server/Databases/NHIbernate/FilterExpressionConverter.cs b/Shoko.Server/Databases/NHIbernate/FilterExpressionConverter.cs new file mode 100644 index 000000000..f55c027c4 --- /dev/null +++ b/Shoko.Server/Databases/NHIbernate/FilterExpressionConverter.cs @@ -0,0 +1,217 @@ +using System; +using System.ComponentModel; +using System.Data; +using System.Data.Common; +using Newtonsoft.Json; +using NHibernate; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; +using NLog; +using Shoko.Server.Filters; + +namespace Shoko.Server.Databases.NHIbernate; + +public class FilterExpressionConverter : TypeConverter, IUserType +{ + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return typeof(FilterExpression<bool>).IsAssignableFrom(sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value) + { + var s = value as string ?? throw new ArgumentException("Can only convert from string"); + return JsonConvert.DeserializeObject(s, new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore, + TypeNameHandling = TypeNameHandling.Objects, + Error = (_, args) => + { + LogManager.GetCurrentClassLogger().Error(args.ErrorContext.Error); + args.ErrorContext.Handled = true; + } + //Converters = new List<JsonConverter> { new FilterExpressionJsonConverter() }, + }); + } + + /// <summary> + /// Converts the given value object to the specified type + /// </summary> + /// <param name="context">Ignored</param> + /// <param name="culture">Ignored</param> + /// <param name="value">The <see cref="T:System.Object"/> to convert.</param> + /// <param name="destinationType">The <see cref="T:System.Type"/> to convert the <paramref name="value"/> parameter to.</param> + /// <returns> + /// An <see cref="T:System.Object"/> that represents the converted value. The value will be 1 if <paramref name="value"/> is true, otherwise 0 + /// </returns> + /// <exception cref="T:System.ArgumentNullException">The <paramref name="destinationType"/> parameter is <see langword="null"/>.</exception> + /// <exception cref="T:System.NotSupportedException">The conversion could not be performed.</exception> + public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value, Type destinationType) + { + if (value == null) return null; + return JsonConvert.SerializeObject(value, new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore, + TypeNameHandling = TypeNameHandling.Objects + }); + } + + + /// <summary> + /// Creates an instance of the Type that this <see cref="T:System.ComponentModel.TypeConverter"/> is associated with (bool) + /// </summary> + /// <param name="context">ignored.</param> + /// <param name="propertyValues">ignored.</param> + /// <returns> + /// An <see cref="T:System.Object"/> of type bool. It always returns 'true' for this converter. + /// </returns> + public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) + { + return true; + } + + #region IUserType Members + + /// <summary> + /// Reconstruct an object from the cacheable representation. At the very least this + /// method should perform a deep copy if the type is mutable. (optional operation) + /// </summary> + /// <param name="cached">the object to be cached</param> + /// <param name="owner">the owner of the cached object</param> + /// <returns> + /// a reconstructed object from the cacheable representation + /// </returns> + public object Assemble(object cached, object owner) + { + return DeepCopy(cached); + } + + /// <summary> + /// Return a deep copy of the persistent state, stopping at entities and at collections. + /// </summary> + /// <param name="value">generally a collection element or entity field</param> + /// <returns>a copy</returns> + public object DeepCopy(object value) + { + return value; + } + + /// <summary> + /// Transform the object into its cacheable representation. At the very least this + /// method should perform a deep copy if the type is mutable. That may not be enough + /// for some implementations, however; for example, associations must be cached as + /// identifier values. (optional operation) + /// </summary> + /// <param name="value">the object to be cached</param> + /// <returns>a cacheable representation of the object</returns> + public object Disassemble(object value) + { + return DeepCopy(value); + } + + /// <summary> + /// Returns a hash code for this instance. + /// </summary> + /// <param name="x">The x.</param> + /// <returns> + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// </returns> + public int GetHashCode(object x) + { + return x == null ? base.GetHashCode() : x.GetHashCode(); + } + + /// <summary> + /// Are objects of this type mutable? + /// </summary> + /// <value></value> + public bool IsMutable => true; + + /// <summary> + /// Retrieve an instance of the mapped class from a JDBC resultset. + /// Implementors should handle possibility of null values. + /// </summary> + /// <param name="rs">a IDataReader</param> + /// <param name="names">column names</param> + /// <param name="impl"></param> + /// <param name="owner">the containing entity</param> + /// <returns></returns> + /// <exception cref="T:NHibernate.HibernateException">HibernateException</exception> + public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor impl, object owner) + { + var rawValue = NHibernateUtil.String.NullSafeGet(rs, names[0], impl); + return rawValue == null ? null : ConvertFrom(null!, null!, rawValue); + } + + /// <summary> + /// Write an instance of the mapped class to a prepared statement. + /// Implementors should handle possibility of null values. + /// A multi-column type should be written to parameters starting from index. + /// </summary> + /// <param name="cmd">a IDbCommand</param> + /// <param name="value">the object to write</param> + /// <param name="index">command parameter index</param> + /// <param name="session"></param> + /// <exception cref="T:NHibernate.HibernateException">HibernateException</exception> + public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session) + { + ((IDataParameter)cmd.Parameters[index]).Value = + value == null ? DBNull.Value : ConvertTo(null, null, value, typeof(string)); + } + + /// <summary> + /// During merge, replace the existing (<paramref name="target"/>) value in the entity + /// we are merging to with a new (<paramref name="original"/>) value from the detached + /// entity we are merging. For immutable objects, or null values, it is safe to simply + /// return the first parameter. For mutable objects, it is safe to return a copy of the + /// first parameter. For objects with component values, it might make sense to + /// recursively replace component values. + /// </summary> + /// <param name="original">the value from the detached entity being merged</param> + /// <param name="target">the value in the managed entity</param> + /// <param name="owner">the managed entity</param> + /// <returns>the value to be merged</returns> + public object Replace(object original, object target, object owner) + { + return original; + } + + /// <summary> + /// The type returned by <c>NullSafeGet()</c> + /// </summary> + public Type ReturnedType => typeof(string); + + /// <summary> + /// The SQL types for the columns mapped by this type. + /// </summary> + /// <value></value> + public SqlType[] SqlTypes => new[] { NHibernateUtil.String.SqlType }; + + /// <summary> + /// Determines whether the specified <see cref="System.Object"/> is equal to this instance. + /// </summary> + /// <param name="x">The <see cref="System.Object"/> to compare with this instance.</param> + /// <param name="y">The y.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + bool IUserType.Equals(object x, object y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + return x != null && y != null && x.Equals(y); + } + + #endregion +} diff --git a/Shoko.Server/Databases/NHibernateDependencyInjector.cs b/Shoko.Server/Databases/NHIbernate/NHibernateDependencyInjector.cs similarity index 98% rename from Shoko.Server/Databases/NHibernateDependencyInjector.cs rename to Shoko.Server/Databases/NHIbernate/NHibernateDependencyInjector.cs index f943cc235..b31accfe3 100644 --- a/Shoko.Server/Databases/NHibernateDependencyInjector.cs +++ b/Shoko.Server/Databases/NHIbernate/NHibernateDependencyInjector.cs @@ -5,7 +5,7 @@ using NHibernate; using NHibernate.Type; -namespace Shoko.Server.Databases; +namespace Shoko.Server.Databases.NHIbernate; public class NHibernateDependencyInjector : EmptyInterceptor { diff --git a/Shoko.Server/Databases/TypeConverters/TitleLanguageConverter.cs b/Shoko.Server/Databases/NHIbernate/TitleLanguageConverter.cs similarity index 99% rename from Shoko.Server/Databases/TypeConverters/TitleLanguageConverter.cs rename to Shoko.Server/Databases/NHIbernate/TitleLanguageConverter.cs index f439e7abd..30ae41ef1 100644 --- a/Shoko.Server/Databases/TypeConverters/TitleLanguageConverter.cs +++ b/Shoko.Server/Databases/NHIbernate/TitleLanguageConverter.cs @@ -1,15 +1,15 @@ using System; using System.ComponentModel; -using NHibernate.SqlTypes; -using NHibernate.UserTypes; using System.Data; using System.Data.Common; using NHibernate; using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; -namespace Shoko.Server.Databases.TypeConverters; +namespace Shoko.Server.Databases.NHIbernate; public class TitleLanguageConverter : TypeConverter, IUserType { diff --git a/Shoko.Server/Databases/TypeConverters/TitleTypeConverter.cs b/Shoko.Server/Databases/NHIbernate/TitleTypeConverter.cs similarity index 99% rename from Shoko.Server/Databases/TypeConverters/TitleTypeConverter.cs rename to Shoko.Server/Databases/NHIbernate/TitleTypeConverter.cs index 6e389e249..3a04954e0 100644 --- a/Shoko.Server/Databases/TypeConverters/TitleTypeConverter.cs +++ b/Shoko.Server/Databases/NHIbernate/TitleTypeConverter.cs @@ -1,15 +1,15 @@ using System; using System.ComponentModel; -using NHibernate.SqlTypes; -using NHibernate.UserTypes; using System.Data; using System.Data.Common; using NHibernate; using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Plugin.Abstractions.Extensions; -namespace Shoko.Server.Databases.TypeConverters; +namespace Shoko.Server.Databases.NHIbernate; public class TitleTypeConverter : TypeConverter, IUserType { diff --git a/Shoko.Server/Databases/SQLServer.cs b/Shoko.Server/Databases/SQLServer.cs index 0cd73c560..4d1bde9d7 100644 --- a/Shoko.Server/Databases/SQLServer.cs +++ b/Shoko.Server/Databases/SQLServer.cs @@ -10,6 +10,7 @@ using NHibernate.AdoNet; using Shoko.Commons.Extensions; using Shoko.Commons.Properties; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Repositories; using Shoko.Server.Server; using Shoko.Server.Settings; @@ -22,7 +23,7 @@ namespace Shoko.Server.Databases; public class SQLServer : BaseDatabase<SqlConnection> { public override string Name { get; } = "SQLServer"; - public override int RequiredVersion { get; } = 111; + public override int RequiredVersion { get; } = 112; public override void BackupDatabase(string fullfilename) { @@ -680,6 +681,13 @@ public override bool HasVersionsTable() new DatabaseCommand(110, 2, "ALTER TABLE VideoLocal ADD LastAVDumpVersion nvarchar(128);"), new DatabaseCommand(111, 1, DatabaseFixes.FixAnimeSourceLinks), new DatabaseCommand(111, 2, DatabaseFixes.FixOrphanedShokoEpisodes), + new DatabaseCommand(112, 1, + "CREATE TABLE FilterPreset( FilterPresetID INT IDENTITY(1,1), ParentFilterPresetID int, Name nvarchar(250) NOT NULL, FilterType int NOT NULL, Locked bit NOT NULL, Hidden bit NOT NULL, ApplyAtSeriesLevel bit NOT NULL, Expression nvarchar(max), SortingExpression nvarchar(max) ); "), + new DatabaseCommand(112, 2, + "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), + new DatabaseCommand(112, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), + new DatabaseCommand(112, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), + new DatabaseCommand(112, 5, DatabaseFixes.DropGroupFilter), }; private static Tuple<bool, string> DropDefaultsOnAnimeEpisode_User(object connection) diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index f4d213414..c82f63a49 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -8,10 +8,10 @@ using FluentNHibernate.Cfg.Db; using NHibernate; using Shoko.Commons.Properties; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Databases.SqliteFixes; using Shoko.Server.Repositories; using Shoko.Server.Server; -using Shoko.Server.Settings; using Shoko.Server.Utilities; // ReSharper disable InconsistentNaming @@ -22,7 +22,7 @@ public class SQLite : BaseDatabase<SqliteConnection> { public override string Name { get; } = "SQLite"; - public override int RequiredVersion { get; } = 104; + public override int RequiredVersion { get; } = 105; public override void BackupDatabase(string fullfilename) @@ -672,6 +672,13 @@ public override void CreateDatabase() new(103, 2, "ALTER TABLE VideoLocal ADD LastAVDumpVersion text;"), new(104, 1, DatabaseFixes.FixAnimeSourceLinks), new(104, 2, DatabaseFixes.FixOrphanedShokoEpisodes), + new DatabaseCommand(105, 1, + "CREATE TABLE FilterPreset( FilterPresetID INTEGER PRIMARY KEY AUTOINCREMENT, ParentFilterPresetID int, Name text NOT NULL, FilterType int NOT NULL, Locked int NOT NULL, Hidden int NOT NULL, ApplyAtSeriesLevel int NOT NULL, Expression text, SortingExpression text ); "), + new DatabaseCommand(105, 2, + "CREATE INDEX IX_FilterPreset_ParentFilterPresetID ON FilterPreset(ParentFilterPresetID); CREATE INDEX IX_FilterPreset_Name ON FilterPreset(Name); CREATE INDEX IX_FilterPreset_FilterType ON FilterPreset(FilterType); CREATE INDEX IX_FilterPreset_LockedHidden ON FilterPreset(Locked, Hidden);"), + new DatabaseCommand(105, 3, "DELETE FROM GroupFilter WHERE FilterType = 2"), + new DatabaseCommand(105, 4, DatabaseFixes.MigrateGroupFilterToFilterPreset), + new DatabaseCommand(105, 5, DatabaseFixes.DropGroupFilter), }; private static Tuple<bool, string> DropLanguage(object connection) diff --git a/Shoko.Server/Extensions/StringExtensions.cs b/Shoko.Server/Extensions/StringExtensions.cs index 97d7c7dba..0caef6c3d 100644 --- a/Shoko.Server/Extensions/StringExtensions.cs +++ b/Shoko.Server/Extensions/StringExtensions.cs @@ -168,4 +168,12 @@ public static string SplitCamelCaseToWords(this string strInput) return strOutput.ToString(); } + + public static string GetSortName(this string name) + { + if (name.StartsWith("A ")) name = name[2..]; + if (name.StartsWith("An ")) name = name[3..]; + if (name.StartsWith("The ")) name = name[4..]; + return name; + } } diff --git a/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs new file mode 100644 index 000000000..d4fdbba87 --- /dev/null +++ b/Shoko.Server/Filters/Files/HasAudioLanguageExpression.cs @@ -0,0 +1,65 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Files; + +public class HasAudioLanguageExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasAudioLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasAudioLanguageExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.AudioLanguages.Contains(Parameter); + } + + protected bool Equals(HasAudioLanguageExpression other) + { + return base.Equals(other) && string.Equals(Parameter, other.Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasAudioLanguageExpression)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(Parameter, StringComparer.InvariantCultureIgnoreCase); + return hashCode.ToHashCode(); + } + + public static bool operator ==(HasAudioLanguageExpression left, HasAudioLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasAudioLanguageExpression left, HasAudioLanguageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs new file mode 100644 index 000000000..d20510e8d --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSharedAudioLanguageExpression.cs @@ -0,0 +1,65 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Files; + +public class HasSharedAudioLanguageExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasSharedAudioLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasSharedAudioLanguageExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.SharedAudioLanguages.Contains(Parameter); + } + + protected bool Equals(HasSharedAudioLanguageExpression other) + { + return base.Equals(other) && string.Equals(Parameter, other.Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSharedAudioLanguageExpression)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(Parameter, StringComparer.InvariantCultureIgnoreCase); + return hashCode.ToHashCode(); + } + + public static bool operator ==(HasSharedAudioLanguageExpression left, HasSharedAudioLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSharedAudioLanguageExpression left, HasSharedAudioLanguageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs new file mode 100644 index 000000000..a117d77d2 --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSharedSubtitleLanguageExpression.cs @@ -0,0 +1,65 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Files; + +public class HasSharedSubtitleLanguageExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasSharedSubtitleLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasSharedSubtitleLanguageExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.SharedSubtitleLanguages.Contains(Parameter); + } + + protected bool Equals(HasSharedSubtitleLanguageExpression other) + { + return base.Equals(other) && string.Equals(Parameter, other.Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSharedSubtitleLanguageExpression)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(Parameter, StringComparer.InvariantCultureIgnoreCase); + return hashCode.ToHashCode(); + } + + public static bool operator ==(HasSharedSubtitleLanguageExpression left, HasSharedSubtitleLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSharedSubtitleLanguageExpression left, HasSharedSubtitleLanguageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs new file mode 100644 index 000000000..6e1f67ea2 --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSharedVideoSourceExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Files; + +public class HasSharedVideoSourceExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasSharedVideoSourceExpression(string parameter) + { + Parameter = parameter; + } + public HasSharedVideoSourceExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.SharedVideoSources.Contains(Parameter); + } + + protected bool Equals(HasSharedVideoSourceExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSharedVideoSourceExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasSharedVideoSourceExpression left, HasSharedVideoSourceExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSharedVideoSourceExpression left, HasSharedVideoSourceExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs new file mode 100644 index 000000000..25ada250f --- /dev/null +++ b/Shoko.Server/Filters/Files/HasSubtitleLanguageExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Files; + +public class HasSubtitleLanguageExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasSubtitleLanguageExpression(string parameter) + { + Parameter = parameter; + } + public HasSubtitleLanguageExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.SubtitleLanguages.Contains(Parameter); + } + + protected bool Equals(HasSubtitleLanguageExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasSubtitleLanguageExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasSubtitleLanguageExpression left, HasSubtitleLanguageExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasSubtitleLanguageExpression left, HasSubtitleLanguageExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs new file mode 100644 index 000000000..73ad0ba5c --- /dev/null +++ b/Shoko.Server/Filters/Files/HasVideoSourceExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Files; + +public class HasVideoSourceExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasVideoSourceExpression(string parameter) + { + Parameter = parameter; + } + public HasVideoSourceExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.VideoSources.Contains(Parameter); + } + + protected bool Equals(HasVideoSourceExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasVideoSourceExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasVideoSourceExpression left, HasVideoSourceExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasVideoSourceExpression left, HasVideoSourceExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs new file mode 100644 index 000000000..8f8102afa --- /dev/null +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Filters.SortingSelectors; +using Shoko.Server.Models; +using Shoko.Server.Repositories; +using Shoko.Server.Repositories.Cached; + +namespace Shoko.Server.Filters; + +public class FilterEvaluator +{ + private readonly AnimeGroupRepository _groups; + + private readonly AnimeSeriesRepository _series; + + public FilterEvaluator() + { + _series = RepoFactory.AnimeSeries; + _groups = RepoFactory.AnimeGroup; + } + + public FilterEvaluator(AnimeGroupRepository groups, AnimeSeriesRepository series) + { + _groups = groups; + _series = series; + } + + /// <summary> + /// Evaluate the given filter, applying the necessary logic + /// </summary> + /// <param name="filter"></param> + /// <param name="userID"></param> + /// <returns>SeriesIDs, grouped by the direct parent GroupID</returns> + /// <exception cref="ArgumentNullException"></exception> + public IEnumerable<IGrouping<int, int>> EvaluateFilter(FilterPreset filter, int? userID) + { + ArgumentNullException.ThrowIfNull(filter); + var needsUser = filter.Expression?.UserDependent ?? false; + if (needsUser && userID == null) + { + throw new ArgumentNullException(nameof(userID)); + } + + var user = userID != null ? RepoFactory.JMMUser.GetByID(userID.Value) : null; + + var filterables = filter.ApplyAtSeriesLevel switch + { + true when needsUser => _series?.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true).Select(a => + new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? + Array.Empty<FilterableWithID>().AsParallel(), + true => _series?.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true) + .Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel(), + false when needsUser => _groups?.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true) + .Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))) ?? + Array.Empty<FilterableWithID>().AsParallel(), + false => _groups?.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true) + .Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) ?? Array.Empty<FilterableWithID>().AsParallel() + }; + + // Filtering + var filtered = filterables.Where(a => filter.Expression?.Evaluate(a.Filterable) ?? true); + + // ordering + var ordered = OrderFilterables(filter, filtered); + + var result = ordered.GroupBy(a => a.GroupID, a => a.SeriesID); + if (!filter.ApplyAtSeriesLevel) + { + result = result.Select(a => new Grouping(a.Key, _series.GetByGroupID(a.Key).Select(ser => ser.AnimeSeriesID).ToArray())); + } + + return result; + } + + /// <summary> + /// Evaluate the given filter, applying the necessary logic + /// </summary> + /// <param name="filters"></param> + /// <param name="userID"></param> + /// <param name="skipSorting"></param> + /// <returns>SeriesIDs, grouped by the direct parent GroupID</returns> + /// <exception cref="ArgumentNullException"></exception> + public Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>> BatchEvaluateFilters(List<FilterPreset> filters, int? userID, bool skipSorting=false) + { + ArgumentNullException.ThrowIfNull(filters); + if (!filters.Any()) return new Dictionary<FilterPreset, IEnumerable<IGrouping<int, int>>>(); + // count it as a user filter if it needs to sort using a user-dependent expression + var needsUser = filters.Any(a => (a?.Expression?.UserDependent ?? false) || skipSorting && (a?.SortingExpression?.UserDependent ?? false)); + if (needsUser && userID == null) throw new ArgumentNullException(nameof(userID)); + + var user = userID != null ? RepoFactory.JMMUser.GetByID(userID.Value) : null; + var filterables = filters.Any(a => a.ApplyAtSeriesLevel) switch + { + true when needsUser => _series.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true).Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + true => _series.GetAll().AsParallel().Where(a => user?.AllowedSeries(a) ?? true).Select(a => new FilterableWithID(a.AnimeSeriesID, a.AnimeGroupID, a.ToFilterable())), + false when needsUser => _groups.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true).Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToUserDependentFilterable(userID.Value))), + false => _groups.GetAll().AsParallel().Where(a => user?.AllowedGroup(a) ?? true).Select(a => new FilterableWithID(0, a.AnimeGroupID, a.ToFilterable())) + }; + + // Filtering + var filtered = filterables.SelectMany(a => filters.Select(f => (Filter: f, FilterableWithID: a))) + .Where(a => (a.Filter.FilterType & GroupFilterType.Directory) == 0 && (a.Filter.Expression?.Evaluate(a.FilterableWithID.Filterable) ?? true)); + + // ordering + var grouped = filtered.GroupBy(a => a.Filter).ToDictionary(a => a.Key, f => + { + var ordered = skipSorting ? f.Select(a => a.FilterableWithID) : OrderFilterables(f.Key, f.Select(a => a.FilterableWithID)); + + var result = ordered.GroupBy(a => a.GroupID, a => a.SeriesID); + if (!f.Key.ApplyAtSeriesLevel) + result = result.Select(a => new Grouping(a.Key, _series.GetByGroupID(a.Key).Select(ser => ser.AnimeSeriesID).ToArray())); + + return result; + }); + + foreach (var filter in filters.Where(filter => !grouped.ContainsKey(filter))) + grouped.Add(filter, Array.Empty<IGrouping<int, int>>()); + + return grouped; + } + + private static IOrderedEnumerable<FilterableWithID> OrderFilterables(FilterPreset filter, IEnumerable<FilterableWithID> filtered) + { + var nameSorter = new NameSortingSelector(); + var ordered = filter.SortingExpression == null ? filtered.OrderBy(a => nameSorter.Evaluate(a.Filterable)) : + !filter.SortingExpression.Descending ? filtered.OrderBy(a => filter.SortingExpression.Evaluate(a.Filterable)) : + filtered.OrderByDescending(a => filter.SortingExpression.Evaluate(a.Filterable)); + + var next = filter.SortingExpression?.Next; + while (next != null) + { + var expr = next; + ordered = !next.Descending ? ordered.ThenBy(a => expr.Evaluate(a.Filterable)) : ordered.ThenByDescending(a => expr.Evaluate(a.Filterable)); + next = next.Next; + } + + return ordered; + } + + private record FilterableWithID(int SeriesID, int GroupID, IFilterable Filterable); + private record UserFilterableWithID(int UserID, int SeriesID, int GroupID, IFilterable Filterable) : FilterableWithID(SeriesID, GroupID, Filterable); + + private record Grouping(int GroupID, int[] SeriesIDs) : IGrouping<int, int> + { + public IEnumerator<int> GetEnumerator() + { + return ((IEnumerable<int>)SeriesIDs).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int Key => GroupID; + } +} diff --git a/Shoko.Server/Filters/FilterExpression.cs b/Shoko.Server/Filters/FilterExpression.cs new file mode 100644 index 000000000..6453ccb89 --- /dev/null +++ b/Shoko.Server/Filters/FilterExpression.cs @@ -0,0 +1,61 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public class FilterExpression : IFilterExpression +{ + [IgnoreDataMember][JsonIgnore] public virtual bool TimeDependent => false; + [IgnoreDataMember][JsonIgnore] public virtual bool UserDependent => false; + + protected virtual bool Equals(FilterExpression other) + { + return true; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((FilterExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(FilterExpression left, FilterExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(FilterExpression left, FilterExpression right) + { + return !Equals(left, right); + } + + public virtual bool IsType(FilterExpression expression) + { + return expression.GetType() == GetType(); + } +} + +public abstract class FilterExpression<T> : FilterExpression, IFilterExpression<T> +{ + public abstract T Evaluate(IFilterable f); +} diff --git a/Shoko.Server/Filters/FilterExtensions.cs b/Shoko.Server/Filters/FilterExtensions.cs new file mode 100644 index 000000000..2fb756452 --- /dev/null +++ b/Shoko.Server/Filters/FilterExtensions.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Shoko.Commons.Extensions; +using Shoko.Models.Enums; +using Shoko.Server.Extensions; +using Shoko.Server.Models; +using Shoko.Server.Providers.AniDB; +using Shoko.Server.Repositories; +using AnimeType = Shoko.Models.Enums.AnimeType; + +namespace Shoko.Server.Filters; + +public static class FilterExtensions +{ + public static bool IsDirectory(this FilterPreset filter) => (filter.FilterType & GroupFilterType.Directory) != 0; + + public static Filterable ToFilterable(this SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + + var filterable = new Filterable + { + NameDelegate = series.GetSeriesName, + SortingNameDelegate = () => series.GetSeriesName().GetSortName(), + SeriesCountDelegate = () => 1, + AirDateDelegate = () => anime?.AirDate, + MissingEpisodesDelegate = () => series.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => series.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => anime?.GetAllTags() ?? new HashSet<string>(), + CustomTagsDelegate = () => + series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet<string>(), + YearsDelegate = () => GetYears(series), + SeasonsDelegate = () => anime.GetSeasons().ToHashSet(), + HasTvDBLinkDelegate = () => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(series), + HasTMDbLinkDelegate = () => series.Contract?.CrossRefAniDBMovieDB != null, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(series), + HasTraktLinkDelegate = () => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTraktLinkDelegate = () => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + IsFinishedDelegate = + () => series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, + LastAirDateDelegate = () => + series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + AddedDateDelegate = () => series.DateTimeCreated, + LastAddedDateDelegate = () => series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => anime?.EpisodeCountNormal ?? 0, + TotalEpisodeCountDelegate = () => anime?.EpisodeCount ?? 0, + LowestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + AnimeTypesDelegate = () => anime == null + ? new HashSet<string>() + : new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) + { + ((AnimeType)anime.AnimeType).ToString() + }, + VideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + }, + }; + + return filterable; + } + + public static UserDependentFilterable ToUserDependentFilterable(this SVR_AnimeSeries series, int userID) + { + var anime = series.GetAnime(); + var user = series.GetUserRecord(userID); + var vote = anime?.UserVote; + var watchedDates = series.GetVideoLocals().Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a).ToList(); + + var filterable = new UserDependentFilterable + { + NameDelegate = series.GetSeriesName, + SortingNameDelegate = () => series.GetSeriesName().GetSortName(), + SeriesCountDelegate = () => 1, + AirDateDelegate = () => anime?.AirDate, + MissingEpisodesDelegate = () => series.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => series.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => anime?.GetAllTags() ?? new HashSet<string>(), + CustomTagsDelegate = () => + series.Contract?.AniDBAnime?.CustomTags?.Select(a => a.TagName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? new HashSet<string>(), + YearsDelegate = () => GetYears(series), + SeasonsDelegate = () => anime?.GetSeasons().ToHashSet(), + HasTvDBLinkDelegate = () => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(series), + HasTMDbLinkDelegate = () => series.Contract?.CrossRefAniDBMovieDB != null, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(series), + HasTraktLinkDelegate = () => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + HasMissingTraktLinkDelegate = () => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(series.AniDB_ID).Any(), + IsFinishedDelegate = () => series.Contract?.AniDBAnime?.AniDBAnime?.EndDate != null && series.Contract.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now, + LastAirDateDelegate = () => + series.EndDate ?? series.GetAnimeEpisodes().Select(a => a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + AddedDateDelegate = () => series.DateTimeCreated, + LastAddedDateDelegate = () => series.GetVideoLocals().Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => anime?.EpisodeCountNormal ?? 0, + TotalEpisodeCountDelegate = () => anime?.EpisodeCount ?? 0, + LowestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + HighestAniDBRatingDelegate = () => decimal.Round(Convert.ToDecimal(anime?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero), + AnimeTypesDelegate = () => anime == null + ? new HashSet<string>() + : new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) + { + ((AnimeType)anime.AnimeType).ToString() + }, + VideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => series.Contract?.AniDBAnime?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioNames.Any()) audio = audioNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => series.Contract?.AniDBAnime?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleNames = series.GetVideoLocals().Select(b => b.GetAniDBFile()).Where(a => a != null).Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleNames.Any()) subtitles = subtitleNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + }, + IsFavoriteDelegate = () => false, + WatchedEpisodesDelegate = () => user?.WatchedEpisodeCount ?? 0, + UnwatchedEpisodesDelegate = () => user?.UnwatchedEpisodeCount ?? 0, + LowestUserRatingDelegate = () => vote?.VoteValue ?? 0, + HighestUserRatingDelegate = () => vote?.VoteValue ?? 0, + HasVotesDelegate = () => vote != null, + HasPermanentVotesDelegate = () => vote is { VoteType: (int)AniDBVoteType.Anime }, + MissingPermanentVotesDelegate = () => vote is not { VoteType: (int)AniDBVoteType.Anime } && anime?.EndDate != null && anime.EndDate > DateTime.Now, + WatchedDateDelegate = () => watchedDates.FirstOrDefault(), + LastWatchedDateDelegate = () => watchedDates.LastOrDefault() + }; + + return filterable; + } + + private static HashSet<int> GetYears(SVR_AnimeSeries series) + { + var contract = series.Contract?.AniDBAnime; + var startyear = contract?.AniDBAnime?.BeginYear ?? 0; + if (startyear == 0) + { + return new HashSet<int>(); + } + + var endyear = contract?.AniDBAnime?.EndYear ?? 0; + if (endyear == 0) + { + endyear = DateTime.Today.Year; + } + + if (endyear < startyear) + { + endyear = startyear; + } + + if (startyear == endyear) + { + return new HashSet<int> + { + startyear + }; + } + + return new HashSet<int>(Enumerable.Range(startyear, endyear - startyear + 1).Where(contract.IsInYear)); + } + + private static bool HasMissingTMDbLink(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + if (anime == null) + { + return false; + } + + // TODO update this with the TMDB refactor + if (anime.AnimeType != (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + + return series.Contract?.CrossRefAniDBMovieDB == null; + } + + private static bool HasMissingTvDBLink(SVR_AnimeSeries series) + { + var anime = series.GetAnime(); + if (anime == null) + { + return false; + } + + if (anime.AnimeType == (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); + } + + public static Filterable ToFilterable(this SVR_AnimeGroup group) + { + var series = group.GetAllSeries(true); + var anime = group.Anime; + + var filterable = new Filterable + { + NameDelegate = () => group.GroupName, + SortingNameDelegate = () => group.GroupName.GetSortName(), + SeriesCountDelegate = () => series.Count, + AirDateDelegate = () => group.Contract.Stat_AirDate_Min, + LastAirDateDelegate = () => group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + MissingEpisodesDelegate = () => group.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => group.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => group.Contract?.Stat_AllTags ?? new HashSet<string>(), + CustomTagsDelegate = () => group.Contract?.Stat_AllCustomTags ?? new HashSet<string>(), + YearsDelegate = () => group.Contract?.Stat_AllYears ?? new HashSet<int>(), + SeasonsDelegate = () => group.Contract?.Stat_AllSeasons.Select(a => + { + var parts = a.Split(' '); + return (int.Parse(parts[1]), Enum.Parse<AnimeSeason>(parts[0])); + }).ToHashSet(), + HasTvDBLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(group), + HasTMDbLinkDelegate = () => group.Contract?.Stat_HasMovieDBLink ?? false, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(group), + HasTraktLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTraktLinkDelegate = () => series.Any(a => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + IsFinishedDelegate = () => group.Contract?.Stat_HasFinishedAiring ?? false, + AddedDateDelegate = () => group.DateTimeCreated, + LastAddedDateDelegate = () => series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), + TotalEpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), + LowestAniDBRatingDelegate = () => anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Min(), + HighestAniDBRatingDelegate = () => anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty().Max(), + AnimeTypesDelegate = () => new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + VideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioLanguageNames.Any()) + audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleLanguageNames.Any()) + subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + } + }; + + return filterable; + } + + public static Filterable ToUserDependentFilterable(this SVR_AnimeGroup group, int userID) + { + var series = group.GetAllSeries(true); + var anime = group.Anime; + var user = group.GetUserRecord(userID); + var vote = anime.Select(a => a.UserVote).Where(a => a is { VoteType: (int)VoteType.AnimePermanent or (int)VoteType.AnimeTemporary }) + .Select(a => a.VoteValue).OrderBy(a => a).ToList(); + var watchedDates = series.SelectMany(a => a.GetVideoLocals()).Select(a => a.GetUserRecord(userID)?.WatchedDate).Where(a => a != null).OrderBy(a => a) + .ToList(); + + var filterable = new UserDependentFilterable + { + NameDelegate = () => group.GroupName, + SortingNameDelegate = () => group.GroupName.GetSortName(), + SeriesCountDelegate = () => series.Count, + AirDateDelegate = () => group.Contract.Stat_AirDate_Min, + LastAirDateDelegate = () => group.Contract?.Stat_EndDate ?? group.GetAllSeries().SelectMany(a => a.GetAnimeEpisodes()).Select(a => + a.AniDB_Episode?.GetAirDateAsDate()).Where(a => a != null).DefaultIfEmpty().Max(), + MissingEpisodesDelegate = () => group.Contract?.MissingEpisodeCount ?? 0, + MissingEpisodesCollectingDelegate = () => group.Contract?.MissingEpisodeCountGroups ?? 0, + TagsDelegate = () => group.Contract?.Stat_AllTags ?? new HashSet<string>(), + CustomTagsDelegate = () => group.Contract?.Stat_AllCustomTags ?? new HashSet<string>(), + YearsDelegate = () => group.Contract?.Stat_AllYears ?? new HashSet<int>(), + SeasonsDelegate = () => group.Contract?.Stat_AllSeasons.Select(a => + { + var parts = a.Split(' '); + return (int.Parse(parts[1]), Enum.Parse<AnimeSeason>(parts[0])); + }).ToHashSet(), + HasTvDBLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTvDbLinkDelegate = () => HasMissingTvDBLink(group), + HasTMDbLinkDelegate = () => group.Contract?.Stat_HasMovieDBLink ?? false, + HasMissingTMDbLinkDelegate = () => HasMissingTMDbLink(group), + HasTraktLinkDelegate = () => series.Any(a => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + HasMissingTraktLinkDelegate = () => series.Any(a => !RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(a.AniDB_ID).Any()), + IsFinishedDelegate = () => group.Contract?.Stat_HasFinishedAiring ?? false, + AddedDateDelegate = () => group.DateTimeCreated, + LastAddedDateDelegate = () => series.SelectMany(a => a.GetVideoLocals()).Select(a => a.DateTimeCreated).DefaultIfEmpty().Max(), + EpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCountNormal ?? 0), + TotalEpisodeCountDelegate = () => series.Sum(a => a.GetAnime()?.EpisodeCount ?? 0), + LowestAniDBRatingDelegate = () => + anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty() + .Min(), + HighestAniDBRatingDelegate = () => + anime.Select(a => decimal.Round(Convert.ToDecimal(a?.Rating ?? 0) / 100, 1, MidpointRounding.AwayFromZero)).DefaultIfEmpty() + .Max(), + AnimeTypesDelegate = () => new HashSet<string>(anime.Select(a => ((AnimeType)a.AnimeType).ToString()), StringComparer.InvariantCultureIgnoreCase), + VideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality ?? new HashSet<string>(), + SharedVideoSourcesDelegate = () => group.Contract?.Stat_AllVideoQuality_Episodes ?? new HashSet<string>(), + AudioLanguagesDelegate = () => group.Contract?.Stat_AudioLanguages ?? new HashSet<string>(), + SharedAudioLanguagesDelegate = () => + { + var audio = new HashSet<string>(); + var audioLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Languages.Select(b => b.LanguageName)); + if (audioLanguageNames.Any()) + audio = audioLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)) + .ToHashSet(); + return audio; + }, + SubtitleLanguagesDelegate = () => group.Contract?.Stat_SubtitleLanguages ?? new HashSet<string>(), + SharedSubtitleLanguagesDelegate = () => + { + var subtitles = new HashSet<string>(); + var subtitleLanguageNames = series.SelectMany(a => a.GetVideoLocals().Select(b => b.GetAniDBFile())).Where(a => a != null) + .Select(a => a.Subtitles.Select(b => b.LanguageName)); + if (subtitleLanguageNames.Any()) + subtitles = subtitleLanguageNames.Aggregate((a, b) => a.Intersect(b, StringComparer.InvariantCultureIgnoreCase)).ToHashSet(); + return subtitles; + }, + IsFavoriteDelegate = () => user?.IsFave == 1, + WatchedEpisodesDelegate = () => user?.WatchedEpisodeCount ?? 0, + UnwatchedEpisodesDelegate = () => user?.UnwatchedEpisodeCount ?? 0, + LowestUserRatingDelegate = () => vote.FirstOrDefault(), + HighestUserRatingDelegate = () => vote.LastOrDefault(), + HasVotesDelegate = () => vote.Any(), + HasPermanentVotesDelegate = () => anime.Select(a => a.UserVote).Any(a => a is { VoteType: (int)VoteType.AnimePermanent }), + MissingPermanentVotesDelegate = () => anime.Any(a => a.UserVote is not { VoteType: (int)VoteType.AnimePermanent } && a.EndDate != null && a.EndDate > DateTime.Now), + WatchedDateDelegate = () => watchedDates.FirstOrDefault(), + LastWatchedDateDelegate = () => watchedDates.LastOrDefault() + }; + + return filterable; + } + + private static bool HasMissingTMDbLink(SVR_AnimeGroup group) + { + return group.GetAllSeries().Any(series => + { + var anime = series.GetAnime(); + if (anime == null) + { + return false; + } + + // TODO update this with the TMDB refactor + if (anime.AnimeType != (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + + return series.Contract?.CrossRefAniDBMovieDB == null; + }); + } + + private static bool HasMissingTvDBLink(SVR_AnimeGroup group) + { + return group.GetAllSeries().Any(series => + { + var anime = series.GetAnime(); + if (anime == null) + { + return false; + } + + if (anime.AnimeType == (int)AnimeType.Movie) + { + return false; + } + + if (anime.Restricted > 0) + { + return false; + } + + return !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(series.AniDB_ID).Any(); + }); + } +} diff --git a/Shoko.Server/Filters/Filterable.cs b/Shoko.Server/Filters/Filterable.cs new file mode 100644 index 000000000..cd5cf9f27 --- /dev/null +++ b/Shoko.Server/Filters/Filterable.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public class Filterable : IFilterable +{ + + private readonly Lazy<DateTime> _addedDate; + private readonly Func<DateTime> _addedDateDelegate; + + private readonly Lazy<DateTime?> _airDate; + private readonly Func<DateTime?> _airDateDelegate; + + private readonly Lazy<IReadOnlySet<string>> _animeTypes; + private readonly Func<IReadOnlySet<string>> _animeTypesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _audioLanguages; + private readonly Func<IReadOnlySet<string>> _audioLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _customTags; + private readonly Func<IReadOnlySet<string>> _customTagsDelegate; + + private readonly Lazy<int> _episodeCount; + private readonly Func<int> _episodeCountDelegate; + + private readonly Lazy<bool> _hasMissingTMDbLink; + private readonly Func<bool> _hasMissingTmDbLinkDelegate; + + private readonly Lazy<bool> _hasMissingTraktLink; + private readonly Func<bool> _hasMissingTraktLinkDelegate; + + private readonly Lazy<bool> _hasMissingTvDBLink; + private readonly Func<bool> _hasMissingTvDbLinkDelegate; + + private readonly Lazy<bool> _hasTMDbLink; + private readonly Func<bool> _hasTmDbLinkDelegate; + + private readonly Lazy<bool> _hasTraktLink; + private readonly Func<bool> _hasTraktLinkDelegate; + + private readonly Lazy<bool> _hasTvDBLink; + private readonly Func<bool> _hasTvDBLinkDelegate; + + private readonly Lazy<decimal> _highestAniDBRating; + private readonly Func<decimal> _highestAniDBRatingDelegate; + + private readonly Lazy<bool> _isFinished; + private readonly Func<bool> _isFinishedDelegate; + + private readonly Lazy<DateTime> _lastAddedDate; + private readonly Func<DateTime> _lastAddedDateDelegate; + + private readonly Lazy<DateTime?> _lastAirDate; + private readonly Func<DateTime?> _lastAirDateDelegate; + + private readonly Lazy<decimal> _lowestAniDBRating; + private readonly Func<decimal> _lowestAniDBRatingDelegate; + + private readonly Lazy<int> _missingEpisodes; + + private readonly Lazy<int> _missingEpisodesCollecting; + private readonly Func<int> _missingEpisodesCollectingDelegate; + private readonly Func<int> _missingEpisodesDelegate; + + private readonly Lazy<string> _name; + private readonly Func<string> _nameDelegate; + + private readonly Lazy<IReadOnlySet<(int year, AnimeSeason season)>> _seasons; + private readonly Func<IReadOnlySet<(int year, AnimeSeason season)>> _seasonsDelegate; + + private readonly Lazy<int> _seriesCount; + private readonly Func<int> _seriesCountDelegate; + + private readonly Lazy<IReadOnlySet<string>> _sharedAudioLanguages; + private readonly Func<IReadOnlySet<string>> _sharedAudioLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _sharedSubtitleLanguages; + private readonly Func<IReadOnlySet<string>> _sharedSubtitleLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _sharedVideoSources; + private readonly Func<IReadOnlySet<string>> _sharedVideoSourcesDelegate; + + private readonly Lazy<string> _sortingName; + private readonly Func<string> _sortingNameDelegate; + + private readonly Lazy<IReadOnlySet<string>> _subtitleLanguages; + private readonly Func<IReadOnlySet<string>> _subtitleLanguagesDelegate; + + private readonly Lazy<IReadOnlySet<string>> _tags; + private readonly Func<IReadOnlySet<string>> _tagsDelegate; + + private readonly Lazy<int> _totalEpisodeCount; + private readonly Func<int> _totalEpisodeCountDelegate; + + private readonly Lazy<IReadOnlySet<string>> _videoSources; + private readonly Func<IReadOnlySet<string>> _videoSourcesDelegate; + + private readonly Lazy<IReadOnlySet<int>> _years; + private readonly Func<IReadOnlySet<int>> _yearsDelegate; + + public string Name + { + get => _name.Value; + init => throw new NotSupportedException(); + } + + public Func<string> NameDelegate + { + get => _nameDelegate; + init + { + _nameDelegate = value; + _name = new Lazy<string>(_nameDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public string SortingName + { + get => _sortingName.Value; + init => throw new NotSupportedException(); + } + + public Func<string> SortingNameDelegate + { + get => _sortingNameDelegate; + init + { + _sortingNameDelegate = value; + _sortingName = new Lazy<string>(_sortingNameDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int SeriesCount + { + get => _seriesCount.Value; + init => throw new NotSupportedException(); + } + + public Func<int> SeriesCountDelegate + { + get => _seriesCountDelegate; + init + { + _seriesCountDelegate = value; + _seriesCount = new Lazy<int>(_seriesCountDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int MissingEpisodes + { + get => _missingEpisodes.Value; + init => throw new NotSupportedException(); + } + + public Func<int> MissingEpisodesDelegate + { + get => _missingEpisodesDelegate; + init + { + _missingEpisodesDelegate = value; + _missingEpisodes = new Lazy<int>(_missingEpisodesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int MissingEpisodesCollecting + { + get => _missingEpisodesCollecting.Value; + init => throw new NotSupportedException(); + } + + public Func<int> MissingEpisodesCollectingDelegate + { + get => _missingEpisodesCollectingDelegate; + init + { + _missingEpisodesCollectingDelegate = value; + _missingEpisodesCollecting = new Lazy<int>(_missingEpisodesCollectingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> Tags + { + get => _tags.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> TagsDelegate + { + get => _tagsDelegate; + init + { + _tagsDelegate = value; + _tags = new Lazy<IReadOnlySet<string>>(_tagsDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> CustomTags + { + get => _customTags.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> CustomTagsDelegate + { + get => _customTagsDelegate; + init + { + _customTagsDelegate = value; + _customTags = new Lazy<IReadOnlySet<string>>(_customTagsDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<int> Years + { + get => _years.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<int>> YearsDelegate + { + get => _yearsDelegate; + init + { + _yearsDelegate = value; + _years = new Lazy<IReadOnlySet<int>>(_yearsDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<(int year, AnimeSeason season)> Seasons + { + get => _seasons.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<(int year, AnimeSeason season)>> SeasonsDelegate + { + get => _seasonsDelegate; + init + { + _seasonsDelegate = value; + _seasons = new Lazy<IReadOnlySet<(int year, AnimeSeason season)>>(_seasonsDelegate); + } + } + + public bool HasTvDBLink + { + get => _hasTvDBLink.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasTvDBLinkDelegate + { + get => _hasTvDBLinkDelegate; + init + { + _hasTvDBLinkDelegate = value; + _hasTvDBLink = new Lazy<bool>(_hasTvDBLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasMissingTvDbLink + { + get => _hasMissingTvDBLink.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasMissingTvDbLinkDelegate + { + get => _hasMissingTvDbLinkDelegate; + init + { + _hasMissingTvDbLinkDelegate = value; + _hasMissingTvDBLink = new Lazy<bool>(_hasMissingTvDbLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasTMDbLink + { + get => _hasTMDbLink.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasTMDbLinkDelegate + { + get => _hasTmDbLinkDelegate; + init + { + _hasTmDbLinkDelegate = value; + _hasTMDbLink = new Lazy<bool>(_hasTmDbLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasMissingTMDbLink + { + get => _hasMissingTMDbLink.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasMissingTMDbLinkDelegate + { + get => _hasMissingTmDbLinkDelegate; + init + { + _hasMissingTmDbLinkDelegate = value; + _hasMissingTMDbLink = new Lazy<bool>(_hasMissingTmDbLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasTraktLink + { + get => _hasTraktLink.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasTraktLinkDelegate + { + get => _hasTraktLinkDelegate; + init + { + _hasTraktLinkDelegate = value; + _hasTraktLink = new Lazy<bool>(_hasTraktLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasMissingTraktLink + { + get => _hasMissingTraktLink.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasMissingTraktLinkDelegate + { + get => _hasMissingTraktLinkDelegate; + init + { + _hasMissingTraktLinkDelegate = value; + _hasMissingTraktLink = new Lazy<bool>(_hasMissingTraktLinkDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool IsFinished + { + get => _isFinished.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> IsFinishedDelegate + { + get => _isFinishedDelegate; + init + { + _isFinishedDelegate = value; + _isFinished = new Lazy<bool>(_isFinishedDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public DateTime? AirDate + { + get => _airDate.Value; + init => throw new NotSupportedException(); + } + + public Func<DateTime?> AirDateDelegate + { + get => _airDateDelegate; + init + { + _airDateDelegate = value; + _airDate = new Lazy<DateTime?>(_airDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public DateTime? LastAirDate + { + get => _lastAirDate.Value; + init => throw new NotSupportedException(); + } + + public Func<DateTime?> LastAirDateDelegate + { + get => _lastAirDateDelegate; + init + { + _lastAirDateDelegate = value; + _lastAirDate = new Lazy<DateTime?>(_lastAirDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public DateTime AddedDate + { + get => _addedDate.Value; + init => throw new NotSupportedException(); + } + + public Func<DateTime> AddedDateDelegate + { + get => _addedDateDelegate; + init + { + _addedDateDelegate = value; + _addedDate = new Lazy<DateTime>(_addedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public DateTime LastAddedDate + { + get => _lastAddedDate.Value; + init => throw new NotSupportedException(); + } + + public Func<DateTime> LastAddedDateDelegate + { + get => _lastAddedDateDelegate; + init + { + _lastAddedDateDelegate = value; + _lastAddedDate = new Lazy<DateTime>(_lastAddedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int EpisodeCount + { + get => _episodeCount.Value; + init => throw new NotSupportedException(); + } + + public Func<int> EpisodeCountDelegate + { + get => _episodeCountDelegate; + init + { + _episodeCountDelegate = value; + _episodeCount = new Lazy<int>(_episodeCountDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int TotalEpisodeCount + { + get => _totalEpisodeCount.Value; + init => throw new NotSupportedException(); + } + + public Func<int> TotalEpisodeCountDelegate + { + get => _totalEpisodeCountDelegate; + init + { + _totalEpisodeCountDelegate = value; + _totalEpisodeCount = new Lazy<int>(_totalEpisodeCountDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public decimal LowestAniDBRating + { + get => _lowestAniDBRating.Value; + init => throw new NotSupportedException(); + } + + public Func<decimal> LowestAniDBRatingDelegate + { + get => _lowestAniDBRatingDelegate; + init + { + _lowestAniDBRatingDelegate = value; + _lowestAniDBRating = new Lazy<decimal>(_lowestAniDBRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public decimal HighestAniDBRating + { + get => _highestAniDBRating.Value; + init => throw new NotSupportedException(); + } + + public Func<decimal> HighestAniDBRatingDelegate + { + get => _highestAniDBRatingDelegate; + init + { + _highestAniDBRatingDelegate = value; + _highestAniDBRating = new Lazy<decimal>(_highestAniDBRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> VideoSources + { + get => _videoSources.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> VideoSourcesDelegate + { + get => _videoSourcesDelegate; + init + { + _videoSourcesDelegate = value; + _videoSources = new Lazy<IReadOnlySet<string>>(_videoSourcesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> SharedVideoSources + { + get => _sharedVideoSources.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> SharedVideoSourcesDelegate + { + get => _sharedVideoSourcesDelegate; + init + { + _sharedVideoSourcesDelegate = value; + _sharedVideoSources = new Lazy<IReadOnlySet<string>>(_sharedVideoSourcesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> AnimeTypes + { + get => _animeTypes.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> AnimeTypesDelegate + { + get => _animeTypesDelegate; + init + { + _animeTypesDelegate = value; + _animeTypes = new Lazy<IReadOnlySet<string>>(_animeTypesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> AudioLanguages + { + get => _audioLanguages.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> AudioLanguagesDelegate + { + get => _audioLanguagesDelegate; + init + { + _audioLanguagesDelegate = value; + _audioLanguages = new Lazy<IReadOnlySet<string>>(_audioLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> SharedAudioLanguages + { + get => _sharedAudioLanguages.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> SharedAudioLanguagesDelegate + { + get => _sharedAudioLanguagesDelegate; + init + { + _sharedAudioLanguagesDelegate = value; + _sharedAudioLanguages = new Lazy<IReadOnlySet<string>>(_sharedAudioLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> SubtitleLanguages + { + get => _subtitleLanguages.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> SubtitleLanguagesDelegate + { + get => _subtitleLanguagesDelegate; + init + { + _subtitleLanguagesDelegate = value; + _subtitleLanguages = new Lazy<IReadOnlySet<string>>(_subtitleLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public IReadOnlySet<string> SharedSubtitleLanguages + { + get => _sharedSubtitleLanguages.Value; + init => throw new NotSupportedException(); + } + + public Func<IReadOnlySet<string>> SharedSubtitleLanguagesDelegate + { + get => _sharedSubtitleLanguagesDelegate; + init + { + _sharedSubtitleLanguagesDelegate = value; + _sharedSubtitleLanguages = new Lazy<IReadOnlySet<string>>(_sharedSubtitleLanguagesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } +} diff --git a/Shoko.Server/Filters/Functions/DateAddFunction.cs b/Shoko.Server/Filters/Functions/DateAddFunction.cs new file mode 100644 index 000000000..fb0be3cc4 --- /dev/null +++ b/Shoko.Server/Filters/Functions/DateAddFunction.cs @@ -0,0 +1,79 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Functions; + +public class DateAddFunction : FilterExpression<DateTime?>, IWithDateSelectorParameter, IWithTimeSpanParameter +{ + public DateAddFunction() + { + } + + public DateAddFunction(FilterExpression<DateTime?> selector, TimeSpan parameter) + { + Selector = selector; + Parameter = parameter; + } + + public FilterExpression<DateTime?> Selector { get; set; } + public TimeSpan Parameter { get; set; } + + public override bool TimeDependent => Selector.TimeDependent; + public override bool UserDependent => Selector.UserDependent; + + public FilterExpression<DateTime?> Left + { + get => Selector; + set => Selector = value; + } + + public override DateTime? Evaluate(IFilterable f) + { + return Selector.Evaluate(f) + Parameter; + } + + protected bool Equals(DateAddFunction other) + { + return base.Equals(other) && Equals(Selector, other.Selector) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateAddFunction)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Selector, Parameter); + } + + public static bool operator ==(DateAddFunction left, DateAddFunction right) + { + return Equals(left, right); + } + + public static bool operator !=(DateAddFunction left, DateAddFunction right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateAddFunction exp && Left.IsType(exp.Left) && Selector.IsType(exp.Selector); + } +} diff --git a/Shoko.Server/Filters/Functions/DateDiffFunction.cs b/Shoko.Server/Filters/Functions/DateDiffFunction.cs new file mode 100644 index 000000000..669f3bf35 --- /dev/null +++ b/Shoko.Server/Filters/Functions/DateDiffFunction.cs @@ -0,0 +1,76 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Functions; + +public class DateDiffFunction : FilterExpression<DateTime?>, IWithDateSelectorParameter, IWithTimeSpanParameter +{ + public DateDiffFunction(FilterExpression<DateTime?> selector, TimeSpan parameter) + { + Selector = selector; + Parameter = parameter; + } + public DateDiffFunction() { } + + public FilterExpression<DateTime?> Selector { get; set; } + public TimeSpan Parameter { get; set; } + + public override bool TimeDependent => Selector.TimeDependent; + public override bool UserDependent => Selector.UserDependent; + + public FilterExpression<DateTime?> Left + { + get => Selector; + set => Selector = value; + } + + public override DateTime? Evaluate(IFilterable f) + { + return Selector.Evaluate(f) - Parameter; + } + + protected bool Equals(DateDiffFunction other) + { + return base.Equals(other) && Equals(Selector, other.Selector) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateDiffFunction)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Selector, Parameter); + } + + public static bool operator ==(DateDiffFunction left, DateDiffFunction right) + { + return Equals(left, right); + } + + public static bool operator !=(DateDiffFunction left, DateDiffFunction right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateDiffFunction exp && Left.IsType(exp.Left) && Selector.IsType(exp.Selector); + } +} diff --git a/Shoko.Server/Filters/Functions/TodayFunction.cs b/Shoko.Server/Filters/Functions/TodayFunction.cs new file mode 100644 index 000000000..615266491 --- /dev/null +++ b/Shoko.Server/Filters/Functions/TodayFunction.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Functions; + +public class TodayFunction : FilterExpression<DateTime?> +{ + public override bool TimeDependent => true; + public override bool UserDependent => false; + + public override DateTime? Evaluate(IFilterable f) + { + return DateTime.Today; + } + + protected bool Equals(TodayFunction other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((TodayFunction)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(TodayFunction left, TodayFunction right) + { + return Equals(left, right); + } + + public static bool operator !=(TodayFunction left, TodayFunction right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs new file mode 100644 index 000000000..3f10e1adb --- /dev/null +++ b/Shoko.Server/Filters/Info/HasAnimeTypeExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasAnimeTypeExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasAnimeTypeExpression(string parameter) + { + Parameter = parameter; + } + public HasAnimeTypeExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.AnimeTypes.Contains(Parameter); + } + + protected bool Equals(HasAnimeTypeExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasAnimeTypeExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasAnimeTypeExpression left, HasAnimeTypeExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasAnimeTypeExpression left, HasAnimeTypeExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasCustomTagExpression.cs b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs new file mode 100644 index 000000000..5d7690dcf --- /dev/null +++ b/Shoko.Server/Filters/Info/HasCustomTagExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasCustomTagExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasCustomTagExpression(string parameter) + { + Parameter = parameter; + } + public HasCustomTagExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.CustomTags.Contains(Parameter); + } + + protected bool Equals(HasCustomTagExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasCustomTagExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasCustomTagExpression left, HasCustomTagExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasCustomTagExpression left, HasCustomTagExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs new file mode 100644 index 000000000..0265ad948 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesCollectingExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasMissingEpisodesCollectingExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.MissingEpisodesCollecting > 0; + } + + protected bool Equals(HasMissingEpisodesCollectingExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasMissingEpisodesCollectingExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasMissingEpisodesCollectingExpression left, HasMissingEpisodesCollectingExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasMissingEpisodesCollectingExpression left, HasMissingEpisodesCollectingExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs new file mode 100644 index 000000000..8ac6091c4 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasMissingEpisodesExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasMissingEpisodesExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.MissingEpisodes > 0; + } + + protected bool Equals(HasMissingEpisodesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasMissingEpisodesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasMissingEpisodesExpression left, HasMissingEpisodesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasMissingEpisodesExpression left, HasMissingEpisodesExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasNameExpression.cs b/Shoko.Server/Filters/Info/HasNameExpression.cs new file mode 100644 index 000000000..89eee6721 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasNameExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasNameExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasNameExpression(string parameter) + { + Parameter = parameter; + } + public HasNameExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.Name.Equals(Parameter, StringComparison.InvariantCultureIgnoreCase); + } + + protected bool Equals(HasAnimeTypeExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasAnimeTypeExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasNameExpression left, HasNameExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasNameExpression left, HasNameExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs new file mode 100644 index 000000000..8839975d7 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasTMDbLinkExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasTMDbLinkExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.HasTMDbLink; + } + + protected bool Equals(HasTMDbLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTMDbLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasTMDbLinkExpression left, HasTMDbLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTMDbLinkExpression left, HasTMDbLinkExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasTagExpression.cs b/Shoko.Server/Filters/Info/HasTagExpression.cs new file mode 100644 index 000000000..34cbeb4b6 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasTagExpression.cs @@ -0,0 +1,62 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasTagExpression : FilterExpression<bool>, IWithStringParameter +{ + public HasTagExpression(string parameter) + { + Parameter = parameter; + } + public HasTagExpression() { } + + public string Parameter { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.Tags.Contains(Parameter); + } + + protected bool Equals(HasTagExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTagExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(HasTagExpression left, HasTagExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTagExpression left, HasTagExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs new file mode 100644 index 000000000..171d08bb3 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasTraktLinkExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasTraktLinkExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.HasTraktLink; + } + + protected bool Equals(HasTraktLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTraktLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasTraktLinkExpression left, HasTraktLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTraktLinkExpression left, HasTraktLinkExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs new file mode 100644 index 000000000..9c819f591 --- /dev/null +++ b/Shoko.Server/Filters/Info/HasTvDBLinkExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class HasTvDBLinkExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.HasTvDBLink; + } + + protected bool Equals(HasTvDBLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasTvDBLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasTvDBLinkExpression left, HasTvDBLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasTvDBLinkExpression left, HasTvDBLinkExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/InSeasonExpression.cs b/Shoko.Server/Filters/Info/InSeasonExpression.cs new file mode 100644 index 000000000..c0c90522c --- /dev/null +++ b/Shoko.Server/Filters/Info/InSeasonExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class InSeasonExpression : FilterExpression<bool>, IWithNumberParameter, IWithSecondStringParameter +{ + public InSeasonExpression(int year, AnimeSeason season) + { + Year = year; + Season = season; + } + public InSeasonExpression() { } + + public int Year { get; set; } + public AnimeSeason Season { get; set; } + public override bool TimeDependent => false; + public override bool UserDependent => false; + + double IWithNumberParameter.Parameter + { + get => Year; + set => Year = (int)value; + } + + string IWithSecondStringParameter.SecondParameter + { + get => Season.ToString(); + set => Season = Enum.Parse<AnimeSeason>(value); + } + + public override bool Evaluate(IFilterable filterable) + { + return filterable.Seasons.Contains((Year, Season)); + } + + protected bool Equals(InSeasonExpression other) + { + return base.Equals(other) && Year == other.Year && Season == other.Season; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((InSeasonExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Year, (int)Season); + } + + public static bool operator ==(InSeasonExpression left, InSeasonExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(InSeasonExpression left, InSeasonExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/InYearExpression.cs b/Shoko.Server/Filters/Info/InYearExpression.cs new file mode 100644 index 000000000..96194b880 --- /dev/null +++ b/Shoko.Server/Filters/Info/InYearExpression.cs @@ -0,0 +1,68 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class InYearExpression : FilterExpression<bool>, IWithNumberParameter +{ + public InYearExpression(int parameter) + { + Parameter = parameter; + } + public InYearExpression() { } + + public int Parameter { get; set; } + public override bool TimeDependent => true; + public override bool UserDependent => false; + + double IWithNumberParameter.Parameter + { + get => Parameter; + set => Parameter = (int)value; + } + + public override bool Evaluate(IFilterable filterable) + { + return filterable.Years.Contains(Parameter); + } + + protected bool Equals(InYearExpression other) + { + return base.Equals(other) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((InYearExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Parameter); + } + + public static bool operator ==(InYearExpression left, InYearExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(InYearExpression left, InYearExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/IsFinishedExpression.cs b/Shoko.Server/Filters/Info/IsFinishedExpression.cs new file mode 100644 index 000000000..d2711fcaa --- /dev/null +++ b/Shoko.Server/Filters/Info/IsFinishedExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +public class IsFinishedExpression : FilterExpression<bool> +{ + public override bool TimeDependent => true; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.IsFinished; + } + + protected bool Equals(IsFinishedExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((IsFinishedExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(IsFinishedExpression left, IsFinishedExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(IsFinishedExpression left, IsFinishedExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs new file mode 100644 index 000000000..9fd79cf7f --- /dev/null +++ b/Shoko.Server/Filters/Info/MissingTMDbLinkExpression.cs @@ -0,0 +1,57 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +/// <summary> +/// Missing Links include logic for whether a link should exist +/// </summary> +public class MissingTMDbLinkExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.HasMissingTMDbLink; + } + + protected bool Equals(MissingTMDbLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingTMDbLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingTMDbLinkExpression left, MissingTMDbLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingTMDbLinkExpression left, MissingTMDbLinkExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs new file mode 100644 index 000000000..7e1efc6bf --- /dev/null +++ b/Shoko.Server/Filters/Info/MissingTraktLinkExpression.cs @@ -0,0 +1,57 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +/// <summary> +/// Missing Links include logic for whether a link should exist +/// </summary> +public class MissingTraktLinkExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.HasMissingTraktLink; + } + + protected bool Equals(MissingTraktLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingTraktLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingTraktLinkExpression left, MissingTraktLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingTraktLinkExpression left, MissingTraktLinkExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs new file mode 100644 index 000000000..8a3c804a5 --- /dev/null +++ b/Shoko.Server/Filters/Info/MissingTvDBLinkExpression.cs @@ -0,0 +1,57 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Info; + +/// <summary> +/// Missing Links include logic for whether a link should exist +/// </summary> +public class MissingTvDBLinkExpression : FilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override bool Evaluate(IFilterable filterable) + { + return filterable.HasMissingTvDbLink; + } + + protected bool Equals(MissingTvDBLinkExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingTvDBLinkExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingTvDBLinkExpression left, MissingTvDBLinkExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingTvDBLinkExpression left, MissingTvDBLinkExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Interfaces/IFilterExpression.cs b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs new file mode 100644 index 000000000..07341ed18 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IFilterExpression.cs @@ -0,0 +1,17 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IFilterExpression +{ + bool TimeDependent { get; } + bool UserDependent { get; } +} + +public interface IFilterExpression<out T> +{ + T Evaluate(IFilterable f); +} + +public interface IUserDependentFilterExpression<out T> +{ + T Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Filters/Interfaces/IFilterable.cs b/Shoko.Server/Filters/Interfaces/IFilterable.cs new file mode 100644 index 000000000..ae3b87e41 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IFilterable.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using Shoko.Models.Enums; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IFilterable +{ + /// <summary> + /// Name + /// </summary> + string Name { get; init; } + + /// <summary> + /// Sorting Name + /// </summary> + string SortingName { get; init; } + + /// <summary> + /// The number of series in a group + /// </summary> + int SeriesCount { get; init; } + + /// <summary> + /// Number of Missing Episodes + /// </summary> + int MissingEpisodes { get; init; } + + /// <summary> + /// Number of Missing Episodes from Groups that you have + /// </summary> + int MissingEpisodesCollecting { get; init; } + + /// <summary> + /// All of the tags + /// </summary> + IReadOnlySet<string> Tags { get; init; } + + /// <summary> + /// All of the custom tags + /// </summary> + IReadOnlySet<string> CustomTags { get; init; } + + /// <summary> + /// The years this aired in + /// </summary> + IReadOnlySet<int> Years { get; init; } + + /// <summary> + /// The seasons this aired in + /// </summary> + IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + + /// <summary> + /// Has at least one TvDB Link + /// </summary> + bool HasTvDBLink { get; init; } + + /// <summary> + /// Missing at least one TvDB Link + /// </summary> + bool HasMissingTvDbLink { get; init; } + + /// <summary> + /// Has at least one TMDb Link + /// </summary> + bool HasTMDbLink { get; init; } + + /// <summary> + /// Missing at least one TMDb Link + /// </summary> + bool HasMissingTMDbLink { get; init; } + + /// <summary> + /// Has at least one Trakt Link + /// </summary> + bool HasTraktLink { get; init; } + + /// <summary> + /// Missing at least one Trakt Link + /// </summary> + bool HasMissingTraktLink { get; init; } + + /// <summary> + /// Has Finished airing + /// </summary> + bool IsFinished { get; init; } + + /// <summary> + /// First Air Date + /// </summary> + DateTime? AirDate { get; init; } + + /// <summary> + /// Latest Air Date + /// </summary> + DateTime? LastAirDate { get; init; } + + /// <summary> + /// When it was first added to the collection + /// </summary> + DateTime AddedDate { get; init; } + + /// <summary> + /// When it was most recently added to the collection + /// </summary> + DateTime LastAddedDate { get; init; } + + /// <summary> + /// Highest Episode Count + /// </summary> + int EpisodeCount { get; init; } + + /// <summary> + /// Total Episode Count + /// </summary> + int TotalEpisodeCount { get; init; } + + /// <summary> + /// Lowest AniDB Rating (0-10) + /// </summary> + decimal LowestAniDBRating { get; init; } + + /// <summary> + /// Highest AniDB Rating (0-10) + /// </summary> + decimal HighestAniDBRating { get; init; } + + /// <summary> + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. + /// </summary> + IReadOnlySet<string> VideoSources { get; init; } + + /// <summary> + /// The sources that the video came from, such as TV, Web, DVD, Blu-ray, etc. (only sources that are in every file) + /// </summary> + IReadOnlySet<string> SharedVideoSources { get; init; } + + /// <summary> + /// The anime types (movie, series, ova, etc) + /// </summary> + IReadOnlySet<string> AnimeTypes { get; init; } + + /// <summary> + /// Audio Languages + /// </summary> + IReadOnlySet<string> AudioLanguages { get; init; } + + /// <summary> + /// Audio Languages (only languages that are in every file) + /// </summary> + IReadOnlySet<string> SharedAudioLanguages { get; init; } + + /// <summary> + /// Subtitle Languages + /// </summary> + IReadOnlySet<string> SubtitleLanguages { get; init; } + + /// <summary> + /// Subtitle Languages (only languages that are in every file) + /// </summary> + IReadOnlySet<string> SharedSubtitleLanguages { get; init; } +} diff --git a/Shoko.Server/Filters/Interfaces/ISortingExpression.cs b/Shoko.Server/Filters/Interfaces/ISortingExpression.cs new file mode 100644 index 000000000..b016e2771 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/ISortingExpression.cs @@ -0,0 +1,9 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface ISortingExpression : IFilterExpression<object> +{ +} + +public interface IUserDependentSortingExpression : ISortingExpression, IUserDependentFilterExpression<object> +{ +} diff --git a/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs new file mode 100644 index 000000000..79468552b --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IUserDependentFilterable.cs @@ -0,0 +1,56 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IUserDependentFilterable : IFilterable +{ + /// <summary> + /// Probably will be removed in the future. Custom Tags would handle this better + /// </summary> + bool IsFavorite { get; init; } + + /// <summary> + /// The number of episodes watched + /// </summary> + int WatchedEpisodes { get; init; } + + /// <summary> + /// The number of episodes that have not been watched + /// </summary> + int UnwatchedEpisodes { get; init; } + + /// <summary> + /// Has any user votes + /// </summary> + bool HasVotes { get; init; } + + /// <summary> + /// Has permanent (after finishing) user votes + /// </summary> + bool HasPermanentVotes { get; init; } + + /// <summary> + /// Has permanent (after finishing) user votes + /// </summary> + bool MissingPermanentVotes { get; init; } + + /// <summary> + /// First Watched Date + /// </summary> + DateTime? WatchedDate { get; init; } + + /// <summary> + /// Latest Watched Date + /// </summary> + DateTime? LastWatchedDate { get; init; } + + /// <summary> + /// Lowest User Rating (0-10) + /// </summary> + decimal LowestUserRating { get; init; } + + /// <summary> + /// Highest User Rating (0-10) + /// </summary> + public decimal HighestUserRating { get; init; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithDateParameter.cs b/Shoko.Server/Filters/Interfaces/IWithDateParameter.cs new file mode 100644 index 000000000..a3ef416d3 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithDateParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithDateParameter +{ + DateTime Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs new file mode 100644 index 000000000..f011a3403 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithDateSelectorParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithDateSelectorParameter +{ + FilterExpression<DateTime?> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs b/Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs new file mode 100644 index 000000000..ef65c4405 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithExpressionParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithExpressionParameter +{ + FilterExpression<bool> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs b/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs new file mode 100644 index 000000000..63be734a8 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithNumberParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithNumberParameter +{ + double Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs new file mode 100644 index 000000000..684438346 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithNumberSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithNumberSelectorParameter +{ + FilterExpression<double> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs new file mode 100644 index 000000000..d8f4129f9 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondDateSelectorParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondDateSelectorParameter +{ + FilterExpression<DateTime?> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs new file mode 100644 index 000000000..df13d480d --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondExpressionParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondExpressionParameter +{ + FilterExpression<bool> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs new file mode 100644 index 000000000..6aac4dc2c --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondNumberSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondNumberSelectorParameter +{ + FilterExpression<double> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs new file mode 100644 index 000000000..0cf944bdc --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondStringParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondStringParameter +{ + string SecondParameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs new file mode 100644 index 000000000..fbe68ec85 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithSecondStringSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithSecondStringSelectorParameter +{ + FilterExpression<string> Right { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithStringParameter.cs b/Shoko.Server/Filters/Interfaces/IWithStringParameter.cs new file mode 100644 index 000000000..a879c64c7 --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithStringParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithStringParameter +{ + string Parameter { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs b/Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs new file mode 100644 index 000000000..3d96d04ee --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithStringSelectorParameter.cs @@ -0,0 +1,6 @@ +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithStringSelectorParameter +{ + FilterExpression<string> Left { get; set; } +} diff --git a/Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs b/Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs new file mode 100644 index 000000000..08902b51d --- /dev/null +++ b/Shoko.Server/Filters/Interfaces/IWithTimeSpanParameter.cs @@ -0,0 +1,8 @@ +using System; + +namespace Shoko.Server.Filters.Interfaces; + +public interface IWithTimeSpanParameter +{ + TimeSpan Parameter { get; set; } +} diff --git a/Shoko.Server/Utilities/GroupFilterSortingCriteria.cs b/Shoko.Server/Filters/Legacy/GroupFilterSortingCriteria.cs similarity index 93% rename from Shoko.Server/Utilities/GroupFilterSortingCriteria.cs rename to Shoko.Server/Filters/Legacy/GroupFilterSortingCriteria.cs index cd8e5fc51..0202dfb1c 100644 --- a/Shoko.Server/Utilities/GroupFilterSortingCriteria.cs +++ b/Shoko.Server/Filters/Legacy/GroupFilterSortingCriteria.cs @@ -1,6 +1,6 @@ using Shoko.Models.Enums; -namespace Shoko.Server; +namespace Shoko.Server.Filters.Legacy; public class GroupFilterSortingCriteria { diff --git a/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs new file mode 100644 index 000000000..19719c461 --- /dev/null +++ b/Shoko.Server/Filters/Legacy/LegacyConditionConverter.cs @@ -0,0 +1,955 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters.Files; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Interfaces; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Logic.Numbers; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.SortingSelectors; +using Shoko.Server.Filters.User; +using Shoko.Server.Models; + +namespace Shoko.Server.Filters.Legacy; + +public static class LegacyConditionConverter +{ + public static bool TryConvertToConditions(FilterPreset filter, out List<GroupFilterCondition> conditions, out GroupFilterBaseCondition baseCondition) + { + // The allowed conversions are: + // Not(...) -> BaseCondition Inverted + // And(And(And(...))) -> Chains of And become the list + // a single condition + + var expression = filter.Expression; + // treat null expression similar to All + if (expression == null) + { + conditions = new List<GroupFilterCondition>(); + baseCondition = GroupFilterBaseCondition.Include; + return true; + } + + if (TryGetSingleCondition(expression, out var condition)) + { + baseCondition = GroupFilterBaseCondition.Include; + conditions = new List<GroupFilterCondition> { condition }; + return true; + } + + var results = new List<GroupFilterCondition>(); + if (expression is NotExpression not) + { + baseCondition = GroupFilterBaseCondition.Exclude; + if (TryGetConditionsRecursive<OrExpression>(not.Left, results)) + { + conditions = results; + return true; + } + } + + baseCondition = GroupFilterBaseCondition.Include; + if (TryGetConditionsRecursive<AndExpression>(expression, results)) + { + conditions = results; + return true; + } + + conditions = null; + return false; + } + + private static bool TryGetSingleCondition(FilterExpression expression, out GroupFilterCondition condition) + { + if (TryGetIncludeCondition(expression, out condition)) return true; + if (TryGetInCondition(expression, out condition)) return true; + return TryGetComparatorCondition(expression, out condition); + } + + + private static bool TryGetConditionsRecursive<T>(FilterExpression expression, List<GroupFilterCondition> conditions) where T : IWithExpressionParameter, IWithSecondExpressionParameter + { + // Do this first, as compound expressions can throw off the following logic + if (TryGetSingleCondition(expression, out var condition)) + { + conditions.Add(condition); + return true; + } + + if (expression is T and) return TryGetConditionsRecursive<T>(and.Left, conditions) && TryGetConditionsRecursive<T>(and.Right, conditions); + return false; + } + + private static bool TryGetIncludeCondition(FilterExpression expression, out GroupFilterCondition condition) + { + var conditionOperator = GroupFilterOperator.Include; + if (expression is NotExpression not) + { + conditionOperator = GroupFilterOperator.Exclude; + expression = not.Left; + } + + condition = null; + var type = expression.GetType(); + if (type == typeof(HasMissingEpisodesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.MissingEpisodes + }; + return true; + } + + if (type == typeof(HasMissingEpisodesCollectingExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.MissingEpisodesCollecting + }; + return true; + } + + if (type == typeof(HasUnwatchedEpisodesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes + }; + return true; + } + + if (type == typeof(HasWatchedEpisodesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes + }; + return true; + } + + if (type == typeof(HasPermanentUserVotesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.UserVoted + }; + return true; + } + + if (type == typeof(HasUserVotesExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.UserVotedAny + }; + return true; + } + + if (type == typeof(HasTvDBLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedTvDBInfo + }; + return true; + } + + if (type == typeof(HasTMDbLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo + }; + return true; + } + + if (type == typeof(HasTraktLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedTraktInfo + }; + return true; + } + + if (type == typeof(IsFavoriteExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.Favourite + }; + return true; + } + + if (type == typeof(IsFinishedExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.FinishedAiring + }; + return true; + } + + if (type == typeof(HasTMDbLinkExpression)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedMovieDBInfo + }; + return true; + } + + if (expression == LegacyMappings.GetCompletedExpression()) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.CompletedSeries + }; + return true; + } + + if (expression == new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression())) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, ConditionType = (int)GroupFilterConditionType.AssignedTvDBOrMovieDBInfo + }; + return true; + } + + return false; + } + + private static bool TryGetInCondition(FilterExpression expression, out GroupFilterCondition condition) + { + var conditionOperator = GroupFilterOperator.In; + if (expression is NotExpression not) + { + conditionOperator = GroupFilterOperator.NotIn; + expression = not.Left; + } + + if (IsInTag(expression, out var tags)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = string.Join(",", tags) + }; + return true; + } + + if (IsInCustomTag(expression, out var customTags)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.CustomTags, + ConditionParameter = string.Join(",", customTags) + }; + return true; + } + + if (IsInAnimeType(expression, out var animeType)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = string.Join(",", animeType) + }; + return true; + } + + if (IsInVideoQuality(expression, out var videoQualities)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.VideoQuality, + ConditionParameter = string.Join(",", videoQualities) + }; + return true; + } + + if (IsInGroup(expression, out var groups)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.AnimeGroup, + ConditionParameter = string.Join(",", groups) + }; + return true; + } + + if (IsInYear(expression, out var years)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.Year, + ConditionParameter = string.Join(",", years) + }; + return true; + } + + if (IsInSeason(expression, out var seasons)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.Season, + ConditionParameter = string.Join(",", seasons.Select(a => a.Season + " " + a.Year)) + }; + return true; + } + + if (IsInAudioLanguage(expression, out var aLanguages)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.AudioLanguage, + ConditionParameter = string.Join(",", aLanguages) + }; + return true; + } + + if (IsInSubtitleLanguage(expression, out var sLanguages)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)conditionOperator, + ConditionType = (int)GroupFilterConditionType.SubtitleLanguage, + ConditionParameter = string.Join(",", sLanguages) + }; + return true; + } + + if (IsInSharedVideoQuality(expression, out var sVideoQuality)) + { + condition = new GroupFilterCondition + { + ConditionOperator = conditionOperator == GroupFilterOperator.NotIn ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionType = (int)GroupFilterConditionType.VideoQuality, + ConditionParameter = string.Join(",", sVideoQuality) + }; + return true; + } + + if (IsInSharedAudioLanguage(expression, out var sALanguages)) + { + condition = new GroupFilterCondition + { + ConditionOperator = conditionOperator == GroupFilterOperator.NotIn ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionType = (int)GroupFilterConditionType.AudioLanguage, + ConditionParameter = string.Join(",", sALanguages) + }; + return true; + } + + if (IsInSharedSubtitleLanguage(expression, out var sSLanguages)) + { + condition = new GroupFilterCondition + { + ConditionOperator = conditionOperator == GroupFilterOperator.NotIn ? (int)GroupFilterOperator.NotInAllEpisodes : (int)GroupFilterOperator.InAllEpisodes, + ConditionType = (int)GroupFilterConditionType.SubtitleLanguage, + ConditionParameter = string.Join(",", sSLanguages) + }; + return true; + } + + condition = null; + return false; + } + + private static bool TryGetComparatorCondition(FilterExpression expression, out GroupFilterCondition condition) + { + condition = null; + if (IsAirDate(expression, out var airDatePara, out var airDateOperator)) + { + var para = airDatePara is DateTime date ? date.ToString("yyyyMMdd") : airDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)airDateOperator, + ConditionType = (int)GroupFilterConditionType.AirDate, + ConditionParameter = para + }; + return true; + } + + if (IsLatestAirDate(expression, out var lastAirDatePara, out var lastAirDateOperator)) + { + var para = lastAirDatePara is DateTime date ? date.ToString("yyyyMMdd") : lastAirDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)lastAirDateOperator, + ConditionType = (int)GroupFilterConditionType.LatestEpisodeAirDate, + ConditionParameter = para + }; + return true; + } + + if (IsSeriesCreatedDate(expression, out var seriesCreatedDatePara, out var seriesCreatedDateOperator)) + { + var para = seriesCreatedDatePara is DateTime date ? date.ToString("yyyyMMdd") : seriesCreatedDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)seriesCreatedDateOperator, + ConditionType = (int)GroupFilterConditionType.SeriesCreatedDate, + ConditionParameter = para + }; + return true; + } + + if (IsEpisodeAddedDate(expression, out var episodeAddedDatePara, out var episodeAddedDateOperator)) + { + var para = episodeAddedDatePara is DateTime date ? date.ToString("yyyyMMdd") : episodeAddedDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)episodeAddedDateOperator, + ConditionType = (int)GroupFilterConditionType.EpisodeAddedDate, + ConditionParameter = para + }; + return true; + } + + if (IsEpisodeWatchedDate(expression, out var episodeWatchedDatePara, out var episodeWatchedDateOperator)) + { + var para = episodeWatchedDatePara is DateTime date ? date.ToString("yyyyMMdd") : episodeWatchedDatePara.ToString(); + condition = new GroupFilterCondition + { + ConditionOperator = (int)episodeWatchedDateOperator, + ConditionType = (int)GroupFilterConditionType.EpisodeWatchedDate, + ConditionParameter = para + }; + return true; + } + + if (IsAniDBRating(expression, out var aniDBRatingPara, out var aniDBRatingOperator)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)aniDBRatingOperator, + ConditionType = (int)GroupFilterConditionType.AniDBRating, + ConditionParameter = aniDBRatingPara.ToString() + }; + return true; + } + + if (IsUserRating(expression, out var userRatingPara, out var userRatingOperator)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)userRatingOperator, + ConditionType = (int)GroupFilterConditionType.UserRating, + ConditionParameter = userRatingPara.ToString() + }; + return true; + } + + if (IsEpisodeCount(expression, out var episodeCountPara, out var episodeCountOperator)) + { + condition = new GroupFilterCondition + { + ConditionOperator = (int)episodeCountOperator, + ConditionType = (int)GroupFilterConditionType.EpisodeCount, + ConditionParameter = episodeCountPara.ToString() + }; + return true; + } + + return false; + } + + private static bool IsInTag(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasTagExpression), parameters); + } + + private static bool IsInCustomTag(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasCustomTagExpression), parameters); + } + + private static bool IsInAnimeType(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasAnimeTypeExpression), parameters); + } + + private static bool IsInVideoQuality(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasVideoSourceExpression), parameters); + } + + private static bool IsInSharedVideoQuality(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSharedVideoSourceExpression), parameters); + } + + private static bool IsInGroup(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasNameExpression), parameters); + } + + private static bool IsInYear(FilterExpression expression, out List<int> parameters) + { + parameters = new List<int>(); + return TryParseIn(expression, typeof(InYearExpression), parameters); + } + + private static bool IsInSeason(FilterExpression expression, out List<(int Year, string Season)> parameters) + { + parameters = new List<(int, string)>(); + return TryParseIn(expression, typeof(InSeasonExpression), parameters); + } + + private static bool IsInAudioLanguage(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasAudioLanguageExpression), parameters); + } + + private static bool IsInSubtitleLanguage(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSubtitleLanguageExpression), parameters); + } + + private static bool IsInSharedAudioLanguage(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSharedAudioLanguageExpression), parameters); + } + + private static bool IsInSharedSubtitleLanguage(FilterExpression expression, out List<string> parameters) + { + parameters = new List<string>(); + return TryParseIn(expression, typeof(HasSharedSubtitleLanguageExpression), parameters); + } + + private static bool IsAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(AirDateSelector), out parameter, out gfOperator); + } + + private static bool IsLatestAirDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(LastAirDateSelector), out parameter, out gfOperator); + } + + private static bool IsSeriesCreatedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(AddedDateSelector), out parameter, out gfOperator); + } + + private static bool IsEpisodeAddedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(LastAddedDateSelector), out parameter, out gfOperator); + } + + private static bool IsEpisodeWatchedDate(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(LastWatchedDateSelector), out parameter, out gfOperator); + } + + private static bool IsAniDBRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(HighestAniDBRatingSelector), out parameter, out gfOperator); + } + + private static bool IsUserRating(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(HighestUserRatingSelector), out parameter, out gfOperator); + } + + private static bool IsEpisodeCount(FilterExpression expression, out object parameter, out GroupFilterOperator gfOperator) + { + return TryParseComparator(expression, typeof(EpisodeCountSelector), out parameter, out gfOperator); + } + + private static bool TryParseIn<T>(FilterExpression expression, Type type, List<T> parameters) + { + if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters) && TryParseIn(or.Right, type, parameters); + if (expression.GetType() != type) return false; + + if (typeof(T) == typeof(string) && expression is IWithStringParameter withString) parameters.Add((T)(object)withString.Parameter); + else if (typeof(T) == typeof(DateTime) && expression is IWithDateParameter withDate) parameters.Add((T)(object)withDate.Parameter); + else if (typeof(T) == typeof(double) && expression is IWithNumberParameter withNumber) parameters.Add((T)(object)withNumber.Parameter); + else if (typeof(T) == typeof(TimeSpan) && expression is IWithTimeSpanParameter withTimeSpan) parameters.Add((T)(object)withTimeSpan.Parameter); + + return true; + + } + + private static bool TryParseIn<T,T1>(FilterExpression expression, Type type, List<(T, T1)> parameters) + { + if (expression is OrExpression or) return TryParseIn(or.Left, type, parameters) && TryParseIn(or.Right, type, parameters); + if (expression.GetType() != type) return false; + + T first = default; + T1 second = default; + if (typeof(T) == typeof(string) && expression is IWithStringParameter withString) first = (T)(object)withString.Parameter; + else if (typeof(T) == typeof(DateTime) && expression is IWithDateParameter withDate) first = (T)(object)withDate.Parameter; + else if (typeof(T) == typeof(int) && expression is IWithNumberParameter withInt) first = (T)(object)Convert.ToInt32(withInt.Parameter); + else if (typeof(T) == typeof(double) && expression is IWithNumberParameter withNumber) first = (T)(object)withNumber.Parameter; + else if (typeof(T) == typeof(TimeSpan) && expression is IWithTimeSpanParameter withTimeSpan) first = (T)(object)withTimeSpan.Parameter; + if (typeof(T1) == typeof(string) && expression is IWithSecondStringParameter withSecondString) second = (T1)(object)withSecondString.SecondParameter; + if (!EqualityComparer<T>.Default.Equals(first, default) && !EqualityComparer<T1>.Default.Equals(second, default)) parameters.Add((first, second)); + + return true; + + } + + private static bool TryParseComparator(FilterExpression expression, Type type, out object parameter, out GroupFilterOperator gfOperator) + { + // comparators share a similar format: + // Expression(selector, selector) or Expression(selector, parameter) + gfOperator = 0; + parameter = null; + switch (expression) + { + // These are inverted because the parameter is second, as compared to the Legacy method, which evaluated as parameter operator selector + case DateGreaterThanExpression dateGreater when dateGreater.Left?.GetType() != type: + return false; + case DateGreaterThanExpression dateGreater: + gfOperator = GroupFilterOperator.LessThan; + parameter = dateGreater.Parameter; + return true; + case DateGreaterThanEqualsExpression dateGreaterEquals when dateGreaterEquals.Left?.GetType() != type: + return false; + case DateGreaterThanEqualsExpression dateGreaterEquals: + { + if (dateGreaterEquals.Right is not DateDiffFunction f) return false; + if (f.Left is not DateAddFunction add) return false; + if (add.Left is not TodayFunction) return false; + if (add.Parameter != TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)) return false; + gfOperator = GroupFilterOperator.LastXDays; + parameter = f.Parameter.TotalDays; + return true; + } + case NumberGreaterThanExpression numberGreater when numberGreater.Left?.GetType() != type: + return false; + case NumberGreaterThanExpression numberGreater: + gfOperator = GroupFilterOperator.LessThan; + parameter = numberGreater.Parameter; + return true; + case DateLessThanExpression dateLess when dateLess.Left?.GetType() != type: + return false; + case DateLessThanExpression dateLess: + gfOperator = GroupFilterOperator.GreaterThan; + parameter = dateLess.Parameter; + return true; + case NumberLessThanExpression numberLess when numberLess.Left?.GetType() != type: + return false; + case NumberLessThanExpression numberLess: + gfOperator = GroupFilterOperator.GreaterThan; + parameter = numberLess.Parameter; + return true; + default: + return false; + } + } + + public static string GetSortingCriteria(FilterPreset filterPreset) + { + return string.Join("|", GetSortingCriteriaList(filterPreset).Select(a => (int)a.SortType + ";" + (int)a.SortDirection)); + } + + public static List<GroupFilterSortingCriteria> GetSortingCriteriaList(FilterPreset filter) + { + var results = new List<GroupFilterSortingCriteria>(); + var expression = filter.SortingExpression; + if (expression == null) + { + results.Add(new GroupFilterSortingCriteria + { + GroupFilterID = filter.FilterPresetID, + SortType = GroupFilterSorting.GroupName + }); + return results; + } + + var current = expression; + while (current != null) + { + var type = current.GetType(); + GroupFilterSorting sortType = 0; + if (type == typeof(AddedDateSortingSelector)) + sortType = GroupFilterSorting.SeriesAddedDate; + else if (type == typeof(LastAddedDateSortingSelector)) + sortType = GroupFilterSorting.EpisodeAddedDate; + else if (type == typeof(LastAirDateSortingSelector)) + sortType = GroupFilterSorting.EpisodeAirDate; + else if (type == typeof(LastWatchedDateSortingSelector)) + sortType = GroupFilterSorting.EpisodeWatchedDate; + else if (type == typeof(NameSortingSelector)) + sortType = GroupFilterSorting.GroupName; + else if (type == typeof(AirDateSortingSelector)) + sortType = GroupFilterSorting.Year; + else if (type == typeof(SeriesCountSortingSelector)) + sortType = GroupFilterSorting.SeriesCount; + else if (type == typeof(UnwatchedCountSortingSelector)) + sortType = GroupFilterSorting.UnwatchedEpisodeCount; + else if (type == typeof(MissingEpisodeCountSortingSelector)) + sortType = GroupFilterSorting.MissingEpisodeCount; + else if (type == typeof(HighestUserRatingSortingSelector)) + sortType = GroupFilterSorting.UserRating; + else if (type == typeof(HighestAniDBRatingSortingSelector)) + sortType = GroupFilterSorting.AniDBRating; + else if (type == typeof(SortingNameSortingSelector)) + sortType = GroupFilterSorting.SortName; + + if (sortType != 0) + { + results.Add(new GroupFilterSortingCriteria + { + GroupFilterID = filter.FilterPresetID, + SortType = sortType, + SortDirection = current.Descending ? GroupFilterSortDirection.Desc : GroupFilterSortDirection.Asc + }); + } + current = current.Next; + } + + return results; + } + + public static FilterExpression<bool> GetExpression(List<GroupFilterCondition> conditions, GroupFilterBaseCondition baseCondition, bool suppressErrors = false) + { + // forward compatibility is easier. Just map the old conditions to an expression + if (conditions == null || conditions.Count < 1) return null; + var first = conditions.Select((a, index) => new {Expression= GetExpression(a, suppressErrors), Index=index}).FirstOrDefault(a => a.Expression != null); + if (first == null) return null; + if (baseCondition == GroupFilterBaseCondition.Exclude) + { + return new NotExpression(conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => + { + var result = GetExpression(b, suppressErrors); + return result == null ? a : new OrExpression(a, result); + })); + } + + return conditions.Count == 1 ? first.Expression : conditions.Skip(first.Index + 1).Aggregate(first.Expression, (a, b) => + { + var result = GetExpression(b, suppressErrors); + return result == null ? a : new AndExpression(a, result); + }); + } + + private static FilterExpression<bool> GetExpression(GroupFilterCondition condition, bool suppressErrors = false) + { + var op = (GroupFilterOperator)condition.ConditionOperator; + var parameter = condition.ConditionParameter; + switch ((GroupFilterConditionType)condition.ConditionType) + { + case GroupFilterConditionType.CompletedSeries: + if (op == GroupFilterOperator.Include) + return LegacyMappings.GetCompletedExpression(); + return new NotExpression(LegacyMappings.GetCompletedExpression()); + case GroupFilterConditionType.FinishedAiring: + if (op == GroupFilterOperator.Include) + return new IsFinishedExpression(); + return new NotExpression(new IsFinishedExpression()); + case GroupFilterConditionType.MissingEpisodes: + if (op == GroupFilterOperator.Include) + return new HasMissingEpisodesExpression(); + return new NotExpression(new HasMissingEpisodesExpression()); + case GroupFilterConditionType.MissingEpisodesCollecting: + if (op == GroupFilterOperator.Include) + return new HasMissingEpisodesCollectingExpression(); + return new NotExpression(new HasMissingEpisodesCollectingExpression()); + case GroupFilterConditionType.HasUnwatchedEpisodes: + if (op == GroupFilterOperator.Include) + return new HasUnwatchedEpisodesExpression(); + return new NotExpression(new HasUnwatchedEpisodesExpression()); + case GroupFilterConditionType.HasWatchedEpisodes: + if (op == GroupFilterOperator.Include) + return new HasWatchedEpisodesExpression(); + return new NotExpression(new HasWatchedEpisodesExpression()); + case GroupFilterConditionType.UserVoted: + if (op == GroupFilterOperator.Include) + return new HasPermanentUserVotesExpression(); + return new NotExpression(new HasPermanentUserVotesExpression()); + case GroupFilterConditionType.UserVotedAny: + if (op == GroupFilterOperator.Include) + return new HasUserVotesExpression(); + return new NotExpression(new HasUserVotesExpression()); + case GroupFilterConditionType.Favourite: + if (op == GroupFilterOperator.Include) + return new IsFavoriteExpression(); + return new NotExpression(new IsFavoriteExpression()); + case GroupFilterConditionType.AssignedTvDBInfo: + if (op == GroupFilterOperator.Include) + return new HasTvDBLinkExpression(); + return new NotExpression(new HasTvDBLinkExpression()); + case GroupFilterConditionType.AssignedMovieDBInfo: + if (op == GroupFilterOperator.Include) + return new HasTMDbLinkExpression(); + return new NotExpression(new HasTMDbLinkExpression()); + case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: + if (op == GroupFilterOperator.Include) + return new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression()); + return new NotExpression(new OrExpression(new HasTvDBLinkExpression(), new HasTMDbLinkExpression())); + case GroupFilterConditionType.AssignedTraktInfo: + if (op == GroupFilterOperator.Include) + return new HasTraktLinkExpression(); + return new NotExpression(new HasTraktLinkExpression()); + case GroupFilterConditionType.Tag: + return LegacyMappings.GetTagExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.CustomTags: + return LegacyMappings.GetCustomTagExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AirDate: + return LegacyMappings.GetAirDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.LatestEpisodeAirDate: + return LegacyMappings.GetLastAirDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.SeriesCreatedDate: + return LegacyMappings.GetAddedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.EpisodeAddedDate: + return LegacyMappings.GetLastAddedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.EpisodeWatchedDate: + return LegacyMappings.GetWatchedDateExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AnimeType: + return LegacyMappings.GetAnimeTypeExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.VideoQuality: + return LegacyMappings.GetVideoQualityExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AudioLanguage: + return LegacyMappings.GetAudioLanguageExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.SubtitleLanguage: + return LegacyMappings.GetSubtitleLanguageExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AnimeGroup: + return LegacyMappings.GetGroupExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AniDBRating: + return LegacyMappings.GetRatingExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.UserRating: + return LegacyMappings.GetUserRatingExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.AssignedMALInfo: + return suppressErrors ? null : throw new NotSupportedException("MAL is Deprecated"); + case GroupFilterConditionType.EpisodeCount: + return LegacyMappings.GetEpisodeCountExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Year: + return LegacyMappings.GetYearExpression(op, parameter, suppressErrors); + case GroupFilterConditionType.Season: + return LegacyMappings.GetSeasonExpression(op, parameter, suppressErrors); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(condition), $@"ConditionType {(GroupFilterConditionType)condition.ConditionType} is not valid"); + } + } + + public static SortingExpression GetSortingExpression(List<GroupFilterSortingCriteria> sorting) + { + SortingExpression expression = null; + SortingExpression result = null; + foreach (var criteria in sorting) + { + SortingExpression expr = criteria.SortType switch + { + GroupFilterSorting.SeriesAddedDate => new AddedDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.EpisodeAddedDate => new LastAddedDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.EpisodeAirDate => new LastAirDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.EpisodeWatchedDate => new LastWatchedDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.GroupName => new NameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.Year => new AirDateSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.SeriesCount => new SeriesCountSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.UnwatchedEpisodeCount => new UnwatchedCountSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.MissingEpisodeCount => new MissingEpisodeCountSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.UserRating => new HighestUserRatingSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.AniDBRating => new HighestAniDBRatingSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.SortName => new SortingNameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + GroupFilterSorting.GroupFilterName => new NameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + }, + _ => new NameSortingSelector + { + Descending = criteria.SortDirection == GroupFilterSortDirection.Desc + } + }; + if (expression == null) result = expression = expr; + else expression.Next = expr; + expression = expression.Next; + } + + return result ?? new NameSortingSelector(); + } + + public static SortingExpression GetSortingExpression(string sorting) + { + if (string.IsNullOrEmpty(sorting)) return new NameSortingSelector(); + var sortCriteriaList = new List<GroupFilterSortingCriteria>(); + var scrit = sorting.Split('|'); + foreach (var sortpair in scrit) + { + var spair = sortpair.Split(';'); + if (spair.Length != 2) + { + continue; + } + + int.TryParse(spair[0], out var stype); + int.TryParse(spair[1], out var sdir); + + if (stype > 0 && sdir > 0) + { + var gfsc = new GroupFilterSortingCriteria + { + GroupFilterID = 0, + SortType = (GroupFilterSorting)stype, + SortDirection = (GroupFilterSortDirection)sdir + }; + sortCriteriaList.Add(gfsc); + } + } + + return GetSortingExpression(sortCriteriaList); + } +} diff --git a/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs new file mode 100644 index 000000000..4b95a1100 --- /dev/null +++ b/Shoko.Server/Filters/Legacy/LegacyFilterConverter.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Linq; +using Shoko.Models.Client; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Models; +using Shoko.Server.Repositories; + +namespace Shoko.Server.Filters.Legacy; + +public class LegacyFilterConverter +{ + private readonly FilterEvaluator _evaluator; + + public LegacyFilterConverter(FilterEvaluator evaluator) + { + _evaluator = evaluator; + } + + public FilterPreset FromClient(CL_GroupFilter model) + { + var expression = LegacyConditionConverter.GetExpression(model.FilterConditions, (GroupFilterBaseCondition)model.BaseCondition); + var filter = new FilterPreset + { + FilterPresetID = model.GroupFilterID, + ParentFilterPresetID = model.ParentGroupFilterID, + Name = model.GroupFilterName, + FilterType = (GroupFilterType)model.FilterType, + Hidden = model.InvisibleInClients == 1, + Locked = model.Locked == 1, + ApplyAtSeriesLevel = model.ApplyToSeries == 1, + SortingExpression = LegacyConditionConverter.GetSortingExpression(model.SortingCriteria), + Expression = expression + }; + return filter; + } + + public FilterPreset FromLegacy(GroupFilter model, List<GroupFilterCondition> conditions) + { + var expression = LegacyConditionConverter.GetExpression(conditions, (GroupFilterBaseCondition)model.BaseCondition); + var filter = new FilterPreset + { + Name = model.GroupFilterName, + FilterType = (GroupFilterType)model.FilterType, + Hidden = model.InvisibleInClients == 1, + Locked = model.Locked == 1, + ApplyAtSeriesLevel = model.ApplyToSeries == 1, + SortingExpression = LegacyConditionConverter.GetSortingExpression(model.SortingCriteria), + Expression = expression + }; + return filter; + } + + public CL_GroupFilter ToClient(FilterPreset filter) + { + if (filter == null) return null; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + if ((filter.Expression?.UserDependent ?? false) || (filter.SortingExpression?.UserDependent ?? false)) + { + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + var results = _evaluator.EvaluateFilter(filter, userID).ToList(); + groupIds[userID] = results.Select(a => a.Key).ToHashSet(); + seriesIds[userID] = results.SelectMany(a => a).ToHashSet(); + } + } + else + { + var results = _evaluator.EvaluateFilter(filter, null).ToList(); + var groupIdSet = results.Select(a => a.Key).ToHashSet(); + var seriesIdSet = results.SelectMany(a => a).ToHashSet(); + foreach (var userID in RepoFactory.JMMUser.GetAll().Select(a => a.JMMUserID)) + { + groupIds[userID] = groupIdSet; + seriesIds[userID] = seriesIdSet; + } + } + + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + conditions?.ForEach(condition => condition.GroupFilterID = filter.FilterPresetID); + var contract = new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }; + return contract; + } + + public Dictionary<FilterPreset, CL_GroupFilter> ToClient(IReadOnlyList<FilterPreset> filters) + { + var result = new Dictionary<FilterPreset, CL_GroupFilter>(); + var userFilters = filters.Where(a => (a?.Expression?.UserDependent ?? false) || (a?.SortingExpression?.UserDependent ?? false)).ToList(); + var otherFilters = filters.Except(userFilters).ToList(); + + // batch evaluate each list, then build the mappings + var userResults = RepoFactory.JMMUser.GetAll() + .SelectMany(user => _evaluator.BatchEvaluateFilters(userFilters, user.JMMUserID).Select(a => (a.Key, user.JMMUserID, a.Value))) + .GroupBy(a => a.Key, a => (a.JMMUserID, a.Value)); + var userModels = userResults.Select(group => + { + var filter = group.Key; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + foreach (var kv in group) + { + groupIds[kv.JMMUserID] = kv.Value.Select(a => a.Key).ToHashSet(); + seriesIds[kv.JMMUserID] = kv.Value.SelectMany(a => a).ToHashSet(); + } + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + conditions?.ForEach(condition => condition.GroupFilterID = filter.FilterPresetID); + return (Filter: filter, new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }); + }); + + foreach (var (filter, model) in userModels) + { + result[filter] = model; + } + + if (otherFilters.Count > 0) + { + var results = _evaluator.BatchEvaluateFilters(otherFilters, null); + var models = results.Select(kv => + { + var filter = kv.Key; + var groupIds = new Dictionary<int, HashSet<int>>(); + var seriesIds = new Dictionary<int, HashSet<int>>(); + var groupIdSet = kv.Value.Select(a => a.Key).ToHashSet(); + var seriesIdSet = kv.Value.SelectMany(a => a).ToHashSet(); + foreach (var user in RepoFactory.JMMUser.GetAll()) + { + groupIds[user.JMMUserID] = groupIdSet.Where(a => user.AllowedGroup(RepoFactory.AnimeGroup.GetByID(a))).ToHashSet(); + seriesIds[user.JMMUserID] = seriesIdSet.Where(a => user.AllowedSeries(RepoFactory.AnimeSeries.GetByID(a))).ToHashSet(); + } + + LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + conditions?.ForEach(condition => condition.GroupFilterID = filter.FilterPresetID); + return (Filter: filter, new CL_GroupFilter + { + GroupFilterID = filter.FilterPresetID, + GroupFilterName = filter.Name, + ApplyToSeries = filter.ApplyAtSeriesLevel ? 1 : 0, + Locked = filter.Locked ? 1 : 0, + FilterType = (int)filter.FilterType, + ParentGroupFilterID = filter.ParentFilterPresetID, + InvisibleInClients = filter.Hidden ? 1 : 0, + BaseCondition = (int)baseCondition, + FilterConditions = conditions, + SortingCriteria = LegacyConditionConverter.GetSortingCriteria(filter), + Groups = groupIds, + Series = seriesIds, + Childs = filter.FilterPresetID == 0 + ? new HashSet<int>() + : RepoFactory.FilterPreset.GetByParentID(filter.FilterPresetID).Select(a => a.FilterPresetID).ToHashSet() + }); + }); + + foreach (var (filter, model) in models) + { + result[filter] = model; + } + } + + return result; + } +} diff --git a/Shoko.Server/Filters/Legacy/LegacyMappings.cs b/Shoko.Server/Filters/Legacy/LegacyMappings.cs new file mode 100644 index 000000000..f3108241b --- /dev/null +++ b/Shoko.Server/Filters/Legacy/LegacyMappings.cs @@ -0,0 +1,483 @@ +using System; +using System.Linq; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Files; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Logic.Numbers; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.User; + +namespace Shoko.Server.Filters.Legacy; + +public class LegacyMappings +{ + public static bool TryParseDate(string sDate, out DateTime result) + { + // yyyyMMdd or yyyy-MM-dd + result = DateTime.Today; + if (sDate.Length != 8 && sDate.Length != 10) return false; + if (!int.TryParse(sDate[..4], out var year)) return false; + int month; + int day; + if (sDate.Length == 8) + { + if (!int.TryParse(sDate[4..6], out month)) return false; + if (!int.TryParse(sDate[6..8], out day)) return false; + } + else + { + if (!int.TryParse(sDate[5..7], out month)) return false; + if (!int.TryParse(sDate[8..10], out day)) return false; + } + + result = new DateTime(year, month, day); + return true; + } + + public static AndExpression GetCompletedExpression() + { + return new AndExpression( + new AndExpression(new NotExpression(new HasUnwatchedEpisodesExpression()), new NotExpression(new HasMissingEpisodesExpression())), + new IsFinishedExpression()); + } + + public static FilterExpression<bool> GetTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (tags.Length == 0) return suppressErrors ? null : throw new ArgumentException(@$"Parameter {parameter} was invalid", nameof(parameter)); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + if (tags.Length == 1) return new HasTagExpression(tags[0]); + + FilterExpression<bool> first = new HasTagExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasTagExpression(b))); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + if (tags.Length == 1) return new NotExpression(new HasTagExpression(tags[0])); + + FilterExpression<bool> first = new HasTagExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasTagExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Tags"); + } + } + + public static FilterExpression<bool> GetCustomTagExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasCustomTagExpression(tags[0]); + + FilterExpression<bool> first = new HasCustomTagExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasCustomTagExpression(b))); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasCustomTagExpression(tags[0])); + + FilterExpression<bool> first = new HasCustomTagExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasCustomTagExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Tags"); + } + } + + public static FilterExpression<bool> GetAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new AirDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetLastAirDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastAirDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new AddedDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetLastAddedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastAddedDateSelector(), op, parameter, suppressErrors); + } + + public static FilterExpression<bool> GetWatchedDateExpression(GroupFilterOperator op, string parameter, bool suppressErrors=false) + { + return GetDateExpression(new LastWatchedDateSelector(), op, parameter, suppressErrors); + } + + private static FilterExpression<bool> GetDateExpression(FilterExpression<DateTime?> selector, GroupFilterOperator op, string parameter, bool suppressErrors) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + switch (op) + { + case GroupFilterOperator.LastXDays: + { + if (!int.TryParse(parameter, out var lastX)) + return suppressErrors ? null : throw new ArgumentException(@"Parameter is not a number", nameof(parameter)); + return new DateGreaterThanEqualsExpression(selector, + new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), + TimeSpan.FromDays(lastX))); + } + case GroupFilterOperator.GreaterThan: + { + if (!TryParseDate(parameter, out var date)) + return suppressErrors + ? null + : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); + return new DateGreaterThanExpression(selector, date); + } + case GroupFilterOperator.LessThan: + { + if (!TryParseDate(parameter, out var date)) + return suppressErrors + ? null + : throw new ArgumentException($@"Parameter {parameter} was not a date in format of yyyyMMdd", nameof(parameter)); + return new DateGreaterThanExpression(selector, date); + } + default: + return suppressErrors + ? null + : throw new ArgumentOutOfRangeException(nameof(op), + $@"ConditionOperator {op} not applicable for Date filters"); + } + } + + public static FilterExpression<bool> GetVideoQualityExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + if (tags.Length <= 1) return new HasVideoSourceExpression(tags[0]); + + FilterExpression<bool> first = new HasVideoSourceExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasVideoSourceExpression(b))); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + if (tags.Length <= 1) return new NotExpression(new HasVideoSourceExpression(tags[0])); + + FilterExpression<bool> first = new HasVideoSourceExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasVideoSourceExpression(b)))); + } + case GroupFilterOperator.InAllEpisodes: + { + if (tags.Length <= 1) return new HasSharedVideoSourceExpression(tags[0]); + + FilterExpression<bool> first = new HasSharedVideoSourceExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSharedVideoSourceExpression(b))); + } + case GroupFilterOperator.NotInAllEpisodes: + { + if (tags.Length <= 1) return new NotExpression(new HasSharedVideoSourceExpression(tags[0])); + + FilterExpression<bool> first = new HasSharedVideoSourceExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSharedVideoSourceExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Video Quality"); + } + } + + public static FilterExpression<bool> GetAudioLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + { + if (tags.Length <= 1) return new HasAudioLanguageExpression(tags[0]); + + FilterExpression<bool> first = new HasAudioLanguageExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAudioLanguageExpression(b))); + } + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + { + if (tags.Length <= 1) return new NotExpression(new HasAudioLanguageExpression(tags[0])); + + FilterExpression<bool> first = new HasAudioLanguageExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAudioLanguageExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Audio Languages"); + } + } + + public static FilterExpression<bool> GetSubtitleLanguageExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + { + if (tags.Length <= 1) return new HasSubtitleLanguageExpression(tags[0]); + + FilterExpression<bool> first = new HasSubtitleLanguageExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSubtitleLanguageExpression(b))); + } + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + { + if (tags.Length <= 1) return new NotExpression(new HasSubtitleLanguageExpression(tags[0])); + + FilterExpression<bool> first = new HasSubtitleLanguageExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasSubtitleLanguageExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Subtitle Languages"); + } + } + + public static FilterExpression<bool> GetAnimeTypeExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + { + if (tags.Length <= 1) return new HasAnimeTypeExpression(tags[0].Replace(" ", "")); + + FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0].Replace(" ", "")); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b.Replace(" ", "")))); + } + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + { + if (tags.Length <= 1) return new NotExpression(new HasAnimeTypeExpression(tags[0].Replace(" ", ""))); + + FilterExpression<bool> first = new HasAnimeTypeExpression(tags[0].Replace(" ", "")); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasAnimeTypeExpression(b.Replace(" ", ""))))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Anime Type"); + } + } + + public static FilterExpression<bool> GetGroupExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + { + if (tags.Length <= 1) return new HasNameExpression(tags[0]); + + FilterExpression<bool> first = new HasNameExpression(tags[0]); + return tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasNameExpression(b))); + } + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + { + if (tags.Length <= 1) return new NotExpression(new HasNameExpression(tags[0])); + + FilterExpression<bool> first = new HasNameExpression(tags[0]); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => new OrExpression(a, new HasNameExpression(b)))); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Group Name"); + } + } + + public static FilterExpression<bool> GetRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!double.TryParse(parameter, out var rating)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLessThanExpression(new HighestAniDBRatingSelector(), rating); + case GroupFilterOperator.LessThan: + return new NumberGreaterThanExpression(new HighestAniDBRatingSelector(), rating); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Rating"); + } + } + + public static FilterExpression<bool> GetUserRatingExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!double.TryParse(parameter, out var rating)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLessThanExpression(new HighestUserRatingSelector(), rating); + case GroupFilterOperator.LessThan: + return new NumberGreaterThanExpression(new HighestUserRatingSelector(), rating); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for User Rating"); + } + } + + public static FilterExpression<bool> GetEpisodeCountExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (!int.TryParse(parameter, out var count)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + switch (op) + { + // These are reversed because we would consider that parameter is greater than the rating, but the expression takes a constant as the second operand + case GroupFilterOperator.GreaterThan: + return new NumberLessThanExpression(new EpisodeCountSelector(), count); + case GroupFilterOperator.LessThan: + return new NumberGreaterThanExpression(new HighestUserRatingSelector(), count); + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Episode Count"); + } + } + + public static FilterExpression<bool> GetYearExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.In: + case GroupFilterOperator.Include: + { + if (!int.TryParse(tags[0], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + if (tags.Length <= 1) return new InYearExpression(firstYear); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return tags.Skip(1).Aggregate(first, (a, b) => + { + if (!int.TryParse(b, out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + return new OrExpression(a, new InYearExpression(year)); + }); + } + case GroupFilterOperator.NotIn: + case GroupFilterOperator.Exclude: + { + if (!int.TryParse(tags[0], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + if (tags.Length <= 1) return new NotExpression(new InYearExpression(firstYear)); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => + { + if (!int.TryParse(b, out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} is not a number", nameof(parameter)); + return new OrExpression(a, new InYearExpression(year)); + })); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Years"); + } + } + + public static FilterExpression<bool> GetSeasonExpression(GroupFilterOperator op, string parameter, bool suppressErrors = false) + { + if (string.IsNullOrEmpty(parameter)) return suppressErrors ? null : throw new ArgumentNullException(nameof(parameter)); + var tags = parameter.Split(new[] + { + '|', ',' + }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + switch (op) + { + case GroupFilterOperator.Include: + case GroupFilterOperator.In: + { + var firstParts = tags[0].Split(' '); + if (!int.TryParse(firstParts[1], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(firstParts[0], out var firstSeason)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + if (tags.Length <= 1) return new InSeasonExpression(firstYear, firstSeason); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return tags.Skip(1).Aggregate(first, (a, b) => + { + var parts = b.Split(' '); + if (!int.TryParse(parts[1], out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(parts[0], out var season)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + return new OrExpression(a, new InSeasonExpression(year, season)); + }); + } + case GroupFilterOperator.Exclude: + case GroupFilterOperator.NotIn: + { + var firstParts = tags[0].Split(' '); + if (!int.TryParse(firstParts[1], out var firstYear)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(firstParts[0], out var firstSeason)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + if (tags.Length <= 1) return new NotExpression(new InSeasonExpression(firstYear, firstSeason)); + + FilterExpression<bool> first = new InYearExpression(firstYear); + return new NotExpression(tags.Skip(1).Aggregate(first, (a, b) => + { + var parts = b.Split(' '); + if (!int.TryParse(parts[1], out var year)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid year", nameof(parameter)); + if (!Enum.TryParse<AnimeSeason>(parts[0], out var season)) + return suppressErrors ? null : throw new ArgumentException($@"Parameter {parameter} does not have a valid season", nameof(parameter)); + return new OrExpression(a, new InSeasonExpression(year, season)); + })); + } + default: + return suppressErrors ? null : throw new ArgumentOutOfRangeException(nameof(op), $@"ConditionOperator {op} not applicable for Seasons"); + } + } +} diff --git a/Shoko.Server/Filters/Logic/AndExpression.cs b/Shoko.Server/Filters/Logic/AndExpression.cs new file mode 100644 index 000000000..7180a7429 --- /dev/null +++ b/Shoko.Server/Filters/Logic/AndExpression.cs @@ -0,0 +1,71 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic; + +public class AndExpression : FilterExpression<bool>, IWithExpressionParameter, IWithSecondExpressionParameter +{ + public AndExpression(FilterExpression<bool> left, FilterExpression<bool> right) + { + Left = left; + Right = right; + } + + public AndExpression() { } + + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; + public override bool UserDependent => Left.UserDependent || Right.UserDependent; + + public FilterExpression<bool> Left { get; set; } + public FilterExpression<bool> Right { get; set; } + + public override bool Evaluate(IFilterable filterable) + { + return Left.Evaluate(filterable) && Right.Evaluate(filterable); + } + + protected bool Equals(AndExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AndExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right); + } + + public static bool operator ==(AndExpression left, AndExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(AndExpression left, AndExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is AndExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs new file mode 100644 index 000000000..1405c7ef3 --- /dev/null +++ b/Shoko.Server/Filters/Logic/DateTimes/DateEqualsExpression.cs @@ -0,0 +1,94 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.DateTimes; + +public class DateEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter +{ + public DateEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + { + Left = left; + Right = right; + } + public DateEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public DateEqualsExpression() { } + + public FilterExpression<DateTime?> Left { get; set; } + public FilterExpression<DateTime?> Right { get; set; } + public DateTime Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) + { + return true; + } + + if (dateIsNull) + { + return false; + } + + if (operandIsNull) + { + return false; + } + + return (date > operand ? date - operand : operand - date).Value.TotalDays < 1; + } + + protected bool Equals(DateEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(DateEqualsExpression left, DateEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(DateEqualsExpression left, DateEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs new file mode 100644 index 000000000..0b1e64ea0 --- /dev/null +++ b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanEqualsExpression.cs @@ -0,0 +1,94 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.DateTimes; + +public class DateGreaterThanEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter +{ + public DateGreaterThanEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + { + Left = left; + Right = right; + } + public DateGreaterThanEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public DateGreaterThanEqualsExpression() { } + + public FilterExpression<DateTime?> Left { get; set; } + public FilterExpression<DateTime?> Right { get; set; } + public DateTime Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) + { + return true; + } + + if (dateIsNull) + { + return false; + } + + if (operandIsNull) + { + return false; + } + + return date >= operand; + } + + protected bool Equals(DateGreaterThanEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateGreaterThanEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(DateGreaterThanEqualsExpression left, DateGreaterThanEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(DateGreaterThanEqualsExpression left, DateGreaterThanEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateGreaterThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs new file mode 100644 index 000000000..1c10fa2be --- /dev/null +++ b/Shoko.Server/Filters/Logic/DateTimes/DateGreaterThanExpression.cs @@ -0,0 +1,94 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.DateTimes; + +public class DateGreaterThanExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter +{ + public DateGreaterThanExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + { + Left = left; + Right = right; + } + public DateGreaterThanExpression(FilterExpression<DateTime?> left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public DateGreaterThanExpression() { } + + public FilterExpression<DateTime?> Left { get; set; } + public FilterExpression<DateTime?> Right { get; set; } + public DateTime Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) + { + return false; + } + + if (dateIsNull) + { + return true; + } + + if (operandIsNull) + { + return true; + } + + return date > operand; + } + + protected bool Equals(DateGreaterThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateGreaterThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(DateGreaterThanExpression left, DateGreaterThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(DateGreaterThanExpression left, DateGreaterThanExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateGreaterThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs new file mode 100644 index 000000000..c4419741b --- /dev/null +++ b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanEqualsExpression.cs @@ -0,0 +1,87 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.DateTimes; + +public class DateLessThanEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter +{ + public DateLessThanEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + { + Left = left; + Right = right; + } + public DateLessThanEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public DateLessThanEqualsExpression() { } + + public FilterExpression<DateTime?> Left { get; set; } + public FilterExpression<DateTime?> Right { get; set; } + public DateTime Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + if (date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch) + { + return false; + } + + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + if (operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch) + { + return false; + } + + return date <= operand; + } + + protected bool Equals(DateLessThanEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateLessThanEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(DateLessThanEqualsExpression left, DateLessThanEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(DateLessThanEqualsExpression left, DateLessThanEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateLessThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs new file mode 100644 index 000000000..55f01d196 --- /dev/null +++ b/Shoko.Server/Filters/Logic/DateTimes/DateLessThanExpression.cs @@ -0,0 +1,94 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.DateTimes; + +public class DateLessThanExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter +{ + public DateLessThanExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + { + Left = left; + Right = right; + } + public DateLessThanExpression(FilterExpression<DateTime?> left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public DateLessThanExpression() { } + + public FilterExpression<DateTime?> Left { get; set; } + public FilterExpression<DateTime?> Right { get; set; } + public DateTime Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) + { + return true; + } + + if (dateIsNull) + { + return false; + } + + if (operandIsNull) + { + return false; + } + + return date < operand; + } + + protected bool Equals(DateLessThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateLessThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(DateLessThanExpression left, DateLessThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(DateLessThanExpression left, DateLessThanExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateLessThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs b/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs new file mode 100644 index 000000000..00b0b1c49 --- /dev/null +++ b/Shoko.Server/Filters/Logic/DateTimes/DateNotEqualsExpression.cs @@ -0,0 +1,94 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.DateTimes; + +public class DateNotEqualsExpression : FilterExpression<bool>, IWithDateSelectorParameter, IWithSecondDateSelectorParameter, IWithDateParameter +{ + public DateNotEqualsExpression(FilterExpression<DateTime?> left, FilterExpression<DateTime?> right) + { + Left = left; + Right = right; + } + public DateNotEqualsExpression(FilterExpression<DateTime?> left, DateTime parameter) + { + Left = left; + Parameter = parameter; + } + public DateNotEqualsExpression() { } + + public FilterExpression<DateTime?> Left { get; set; } + public FilterExpression<DateTime?> Right { get; set; } + public DateTime Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var date = Left.Evaluate(filterable); + var dateIsNull = date == null || date.Value == DateTime.MinValue || date.Value == DateTime.MaxValue || date.Value == DateTime.UnixEpoch; + var operand = Right == null ? Parameter : Right.Evaluate(filterable); + var operandIsNull = operand == null || operand.Value == DateTime.MinValue || operand.Value == DateTime.MaxValue || operand.Value == DateTime.UnixEpoch; + if (dateIsNull && operandIsNull) + { + return false; + } + + if (dateIsNull) + { + return true; + } + + if (operandIsNull) + { + return true; + } + + return (date > operand ? date - operand : operand - date).Value.TotalDays >= 1; + } + + protected bool Equals(DateNotEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter.Equals(other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((DateNotEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(DateNotEqualsExpression left, DateNotEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(DateNotEqualsExpression left, DateNotEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is DateNotEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/NotExpression.cs b/Shoko.Server/Filters/Logic/NotExpression.cs new file mode 100644 index 000000000..9686bbc37 --- /dev/null +++ b/Shoko.Server/Filters/Logic/NotExpression.cs @@ -0,0 +1,68 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic; + +public class NotExpression : FilterExpression<bool>, IWithExpressionParameter +{ + public NotExpression(FilterExpression<bool> left) + { + Left = left; + } + + public NotExpression() { } + public override bool TimeDependent => Left.TimeDependent; + public override bool UserDependent => Left.UserDependent; + + public FilterExpression<bool> Left { get; set; } + + public override bool Evaluate(IFilterable filterable) + { + return !Left.Evaluate(filterable); + } + + protected bool Equals(NotExpression other) + { + return base.Equals(other) && Equals(Left, other.Left); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NotExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left); + } + + public static bool operator ==(NotExpression left, NotExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NotExpression left, NotExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NotExpression exp && Left.IsType(exp.Left); + } +} diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs new file mode 100644 index 000000000..1fba5ac52 --- /dev/null +++ b/Shoko.Server/Filters/Logic/Numbers/NumberEqualsExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Numbers; + +public class NumberEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter +{ + public NumberEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) + { + Left = left; + Right = right; + } + public NumberEqualsExpression(FilterExpression<double> left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NumberEqualsExpression() { } + + public FilterExpression<double> Left { get; set; } + public FilterExpression<double> Right { get; set; } + public double Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; + return Math.Abs(left - right) < 0.001D; + } + + protected bool Equals(NumberEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NumberEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NumberEqualsExpression left, NumberEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NumberEqualsExpression left, NumberEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs new file mode 100644 index 000000000..762aa1cda --- /dev/null +++ b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanEqualsExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Numbers; + +public class NumberGreaterThanEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter +{ + public NumberGreaterThanEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) + { + Left = left; + Right = right; + } + public NumberGreaterThanEqualsExpression(FilterExpression<double> left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NumberGreaterThanEqualsExpression() { } + + public FilterExpression<double> Left { get; set; } + public FilterExpression<double> Right { get; set; } + public double Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; + return Math.Abs(left - right) < 0.001D || left > right; + } + + protected bool Equals(NumberGreaterThanEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NumberGreaterThanEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NumberGreaterThanEqualsExpression left, NumberGreaterThanEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NumberGreaterThanEqualsExpression left, NumberGreaterThanEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberGreaterThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs new file mode 100644 index 000000000..da2d67873 --- /dev/null +++ b/Shoko.Server/Filters/Logic/Numbers/NumberGreaterThanExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Numbers; + +public class NumberGreaterThanExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter +{ + public NumberGreaterThanExpression(FilterExpression<double> left, FilterExpression<double> right) + { + Left = left; + Right = right; + } + public NumberGreaterThanExpression(FilterExpression<double> left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NumberGreaterThanExpression() { } + + public FilterExpression<double> Left { get; set; } + public FilterExpression<double> Right { get; set; } + public double Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; + return left > right; + } + + protected bool Equals(NumberGreaterThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NumberGreaterThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NumberGreaterThanExpression left, NumberGreaterThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NumberGreaterThanExpression left, NumberGreaterThanExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberGreaterThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs new file mode 100644 index 000000000..6b72be3dc --- /dev/null +++ b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanEqualsExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Numbers; + +public class NumberLessThanEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter +{ + public NumberLessThanEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) + { + Left = left; + Right = right; + } + public NumberLessThanEqualsExpression(FilterExpression<double> left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NumberLessThanEqualsExpression() { } + + public FilterExpression<double> Left { get; set; } + public FilterExpression<double> Right { get; set; } + public double Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; + return Math.Abs(left - right) < 0.001D || left < right; + } + + protected bool Equals(NumberLessThanEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NumberLessThanEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NumberLessThanEqualsExpression left, NumberLessThanEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NumberLessThanEqualsExpression left, NumberLessThanEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberLessThanEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs new file mode 100644 index 000000000..b53da6fa8 --- /dev/null +++ b/Shoko.Server/Filters/Logic/Numbers/NumberLessThanExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Numbers; + +public class NumberLessThanExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter +{ + public NumberLessThanExpression(FilterExpression<double> left, FilterExpression<double> right) + { + Left = left; + Right = right; + } + public NumberLessThanExpression(FilterExpression<double> left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NumberLessThanExpression() { } + + public FilterExpression<double> Left { get; set; } + public FilterExpression<double> Right { get; set; } + public double Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; + return left < right; + } + + protected bool Equals(NumberLessThanExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NumberLessThanExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NumberLessThanExpression left, NumberLessThanExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NumberLessThanExpression left, NumberLessThanExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberLessThanExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs b/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs new file mode 100644 index 000000000..23f07d9a6 --- /dev/null +++ b/Shoko.Server/Filters/Logic/Numbers/NumberNotEqualsExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Numbers; + +public class NumberNotEqualsExpression : FilterExpression<bool>, IWithNumberSelectorParameter, IWithSecondNumberSelectorParameter, IWithNumberParameter +{ + public NumberNotEqualsExpression(FilterExpression<double> left, FilterExpression<double> right) + { + Left = left; + Right = right; + } + public NumberNotEqualsExpression(FilterExpression<double> left, double parameter) + { + Left = left; + Parameter = parameter; + } + public NumberNotEqualsExpression() { } + + public FilterExpression<double> Left { get; set; } + public FilterExpression<double> Right { get; set; } + public double Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Right?.Evaluate(filterable) ?? Parameter; + return Math.Abs(left - right) >= 0.001D; + } + + protected bool Equals(NumberNotEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Nullable.Equals(Parameter, other.Parameter); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((NumberNotEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(NumberNotEqualsExpression left, NumberNotEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(NumberNotEqualsExpression left, NumberNotEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is NumberNotEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/OrExpression.cs b/Shoko.Server/Filters/Logic/OrExpression.cs new file mode 100644 index 000000000..1ea617c9b --- /dev/null +++ b/Shoko.Server/Filters/Logic/OrExpression.cs @@ -0,0 +1,71 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic; + +public class OrExpression : FilterExpression<bool>, IWithExpressionParameter, IWithSecondExpressionParameter +{ + public OrExpression(FilterExpression<bool> left, FilterExpression<bool> right) + { + Left = left; + Right = right; + } + + public OrExpression() { } + + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; + public override bool UserDependent => Left.UserDependent || Right.UserDependent; + + public FilterExpression<bool> Left { get; set; } + public FilterExpression<bool> Right { get; set; } + + public override bool Evaluate(IFilterable filterable) + { + return Left.Evaluate(filterable) || Right.Evaluate(filterable); + } + + protected bool Equals(OrExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((OrExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right); + } + + public static bool operator ==(OrExpression left, OrExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(OrExpression left, OrExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is OrExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs new file mode 100644 index 000000000..42f59c9d0 --- /dev/null +++ b/Shoko.Server/Filters/Logic/Strings/StringContainsExpression.cs @@ -0,0 +1,84 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Strings; + +public class StringContainsExpression : FilterExpression<bool>, IWithStringSelectorParameter, IWithSecondStringSelectorParameter, IWithStringParameter +{ + public StringContainsExpression(FilterExpression<string> left, FilterExpression<string> right) + { + Left = left; + Right = right; + } + + public StringContainsExpression(FilterExpression<string> left, string parameter) + { + Left = left; + Parameter = parameter; + } + + public StringContainsExpression() { } + + public FilterExpression<string> Left { get; set; } + public FilterExpression<string> Right { get; set; } + public string Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right?.Evaluate(filterable); + if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) + { + return false; + } + + return left.Contains(right, StringComparison.InvariantCultureIgnoreCase); + } + + protected bool Equals(StringContainsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((StringContainsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(StringContainsExpression left, StringContainsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(StringContainsExpression left, StringContainsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is StringContainsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs new file mode 100644 index 000000000..2e3e1bfcd --- /dev/null +++ b/Shoko.Server/Filters/Logic/Strings/StringEqualsExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Strings; + +public class StringEqualsExpression : FilterExpression<bool>, IWithStringSelectorParameter, IWithSecondStringSelectorParameter, IWithStringParameter +{ + public StringEqualsExpression(FilterExpression<string> left, FilterExpression<string> right) + { + Left = left; + Right = right; + } + public StringEqualsExpression(FilterExpression<string> left, string parameter) + { + Left = left; + Parameter = parameter; + } + public StringEqualsExpression() { } + + public FilterExpression<string> Left { get; set; } + public FilterExpression<string> Right { get; set; } + public string Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right?.Evaluate(filterable); + return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); + } + + protected bool Equals(StringEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((StringEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(StringEqualsExpression left, StringEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(StringEqualsExpression left, StringEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is StringEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs b/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs new file mode 100644 index 000000000..a578807eb --- /dev/null +++ b/Shoko.Server/Filters/Logic/Strings/StringNotEqualsExpression.cs @@ -0,0 +1,77 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic.Strings; + +public class StringNotEqualsExpression : FilterExpression<bool>, IWithStringSelectorParameter, IWithSecondStringSelectorParameter, IWithStringParameter +{ + public StringNotEqualsExpression(FilterExpression<string> left, FilterExpression<string> right) + { + Left = left; + Right = right; + } + public StringNotEqualsExpression(FilterExpression<string> left, string parameter) + { + Left = left; + Parameter = parameter; + } + public StringNotEqualsExpression() { } + + public FilterExpression<string> Left { get; set; } + public FilterExpression<string> Right { get; set; } + public string Parameter { get; set; } + public override bool TimeDependent => Left.TimeDependent || (Right?.TimeDependent ?? false); + public override bool UserDependent => Left.UserDependent || (Right?.UserDependent ?? false); + + public override bool Evaluate(IFilterable filterable) + { + var left = Left.Evaluate(filterable); + var right = Parameter ?? Right?.Evaluate(filterable); + return !string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); + } + + protected bool Equals(StringNotEqualsExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right) && Parameter == other.Parameter; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((StringNotEqualsExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right, Parameter); + } + + public static bool operator ==(StringNotEqualsExpression left, StringNotEqualsExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(StringNotEqualsExpression left, StringNotEqualsExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is StringNotEqualsExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Logic/XorExpression.cs b/Shoko.Server/Filters/Logic/XorExpression.cs new file mode 100644 index 000000000..50e014b78 --- /dev/null +++ b/Shoko.Server/Filters/Logic/XorExpression.cs @@ -0,0 +1,71 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Logic; + +public class XorExpression : FilterExpression<bool>, IWithExpressionParameter, IWithSecondExpressionParameter +{ + public XorExpression(FilterExpression<bool> left, FilterExpression<bool> right) + { + Left = left; + Right = right; + } + + public XorExpression() { } + + public override bool TimeDependent => Left.TimeDependent || Right.TimeDependent; + public override bool UserDependent => Left.UserDependent || Right.UserDependent; + + public FilterExpression<bool> Left { get; set; } + public FilterExpression<bool> Right { get; set; } + + public override bool Evaluate(IFilterable filterable) + { + return Left.Evaluate(filterable) ^ Right.Evaluate(filterable); + } + + protected bool Equals(XorExpression other) + { + return base.Equals(other) && Equals(Left, other.Left) && Equals(Right, other.Right); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((XorExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Left, Right); + } + + public static bool operator ==(XorExpression left, XorExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(XorExpression left, XorExpression right) + { + return !Equals(left, right); + } + + public override bool IsType(FilterExpression expression) + { + return expression is XorExpression exp && Left.IsType(exp.Left) && (Right?.IsType(exp.Right) ?? true); + } +} diff --git a/Shoko.Server/Filters/Selectors/AddedDateSelector.cs b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs new file mode 100644 index 000000000..b52a97085 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/AddedDateSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class AddedDateSelector : FilterExpression<DateTime?> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override DateTime? Evaluate(IFilterable f) + { + return f.AddedDate; + } + + protected bool Equals(AddedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AddedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AddedDateSelector left, AddedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AddedDateSelector left, AddedDateSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/AirDateSelector.cs b/Shoko.Server/Filters/Selectors/AirDateSelector.cs new file mode 100644 index 000000000..cd92486c3 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/AirDateSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class AirDateSelector : FilterExpression<DateTime?> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override DateTime? Evaluate(IFilterable f) + { + return f.AirDate; + } + + protected bool Equals(AirDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AirDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AirDateSelector left, AirDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AirDateSelector left, AirDateSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs new file mode 100644 index 000000000..b1440d9e9 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/AudioLanguageCountSelector.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class AudioLanguageCountSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return f.AudioLanguages.Count; + } + + protected bool Equals(AudioLanguageCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((AudioLanguageCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(AudioLanguageCountSelector left, AudioLanguageCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(AudioLanguageCountSelector left, AudioLanguageCountSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs new file mode 100644 index 000000000..103942d2a --- /dev/null +++ b/Shoko.Server/Filters/Selectors/EpisodeCountSelector.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class EpisodeCountSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return f.EpisodeCount; + } + + protected bool Equals(EpisodeCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((EpisodeCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(EpisodeCountSelector left, EpisodeCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(EpisodeCountSelector left, EpisodeCountSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs new file mode 100644 index 000000000..195059410 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/HighestAniDBRatingSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class HighestAniDBRatingSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return Convert.ToDouble(f.HighestAniDBRating); + } + + protected bool Equals(HighestAniDBRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HighestAniDBRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HighestAniDBRatingSelector left, HighestAniDBRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(HighestAniDBRatingSelector left, HighestAniDBRatingSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs new file mode 100644 index 000000000..5fdc87660 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/HighestUserRatingSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class HighestUserRatingSelector : UserDependentFilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override double Evaluate(IUserDependentFilterable f) + { + return Convert.ToDouble(f.HighestUserRating); + } + + protected bool Equals(HighestUserRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HighestUserRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HighestUserRatingSelector left, HighestUserRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(HighestUserRatingSelector left, HighestUserRatingSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs new file mode 100644 index 000000000..982378940 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/LastAddedDateSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class LastAddedDateSelector : FilterExpression<DateTime?> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override DateTime? Evaluate(IFilterable f) + { + return f.LastAddedDate; + } + + protected bool Equals(LastAddedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LastAddedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LastAddedDateSelector left, LastAddedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LastAddedDateSelector left, LastAddedDateSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs new file mode 100644 index 000000000..ca6ae648b --- /dev/null +++ b/Shoko.Server/Filters/Selectors/LastAirDateSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class LastAirDateSelector : FilterExpression<DateTime?> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override DateTime? Evaluate(IFilterable f) + { + return f.LastAirDate; + } + + protected bool Equals(LastAirDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LastAirDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LastAirDateSelector left, LastAirDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LastAirDateSelector left, LastAirDateSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs new file mode 100644 index 000000000..e1054fca4 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/LastWatchedDateSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class LastWatchedDateSelector : UserDependentFilterExpression<DateTime?> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override DateTime? Evaluate(IUserDependentFilterable f) + { + return f.LastWatchedDate; + } + + protected bool Equals(LastWatchedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LastWatchedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LastWatchedDateSelector left, LastWatchedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LastWatchedDateSelector left, LastWatchedDateSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs new file mode 100644 index 000000000..40db18d91 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/LowestAniDBRatingSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class LowestAniDBRatingSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return Convert.ToDouble(f.LowestAniDBRating); + } + + protected bool Equals(LowestAniDBRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LowestAniDBRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LowestAniDBRatingSelector left, LowestAniDBRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LowestAniDBRatingSelector left, LowestAniDBRatingSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs new file mode 100644 index 000000000..6026592e6 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/LowestUserRatingSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class LowestUserRatingSelector : UserDependentFilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override double Evaluate(IUserDependentFilterable f) + { + return Convert.ToDouble(f.LowestUserRating); + } + + protected bool Equals(LowestUserRatingSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((LowestUserRatingSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(LowestUserRatingSelector left, LowestUserRatingSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(LowestUserRatingSelector left, LowestUserRatingSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs b/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs new file mode 100644 index 000000000..c1b78cad4 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/SeriesCountSelector.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class SeriesCountSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return f.SeriesCount; + } + + protected bool Equals(SeriesCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((SeriesCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(SeriesCountSelector left, SeriesCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(SeriesCountSelector left, SeriesCountSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs new file mode 100644 index 000000000..07f6b6f0c --- /dev/null +++ b/Shoko.Server/Filters/Selectors/SubtitleLanguageCountSelector.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class SubtitleLanguageCountSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return f.SubtitleLanguages.Count; + } + + protected bool Equals(SubtitleLanguageCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((SubtitleLanguageCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(SubtitleLanguageCountSelector left, SubtitleLanguageCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(SubtitleLanguageCountSelector left, SubtitleLanguageCountSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs new file mode 100644 index 000000000..60560f0b9 --- /dev/null +++ b/Shoko.Server/Filters/Selectors/TotalEpisodeCountSelector.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class TotalEpisodeCountSelector : FilterExpression<double> +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override double Evaluate(IFilterable f) + { + return f.TotalEpisodeCount; + } + + protected bool Equals(TotalEpisodeCountSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((TotalEpisodeCountSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(TotalEpisodeCountSelector left, TotalEpisodeCountSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(TotalEpisodeCountSelector left, TotalEpisodeCountSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs new file mode 100644 index 000000000..20cbcd23b --- /dev/null +++ b/Shoko.Server/Filters/Selectors/WatchedDateSelector.cs @@ -0,0 +1,55 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.Selectors; + +public class WatchedDateSelector : UserDependentFilterExpression<DateTime?> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override DateTime? Evaluate(IUserDependentFilterable f) + { + return f.WatchedDate; + } + + protected bool Equals(WatchedDateSelector other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((WatchedDateSelector)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(WatchedDateSelector left, WatchedDateSelector right) + { + return Equals(left, right); + } + + public static bool operator !=(WatchedDateSelector left, WatchedDateSelector right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/SortingExpression.cs b/Shoko.Server/Filters/SortingExpression.cs new file mode 100644 index 000000000..bbc663c14 --- /dev/null +++ b/Shoko.Server/Filters/SortingExpression.cs @@ -0,0 +1,9 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public abstract class SortingExpression : FilterExpression<object>, ISortingExpression +{ + public bool Descending { get; set; } // take advantage of default(bool) being false + public SortingExpression Next { get; set; } +} diff --git a/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs new file mode 100644 index 000000000..248eec808 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/AddedDateSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class AddedDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.AddedDate; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs new file mode 100644 index 000000000..b999b0786 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/AirDateSortingSelector.cs @@ -0,0 +1,16 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class AirDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public DateTime DefaultValue { get; set; } + + public override object Evaluate(IFilterable f) + { + return f.AirDate ?? DefaultValue; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs new file mode 100644 index 000000000..f29c4951a --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/AudioLanguageCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class AudioLanguageCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.AudioLanguages.Count; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs new file mode 100644 index 000000000..63fd55819 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/EpisodeCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class EpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.EpisodeCount; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs new file mode 100644 index 000000000..6f49b0387 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/HighestAniDBRatingSortingSelector.cs @@ -0,0 +1,15 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class HighestAniDBRatingSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return Convert.ToDouble(f.HighestAniDBRating); + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs new file mode 100644 index 000000000..0656e729d --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/HighestUserRatingSortingSelector.cs @@ -0,0 +1,15 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class HighestUserRatingSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override object Evaluate(IUserDependentFilterable f) + { + return Convert.ToDouble(f.HighestUserRating); + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs new file mode 100644 index 000000000..5c38f6364 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LastAddedDateSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LastAddedDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.LastAddedDate; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs new file mode 100644 index 000000000..3d8d49317 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LastAirDateSortingSelector.cs @@ -0,0 +1,16 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LastAirDateSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + public DateTime DefaultValue { get; set; } + + public override object Evaluate(IFilterable f) + { + return f.LastAirDate ?? DefaultValue; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs new file mode 100644 index 000000000..b14ae0eaf --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LastWatchedDateSortingSelector.cs @@ -0,0 +1,16 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LastWatchedDateSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public DateTime DefaultValue { get; set; } + + public override object Evaluate(IUserDependentFilterable f) + { + return f.LastWatchedDate ?? DefaultValue; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs new file mode 100644 index 000000000..d8f479cb2 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LowestAniDBRatingSortingSelector.cs @@ -0,0 +1,15 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LowestAniDBRatingSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return Convert.ToDouble(f.LowestAniDBRating); + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs new file mode 100644 index 000000000..6fe4db933 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/LowestUserRatingSortingSelector.cs @@ -0,0 +1,15 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class LowestUserRatingSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override object Evaluate(IUserDependentFilterable f) + { + return Convert.ToDouble(f.LowestUserRating); + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs new file mode 100644 index 000000000..dee2b5ca5 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCollectingCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class MissingEpisodeCollectingCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.MissingEpisodesCollecting; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs new file mode 100644 index 000000000..02a079504 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/MissingEpisodeCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class MissingEpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.MissingEpisodes; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs new file mode 100644 index 000000000..19cbb8ccf --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/NameSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class NameSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override string Evaluate(IFilterable f) + { + return f.Name; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs new file mode 100644 index 000000000..73e91c6f4 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/SeriesCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class SeriesCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.SeriesCount; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs new file mode 100644 index 000000000..eb9bcef22 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/SortingNameSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class SortingNameSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.SortingName; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs new file mode 100644 index 000000000..72e327c89 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/SubtitleLanguageCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class SubtitleLanguageCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.SubtitleLanguages.Count; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs new file mode 100644 index 000000000..c5d32dde6 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/TotalEpisodeCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class TotalEpisodeCountSortingSelector : SortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => false; + + public override object Evaluate(IFilterable f) + { + return f.TotalEpisodeCount; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs new file mode 100644 index 000000000..fa1fa1207 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/UnwatchedCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class UnwatchedCountSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override object Evaluate(IUserDependentFilterable f) + { + return f.UnwatchedEpisodes; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs new file mode 100644 index 000000000..ffae07bfe --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/WatchedCountSortingSelector.cs @@ -0,0 +1,14 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class WatchedCountSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override object Evaluate(IUserDependentFilterable f) + { + return f.WatchedEpisodes; + } +} diff --git a/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs new file mode 100644 index 000000000..84c6670a4 --- /dev/null +++ b/Shoko.Server/Filters/SortingSelectors/WatchedDateSortingSelector.cs @@ -0,0 +1,16 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.SortingSelectors; + +public class WatchedDateSortingSelector : UserDependentSortingExpression +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + public DateTime DefaultValue { get; set; } + + public override object Evaluate(IUserDependentFilterable f) + { + return f.WatchedDate ?? DefaultValue; + } +} diff --git a/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs new file mode 100644 index 000000000..7306d4061 --- /dev/null +++ b/Shoko.Server/Filters/User/HasPermanentUserVotesExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.User; + +public class HasPermanentUserVotesExpression : UserDependentFilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override bool Evaluate(IUserDependentFilterable filterable) + { + return filterable.HasPermanentVotes; + } + + protected bool Equals(HasPermanentUserVotesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasPermanentUserVotesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasPermanentUserVotesExpression left, HasPermanentUserVotesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasPermanentUserVotesExpression left, HasPermanentUserVotesExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs new file mode 100644 index 000000000..f3bdda9b4 --- /dev/null +++ b/Shoko.Server/Filters/User/HasUnwatchedEpisodesExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.User; + +public class HasUnwatchedEpisodesExpression : UserDependentFilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override bool Evaluate(IUserDependentFilterable filterable) + { + return filterable.UnwatchedEpisodes > 0; + } + + protected bool Equals(HasUnwatchedEpisodesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasUnwatchedEpisodesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasUnwatchedEpisodesExpression left, HasUnwatchedEpisodesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasUnwatchedEpisodesExpression left, HasUnwatchedEpisodesExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/User/HasUserVotesExpression.cs b/Shoko.Server/Filters/User/HasUserVotesExpression.cs new file mode 100644 index 000000000..436cbb519 --- /dev/null +++ b/Shoko.Server/Filters/User/HasUserVotesExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.User; + +public class HasUserVotesExpression : UserDependentFilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override bool Evaluate(IUserDependentFilterable filterable) + { + return filterable.HasVotes; + } + + protected bool Equals(HasUserVotesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasUserVotesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasUserVotesExpression left, HasUserVotesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasUserVotesExpression left, HasUserVotesExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs new file mode 100644 index 000000000..328b9690f --- /dev/null +++ b/Shoko.Server/Filters/User/HasWatchedEpisodesExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.User; + +public class HasWatchedEpisodesExpression : UserDependentFilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override bool Evaluate(IUserDependentFilterable filterable) + { + return filterable.WatchedEpisodes > 0; + } + + protected bool Equals(HasWatchedEpisodesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((HasWatchedEpisodesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(HasWatchedEpisodesExpression left, HasWatchedEpisodesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(HasWatchedEpisodesExpression left, HasWatchedEpisodesExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/User/IsFavoriteExpression.cs b/Shoko.Server/Filters/User/IsFavoriteExpression.cs new file mode 100644 index 000000000..38a585196 --- /dev/null +++ b/Shoko.Server/Filters/User/IsFavoriteExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.User; + +public class IsFavoriteExpression : UserDependentFilterExpression<bool> +{ + public override bool TimeDependent => false; + public override bool UserDependent => true; + + public override bool Evaluate(IUserDependentFilterable filterable) + { + return filterable.IsFavorite; + } + + protected bool Equals(IsFavoriteExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((IsFavoriteExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(IsFavoriteExpression left, IsFavoriteExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(IsFavoriteExpression left, IsFavoriteExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs new file mode 100644 index 000000000..8d149e981 --- /dev/null +++ b/Shoko.Server/Filters/User/MissingPermanentUserVotesExpression.cs @@ -0,0 +1,54 @@ +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters.User; + +public class MissingPermanentUserVotesExpression : UserDependentFilterExpression<bool> +{ + public override bool TimeDependent => true; + public override bool UserDependent => true; + + public override bool Evaluate(IUserDependentFilterable filterable) + { + return filterable.MissingPermanentVotes; + } + + protected bool Equals(MissingPermanentUserVotesExpression other) + { + return base.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MissingPermanentUserVotesExpression)obj); + } + + public override int GetHashCode() + { + return GetType().FullName!.GetHashCode(); + } + + public static bool operator ==(MissingPermanentUserVotesExpression left, MissingPermanentUserVotesExpression right) + { + return Equals(left, right); + } + + public static bool operator !=(MissingPermanentUserVotesExpression left, MissingPermanentUserVotesExpression right) + { + return !Equals(left, right); + } +} diff --git a/Shoko.Server/Filters/UserDependentFilterExpression.cs b/Shoko.Server/Filters/UserDependentFilterExpression.cs new file mode 100644 index 000000000..e90b02217 --- /dev/null +++ b/Shoko.Server/Filters/UserDependentFilterExpression.cs @@ -0,0 +1,20 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public abstract class UserDependentFilterExpression<T> : FilterExpression<T>, IUserDependentFilterExpression<T> +{ + + public abstract T Evaluate(IUserDependentFilterable f); + + public override T Evaluate(IFilterable f) + { + if (UserDependent && f is not IUserDependentFilterable) + { + throw new ArgumentException("User Dependent Filter was given an Filterable, rather than an UserDependentFilterable"); + } + + return Evaluate((IUserDependentFilterable)f); + } +} diff --git a/Shoko.Server/Filters/UserDependentFilterable.cs b/Shoko.Server/Filters/UserDependentFilterable.cs new file mode 100644 index 000000000..19b34918d --- /dev/null +++ b/Shoko.Server/Filters/UserDependentFilterable.cs @@ -0,0 +1,198 @@ +using System; +using System.Threading; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public class UserDependentFilterable : Filterable, IUserDependentFilterable +{ + private readonly Lazy<bool> _hasPermanentVotes; + private readonly Func<bool> _hasPermanentVotesDelegate; + + private readonly Lazy<bool> _hasVotes; + private readonly Func<bool> _hasVotesDelegate; + + private readonly Lazy<decimal> _highestUserRating; + private readonly Func<decimal> _highestUserRatingDelegate; + + private readonly Lazy<bool> _isFavorite; + private readonly Func<bool> _isFavoriteDelegate; + + private readonly Lazy<DateTime?> _lastWatchedDate; + private readonly Func<DateTime?> _lastWatchedDateDelegate; + + private readonly Lazy<decimal> _lowestUserRating; + private readonly Func<decimal> _lowestUserRatingDelegate; + + private readonly Lazy<bool> _missingPermanentVotes; + private readonly Func<bool> _missingPermanentVotesDelegate; + + private readonly Lazy<int> _unwatchedEpisodes; + private readonly Func<int> _unwatchedEpisodesDelegate; + + private readonly Lazy<DateTime?> _watchedDate; + private readonly Func<DateTime?> _watchedDateDelegate; + + private readonly Lazy<int> _watchedEpisodes; + private readonly Func<int> _watchedEpisodesDelegate; + + public bool IsFavorite + { + get => _isFavorite.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> IsFavoriteDelegate + { + get => _isFavoriteDelegate; + init + { + _isFavoriteDelegate = value; + _isFavorite = new Lazy<bool>(_isFavoriteDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int WatchedEpisodes + { + get => _watchedEpisodes.Value; + init => throw new NotSupportedException(); + } + + public Func<int> WatchedEpisodesDelegate + { + get => _watchedEpisodesDelegate; + init + { + _watchedEpisodesDelegate = value; + _watchedEpisodes = new Lazy<int>(_watchedEpisodesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public int UnwatchedEpisodes + { + get => _unwatchedEpisodes.Value; + init => throw new NotSupportedException(); + } + + public Func<int> UnwatchedEpisodesDelegate + { + get => _unwatchedEpisodesDelegate; + init + { + _unwatchedEpisodesDelegate = value; + _unwatchedEpisodes = new Lazy<int>(_unwatchedEpisodesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasVotes + { + get => _hasVotes.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasVotesDelegate + { + get => _hasVotesDelegate; + init + { + _hasVotesDelegate = value; + _hasVotes = new Lazy<bool>(_hasVotesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool HasPermanentVotes + { + get => _hasPermanentVotes.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> HasPermanentVotesDelegate + { + get => _hasPermanentVotesDelegate; + init + { + _hasPermanentVotesDelegate = value; + _hasPermanentVotes = new Lazy<bool>(_hasPermanentVotesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public bool MissingPermanentVotes + { + get => _missingPermanentVotes.Value; + init => throw new NotSupportedException(); + } + + public Func<bool> MissingPermanentVotesDelegate + { + get => _missingPermanentVotesDelegate; + init + { + _missingPermanentVotesDelegate = value; + _missingPermanentVotes = new Lazy<bool>(_missingPermanentVotesDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public DateTime? WatchedDate + { + get => _watchedDate.Value; + init => throw new NotSupportedException(); + } + + public Func<DateTime?> WatchedDateDelegate + { + get => _watchedDateDelegate; + init + { + _watchedDateDelegate = value; + _watchedDate = new Lazy<DateTime?>(_watchedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public DateTime? LastWatchedDate + { + get => _lastWatchedDate.Value; + init => throw new NotSupportedException(); + } + + public Func<DateTime?> LastWatchedDateDelegate + { + get => _lastWatchedDateDelegate; + init + { + _lastWatchedDateDelegate = value; + _lastWatchedDate = new Lazy<DateTime?>(_lastWatchedDateDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public decimal LowestUserRating + { + get => _lowestUserRating.Value; + init => throw new NotSupportedException(); + } + + public Func<decimal> LowestUserRatingDelegate + { + get => _lowestUserRatingDelegate; + init + { + _lowestUserRatingDelegate = value; + _lowestUserRating = new Lazy<decimal>(_lowestUserRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + public decimal HighestUserRating + { + get => _highestUserRating.Value; + init => throw new NotSupportedException(); + } + + public Func<decimal> HighestUserRatingDelegate + { + get => _highestUserRatingDelegate; + init + { + _highestUserRatingDelegate = value; + _highestUserRating = new Lazy<decimal>(_highestUserRatingDelegate, LazyThreadSafetyMode.ExecutionAndPublication); + } + } +} diff --git a/Shoko.Server/Filters/UserDependentSortingExpression.cs b/Shoko.Server/Filters/UserDependentSortingExpression.cs new file mode 100644 index 000000000..869fdbfd0 --- /dev/null +++ b/Shoko.Server/Filters/UserDependentSortingExpression.cs @@ -0,0 +1,19 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Server.Filters; + +public abstract class UserDependentSortingExpression : SortingExpression, IUserDependentSortingExpression +{ + public override object Evaluate(IFilterable f) + { + if (UserDependent && f is not IUserDependentFilterable) + { + throw new ArgumentException("User Dependent Filter was given an Filterable, rather than an UserDependentFilterable"); + } + + return Evaluate((IUserDependentFilterable)f); + } + + public abstract object Evaluate(IUserDependentFilterable f); +} diff --git a/Shoko.Server/Filters/readme.md b/Shoko.Server/Filters/readme.md new file mode 100644 index 000000000..47e41abf2 --- /dev/null +++ b/Shoko.Server/Filters/readme.md @@ -0,0 +1,23 @@ +An Expression is anything that transforms data: a method that takes zero or more arguments and returns a result. +Expressions are stored with TPH discriminated on Type. Expressions should not have more than 5 Arguments of each type. If it would, then it +should be redesigned. For example, `And(HasTag("comedy"),HasTag("action"))`. This is to keep the database schema +reasonable and expressions simple. I considered a one-to-many map for arguments, and making that automatic for +navigation properties seemed like a lot of work when we can design around it. + +Acceptable database types (can be mapped to other CLR types like enums) for Arguments are: + +- String +- Double (integers should be coerced to double for simplicity) +- DateTime + +FilterExpression is a single Expression, whether that's something like "Or" or "HasTag" +in `And(Or(HasTag('comedy'), HasTag('action')), Not(HasTag('18 restricted')))`. Expressions should be the least amount +of work possible. NAND can be expressed as `Not(And())`. Exceptions can be made if it would take more than 2 or 3 +operations, such as XOR. An Exception was made for GreaterThanEqual, NotEqual, and LessThanEqual, only because most +people would expect those to exist. + +This is all a design for the back end. Expressions can be communicated in many ways. For example, the above could be +written `tag: 'comedy' or tag: 'action' and not tag: '18 restricted'` +or `HasAnyTags('comedy', 'action') && HasNoTags('18 restriced')`. It just depends on how you map the input. + +See https://en.m.wikipedia.org/wiki/Binary_expression_tree diff --git a/Shoko.Server/Import/Importer.cs b/Shoko.Server/Import/Importer.cs index fe688b9b5..212e48078 100755 --- a/Shoko.Server/Import/Importer.cs +++ b/Shoko.Server/Import/Importer.cs @@ -1139,11 +1139,6 @@ public static void UpdateAllStats() ser.QueueUpdateStats(); } - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - gf.QueueUpdate(); - } - commandFactory.CreateAndSave<CommandRequest_RefreshGroupFilter>(c => c.GroupFilterID = 0); } @@ -1199,52 +1194,6 @@ public static int UpdateAniDBFileData(bool missingInfo, bool outOfDate, bool dry return vidsToUpdate.Count; } - public static void CheckForDayFilters() - { - var sched = - RepoFactory.ScheduledUpdate.GetByUpdateType((int)ScheduledUpdateType.DayFiltersUpdate); - if (sched != null) - { - if (DateTime.Now.Day == sched.LastUpdate.Day) - { - return; - } - } - //Get GroupFiters that change daily - - var conditions = new HashSet<GroupFilterConditionType> - { - GroupFilterConditionType.AirDate, - GroupFilterConditionType.LatestEpisodeAirDate, - GroupFilterConditionType.SeriesCreatedDate, - GroupFilterConditionType.EpisodeWatchedDate, - GroupFilterConditionType.EpisodeAddedDate - }; - var evalfilters = RepoFactory.GroupFilter.GetWithConditionsTypes(conditions) - .Where( - a => a.Conditions.Any(b => conditions.Contains(b.GetConditionTypeEnum()) && - b.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays)) - .ToList(); - foreach (var g in evalfilters) - { - g.CalculateGroupsAndSeries(); - } - - RepoFactory.GroupFilter.Save(evalfilters); - - if (sched == null) - { - sched = new ScheduledUpdate - { - UpdateDetails = string.Empty, UpdateType = (int)ScheduledUpdateType.DayFiltersUpdate - }; - } - - sched.LastUpdate = DateTime.Now; - RepoFactory.ScheduledUpdate.Save(sched); - } - - public static void CheckForTvDBUpdates(bool forceRefresh) { var settings = Utils.SettingsProvider.GetSettings(); diff --git a/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs b/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs index 808d3f0de..39b11be4e 100644 --- a/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs +++ b/Shoko.Server/Mappings/AniDB_Anime_TitleMap.cs @@ -1,6 +1,6 @@ using FluentNHibernate.Mapping; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; -using Shoko.Server.Databases.TypeConverters; namespace Shoko.Server.Mappings; diff --git a/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs b/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs index bd4a7c6bf..cb00e192c 100644 --- a/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs +++ b/Shoko.Server/Mappings/AniDB_Episode_TitleMap.cs @@ -1,5 +1,5 @@ using FluentNHibernate.Mapping; -using Shoko.Server.Databases.TypeConverters; +using Shoko.Server.Databases.NHIbernate; using Shoko.Server.Models; namespace Shoko.Server.Mappings; diff --git a/Shoko.Server/Mappings/FilterPresetMap.cs b/Shoko.Server/Mappings/FilterPresetMap.cs new file mode 100644 index 000000000..4c62d66ff --- /dev/null +++ b/Shoko.Server/Mappings/FilterPresetMap.cs @@ -0,0 +1,26 @@ +using FluentNHibernate.Mapping; +using Shoko.Models.Enums; +using Shoko.Server.Databases.NHIbernate; +using Shoko.Server.Models; + +namespace Shoko.Server.Mappings; + +public class FilterPresetMap : ClassMap<FilterPreset> +{ + public FilterPresetMap() + { + Table("FilterPreset"); + Not.LazyLoad(); + Id(x => x.FilterPresetID); + Map(x => x.ParentFilterPresetID).Nullable(); + References(a => a.Parent).Column("ParentFilterPresetID").ReadOnly(); + HasMany(x => x.Children).Fetch.Join().KeyColumn("ParentFilterPresetID").ReadOnly(); + Map(x => x.Name).Not.Nullable(); + Map(x => x.FilterType).Not.Nullable().CustomType<GroupFilterType>(); + Map(x => x.Locked).Not.Nullable(); + Map(x => x.Hidden).Not.Nullable(); + Map(x => x.ApplyAtSeriesLevel).Not.Nullable(); + Map(x => x.Expression).Nullable().CustomType<FilterExpressionConverter>(); + Map(x => x.SortingExpression).Nullable().CustomType<FilterExpressionConverter>(); + } +} diff --git a/Shoko.Server/Mappings/GroupFilterConditionMap.cs b/Shoko.Server/Mappings/GroupFilterConditionMap.cs deleted file mode 100644 index 214063a54..000000000 --- a/Shoko.Server/Mappings/GroupFilterConditionMap.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Models.Server; - -namespace Shoko.Server.Mappings; - -public class GroupFilterConditionMap : ClassMap<GroupFilterCondition> -{ - public GroupFilterConditionMap() - { - Not.LazyLoad(); - Id(x => x.GroupFilterConditionID); - - Map(x => x.ConditionOperator).Not.Nullable(); - Map(x => x.ConditionParameter); - Map(x => x.ConditionType).Not.Nullable(); - Map(x => x.GroupFilterID).Not.Nullable(); - } -} diff --git a/Shoko.Server/Mappings/GroupFilterMap.cs b/Shoko.Server/Mappings/GroupFilterMap.cs deleted file mode 100644 index 784ea41ac..000000000 --- a/Shoko.Server/Mappings/GroupFilterMap.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Server.Models; - -namespace Shoko.Server.Mappings; - -public class GroupFilterMap : ClassMap<SVR_GroupFilter> -{ - public GroupFilterMap() - { - Table("GroupFilter"); - - Not.LazyLoad(); - Id(x => x.GroupFilterID); - - Map(x => x.GroupFilterName); - Map(x => x.ApplyToSeries).Not.Nullable(); - Map(x => x.BaseCondition).Not.Nullable(); - Map(x => x.SortingCriteria); - Map(x => x.Locked); - Map(x => x.FilterType); - Map(x => x.GroupsIdsVersion).Not.Nullable(); - Map(x => x.GroupsIdsString).Nullable().CustomType("StringClob"); - Map(x => x.SeriesIdsVersion).Not.Nullable(); - Map(x => x.SeriesIdsString).Nullable().CustomType("StringClob"); - Map(x => x.GroupConditionsVersion).Not.Nullable(); - Map(x => x.GroupConditions).Nullable().CustomType("StringClob"); - Map(x => x.ParentGroupFilterID); - Map(x => x.InvisibleInClients); - } -} diff --git a/Shoko.Server/Models/FilterPreset.cs b/Shoko.Server/Models/FilterPreset.cs new file mode 100644 index 000000000..bac634d51 --- /dev/null +++ b/Shoko.Server/Models/FilterPreset.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Server.Filters; + +namespace Shoko.Server.Models; + +public class FilterPreset +{ + public int FilterPresetID { get; set; } + public virtual FilterPreset Parent { get; set; } + public virtual IEnumerable<FilterPreset> Children { get; set; } + public int? ParentFilterPresetID { get; set; } + public string Name { get; set; } + public bool ApplyAtSeriesLevel { get; set; } + public bool Locked { get; set; } + public GroupFilterType FilterType { get; set; } + public bool Hidden { get; set; } + + public FilterExpression<bool> Expression { get; set; } + public SortingExpression SortingExpression { get; set; } +} diff --git a/Shoko.Server/Models/SVR_AniDB_Anime.cs b/Shoko.Server/Models/SVR_AniDB_Anime.cs index ba4a88ba5..d134619b1 100644 --- a/Shoko.Server/Models/SVR_AniDB_Anime.cs +++ b/Shoko.Server/Models/SVR_AniDB_Anime.cs @@ -115,7 +115,7 @@ public List<CrossRef_AniDB_TraktV2> GetCrossRefTraktV2() public List<CrossRef_AniDB_TraktV2> GetCrossRefTraktV2(ISession session) { - return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(session, AnimeID); + return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AnimeID); } public List<CrossRef_AniDB_MAL> GetCrossRefMAL() @@ -598,20 +598,20 @@ private string GetFormattedTitle(List<SVR_AniDB_Anime_Title> titles = null) titles ??= GetTitles(); // Check each preferred language in order. - foreach (var thisLanguage in Languages.PreferredNamingLanguages.Select(a => a.Language)) + foreach (var thisLanguage in Languages.PreferredNamingLanguageNames) { // First check the main title. - var title = titles.FirstOrDefault(title => title.TitleType == TitleType.Main && title.Language == thisLanguage); + var title = titles.FirstOrDefault(t => t.TitleType == TitleType.Main && t.Language == thisLanguage); if (title != null) return title.Title; // Then check for an official title. - title = titles.FirstOrDefault(title => title.TitleType == TitleType.Official && title.Language == thisLanguage); + title = titles.FirstOrDefault(t => t.TitleType == TitleType.Official && t.Language == thisLanguage); if (title != null) return title.Title; // Then check for _any_ title at all, if there is no main or official title in the langugage. if (Utils.SettingsProvider.GetSettings().LanguageUseSynonyms) { - title = titles.FirstOrDefault(title => title.Language == thisLanguage); + title = titles.FirstOrDefault(t => t.Language == thisLanguage); if (title != null) return title.Title; } } @@ -637,7 +637,8 @@ public AniDB_Vote UserVote } } - public string PreferredTitle => GetFormattedTitle(); + private string _cachedTitle; + public string PreferredTitle => _cachedTitle == null ? (_cachedTitle = GetFormattedTitle()) : _cachedTitle; [XmlIgnore] public List<AniDB_Episode> AniDBEpisodes => RepoFactory.AniDB_Episode.GetByAnimeID(AnimeID); diff --git a/Shoko.Server/Models/SVR_AnimeGroup.cs b/Shoko.Server/Models/SVR_AnimeGroup.cs index 85bd00def..ae948c420 100644 --- a/Shoko.Server/Models/SVR_AnimeGroup.cs +++ b/Shoko.Server/Models/SVR_AnimeGroup.cs @@ -197,7 +197,7 @@ public static void RenameAllGroups() public List<SVR_AniDB_Anime> Anime => - GetSeries().Select(serie => serie.GetAnime()).Where(anime => anime != null).ToList(); + RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID).Select(s => s.GetAnime()).Where(anime => anime != null).ToList(); public decimal AniDBRating { @@ -283,9 +283,9 @@ public void SetMainSeries(SVR_AnimeSeries series) if (series == null && (IsManuallyNamed == 0 || OverrideDescription == 0)) series = GetMainSeries(); if (IsManuallyNamed == 0) - GroupName = SortName = series.GetSeriesName(); + GroupName = SortName = series!.GetSeriesName(); if (OverrideDescription == 0) - Description = series.GetAnime().Description; + Description = series!.GetAnime().Description; // Save the changes for this group only. DateTimeUpdated = DateTime.Now; @@ -294,29 +294,21 @@ public void SetMainSeries(SVR_AnimeSeries series) public List<SVR_AnimeSeries> GetSeries() { - var seriesList = RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID) - .OrderBy(a => a.AirDate) - .ToList(); + // Make sure the default/main series is the first, if it's directly within the group + if (DefaultAnimeSeriesID == null && MainAniDBAnimeID == null) + return RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID).OrderBy(a => a.AirDate).ToList(); - // Make sure the default/main series is the first, if it's directly - // within the group. - if (DefaultAnimeSeriesID.HasValue || MainAniDBAnimeID.HasValue) - { - SVR_AnimeSeries mainSeries = null; - if (DefaultAnimeSeriesID.HasValue) - mainSeries = seriesList.FirstOrDefault(ser => ser.AnimeSeriesID == DefaultAnimeSeriesID.Value); - - if (mainSeries == null && MainAniDBAnimeID.HasValue) - mainSeries = seriesList.FirstOrDefault(ser => ser.AniDB_ID == MainAniDBAnimeID.Value); + SVR_AnimeSeries mainSeries = null; + if (DefaultAnimeSeriesID.HasValue) mainSeries = RepoFactory.AnimeSeries.GetByID(DefaultAnimeSeriesID.Value); + if (mainSeries == null && MainAniDBAnimeID.HasValue) mainSeries = RepoFactory.AnimeSeries.GetByAnimeID(MainAniDBAnimeID.Value); - if (mainSeries != null) - { - seriesList.Remove(mainSeries); - seriesList.Insert(0, mainSeries); - } - } + var seriesList = RepoFactory.AnimeSeries.GetByGroupID(AnimeGroupID).OrderBy(a => a.AirDate).ToList(); + if (mainSeries == null) return seriesList; + seriesList.Remove(mainSeries); + seriesList.Insert(0, mainSeries); return seriesList; + } public List<SVR_AnimeSeries> GetAllSeries(bool skipSorting = false) @@ -1248,68 +1240,6 @@ private static ILookup<int, string> GetVideoQualities(IEnumerable<int> groupIds .Distinct().Select(a => (GroupID: id, Source: a))).ToLookup(a => a.GroupID, a => a.Source); } - public void DeleteFromFilters() - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - var change = false; - foreach (var k in gf.GroupsIds.Keys) - { - if (gf.GroupsIds[k].Contains(AnimeGroupID)) - { - gf.GroupsIds[k].Remove(AnimeGroupID); - change = true; - } - } - - if (change) - { - RepoFactory.GroupFilter.Save(gf); - } - } - } - - public void UpdateGroupFilters(HashSet<GroupFilterConditionType> types, SVR_JMMUser user = null) - { - IReadOnlyList<SVR_JMMUser> users = new List<SVR_JMMUser> { user }; - if (user == null) - { - users = RepoFactory.JMMUser.GetAll(); - } - - var tosave = new List<SVR_GroupFilter>(); - - var n = new HashSet<GroupFilterConditionType>(types); - var gfs = RepoFactory.GroupFilter.GetWithConditionTypesAndAll(n); - logger.Trace($"Updating {gfs.Count} Group Filters from Group {GroupName}"); - foreach (var gf in gfs) - { - if (gf.UpdateGroupFilterFromGroup(Contract, null)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - - foreach (var u in users) - { - var cgrp = GetUserContract(u.JMMUserID, n); - - if (gf.UpdateGroupFilterFromGroup(cgrp, u)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - } - } - - RepoFactory.GroupFilter.Save(tosave); - } - - public static void GetAnimeGroupsRecursive(int animeGroupID, ref List<SVR_AnimeGroup> groupList) { var grp = RepoFactory.AnimeGroup.GetByID(animeGroupID); @@ -1353,43 +1283,6 @@ public void DeleteGroup(bool updateParent = true) subGroup.DeleteGroup(false); } - var gfs = - RepoFactory.GroupFilter.GetWithConditionsTypes(new HashSet<GroupFilterConditionType> - { - GroupFilterConditionType.AnimeGroup - }); - foreach (var gf in gfs) - { - var c = gf.Conditions.RemoveAll(a => - { - if (a.ConditionType != (int)GroupFilterConditionType.AnimeGroup) - { - return false; - } - - if (!int.TryParse(a.ConditionParameter, out var thisGrpID)) - { - return false; - } - - if (thisGrpID != AnimeGroupID) - { - return false; - } - - return true; - }); - if (gf.Conditions.Count <= 0) - { - RepoFactory.GroupFilter.Delete(gf.GroupFilterID); - } - else - { - gf.CalculateGroupsAndSeries(); - RepoFactory.GroupFilter.Save(gf); - } - } - RepoFactory.AnimeGroup.Delete(this); // finally update stats diff --git a/Shoko.Server/Models/SVR_AnimeGroup_User.cs b/Shoko.Server/Models/SVR_AnimeGroup_User.cs index 812e8e607..92e2b5863 100644 --- a/Shoko.Server/Models/SVR_AnimeGroup_User.cs +++ b/Shoko.Server/Models/SVR_AnimeGroup_User.cs @@ -60,24 +60,6 @@ public SVR_AnimeGroup_User(int userID, int groupID) StoppedCount = 0; } - - public void UpdateGroupFilters(HashSet<GroupFilterConditionType> types) - { - var grp = RepoFactory.AnimeGroup.GetByID(AnimeGroupID); - var usr = RepoFactory.JMMUser.GetByID(JMMUserID); - if (grp != null && usr != null) - { - grp.UpdateGroupFilters(types, usr); - } - } - - public void DeleteFromFilters() - { - var toSave = RepoFactory.GroupFilter.GetAll().AsParallel() - .Where(gf => gf.DeleteGroupFromFilters(JMMUserID, AnimeGroupID)).ToList(); - RepoFactory.GroupFilter.Save(toSave); - } - public void UpdatePlexKodiContracts() { var grp = RepoFactory.AnimeGroup.GetByID(AnimeGroupID); diff --git a/Shoko.Server/Models/SVR_AnimeSeries.cs b/Shoko.Server/Models/SVR_AnimeSeries.cs index 7d2bb0fe1..e23ffda00 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries.cs @@ -191,15 +191,15 @@ public string GetSeriesName() if (!string.IsNullOrEmpty(SeriesNameOverride)) return SeriesNameOverride; + if (Utils.SettingsProvider.GetSettings().SeriesNameSource == DataSourceType.AniDB) + return GetAnime().PreferredTitle; + // Try to find the TvDB title if we prefer TvDB titles. - if (Utils.SettingsProvider.GetSettings().SeriesNameSource == DataSourceType.TvDB) - { - var tvdbShows = GetTvDBSeries(); - var tvdbShowTitle = tvdbShows - .FirstOrDefault(show => show.SeriesName.Contains("**DUPLICATE", StringComparison.InvariantCultureIgnoreCase))?.SeriesName; - if (!string.IsNullOrEmpty(tvdbShowTitle)) - return tvdbShowTitle; - } + var tvdbShows = GetTvDBSeries(); + var tvdbShowTitle = tvdbShows + .FirstOrDefault(show => !show.SeriesName.Contains("**DUPLICATE", StringComparison.InvariantCultureIgnoreCase))?.SeriesName; + if (!string.IsNullOrEmpty(tvdbShowTitle)) + return tvdbShowTitle; // Otherwise just return the anidb title. return GetAnime().PreferredTitle; @@ -278,7 +278,6 @@ public List<SVR_AnimeEpisode> GetAnimeEpisodes(bool orderList = false, bool incl { if (orderList) { - // TODO: Convert to a LINQ query once we've switched to EF Core. return RepoFactory.AnimeEpisode.GetBySeriesID(AnimeSeriesID) .Where(episode => includeHidden || !episode.IsHidden) .Select(episode => (episode, anidbEpisode: episode.AniDB_Episode)) @@ -289,7 +288,6 @@ public List<SVR_AnimeEpisode> GetAnimeEpisodes(bool orderList = false, bool incl } if (!includeHidden) { - // TODO: Convert to a LINQ query once we've switched to EF Core. return RepoFactory.AnimeEpisode.GetBySeriesID(AnimeSeriesID) .Where(episode => !episode.IsHidden) .ToList(); @@ -377,7 +375,7 @@ public List<TvDB_Series> GetTvDBSeries() public List<CrossRef_AniDB_TraktV2> GetCrossRefTraktV2(ISession session) { - return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(session, AniDB_ID); + return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AniDB_ID); } public List<Trakt_Show> GetTraktShow() @@ -707,44 +705,31 @@ public SVR_AniDB_Anime GetAnime() return RepoFactory.AniDB_Anime.GetByAnimeID(AniDB_ID); } + private DateTime? _airDate; public DateTime? AirDate { get { + if (_airDate != null) return _airDate; var anime = GetAnime(); if (anime?.AirDate != null) - { - return anime.AirDate.Value; - } + return _airDate = anime.AirDate.Value; // This will be slower, but hopefully more accurate - var ep = GetAnimeEpisodes() - .Select(a => a.AniDB_Episode) - .Where(a => (a.EpisodeType == (int)EpisodeType.Episode) && a.LengthSeconds > 0) - .Select(a => a.GetAirDateAsDate()) - .Where(a => a != null) - .OrderBy(a => a) - .FirstOrDefault(); - if (ep != null) - { - return ep.Value; - } - - return null; + var ep = RepoFactory.AniDB_Episode.GetByAnimeID(AniDB_ID) + .Where(a => a.EpisodeType == (int)EpisodeType.Episode && a.LengthSeconds > 0 && a.AirDate != 0) + .MinBy(a => a.AirDate); + return _airDate = ep?.GetAirDateAsDate(); } } + private DateTime? _endDate; public DateTime? EndDate { get { - var anime = GetAnime(); - if (anime?.EndDate != null) - { - return anime.EndDate.Value; - } - - return null; + if (_endDate != null) return _endDate; + return _endDate = GetAnime()?.EndDate; } } @@ -935,67 +920,6 @@ public List<SVR_AnimeGroup> AllGroupsAbove } } - public void UpdateGroupFilters(HashSet<GroupFilterConditionType> types, SVR_JMMUser user = null) - { - IReadOnlyList<SVR_JMMUser> users = new List<SVR_JMMUser> { user }; - if (user == null) - { - users = RepoFactory.JMMUser.GetAll(); - } - - var tosave = new List<SVR_GroupFilter>(); - - var n = new HashSet<GroupFilterConditionType>(types); - var gfs = RepoFactory.GroupFilter.GetWithConditionTypesAndAll(n); - logger.Trace($"Updating {gfs.Count} Group Filters from Series {GetAnime().MainTitle}"); - foreach (var gf in gfs) - { - if (gf.UpdateGroupFilterFromSeries(Contract, null)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - - foreach (var u in users) - { - var cgrp = GetUserContract(u.JMMUserID, n); - - if (gf.UpdateGroupFilterFromSeries(cgrp, u)) - { - if (!tosave.Contains(gf)) - { - tosave.Add(gf); - } - } - } - } - - RepoFactory.GroupFilter.Save(tosave); - } - - public void DeleteFromFilters() - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - var change = false; - foreach (var k in gf.SeriesIds.Keys) - { - if (gf.SeriesIds[k].Contains(AnimeSeriesID)) - { - gf.SeriesIds[k].Remove(AnimeSeriesID); - change = true; - } - } - - if (change) - { - RepoFactory.GroupFilter.Save(gf); - } - } - } - public static Dictionary<int, HashSet<GroupFilterConditionType>> BatchUpdateContracts(ISessionWrapper session, IReadOnlyCollection<SVR_AnimeSeries> seriesBatch, bool onlyStats = false) { diff --git a/Shoko.Server/Models/SVR_AnimeSeries_User.cs b/Shoko.Server/Models/SVR_AnimeSeries_User.cs index cc7ef9669..13490314b 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries_User.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries_User.cs @@ -82,35 +82,4 @@ public static HashSet<GroupFilterConditionType> GetConditionTypesChanged(SVR_Ani return h; } - - public void UpdateGroupFilter(HashSet<GroupFilterConditionType> types) - { - var ser = RepoFactory.AnimeSeries.GetByID(AnimeSeriesID); - var usr = RepoFactory.JMMUser.GetByID(JMMUserID); - if (ser != null && usr != null) - { - ser.UpdateGroupFilters(types, usr); - } - } - - public void DeleteFromFilters() - { - foreach (var gf in RepoFactory.GroupFilter.GetAll()) - { - var change = false; - if (gf.SeriesIds.ContainsKey(JMMUserID)) - { - if (gf.SeriesIds[JMMUserID].Contains(AnimeSeriesID)) - { - gf.SeriesIds[JMMUserID].Remove(AnimeSeriesID); - change = true; - } - } - - if (change) - { - RepoFactory.GroupFilter.Save(gf); - } - } - } } diff --git a/Shoko.Server/Models/SVR_GroupFilter.cs b/Shoko.Server/Models/SVR_GroupFilter.cs deleted file mode 100644 index 207799571..000000000 --- a/Shoko.Server/Models/SVR_GroupFilter.cs +++ /dev/null @@ -1,2232 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using FluentNHibernate.MappingModel; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using NLog; -using Shoko.Commons.Extensions; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Server.Commands; -using Shoko.Server.Extensions; -using Shoko.Server.Repositories; -using Shoko.Server.Server; -using Shoko.Server.Utilities; - -namespace Shoko.Server.Models; - -public class SVR_GroupFilter : GroupFilter -{ - private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); - public int GroupsIdsVersion { get; set; } - public string GroupsIdsString { get; set; } - - public int GroupConditionsVersion { get; set; } - public string GroupConditions { get; set; } - - public int SeriesIdsVersion { get; set; } - public string SeriesIdsString { get; set; } - - public const int GROUPFILTER_VERSION = 3; - public const int GROUPCONDITIONS_VERSION = 1; - public const int SERIEFILTER_VERSION = 2; - - - internal Dictionary<int, HashSet<int>> _groupsId = new(); - internal Dictionary<int, HashSet<int>> _seriesId = new(); - internal List<GroupFilterCondition> _conditions = new(); - - public SVR_GroupFilter Parent => - ParentGroupFilterID.HasValue ? RepoFactory.GroupFilter.GetByID(ParentGroupFilterID.Value) : null; - - public SVR_GroupFilter TopLevelGroupFilter - { - get - { - var parent = Parent; - if (parent == null) - { - return this; - } - - while (true) - { - var next = parent.Parent; - if (next == null) - { - return parent; - } - - parent = next; - } - } - } - - /// <summary> - /// It is considered locked if it's not user defined or if the locked flag - /// is set. - /// </summary> - public bool IsLocked - { - get => !((GroupFilterType)FilterType).HasFlag(GroupFilterType.UserDefined) || (Locked.HasValue && Locked.Value == 1); - set => Locked = value ? 1 : 0; - } - - /// <summary> - /// Check if the group filter can contain sub-filters. - /// </summary> - public bool IsDirectory - { - get => ((GroupFilterType)FilterType).HasFlag(GroupFilterType.Directory); - set => FilterType = value ? FilterType | (int)GroupFilterType.Directory : FilterType & ~(int)GroupFilterType.Directory; - } - - /// <summary> - /// Indicates the group filter should be hidden unless explictly requested - /// for. - /// </summary> - public bool IsHidden - { - get => InvisibleInClients == 1; - set => InvisibleInClients = value ? 1 : 0; - } - - public virtual HashSet<GroupFilterConditionType> Types => - new HashSet<GroupFilterConditionType>( - Conditions.Select(a => a.ConditionType).Cast<GroupFilterConditionType>()); - - public virtual Dictionary<int, HashSet<int>> GroupsIds - { - get - { - if (_groupsId.Count != 0 || GroupsIdsVersion != GROUPFILTER_VERSION) - { - return _groupsId; - } - - var vals = JsonConvert.DeserializeObject<Dictionary<int, List<int>>>(GroupsIdsString); - if (vals == null) - { - return _groupsId; - } - - _groupsId = vals.ToDictionary(a => a.Key, a => new HashSet<int>(a.Value)); - return _groupsId; - } - set => _groupsId = value; - } - - public virtual Dictionary<int, HashSet<int>> SeriesIds - { - get - { - if (_seriesId.Count != 0 || SeriesIdsVersion != SERIEFILTER_VERSION) - { - return _seriesId; - } - - var vals = JsonConvert.DeserializeObject<Dictionary<int, List<int>>>(SeriesIdsString); - if (vals == null) - { - return _seriesId; - } - - _seriesId = vals.ToDictionary(a => a.Key, a => new HashSet<int>(a.Value)); - - return _seriesId; - } - set => _seriesId = value; - } - - public virtual List<GroupFilterCondition> Conditions - { - get - { - if (_conditions.Count == 0 && !string.IsNullOrEmpty(GroupConditions)) - { - _conditions = JsonConvert.DeserializeObject<List<GroupFilterCondition>>(GroupConditions); - } - - return _conditions; - } - set - { - if (value != null) - { - _conditions = value; - GroupConditions = JsonConvert.SerializeObject(_conditions); - } - } - } - - public override string ToString() - { - return $"{GroupFilterID} - {GroupFilterName}"; - } - - public List<GroupFilterSortingCriteria> SortCriteriaList - { - get - { - var sortCriteriaList = new List<GroupFilterSortingCriteria>(); - - if (!string.IsNullOrEmpty(SortingCriteria)) - { - var scrit = SortingCriteria.Split('|'); - foreach (var sortpair in scrit) - { - var spair = sortpair.Split(';'); - if (spair.Length != 2) - { - continue; - } - - int.TryParse(spair[0], out var stype); - int.TryParse(spair[1], out var sdir); - - if (stype > 0 && sdir > 0) - { - var gfsc = new GroupFilterSortingCriteria - { - GroupFilterID = GroupFilterID, - SortType = (GroupFilterSorting)stype, - SortDirection = (GroupFilterSortDirection)sdir - }; - sortCriteriaList.Add(gfsc); - } - } - } - - return sortCriteriaList; - } - - set - { - var crts = value.Select(a => $"{(int)a.SortType};{(int)a.SortDirection}"); - SortingCriteria = string.Join("|", crts); - } - } - - public bool DeleteGroupFromFilters(int userID, int groupID) - { - return WriteLock( - () => - { - if (!GroupsIds.ContainsKey(userID)) - { - return false; - } - - if (!GroupsIds[userID].Contains(groupID)) - { - return false; - } - - GroupsIds[userID].Remove(groupID); - return true; - } - ); - } - - public bool RemoveUser(int userID) - { - var changed = WriteLock( - () => - { - var changed = false; - if (GroupsIds.ContainsKey(userID)) - { - GroupsIds.Remove(userID); - changed = true; - } - - if (SeriesIds.ContainsKey(userID)) - { - SeriesIds.Remove(userID); - changed = true; - } - - return changed; - } - ); - - if (!changed) - { - return false; - } - - UpdateEntityReferenceStrings(); - return true; - } - - public CL_GroupFilter ToClient() - { - if (Conditions.FirstOrDefault(a => a.GroupFilterID == 0) != null) - { - Conditions.ForEach(a => a.GroupFilterID = GroupFilterID); - RepoFactory.GroupFilter.Save(this); - } - - var contract = new CL_GroupFilter - { - GroupFilterID = GroupFilterID, - GroupFilterName = GroupFilterName, - ApplyToSeries = ApplyToSeries, - BaseCondition = BaseCondition, - SortingCriteria = SortingCriteria, - Locked = Locked, - FilterType = FilterType, - ParentGroupFilterID = ParentGroupFilterID, - InvisibleInClients = InvisibleInClients, - FilterConditions = Conditions, - Groups = GroupsIds, - Series = SeriesIds, - Childs = GroupFilterID == 0 - ? new HashSet<int>() - : RepoFactory.GroupFilter.GetByParentID(GroupFilterID).Select(a => a.GroupFilterID).ToHashSet() - }; - return contract; - } - - public static SVR_GroupFilter FromClient(CL_GroupFilter gfc) - { - var gf = new SVR_GroupFilter - { - GroupFilterID = gfc.GroupFilterID, - GroupFilterName = gfc.GroupFilterName, - ApplyToSeries = gfc.ApplyToSeries, - BaseCondition = gfc.BaseCondition, - SortingCriteria = gfc.SortingCriteria, - Locked = gfc.Locked, - InvisibleInClients = gfc.InvisibleInClients, - ParentGroupFilterID = gfc.ParentGroupFilterID, - FilterType = gfc.FilterType, - Conditions = gfc.FilterConditions, - GroupsIds = gfc.Groups ?? new Dictionary<int, HashSet<int>>(), - SeriesIds = gfc.Series ?? new Dictionary<int, HashSet<int>>() - }; - if (gf.GroupFilterID != 0) - { - gf.Conditions.ForEach(a => a.GroupFilterID = gf.GroupFilterID); - } - - return gf; - } - - public CL_GroupFilterExtended ToClientExtended(SVR_JMMUser user) - { - var contract = new CL_GroupFilterExtended { GroupFilter = ToClient(), GroupCount = 0, SeriesCount = 0 }; - - ReadLock( - () => - { - if (GroupsIds.ContainsKey(user.JMMUserID)) - { - contract.GroupCount = GroupsIds[user.JMMUserID].Count; - } - - if (SeriesIds.ContainsKey(user.JMMUserID)) - { - contract.SeriesCount = SeriesIds[user.JMMUserID].Count; - } - } - ); - - return contract; - } - - public bool UpdateGroupFilterFromSeries(CL_AnimeSeries_User ser, JMMUser user) - { - if (ser == null) - { - return false; - } - - bool result; - if (ApplyToSeries == 1) - { - result = CalculateGroupFilterSeries(RepoFactory.AnimeSeries.GetAll().Select(a => a.AnimeSeriesID).ToHashSet(), ser, user); - if (!result) - { - return false; - } - - WriteLock( - () => - { - GroupsIds[user?.JMMUserID ?? 0] = SeriesIds[user?.JMMUserID ?? 0] - .Select(a => RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(a => a != -1).ToHashSet(); - } - ); - } - else - { - result = false; - // Top Level Group - int? groupID = ser.AnimeGroupID; - while (true) - { - if (groupID == null) - { - break; - } - - var grp = RepoFactory.AnimeGroup.GetByID(groupID.Value); - if (grp != null) - { - groupID = grp.AnimeGroupParentID; - } - else - { - break; - } - } - - if (groupID == null) - { - return false; - } - - var group = RepoFactory.AnimeGroup.GetByID(groupID.Value); - - var contract = group?.Contract; - if (user != null) - { - contract = group?.GetUserContract(user.JMMUserID); - } - - if (contract == null) - { - return false; - } - - result |= CalculateGroupFilterGroups(RepoFactory.AnimeGroup.GetAll().Select(a => a.AnimeGroupID).ToHashSet(), contract, user); - if (!result) - { - return false; - } - - WriteLock( - () => - { - SeriesIds[user?.JMMUserID ?? 0] = GroupsIds[user?.JMMUserID ?? 0] - .SelectMany( - a => - RepoFactory.AnimeGroup.GetByID(a)?.GetAllSeries()?.Select(b => b?.AnimeSeriesID ?? -1) - ) - .Where(a => a != -1).ToHashSet(); - } - ); - } - - return true; - } - - public bool UpdateGroupFilterFromGroup(CL_AnimeGroup_User grp, JMMUser user) - { - if (grp == null) - { - return false; - } - - bool result; - if (ApplyToSeries == 1) - { - result = false; - var sers = new List<SVR_AnimeSeries>(); - SVR_AnimeGroup.GetAnimeSeriesRecursive(grp.AnimeGroupID, ref sers); - - foreach (var ser in sers) - { - var contract = ser.Contract; - if (user != null) - { - contract = ser.GetUserContract(user.JMMUserID); - } - - result |= CalculateGroupFilterSeries(RepoFactory.AnimeSeries.GetAll().Select(a => a.AnimeSeriesID).ToHashSet(), contract, user); - } - - if (!result) - { - return false; - } - - WriteLock( - () => - { - GroupsIds[user?.JMMUserID ?? 0] = SeriesIds[user?.JMMUserID ?? 0] - .Select(a => RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(a => a != -1).ToHashSet(); - } - ); - } - else - { - result = CalculateGroupFilterGroups(RepoFactory.AnimeGroup.GetAll().Select(a => a.AnimeGroupID).ToHashSet(), grp, user); - if (!result) - { - return false; - } - - WriteLock( - () => - { - SeriesIds[user?.JMMUserID ?? 0] = GroupsIds[user?.JMMUserID ?? 0].SelectMany( - a => RepoFactory.AnimeGroup.GetByID(a) - ?.GetAllSeries() - ?.Select(b => b?.AnimeSeriesID ?? -1) - ) - .Where(a => a != -1) - .ToHashSet(); - } - ); - } - - return true; - } - - - private bool CalculateGroupFilterSeries(HashSet<int> allSeriesIds, CL_AnimeSeries_User ser, JMMUser user) - { - if (ser == null) - { - return false; - } - - var seriesIds = ReadLock(() => SeriesIds.TryGetValue(user?.JMMUserID ?? 0, out var seriesIds) ? seriesIds : null); - - var change = false; - if (seriesIds == null) - seriesIds = new HashSet<int>(); - else - change = seriesIds.RemoveWhere(a => !allSeriesIds.Contains(a)) > 0; - - if (EvaluateGroupFilter(ser, user)) - { - change |= seriesIds.Add(ser.AnimeSeriesID); - } - else - { - change |= seriesIds.Remove(ser.AnimeSeriesID); - } - - WriteLock(() => SeriesIds[user?.JMMUserID ?? 0] = seriesIds); - - return change; - } - - private bool CalculateGroupFilterGroups(HashSet<int> allGroupIds, CL_AnimeGroup_User grp, JMMUser user) - { - if (grp == null) return false; - - var groupIds = ReadLock(() => - GroupsIds.TryGetValue(user?.JMMUserID ?? 0, out var groupIds) ? new HashSet<int>(groupIds) : null); - - var change = false; - if (groupIds == null) - groupIds = new HashSet<int>(); - else - change = groupIds.RemoveWhere(a => !allGroupIds.Contains(a)) > 0; - - if (EvaluateGroupFilter(grp, user)) - { - change |= groupIds.Add(grp.AnimeGroupID); - } - else - { - change |= groupIds.Remove(grp.AnimeGroupID); - } - - WriteLock(() => GroupsIds[user?.JMMUserID ?? 0] = groupIds); - - return change; - } - - public void CalculateGroupsAndSeries() - { - if (ApplyToSeries == 1) - { - EvaluateAnimeSeries(); - - var erroredSeries = new HashSet<int>(); - WriteLock( - () => - { - foreach (var user in SeriesIds.Keys) - { - GroupsIds[user] = SeriesIds[user].Select( - a => - { - var id = RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1; - if (id == -1) - { - erroredSeries.Add(a); - } - - return id; - } - ).Where(a => a != -1) - .ToHashSet(); - } - } - ); - foreach (var id in erroredSeries.OrderBy(a => a).ToList()) - { - var ser = RepoFactory.AnimeSeries.GetByID(id); - LogManager.GetCurrentClassLogger() - .Error("While calculating group filters, an AnimeSeries without a group was found: " + - (ser?.GetSeriesName() ?? id.ToString())); - } - } - else - { - EvaluateAnimeGroups(); - - WriteLock( - () => - { - foreach (var user in GroupsIds.Keys) - { - var ids = GroupsIds[user]; - SeriesIds[user] = ids.SelectMany( - a => - RepoFactory.AnimeGroup.GetByID(a)?.GetAllSeries() - ?.Select(b => b?.AnimeSeriesID ?? -1) - ) - .Where(a => a != -1).ToHashSet(); - } - } - ); - } - - if (((GroupFilterType)FilterType).HasFlag(GroupFilterType.Tag)) - { - GroupFilterName = GroupFilterName.Replace('`', '\''); - } - } - - private void EvaluateAnimeGroups() - { - var users = RepoFactory.JMMUser.GetAll(); - // make sure the user has not filtered this out - var allGroupsIds = RepoFactory.AnimeGroup.GetAll().Select(a => a.AnimeGroupID).ToHashSet(); - foreach (var grp in RepoFactory.AnimeGroup.GetAllTopLevelGroups()) - { - foreach (var user in users) - { - CalculateGroupFilterGroups(allGroupsIds, grp.GetUserContract(user.JMMUserID, cloned: false), user); - } - } - } - - private void EvaluateAnimeSeries() - { - var users = RepoFactory.JMMUser.GetAll(); - var allSeries = RepoFactory.AnimeSeries.GetAll(); - var allSeriesIds = allSeries.Select(a => a.AnimeSeriesID).ToHashSet(); - foreach (var ser in allSeries) - { - if (ser.Contract == null) ser.UpdateContract(); - - if (ser.Contract == null) continue; - - CalculateGroupFilterSeries(allSeriesIds, ser.Contract, null); //Default no filter for JMM Client - foreach (var user in users) - { - CalculateGroupFilterSeries(allSeriesIds, ser.GetUserContract(user.JMMUserID, cloned: false), user); - } - } - } - - public static CL_GroupFilter EvaluateContract(CL_GroupFilter gfc) - { - var gf = FromClient(gfc); - if (gf.ApplyToSeries == 1) - { - gf.EvaluateAnimeSeries(); - - gf.WriteLock( - () => - { - foreach (var user in gf.SeriesIds.Keys) - { - gf.GroupsIds[user] = gf.SeriesIds[user] - .Select(a => RepoFactory.AnimeSeries.GetByID(a)?.TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(a => a != -1).ToHashSet(); - } - } - ); - } - else - { - gf.EvaluateAnimeGroups(); - - gf.WriteLock( - () => - { - foreach (var user in gf.GroupsIds.Keys) - { - gf.SeriesIds[user] = gf.GroupsIds[user] - .SelectMany(a => - RepoFactory.AnimeGroup.GetByID(a)?.GetAllSeries()?.Select(b => b?.AnimeSeriesID ?? -1)) - .Where(a => a != -1).ToHashSet(); - } - } - ); - } - - return gf.ToClient(); - } - - - public bool EvaluateGroupFilter(CL_AnimeGroup_User contractGroup, JMMUser curUser) - { - //Directories don't count - if (IsDirectory) - { - return false; - } - - if (contractGroup?.Stat_AllTags == null) - { - return false; - } - - if (curUser?.GetHideCategories().FindInEnumerable(contractGroup.Stat_AllTags) ?? false) - { - return false; - } - - // sub groups don't count - if (contractGroup.AnimeGroupParentID.HasValue) - { - return false; - } - - // first check for anime groups which are included exluded every time - foreach (var gfc in Conditions) - { - if (gfc.GetConditionTypeEnum() != GroupFilterConditionType.AnimeGroup) - { - continue; - } - - int.TryParse(gfc.ConditionParameter, out var groupID); - if (groupID == 0) - { - break; - } - - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Equals && - groupID == contractGroup.AnimeGroupID) - { - return true; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotEquals && - groupID != contractGroup.AnimeGroupID) - { - return true; - } - - return false; - } - - var exclude = BaseCondition == (int)GroupFilterBaseCondition.Exclude; - - return Conditions.All(gfc => exclude ^ EvaluateConditions(contractGroup, gfc)); - } - - private bool EvaluateConditions(CL_AnimeGroup_User contractGroup, GroupFilterCondition gfc) - { - var style = NumberStyles.Number; - var culture = CultureInfo.InvariantCulture; - switch (gfc.GetConditionTypeEnum()) - { - case GroupFilterConditionType.Favourite: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && contractGroup.IsFave == 0) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && contractGroup.IsFave == 1) - { - return false; - } - - break; - - case GroupFilterConditionType.MissingEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - (contractGroup.MissingEpisodeCount > 0 || contractGroup.MissingEpisodeCountGroups > 0) == - false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - (contractGroup.MissingEpisodeCount > 0 || contractGroup.MissingEpisodeCountGroups > 0)) - { - return false; - } - - break; - - case GroupFilterConditionType.MissingEpisodesCollecting: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.MissingEpisodeCountGroups > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.MissingEpisodeCountGroups > 0) - { - return false; - } - - break; - case GroupFilterConditionType.Tag: - var tags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .Where(a => !string.IsNullOrWhiteSpace(a)) - .ToList(); - var tagsFound = - tags.Any( - a => contractGroup.Stat_AllTags.Contains(a)); - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.In || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include) && !tagsFound) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude) && tagsFound) - { - return false; - } - - break; - case GroupFilterConditionType.Year: - var years = new HashSet<int>(); - var parameterStrings = gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var yearString in parameterStrings) - { - if (int.TryParse(yearString.Trim(), out var year)) - { - years.Add(year); - } - } - - if (years.Count <= 0) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.In) && - !contractGroup.Stat_AllYears.FindInEnumerable(years)) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn) && - contractGroup.Stat_AllYears.FindInEnumerable(years)) - { - return false; - } - - break; - case GroupFilterConditionType.Season: - var paramStrings = gfc.ConditionParameter.Trim().Split(','); - - switch (gfc.GetConditionOperatorEnum()) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - return paramStrings.FindInEnumerable(contractGroup.Stat_AllSeasons); - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - return !paramStrings.FindInEnumerable(contractGroup.Stat_AllSeasons); - } - - break; - case GroupFilterConditionType.HasWatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.WatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.WatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.HasUnwatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.UnwatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.UnwatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasTvDBLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasTvDBLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTraktInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasTraktLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasTraktLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMALInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasMALLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasMALLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMovieDBInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasMovieDBLink == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasMovieDBLink) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - !contractGroup.Stat_HasMovieDBOrTvDBLink) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasMovieDBOrTvDBLink) - { - return false; - } - - break; - - case GroupFilterConditionType.CompletedSeries: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_IsComplete == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_IsComplete) - { - return false; - } - - break; - - case GroupFilterConditionType.FinishedAiring: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_HasFinishedAiring == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_HasFinishedAiring) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVoted: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_UserVotePermanent.HasValue == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_UserVotePermanent.HasValue) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVotedAny: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractGroup.Stat_UserVoteOverall.HasValue == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractGroup.Stat_UserVoteOverall.HasValue) - { - return false; - } - - break; - - case GroupFilterConditionType.AirDate: - DateTime filterDate; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDate = DateTime.Today.AddDays(0 - days); - } - else - { - filterDate = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.Stat_AirDate_Min.HasValue || !contractGroup.Stat_AirDate_Max.HasValue) - { - return false; - } - - if (contractGroup.Stat_AirDate_Max.Value < filterDate) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.Stat_AirDate_Min.HasValue || !contractGroup.Stat_AirDate_Max.HasValue) - { - return false; - } - - if (contractGroup.Stat_AirDate_Min.Value > filterDate) - { - return false; - } - } - - break; - case GroupFilterConditionType.LatestEpisodeAirDate: - DateTime filterDateEpisodeLastAired; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeLastAired = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeLastAired = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractGroup.LatestEpisodeAirDate.Value < filterDateEpisodeLastAired) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractGroup.LatestEpisodeAirDate.Value > filterDateEpisodeLastAired) - { - return false; - } - } - - break; - case GroupFilterConditionType.SeriesCreatedDate: - DateTime filterDateSeries; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateSeries = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateSeries = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.Stat_SeriesCreatedDate.HasValue) - { - return false; - } - - if (contractGroup.Stat_SeriesCreatedDate.Value < filterDateSeries) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.Stat_SeriesCreatedDate.HasValue) - { - return false; - } - - if (contractGroup.Stat_SeriesCreatedDate.Value > filterDateSeries) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeWatchedDate: - DateTime filterDateEpsiodeWatched; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpsiodeWatched = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpsiodeWatched = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.WatchedDate.HasValue) - { - return false; - } - - if (contractGroup.WatchedDate.Value < filterDateEpsiodeWatched) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (contractGroup?.WatchedDate == null) - { - return false; - } - - if (contractGroup.WatchedDate.Value > filterDateEpsiodeWatched) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeAddedDate: - DateTime filterDateEpisodeAdded; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeAdded = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeAdded = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractGroup.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractGroup.EpisodeAddedDate.Value < filterDateEpisodeAdded) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractGroup.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractGroup.EpisodeAddedDate.Value > filterDateEpisodeAdded) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeCount: - int.TryParse(gfc.ConditionParameter, out var epCount); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && - contractGroup.Stat_EpisodeCount < epCount) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && - contractGroup.Stat_EpisodeCount > epCount) - { - return false; - } - - break; - - case GroupFilterConditionType.AniDBRating: - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dRating); - var thisRating = contractGroup.Stat_AniDBRating / 100; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && thisRating < dRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && thisRating > dRating) - { - return false; - } - - break; - - case GroupFilterConditionType.UserRating: - if (!contractGroup.Stat_UserVoteOverall.HasValue) - { - return false; - } - - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dUserRating); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && - contractGroup.Stat_UserVoteOverall.Value < dUserRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && - contractGroup.Stat_UserVoteOverall.Value > dUserRating) - { - return false; - } - - break; - - case GroupFilterConditionType.CustomTags: - var ctags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundTag = ctags.FindInEnumerable(contractGroup.Stat_AllCustomTags); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundTag) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundTag) - { - return false; - } - - break; - - case GroupFilterConditionType.AnimeType: - var ctypes = - gfc.ConditionParameter - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => ((int)Commons.Extensions.Models.RawToType(a)).ToString()) - .ToList(); - var foundAnimeType = ctypes.FindInEnumerable(contractGroup.Stat_AnimeTypes); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundAnimeType) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundAnimeType) - { - return false; - } - - break; - - case GroupFilterConditionType.VideoQuality: - var vqs = - gfc.ConditionParameter - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundVid = vqs.FindInEnumerable(contractGroup.Stat_AllVideoQuality); - var foundVidAllEps = vqs.FindInEnumerable(contractGroup.Stat_AllVideoQuality_Episodes); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.InAllEpisodes && !foundVidAllEps) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotInAllEpisodes && foundVidAllEps) - { - return false; - } - - break; - - case GroupFilterConditionType.AudioLanguage: - var als = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundLang = als.FindInEnumerable(contractGroup.Stat_AudioLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundLang) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundLang) - { - return false; - } - - break; - - case GroupFilterConditionType.SubtitleLanguage: - var ass = - gfc.ConditionParameter - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundSub = ass.FindInEnumerable(contractGroup.Stat_SubtitleLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundSub) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundSub) - { - return false; - } - - break; - } - - return true; - } - - public bool EvaluateGroupFilter(CL_AnimeSeries_User contractSerie, JMMUser curUser) - { - //Directories don't count - if (IsDirectory) - { - return false; - } - - if (contractSerie?.AniDBAnime?.AniDBAnime == null) - { - return false; - } - - if (curUser?.GetHideCategories().FindInEnumerable(contractSerie.AniDBAnime.AniDBAnime.GetAllTags()) ?? - false) - { - return false; - } - - var exclude = BaseCondition == (int)GroupFilterBaseCondition.Exclude; - - return Conditions.All(gfc => exclude ^ EvaluateConditions(contractSerie, gfc)); - } - - private bool EvaluateConditions(CL_AnimeSeries_User contractSerie, GroupFilterCondition gfc) - { - var style = NumberStyles.Number; - var culture = CultureInfo.InvariantCulture; - - switch (gfc.GetConditionTypeEnum()) - { - case GroupFilterConditionType.MissingEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - (contractSerie.MissingEpisodeCount > 0 || contractSerie.MissingEpisodeCountGroups > 0) == - false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - (contractSerie.MissingEpisodeCount > 0 || contractSerie.MissingEpisodeCountGroups > 0)) - { - return false; - } - - break; - - case GroupFilterConditionType.MissingEpisodesCollecting: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractSerie.MissingEpisodeCountGroups > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractSerie.MissingEpisodeCountGroups > 0) - { - return false; - } - - break; - case GroupFilterConditionType.Tag: - var tags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .Where(a => !string.IsNullOrWhiteSpace(a)) - .ToList(); - var tagsFound = - tags.Any(a => contractSerie.AniDBAnime.AniDBAnime.GetAllTags().Contains(a)); - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.In || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include) && !tagsFound) - { - return false; - } - - if ((gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude) && tagsFound) - { - return false; - } - - break; - case GroupFilterConditionType.Year: - var years = new HashSet<int>(); - var parameterStrings = gfc.ConditionParameter.Trim().Split(','); - foreach (var yearString in parameterStrings) - { - if (int.TryParse(yearString.Trim(), out var paramYear)) - { - years.Add(paramYear); - } - } - - if (years.Count <= 0) - { - return false; - } - - switch (gfc.GetConditionOperatorEnum()) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - if (years.Any(year => contractSerie.AniDBAnime.IsInYear(year))) - { - return true; - } - - return false; - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - if (years.Any(year => contractSerie.AniDBAnime.IsInYear(year))) - { - return false; - } - - return true; - } - - break; - case GroupFilterConditionType.Season: - var paramStrings = gfc.ConditionParameter.Trim().Split(',').Select(a => - { - var b = a.Trim().Split(' '); - if (!Enum.TryParse(b[0], out AnimeSeason season)) - { - return null; - } - - if (!int.TryParse(b[1], out var year)) - { - return null; - } - - return Tuple.Create(season, year); - }).Where(a => a != null).ToArray(); - - switch (gfc.GetConditionOperatorEnum()) - { - case GroupFilterOperator.Include: - case GroupFilterOperator.In: - return paramStrings.Any(a => contractSerie?.AniDBAnime?.IsInSeason(a.Item1, a.Item2) ?? false); - case GroupFilterOperator.Exclude: - case GroupFilterOperator.NotIn: - return !paramStrings.Any(a => contractSerie?.AniDBAnime?.IsInSeason(a.Item1, a.Item2) ?? false); - } - - break; - case GroupFilterConditionType.HasWatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractSerie.WatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractSerie.WatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.HasUnwatchedEpisodes: - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && - contractSerie.UnwatchedEpisodeCount > 0 == false) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && - contractSerie.UnwatchedEpisodeCount > 0) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBInfo: - if (contractSerie.AniDBAnime.AniDBAnime.AnimeType == (int)AnimeType.Movie || - contractSerie.AniDBAnime.AniDBAnime.Restricted > 0) - { - return false; - } - - var tvDBInfoMissing = !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(contractSerie.AniDB_ID).Any(); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && tvDBInfoMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !tvDBInfoMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMALInfo: - var malMissing = contractSerie.CrossRefAniDBMAL == null || - contractSerie.CrossRefAniDBMAL.Count == 0; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && malMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !malMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedMovieDBInfo: - if (contractSerie.AniDBAnime.AniDBAnime.AnimeType != (int)AnimeType.Movie || - contractSerie.AniDBAnime.AniDBAnime.Restricted > 0) - { - return false; - } - - var movieMissing = contractSerie.CrossRefAniDBMovieDB == null; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && movieMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !movieMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.AssignedTvDBOrMovieDBInfo: - // return true if excluding - if (contractSerie.AniDBAnime.AniDBAnime.Restricted > 0) - { - return false; - } - - var movieLinkMissing = contractSerie.CrossRefAniDBMovieDB == null; - var tvlinkMissing = !RepoFactory.CrossRef_AniDB_TvDB.GetByAnimeID(contractSerie.AniDB_ID).Any(); - var bothMissing = movieLinkMissing && tvlinkMissing; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && bothMissing) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && !bothMissing) - { - return false; - } - - break; - - case GroupFilterConditionType.CompletedSeries: - var completed = contractSerie.AniDBAnime.AniDBAnime.EndDate.HasValue && - contractSerie.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now && - !(contractSerie.MissingEpisodeCount > 0 || - contractSerie.MissingEpisodeCountGroups > 0); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !completed) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && completed) - { - return false; - } - - break; - - case GroupFilterConditionType.FinishedAiring: - var finished = contractSerie.AniDBAnime.AniDBAnime.EndDate.HasValue && - contractSerie.AniDBAnime.AniDBAnime.EndDate.Value < DateTime.Now; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !finished) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && finished) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVoted: - var voted = contractSerie.AniDBAnime.UserVote != null && - contractSerie.AniDBAnime.UserVote.VoteType == (int)AniDBVoteType.Anime; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !voted) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && voted) - { - return false; - } - - break; - - case GroupFilterConditionType.UserVotedAny: - var votedany = contractSerie.AniDBAnime.UserVote != null; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Include && !votedany) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.Exclude && votedany) - { - return false; - } - - break; - - case GroupFilterConditionType.AirDate: - DateTime filterDate; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDate = DateTime.Today.AddDays(0 - days); - } - else - { - filterDate = GroupFilterHelper.GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.AniDBAnime.AniDBAnime.AirDate.HasValue || - contractSerie.AniDBAnime.AniDBAnime.AirDate.Value < filterDate) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractSerie.AniDBAnime.AniDBAnime.AirDate.HasValue || - contractSerie.AniDBAnime.AniDBAnime.AirDate.Value > filterDate) - { - return false; - } - } - - break; - case GroupFilterConditionType.LatestEpisodeAirDate: - DateTime filterDateEpisodeLastAired; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeLastAired = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeLastAired = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractSerie.LatestEpisodeAirDate.Value < filterDateEpisodeLastAired) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractSerie.LatestEpisodeAirDate.HasValue) - { - return false; - } - - if (contractSerie.LatestEpisodeAirDate.Value > filterDateEpisodeLastAired) - { - return false; - } - } - - break; - case GroupFilterConditionType.SeriesCreatedDate: - DateTime filterDateSeries; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateSeries = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateSeries = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (contractSerie.DateTimeCreated < filterDateSeries) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (contractSerie.DateTimeCreated > filterDateSeries) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeWatchedDate: - DateTime filterDateEpsiodeWatched; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpsiodeWatched = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpsiodeWatched = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.WatchedDate.HasValue) - { - return false; - } - - if (contractSerie.WatchedDate.Value < filterDateEpsiodeWatched) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (contractSerie?.WatchedDate == null) - { - return false; - } - - if (contractSerie.WatchedDate.Value > filterDateEpsiodeWatched) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeAddedDate: - DateTime filterDateEpisodeAdded; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - int.TryParse(gfc.ConditionParameter, out var days); - filterDateEpisodeAdded = DateTime.Today.AddDays(0 - days); - } - else - { - filterDateEpisodeAdded = GetDateFromString(gfc.ConditionParameter); - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan || - gfc.GetConditionOperatorEnum() == GroupFilterOperator.LastXDays) - { - if (!contractSerie.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractSerie.EpisodeAddedDate.Value < filterDateEpisodeAdded) - { - return false; - } - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan) - { - if (!contractSerie.EpisodeAddedDate.HasValue) - { - return false; - } - - if (contractSerie.EpisodeAddedDate.Value > filterDateEpisodeAdded) - { - return false; - } - } - - break; - - case GroupFilterConditionType.EpisodeCount: - int.TryParse(gfc.ConditionParameter, out var epCount); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && - contractSerie.AniDBAnime.AniDBAnime.EpisodeCount < epCount) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && - contractSerie.AniDBAnime.AniDBAnime.EpisodeCount > epCount) - { - return false; - } - - break; - - case GroupFilterConditionType.AniDBRating: - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dRating); - var totalVotes = contractSerie.AniDBAnime.AniDBAnime.VoteCount + - contractSerie.AniDBAnime.AniDBAnime.TempVoteCount; - decimal totalRating = contractSerie.AniDBAnime.AniDBAnime.Rating * - contractSerie.AniDBAnime.AniDBAnime.VoteCount + - contractSerie.AniDBAnime.AniDBAnime.TempRating * - contractSerie.AniDBAnime.AniDBAnime.TempVoteCount; - var thisRating = totalVotes == 0 ? 0 : totalRating / totalVotes / 100; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && thisRating < dRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && thisRating > dRating) - { - return false; - } - - break; - - case GroupFilterConditionType.UserRating: - decimal.TryParse(gfc.ConditionParameter, style, culture, out var dUserRating); - decimal val = contractSerie.AniDBAnime.UserVote?.VoteValue ?? 0; - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.GreaterThan && val < dUserRating) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.LessThan && val > dUserRating) - { - return false; - } - - break; - - - case GroupFilterConditionType.CustomTags: - var ctags = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundTag = - ctags.FindInEnumerable(contractSerie.AniDBAnime.CustomTags.Select(a => a.TagName)); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundTag) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundTag) - { - return false; - } - - break; - - case GroupFilterConditionType.AnimeType: - var ctypes = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select( - a => ((int)Commons.Extensions.Models.RawToType(a.ToLowerInvariant())).ToString()) - .ToList(); - var foundAnimeType = ctypes.Contains(contractSerie.AniDBAnime.AniDBAnime.AnimeType.ToString()); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundAnimeType) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundAnimeType) - { - return false; - } - - break; - - case GroupFilterConditionType.VideoQuality: - var vqs = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundVid = vqs.FindInEnumerable(contractSerie.AniDBAnime.Stat_AllVideoQuality); - var foundVidAllEps = - vqs.FindInEnumerable(contractSerie.AniDBAnime.Stat_AllVideoQuality_Episodes); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundVid) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.InAllEpisodes && !foundVidAllEps) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotInAllEpisodes && foundVidAllEps) - { - return false; - } - - break; - - case GroupFilterConditionType.AudioLanguage: - var als = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundLang = als.FindInEnumerable(contractSerie.AniDBAnime.Stat_AudioLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundLang) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundLang) - { - return false; - } - - break; - - case GroupFilterConditionType.SubtitleLanguage: - var ass = - gfc.ConditionParameter.Trim() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.ToLowerInvariant().Trim()) - .ToList(); - var foundSub = ass.FindInEnumerable(contractSerie.AniDBAnime.Stat_AudioLanguages); - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.In && !foundSub) - { - return false; - } - - if (gfc.GetConditionOperatorEnum() == GroupFilterOperator.NotIn && foundSub) - { - return false; - } - - break; - } - - return true; - } - - public static DateTime GetDateFromString(string sDate) - { - try - { - var year = int.Parse(sDate.Substring(0, 4)); - var month = int.Parse(sDate.Substring(4, 2)); - var day = int.Parse(sDate.Substring(6, 2)); - - return new DateTime(year, month, day); - } - catch - { - return DateTime.Today; - } - } - - /// <summary> - /// Updates the <see cref="GroupsIdsString"/> and/or <see cref="SeriesIdsString"/> properties - /// based on the current contents of <see cref="GroupsIds"/> and <see cref="SeriesIds"/>. - /// </summary> - /// <param name="updateGroups"><c>true</c> to update <see cref="GroupsIdsString"/>; otherwise, <c>false</c>.</param> - /// <param name="updateSeries"><c>true</c> to update <see cref="SeriesIds"/>; otherwise, <c>false</c>.</param> - public void UpdateEntityReferenceStrings(bool updateGroups = true, bool updateSeries = true) - { - WriteLock( - () => - { - if (updateGroups) - { - GroupsIdsString = JsonConvert.SerializeObject(GroupsIds); - GroupsIdsVersion = GROUPFILTER_VERSION; - } - - if (updateSeries) - { - SeriesIdsString = JsonConvert.SerializeObject(SeriesIds); - SeriesIdsVersion = SERIEFILTER_VERSION; - } - } - ); - } - - public void QueueUpdate() - { - var commandFactory = Utils.ServiceContainer.GetRequiredService<ICommandRequestFactory>(); - commandFactory.CreateAndSave<CommandRequest_RefreshGroupFilter>(c => c.GroupFilterID = GroupFilterID); - } - - public override bool Equals(object obj) - { - var other = obj as SVR_GroupFilter; - if (other?.ApplyToSeries != ApplyToSeries) - { - return false; - } - - if (other.BaseCondition != BaseCondition) - { - return false; - } - - if (other.FilterType != FilterType) - { - return false; - } - - if (other.InvisibleInClients != InvisibleInClients) - { - return false; - } - - if (other.Locked != Locked) - { - return false; - } - - if (other.ParentGroupFilterID != ParentGroupFilterID) - { - return false; - } - - if (other.GroupFilterName != GroupFilterName) - { - return false; - } - - if (other.SortingCriteria != SortingCriteria) - { - return false; - } - - if (Conditions == null || Conditions.Count == 0) - { - Conditions = RepoFactory.GroupFilterCondition.GetByGroupFilterID(GroupFilterID); - RepoFactory.GroupFilter.Save(this); - } - - if (other.Conditions == null || other.Conditions.Count == 0) - { - other.Conditions = RepoFactory.GroupFilterCondition.GetByGroupFilterID(other.GroupFilterID); - RepoFactory.GroupFilter.Save(other); - } - - if (Conditions != null && other.Conditions != null) - { - if (!Conditions.ContentEquals(other.Conditions)) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - return 0; // Always use equals - } - - private void ReadLock(Action action) - { - _lock.EnterReadLock(); - try - { - action.Invoke(); - } - finally - { - _lock.ExitReadLock(); - } - } - - private T ReadLock<T>(Func<T> action) - { - _lock.EnterReadLock(); - try - { - return action.Invoke(); - } - finally - { - _lock.ExitReadLock(); - } - } - - private void WriteLock(Action action) - { - _lock.EnterWriteLock(); - try - { - action.Invoke(); - } - finally - { - _lock.ExitWriteLock(); - } - } - - private T WriteLock<T>(Func<T> action) - { - _lock.EnterWriteLock(); - try - { - return action.Invoke(); - } - finally - { - _lock.ExitWriteLock(); - } - } -} diff --git a/Shoko.Server/Models/SVR_JMMUser.cs b/Shoko.Server/Models/SVR_JMMUser.cs index 2a9583d35..f03590c70 100644 --- a/Shoko.Server/Models/SVR_JMMUser.cs +++ b/Shoko.Server/Models/SVR_JMMUser.cs @@ -89,7 +89,7 @@ public bool SetAvatarImage(byte[] byteArray, string contentType, string fieldNam } if (!skipSave) - RepoFactory.JMMUser.Save(this, false); + RepoFactory.JMMUser.Save(this); return true; } @@ -100,7 +100,7 @@ public void RemoveAvatarImage(bool skipSave = false) AvatarImageMetadata = null; if (!skipSave) - RepoFactory.JMMUser.Save(this, false); + RepoFactory.JMMUser.Save(this); } #endregion @@ -148,23 +148,6 @@ public static bool CompareUser(JMMUser olduser, JMMUser newuser) return false; } - public void UpdateGroupFilters() - { - IReadOnlyList<SVR_GroupFilter> gfs = RepoFactory.GroupFilter.GetAll(); - List<SVR_AnimeGroup> allGrps = RepoFactory.AnimeGroup.GetAllTopLevelGroups(); // No Need of subgroups - foreach (SVR_GroupFilter gf in gfs) - { - bool change = false; - foreach (SVR_AnimeGroup grp in allGrps) - { - CL_AnimeGroup_User cgrp = grp.GetUserContract(JMMUserID); - change |= gf.UpdateGroupFilterFromGroup(cgrp, this); - } - if (change) - RepoFactory.GroupFilter.Save(gf); - } - } - // IUserIdentity implementation public string UserName { diff --git a/Shoko.Server/Plex/PlexHelper.cs b/Shoko.Server/Plex/PlexHelper.cs index 09a591e82..0ec56144f 100644 --- a/Shoko.Server/Plex/PlexHelper.cs +++ b/Shoko.Server/Plex/PlexHelper.cs @@ -267,7 +267,6 @@ public void SaveUser(JMMUser user) { var existingUser = false; var updateStats = false; - var updateGf = false; SVR_JMMUser jmmUser = null; if (user.JMMUserID != 0) { @@ -283,7 +282,6 @@ public void SaveUser(JMMUser user) { jmmUser = new SVR_JMMUser(); updateStats = true; - updateGf = true; } if (existingUser && jmmUser.IsAniDBUser != user.IsAniDBUser) @@ -292,10 +290,6 @@ public void SaveUser(JMMUser user) } var hcat = string.Join(",", user.HideCategories); - if (jmmUser.HideCategories != hcat) - { - updateGf = true; - } jmmUser.HideCategories = hcat; jmmUser.IsAniDBUser = user.IsAniDBUser; @@ -348,7 +342,7 @@ public void SaveUser(JMMUser user) } } - RepoFactory.JMMUser.Save(jmmUser, updateGf); + RepoFactory.JMMUser.Save(jmmUser); // update stats if (!updateStats) diff --git a/Shoko.Server/PlexAndKodi/Breadcrumbs.cs b/Shoko.Server/PlexAndKodi/Breadcrumbs.cs index 7ca0a850b..a5176a329 100644 --- a/Shoko.Server/PlexAndKodi/Breadcrumbs.cs +++ b/Shoko.Server/PlexAndKodi/Breadcrumbs.cs @@ -26,7 +26,6 @@ public class BreadCrumbs public string ParentIndex { get; set; } private static Dictionary<string, BreadCrumbs> Cache = new(); - //TODO CACHE EVICTION? public BreadCrumbs Update(Video v, bool noart = false) { diff --git a/Shoko.Server/PlexAndKodi/CommonImplementation.cs b/Shoko.Server/PlexAndKodi/CommonImplementation.cs deleted file mode 100644 index 0f8f3aa51..000000000 --- a/Shoko.Server/PlexAndKodi/CommonImplementation.cs +++ /dev/null @@ -1,1475 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Shoko.Commons.Extensions; -using Shoko.Commons.Properties; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Models.Plex.Connections; -using Shoko.Models.PlexAndKodi; -using Shoko.Models.Server; -using Shoko.Server.Commands; -using Shoko.Server.Commands.AniDB; -using Shoko.Server.Databases; -using Shoko.Server.Models; -using Shoko.Server.Plex; -using Shoko.Server.PlexAndKodi.Kodi; -using Shoko.Server.PlexAndKodi.Plex; -using Shoko.Server.Providers.TraktTV; -using Shoko.Server.Repositories; -using Shoko.Server.Repositories.NHibernate; -using Shoko.Server.Settings; -using Shoko.Server.Utilities; -using Directory = Shoko.Models.Plex.Libraries.Directory; -using MediaContainer = Shoko.Models.PlexAndKodi.MediaContainer; -using Stream = System.IO.Stream; - -// ReSharper disable FunctionComplexityOverflow - -namespace Shoko.Server.PlexAndKodi; - -public class CommonImplementation -{ - private readonly ILogger<CommonImplementation> _logger; - private readonly ISettingsProvider _settingsProvider; - - //private functions are use internal - - public CommonImplementation(ILogger<CommonImplementation> logger, ISettingsProvider settingsProvider) - { - _logger = logger; - _settingsProvider = settingsProvider; - } - - public Stream GetSupportImage(string name) - { - if (string.IsNullOrEmpty(name)) - { - return null; - } - - name = Path.GetFileNameWithoutExtension(name); - var man = Resources.ResourceManager; - var dta = (byte[])man.GetObject(name); - if (dta == null || dta.Length == 0) - { - return null; - } - - var ms = new MemoryStream(dta); - ms.Seek(0, SeekOrigin.Begin); - return ms; - } - - public MediaContainer GetFilters(IProvider prov, string uid) - { - int.TryParse(uid, out var t); - var user = t > 0 ? Helper.GetJMMUser(uid) : Helper.GetUser(uid); - if (user == null) - { - return new MediaContainer { ErrorString = "User not found" }; - } - - var userid = user.JMMUserID; - - var info = prov.UseBreadCrumbs - ? new BreadCrumbs { Key = prov.ConstructFiltersUrl(userid), Title = "Anime" } - : null; - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, "Anime", false, false, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); //Normal OPTION VERB - } - - var dirs = new List<Video>(); - try - { - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var allGfs = RepoFactory.GroupFilter.GetTopLevel() - .Where(a => !a.IsHidden && - ( - (a.GroupsIds.ContainsKey(userid) && a.GroupsIds[userid].Count > 0) - || a.IsDirectory) - ) - .ToList(); - - - foreach (var gg in allGfs) - { - var pp = Helper.DirectoryFromFilter(prov, gg, userid); - if (pp != null) - { - dirs.Add(prov, pp, info); - } - } - - var vids = RepoFactory.VideoLocal.GetVideosWithoutEpisodeUnsorted(); - if (vids.Count > 0) - { - var pp = new Shoko.Models.PlexAndKodi.Directory { Type = "show" }; - pp.Key = prov.ShortUrl(prov.ConstructUnsortUrl(userid)); - pp.Title = "Unsort"; - pp.AnimeType = AnimeTypes.AnimeUnsort.ToString(); - pp.Thumb = prov.ConstructSupportImageLink("plex_unsort.png"); - pp.LeafCount = vids.Count; - pp.ViewedLeafCount = 0; - dirs.Add(prov, pp, info); - } - - var playlists = RepoFactory.Playlist.GetAll(); - if (playlists.Count > 0) - { - var pp = new Shoko.Models.PlexAndKodi.Directory { Type = "show" }; - pp.Key = prov.ShortUrl(prov.ConstructPlaylistUrl(userid)); - pp.Title = "Playlists"; - pp.AnimeType = AnimeTypes.AnimePlaylist.ToString(); - pp.Thumb = prov.ConstructSupportImageLink("plex_playlists.png"); - pp.LeafCount = playlists.Count; - pp.ViewedLeafCount = 0; - dirs.Add(prov, pp, info); - } - - dirs = dirs.OrderBy(a => a.Title).ToList(); - } - - ret.MediaContainer.RandomizeArt(prov, dirs); - if (prov.AddPlexPrefsItem) - { - var dir = new Shoko.Models.PlexAndKodi.Directory { Prompt = "Search" }; - dir.Thumb = dir.Art = "/:/plugins/com.plexapp.plugins.myanime/resources/Search.png"; - dir.Key = "/video/jmm/search"; - dir.Title = "Search"; - dir.Search = "1"; - dirs.Add(dir); - } - - if (prov.AddPlexSearchItem) - { - var dir = new Shoko.Models.PlexAndKodi.Directory(); - dir.Thumb = dir.Art = "/:/plugins/com.plexapp.plugins.myanime/resources/Gear.png"; - dir.Key = "/:/plugins/com.plexapp.plugins.myanime/prefs"; - dir.Title = "Preferences"; - dir.Settings = "1"; - dirs.Add(dir); - } - - ret.Childrens = dirs; - var dinfo = prov.GetPlexClient(); - if (dinfo != null) - { - _logger.LogInformation(dinfo.ToString()); - } - - return ret.GetStream(prov); - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new MediaContainer { ErrorString = "System Error, see JMMServer logs for more information" }; - } - } - - public MediaContainer GetMetadata(IProvider prov, string UserId, int type, string Id, string historyinfo, - bool nocast = false, int? filter = null) - { - try - { - var his = prov.UseBreadCrumbs ? BreadCrumbs.FromKey(historyinfo) : null; - var user = Helper.GetJMMUser(UserId); - - switch ((JMMType)type) - { - case JMMType.Group: - return GetItemsFromGroup(prov, user.JMMUserID, Id, his, nocast, filter); - case JMMType.GroupFilter: - return GetGroupsOrSubFiltersFromFilter(prov, user.JMMUserID, Id, his, nocast); - case JMMType.GroupUnsort: - return GetUnsort(prov, user.JMMUserID, his); - case JMMType.Serie: - return GetItemsFromSerie(prov, user.JMMUserID, Id, his, nocast); - case JMMType.Episode: - return GetFromEpisode(prov, user.JMMUserID, Id, his); - case JMMType.File: - return GetFromFile(prov, user.JMMUserID, Id, his); - case JMMType.Playlist: - return GetItemsFromPlaylist(prov, user.JMMUserID, Id, his); - case JMMType.FakeIosThumb: - return FakeParentForIOSThumbnail(prov, Id); - } - - return new MediaContainer { ErrorString = "Unsupported Type" }; - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new MediaContainer { ErrorString = "System Error, see JMMServer logs for more information" }; - } - } - - private MediaContainer GetItemsFromPlaylist(IProvider prov, int userid, string id, BreadCrumbs info) - { - var PlaylistID = -1; - int.TryParse(id, out PlaylistID); - - if (PlaylistID == 0) - { - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var ret = new BaseObject( - prov.NewMediaContainer(MediaContainerTypes.Show, "Playlists", true, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); //Normal - } - - var retPlaylists = new List<Video>(); - var playlists = RepoFactory.Playlist.GetAll(); - var sessionWrapper = session.Wrap(); - - foreach (var playlist in playlists) - { - var dir = new Shoko.Models.PlexAndKodi.Directory - { - Key = prov.ShortUrl(prov.ConstructPlaylistIdUrl(userid, playlist.PlaylistID)), - Title = playlist.PlaylistName, - Id = playlist.PlaylistID, - AnimeType = AnimeTypes.AnimePlaylist.ToString() - }; - var episodeID = -1; - if (int.TryParse(playlist.PlaylistItems.Split('|')[0].Split(';')[1], out episodeID)) - { - var anime = RepoFactory.AnimeEpisode.GetByID(episodeID) - .GetAnimeSeries() - .GetAnime(); - dir.Thumb = anime?.GetDefaultPosterDetailsNoBlanks()?.GenPoster(prov); - dir.Art = anime?.GetDefaultFanartDetailsNoBlanks()?.GenArt(prov); - dir.Banner = anime?.GetDefaultWideBannerDetailsNoBlanks()?.GenArt(prov); - } - else - { - dir.Thumb = prov.ConstructSupportImageLink("plex_404V.png"); - } - - dir.LeafCount = playlist.PlaylistItems.Split('|').Count(); - dir.ViewedLeafCount = 0; - retPlaylists.Add(prov, dir, info); - } - - retPlaylists = retPlaylists.OrderBy(a => a.Title).ToList(); - ret.Childrens = retPlaylists; - return ret.GetStream(prov); - } - } - - if (PlaylistID > 0) - { - var playlist = RepoFactory.Playlist.GetByID(PlaylistID); - var playlistItems = playlist.PlaylistItems.Split('|'); - var vids = new List<Video>(); - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Episode, playlist.PlaylistName, true, - true, - info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); //Normal - } - - foreach (var item in playlistItems) - { - try - { - var episodeID = -1; - int.TryParse(item.Split(';')[1], out episodeID); - if (episodeID < 0) - { - return new MediaContainer { ErrorString = "Invalid Episode ID" }; - } - - var e = RepoFactory.AnimeEpisode.GetByID(episodeID); - if (e == null) - { - return new MediaContainer { ErrorString = "Invalid Episode" }; - } - - var ep = - new KeyValuePair<SVR_AnimeEpisode, CL_AnimeEpisode_User>(e, - e.GetUserContract(userid)); - if (ep.Value != null && ep.Value.LocalFileCount == 0) - { - continue; - } - - var ser = RepoFactory.AnimeSeries.GetByID(ep.Key.AnimeSeriesID); - if (ser == null) - { - return new MediaContainer { ErrorString = "Invalid Series" }; - } - - var con = ser.GetUserContract(userid); - if (con == null) - { - return new MediaContainer { ErrorString = "Invalid Series, Contract not found" }; - } - - var v = Helper.VideoFromAnimeEpisode(prov, con.CrossRefAniDBTvDBV2, ep, userid); - if (v != null && v.Medias != null && v.Medias.Count > 0) - { - Helper.AddInformationFromMasterSeries(v, con, ser.GetPlexContract(userid)); - v.Type = "episode"; - vids.Add(prov, v, info); - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - { - v.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v.ParentThumb, - v.Art ?? v.ParentArt ?? v.GrandparentArt)); - } - - v.ParentKey = null; - } - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - ret.MediaContainer.RandomizeArt(prov, vids); - ret.Childrens = vids; - return ret.GetStream(prov); - } - - return new MediaContainer { ErrorString = "Invalid Playlist" }; - } - - private MediaContainer GetUnsort(IProvider prov, int userid, BreadCrumbs info) - { - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Video, "Unsort", true, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var dirs = new List<Video>(); - var vids = RepoFactory.VideoLocal.GetVideosWithoutEpisode(); - foreach (var v in vids.OrderByDescending(a => a.DateTimeCreated)) - { - try - { - var m = Helper.VideoFromVideoLocal(prov, v, userid); - dirs.Add(prov, m, info); - m.Thumb = prov.ConstructSupportImageLink("plex_404.png"); - m.ParentThumb = prov.ConstructSupportImageLink("plex_unsort.png"); - m.ParentKey = null; - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - - { - m.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, m.ParentThumb, - m.Art ?? m.ParentArt ?? m.GrandparentArt)); - } - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - ret.Childrens = dirs; - return ret.GetStream(prov); - } - - private MediaContainer GetFromFile(IProvider prov, int userid, string Id, BreadCrumbs info) - { - if (!int.TryParse(Id, out var id)) - { - return new MediaContainer { ErrorString = "Invalid File Id" }; - } - - var vi = RepoFactory.VideoLocal.GetByID(id); - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.File, - Path.GetFileNameWithoutExtension(vi.FileName ?? string.Empty), - true, false, info)); - var v2 = Helper.VideoFromVideoLocal(prov, vi, userid); - var dirs = new List<Video>(); - dirs.EppAdd(prov, v2, info, true); - v2.Thumb = prov.ConstructSupportImageLink("plex_404.png"); - v2.ParentThumb = prov.ConstructSupportImageLink("plex_unsort.png"); - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - - { - v2.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v2.ParentThumb, - v2.Art ?? v2.ParentArt ?? v2.GrandparentArt)); - } - - v2.ParentKey = null; - if (prov.UseBreadCrumbs) - { - v2.Key = prov.ShortUrl(ret.MediaContainer.Key); - } - - ret.MediaContainer.Childrens = dirs; - return ret.GetStream(prov); - } - - private MediaContainer GetFromEpisode(IProvider prov, int userid, string Id, BreadCrumbs info) - { - if (!int.TryParse(Id, out var id)) - { - return new MediaContainer { ErrorString = "Invalid Episode Id" }; - } - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Episode, "Episode", true, true, info)); - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var dirs = new List<Video>(); - var sessionWrapper = session.Wrap(); - - var e = RepoFactory.AnimeEpisode.GetByID(id); - if (e == null) - { - return new MediaContainer { ErrorString = "Invalid Episode Id" }; - } - - var ep = - new KeyValuePair<SVR_AnimeEpisode, CL_AnimeEpisode_User>(e, - e.GetUserContract(userid)); - if (ep.Value != null && ep.Value.LocalFileCount == 0) - { - return new MediaContainer { ErrorString = "Episode do not have videolocals" }; - } - - var aep = ep.Key.AniDB_Episode; - if (aep == null) - { - return new MediaContainer { ErrorString = "Invalid Episode AniDB link not found" }; - } - - var ser = RepoFactory.AnimeSeries.GetByID(ep.Key.AnimeSeriesID); - if (ser == null) - { - return new MediaContainer { ErrorString = "Invalid Serie" }; - } - - var anime = ser.GetAnime(); - var con = ser.GetUserContract(userid); - if (con == null) - { - return new MediaContainer { ErrorString = "Invalid Serie, Contract not found" }; - } - - try - { - var v = Helper.VideoFromAnimeEpisode(prov, con.CrossRefAniDBTvDBV2, ep, userid); - if (v != null) - { - var nv = ser.GetPlexContract(userid); - Helper.AddInformationFromMasterSeries(v, con, ser.GetPlexContract(userid), - prov is KodiProvider); - if (v.Medias != null && v.Medias.Count > 0) - { - v.Type = "episode"; - dirs.EppAdd(prov, v, info, true); - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - { - v.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v.ParentThumb, - v.Art ?? v.ParentArt ?? v.GrandparentArt)); - } - - v.ParentKey = null; - } - - if (prov.UseBreadCrumbs) - { - v.Key = prov.ShortUrl(ret.MediaContainer.Key); - } - - ret.MediaContainer.Art = prov.ReplaceSchemeHost(nv.Art ?? nv.ParentArt ?? nv.GrandparentArt); - } - - ret.MediaContainer.Childrens = dirs; - return ret.GetStream(prov); - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - return new MediaContainer { ErrorString = "Episode Not Found" }; - } - - public Dictionary<int, string> GetUsers() - { - var users = new Dictionary<int, string>(); - try - { - foreach (var us in RepoFactory.JMMUser.GetAll()) - { - users.Add(us.JMMUserID, us.Username); - } - - return users; - } - catch - { - return null; - } - } - - public PlexContract_Users GetUsers(IProvider prov) - { - var gfs = new PlexContract_Users(); - try - { - gfs.Users = new List<PlexContract_User>(); - foreach (var us in RepoFactory.JMMUser.GetAll()) - { - var p = new PlexContract_User { id = us.JMMUserID.ToString(), name = us.Username }; - gfs.Users.Add(p); - } - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new PlexContract_Users { ErrorString = "System Error, see JMMServer logs for more information" }; - } - - return gfs; - } - - public Response GetVersion() - { - var rsp = new Response(); - try - { - rsp.Code = "200"; - rsp.Message = Assembly.GetEntryAssembly().GetName().Version.ToString(); - return rsp; - } - catch (Exception e) - { - _logger.LogError(e, e.ToString()); - rsp.Code = "500"; - rsp.Message = "System Error, see JMMServer logs for more information"; - } - - return rsp; - } - - public MediaContainer Search(IProvider prov, string UserId, int lim, string query, bool searchTag, - bool nocast = false) - { - var info = prov.UseBreadCrumbs - ? new BreadCrumbs - { - Key = prov.ConstructSearchUrl(UserId, lim, query, searchTag), Title = "Search for '" + query + "'" - } - : null; - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, "Search for '" + query + "'", true, - true, - info)); - if (lim == 0) - { - lim = 100; - } - - var user = Helper.GetUser(UserId); - if (user == null) - { - return new MediaContainer { ErrorString = "User Not Found" }; - } - - var ls = new List<Video>(); - var cnt = 0; - var series = searchTag - ? RepoFactory.AnimeSeries.GetAll() - .Where( - a => - a.Contract != null && a.Contract.AniDBAnime != null && - a.Contract.AniDBAnime.AniDBAnime != null && - (a.Contract.AniDBAnime.AniDBAnime.GetAllTags() - .Contains(query, - StringComparer.InvariantCultureIgnoreCase) || - a.Contract.AniDBAnime.CustomTags.Select(b => b.TagName) - .Contains(query, StringComparer.InvariantCultureIgnoreCase))) - : RepoFactory.AnimeSeries.GetAll() - .Where( - a => - a.Contract != null && a.Contract.AniDBAnime != null && - a.Contract.AniDBAnime.AniDBAnime != null && - string.Join(",", a.Contract.AniDBAnime.AniDBAnime.AllTitles) - .IndexOf(query, 0, StringComparison.InvariantCultureIgnoreCase) >= 0); - - //List<AniDB_Anime> animes = searchTag ? RepoFactory.AniDB_Anime.SearchByTag(query) : RepoFactory.AniDB_Anime.SearchByName(query); - foreach (var ser in series) - { - if (!user.AllowedSeries(ser)) - { - continue; - } - - Video v = ser.GetPlexContract(user.JMMUserID)?.Clone<Shoko.Models.PlexAndKodi.Directory>(prov); - if (v != null) - { - switch (ser.Contract.AniDBAnime.AniDBAnime.AnimeType) - { - case (int)AnimeType.Movie: - v.SourceTitle = "Anime Movies"; - break; - case (int)AnimeType.OVA: - v.SourceTitle = "Anime Ovas"; - break; - case (int)AnimeType.Other: - v.SourceTitle = "Anime Others"; - break; - case (int)AnimeType.TVSeries: - v.SourceTitle = "Anime Series"; - break; - case (int)AnimeType.TVSpecial: - v.SourceTitle = "Anime Specials"; - break; - case (int)AnimeType.Web: - v.SourceTitle = "Anime Web Clips"; - break; - } - - if (nocast) - { - v.Roles = null; - } - - v.GenerateKey(prov, user.JMMUserID); - ls.Add(prov, v, info); - } - - cnt++; - if (cnt == lim) - { - break; - } - } - - ret.MediaContainer.RandomizeArt(prov, ls); - ret.MediaContainer.Childrens = Helper.ConvertToDirectory(ls, prov); - return ret.GetStream(prov); - } - - public MediaContainer GetItemsFromGroup(IProvider prov, int userid, string GroupId, BreadCrumbs info, - bool nocast, int? filterID) - { - int.TryParse(GroupId, out var groupID); - if (groupID == -1) - { - return new MediaContainer { ErrorString = "Invalid Group Id" }; - } - - var retGroups = new List<Video>(); - var grp = RepoFactory.AnimeGroup.GetByID(groupID); - - if (grp == null) - { - return new MediaContainer { ErrorString = "Invalid Group" }; - } - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, grp.GroupName, false, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var basegrp = grp?.GetUserContract(userid); - if (basegrp != null) - { - var seriesList = grp.GetSeries(); - if (filterID != null) - { - var filter = RepoFactory.GroupFilter.GetByID(filterID.Value); - if (filter != null) - { - if (filter.ApplyToSeries > 0) - { - if (filter.SeriesIds.ContainsKey(userid)) - { - seriesList = - seriesList.Where(a => filter.SeriesIds[userid].Contains(a.AnimeSeriesID)).ToList(); - } - } - } - } - - foreach (var grpChild in grp.GetChildGroups()) - { - var v = grpChild.GetPlexContract(userid); - if (v != null) - { - v.Type = "show"; - v.GenerateKey(prov, userid); - - v.Art = Helper.GetRandomFanartFromVideo(v, prov) ?? v.Art; - v.Banner = Helper.GetRandomBannerFromVideo(v, prov) ?? v.Banner; - - if (nocast) - { - v.Roles = null; - } - - retGroups.Add(prov, v, info); - v.ParentThumb = v.GrandparentThumb = null; - } - } - - foreach (var ser in seriesList) - { - var v = ser.GetPlexContract(userid)?.Clone<Shoko.Models.PlexAndKodi.Directory>(prov); - if (v != null) - { - v.AirDate = ser.AirDate ?? DateTime.MinValue; - v.Group = basegrp; - v.Type = "show"; - v.GenerateKey(prov, userid); - v.Art = Helper.GetRandomFanartFromVideo(v, prov) ?? v.Art; - v.Banner = Helper.GetRandomBannerFromVideo(v, prov) ?? v.Banner; - if (nocast) - { - v.Roles = null; - } - - retGroups.Add(prov, v, info); - v.ParentThumb = v.GrandparentThumb = null; - } - } - } - - ret.MediaContainer.RandomizeArt(prov, retGroups); - ret.Childrens = Helper.ConvertToDirectory(retGroups.OrderBy(a => a.AirDate).ToList(), prov); - FilterExtras(prov, ret.Childrens); - return ret.GetStream(prov); - } - - public Response ToggleWatchedStatusOnEpisode(IProvider prov, string userid, int aep, bool wstatus) - { - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var ep = RepoFactory.AnimeEpisode.GetByID(aep); - if (ep == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - ep.ToggleWatchedStatus(wstatus, true, DateTime.Now, false, usid, true); - var series = ep.GetAnimeSeries(); - series?.UpdateStats(true, false); - series?.AnimeGroup?.TopLevelAnimeGroup?.UpdateStatsFromTopLevel(true, true); - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response ToggleWatchedStatusOnSeries(IProvider prov, string userid, int aep, - bool wstatus) - { - //prov.AddResponseHeaders(); - - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var series = RepoFactory.AnimeSeries.GetByID(aep); - if (series == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - var eps = series.GetAnimeEpisodes(); - foreach (var ep in eps) - { - if (ep.AniDB_Episode == null) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Credits) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Trailer) - { - continue; - } - - ep.ToggleWatchedStatus(wstatus, true, DateTime.Now, false, usid, true); - } - - series.UpdateStats(true, false); - series.AnimeGroup?.TopLevelAnimeGroup?.UpdateStatsFromTopLevel(true, true); - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response ToggleWatchedStatusOnGroup(IProvider prov, string userid, int aep, - bool wstatus) - { - //prov.AddResponseHeaders(); - - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var group = RepoFactory.AnimeGroup.GetByID(aep); - if (group == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - foreach (var series in group.GetAllSeries()) - { - foreach (var ep in series.GetAnimeEpisodes()) - { - if (ep.AniDB_Episode == null) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Credits) - { - continue; - } - - if (ep.EpisodeTypeEnum == EpisodeType.Trailer) - { - continue; - } - - ep.ToggleWatchedStatus(wstatus, true, DateTime.Now, false, usid, true); - } - - series.UpdateStats(true, false); - } - - group.TopLevelAnimeGroup.UpdateStatsFromTopLevel(true, false); - - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response VoteAnime(IProvider prov, string userid, int objid, float vvalue, - int vt) - { - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - if (!int.TryParse(userid, out var usid)) - { - return rsp; - } - - var commandFactory = Utils.ServiceContainer.GetRequiredService<ICommandRequestFactory>(); - if (vt == (int)AniDBVoteType.Episode) - { - var ep = RepoFactory.AnimeEpisode.GetByID(objid); - if (ep == null) - { - rsp.Code = "404"; - rsp.Message = "Episode Not Found"; - return rsp; - } - - var anime = ep.GetAnimeSeries().GetAnime(); - if (anime == null) - { - rsp.Code = "404"; - rsp.Message = "Anime Not Found"; - return rsp; - } - - var msg = string.Format("Voting for anime episode: {0} - Value: {1}", ep.AnimeEpisodeID, - vvalue); - _logger.LogInformation(msg); - - // lets save to the database and assume it will work - var thisVote = RepoFactory.AniDB_Vote.GetByEntityAndType(ep.AnimeEpisodeID, AniDBVoteType.Episode); - - if (thisVote == null) - { - thisVote = new AniDB_Vote { EntityID = ep.AnimeEpisodeID }; - } - - thisVote.VoteType = vt; - - var iVoteValue = 0; - if (vvalue > 0) - { - iVoteValue = (int)(vvalue * 100); - } - else - { - iVoteValue = (int)vvalue; - } - - msg = string.Format("Voting for anime episode Formatted: {0} - Value: {1}", ep.AnimeEpisodeID, - iVoteValue); - _logger.LogInformation(msg); - thisVote.VoteValue = iVoteValue; - RepoFactory.AniDB_Vote.Save(thisVote); - - commandFactory.CreateAndSave<CommandRequest_VoteAnime>( - c => - { - c.AnimeID = anime.AnimeID; - c.VoteType = vt; - c.VoteValue = Convert.ToDecimal(vvalue); - } - ); - } - - if (vt == (int)AniDBVoteType.Anime) - { - var ser = RepoFactory.AnimeSeries.GetByID(objid); - var anime = ser.GetAnime(); - if (anime == null) - { - rsp.Code = "404"; - rsp.Message = "Anime Not Found"; - return rsp; - } - - var msg = string.Format("Voting for anime: {0} - Value: {1}", anime.AnimeID, vvalue); - _logger.LogInformation(msg); - - // lets save to the database and assume it will work - var thisVote = - RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.AnimeTemp) ?? - RepoFactory.AniDB_Vote.GetByEntityAndType(anime.AnimeID, AniDBVoteType.Anime); - - if (thisVote == null) - { - thisVote = new AniDB_Vote { EntityID = anime.AnimeID }; - } - - thisVote.VoteType = vt; - - var iVoteValue = 0; - if (vvalue > 0) - { - iVoteValue = (int)(vvalue * 100); - } - else - { - iVoteValue = (int)vvalue; - } - - msg = string.Format("Voting for anime Formatted: {0} - Value: {1}", anime.AnimeID, iVoteValue); - _logger.LogInformation(msg); - thisVote.VoteValue = iVoteValue; - RepoFactory.AniDB_Vote.Save(thisVote); - commandFactory.CreateAndSave<CommandRequest_VoteAnime>( - c => - { - c.AnimeID = anime.AnimeID; - c.VoteType = vt; - c.VoteValue = Convert.ToDecimal(vvalue); - } - ); - } - - rsp.Code = "200"; - rsp.Message = null; - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - public Response TraktScrobble(IProvider prov, string animeId, int typeTrakt, float progressTrakt, int status) - { - var traktHelper = Utils.ServiceContainer.GetRequiredService<TraktTVHelper>(); - var rsp = new Response { Code = "400", Message = "Bad Request" }; - try - { - var statusTraktV2 = ScrobblePlayingStatus.Start; - switch (status) - { - case (int)ScrobblePlayingStatus.Start: - statusTraktV2 = ScrobblePlayingStatus.Start; - break; - case (int)ScrobblePlayingStatus.Pause: - statusTraktV2 = ScrobblePlayingStatus.Pause; - break; - case (int)ScrobblePlayingStatus.Stop: - statusTraktV2 = ScrobblePlayingStatus.Stop; - break; - } - - progressTrakt = progressTrakt / 10; - - switch (typeTrakt) - { - // Movie - case (int)ScrobblePlayingType.movie: - rsp.Code = traktHelper.Scrobble( - ScrobblePlayingType.movie, animeId, - statusTraktV2, progressTrakt) - .ToString(); - rsp.Message = "Movie Scrobbled"; - break; - // TV episode - case (int)ScrobblePlayingType.episode: - rsp.Code = - traktHelper.Scrobble(ScrobblePlayingType.episode, - animeId, - statusTraktV2, progressTrakt) - .ToString(); - rsp.Message = "Episode Scrobbled"; - break; - //error - } - } - catch (Exception ex) - { - rsp.Code = "500"; - rsp.Message = "Internal Error : " + ex; - _logger.LogError(ex, ex.ToString()); - } - - return rsp; - } - - private MediaContainer FakeParentForIOSThumbnail(IProvider prov, string base64) - { - var ret = new BaseObject(prov.NewMediaContainer(MediaContainerTypes.None, null, false)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var urls = Helper.Base64DecodeUrl(base64).Split('|'); - var thumb = prov.ReplaceSchemeHost(urls[0]); - var art = prov.ReplaceSchemeHost(urls[1]); - var v = new Shoko.Models.PlexAndKodi.Directory - { - Thumb = thumb, - ParentThumb = thumb, - GrandparentThumb = thumb, - Art = art, - ParentArt = art, - GrandparentArt = art - }; - ret.MediaContainer.Thumb = ret.MediaContainer.ParentThumb = ret.MediaContainer.GrandparentThumb = thumb; - ret.MediaContainer.Art = ret.MediaContainer.ParentArt = ret.MediaContainer.GrandparentArt = art; - var vids = new List<Video> { v }; - ret.Childrens = vids; - return ret.GetStream(prov); - } - - private void FilterExtras(IProvider provider, List<Video> videos) - { - foreach (var v in videos) - { - if (!provider.EnableAnimeTitlesInLists) - { - v.Titles = null; - } - - if (!provider.EnableGenresInLists) - { - v.Genres = null; - } - - if (!provider.EnableRolesInLists) - { - v.Roles = null; - } - } - } - - public MediaContainer GetItemsFromSerie(IProvider prov, int userid, string SerieId, BreadCrumbs info, - bool nocast) - { - BaseObject ret = null; - EpisodeType? eptype = null; - int serieID; - if (SerieId.Contains("_")) - { - var ndata = SerieId.Split('_'); - if (!int.TryParse(ndata[0], out var ept)) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - - eptype = (EpisodeType)ept; - if (!int.TryParse(ndata[1], out serieID)) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - } - else - { - if (!int.TryParse(SerieId, out serieID)) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - } - - - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - if (serieID == -1) - { - return new MediaContainer { ErrorString = "Invalid Serie Id" }; - } - - var sessionWrapper = session.Wrap(); - var ser = RepoFactory.AnimeSeries.GetByID(serieID); - if (ser == null) - { - return new MediaContainer { ErrorString = "Invalid Series" }; - } - - var cseries = ser.GetUserContract(userid); - if (cseries == null) - { - return new MediaContainer { ErrorString = "Invalid Series, Contract Not Found" }; - } - - var nv = ser.GetPlexContract(userid); - - - var episodes = ser.GetAnimeEpisodes() - .ToDictionary(a => a, a => a.GetUserContract(userid)); - episodes = episodes.Where(a => a.Value == null || a.Value.LocalFileCount > 0) - .ToDictionary(a => a.Key, a => a.Value); - if (eptype.HasValue) - { - episodes = episodes.Where(a => a.Key.AniDB_Episode != null && a.Key.EpisodeTypeEnum == eptype.Value) - .ToDictionary(a => a.Key, a => a.Value); - } - else - { - var types = episodes.Keys.Where(a => a.AniDB_Episode != null) - .Select(a => a.EpisodeTypeEnum).Distinct().ToList(); - if (types.Count > 1) - { - ret = new BaseObject( - prov.NewMediaContainer(MediaContainerTypes.Show, "Types", false, true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - ret.MediaContainer.Art = cseries.AniDBAnime?.AniDBAnime?.DefaultImageFanart.GenArt(prov); - ret.MediaContainer.LeafCount = - cseries.WatchedEpisodeCount + cseries.UnwatchedEpisodeCount; - ret.MediaContainer.ViewedLeafCount = cseries.WatchedEpisodeCount; - var eps = new List<PlexEpisodeType>(); - foreach (var ee in types) - { - var k2 = new PlexEpisodeType(); - PlexEpisodeType.EpisodeTypeTranslated(k2, ee, - (AnimeType)cseries.AniDBAnime.AniDBAnime.AnimeType, - episodes.Count(a => a.Key.EpisodeTypeEnum == ee)); - eps.Add(k2); - } - - var dirs = new List<Video>(); - //bool converttoseason = true; - - foreach (var ee in eps.OrderBy(a => a.Name)) - { - Video v = new Shoko.Models.PlexAndKodi.Directory - { - Art = nv.Art, Title = ee.Name, AnimeType = "AnimeType", LeafCount = ee.Count - }; - v.ChildCount = v.LeafCount; - v.ViewedLeafCount = 0; - v.Key = prov.ShortUrl(prov.ConstructSerieIdUrl(userid, ee.Type + "_" + ser.AnimeSeriesID)); - v.Thumb = prov.ConstructSupportImageLink(ee.Image); - if (ee.AnimeType == AnimeType.Movie || - ee.AnimeType == AnimeType.OVA) - { - v = Helper.MayReplaceVideo(v, ser, cseries, userid, false, nv); - } - - dirs.Add(prov, v, info, false, true); - } - - ret.Childrens = dirs; - return ret.GetStream(prov); - } - } - - ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Episode, ser.GetSeriesName(), true, - true, info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - ret.MediaContainer.Art = cseries.AniDBAnime?.AniDBAnime?.DefaultImageFanart.GenArt(prov); - ret.MediaContainer.LeafCount = - cseries.WatchedEpisodeCount + cseries.UnwatchedEpisodeCount; - ret.MediaContainer.ViewedLeafCount = cseries.WatchedEpisodeCount; - - // Here we are collapsing to episodes - - var vids = new List<Video>(); - - if (eptype.HasValue && info != null) - { - info.ParentKey = info.GrandParentKey; - } - - var hasRoles = false; - foreach (var ep in episodes) - { - try - { - var v = Helper.VideoFromAnimeEpisode(prov, cseries.CrossRefAniDBTvDBV2, ep, userid); - if (v != null && v.Medias != null && v.Medias.Count > 0) - { - if (nocast && !hasRoles) - { - hasRoles = true; - } - - Helper.AddInformationFromMasterSeries(v, cseries, nv, hasRoles); - v.Type = "episode"; - vids.Add(prov, v, info); - v.GrandparentThumb = v.ParentThumb; - var dinfo = prov.GetPlexClient(); - if (prov.ConstructFakeIosParent && dinfo != null && dinfo.Client == PlexClient.IOS) - { - v.GrandparentKey = - prov.Proxyfy(prov.ConstructFakeIosThumb(userid, v.ParentThumb, - v.Art ?? v.ParentArt ?? v.GrandparentArt)); - } - - v.ParentKey = null; - if (!hasRoles) - { - hasRoles = true; - } - } - } - catch - { - //Fast fix if file do not exist, and still is in db. (Xml Serialization of video info will fail on null) - } - } - - ret.Childrens = vids.OrderBy(a => a.EpisodeNumber).ToList(); - FilterExtras(prov, ret.Childrens); - return ret.GetStream(prov); - } - } - - private MediaContainer GetGroupsOrSubFiltersFromFilter(IProvider prov, int userid, string GroupFilterId, - BreadCrumbs info, bool nocast) - { - //List<Joint> retGroups = new List<Joint>(); - try - { - int.TryParse(GroupFilterId, out var groupFilterID); - using (var session = DatabaseFactory.SessionFactory.OpenSession()) - { - var retGroups = new List<Video>(); - if (groupFilterID == -1) - { - return new MediaContainer { ErrorString = "Invalid Group Filter" }; - } - - var start = DateTime.Now; - - SVR_GroupFilter gf; - gf = RepoFactory.GroupFilter.GetByID(groupFilterID); - if (gf == null) - { - return new MediaContainer { ErrorString = "Invalid Group Filter" }; - } - - var ret = - new BaseObject(prov.NewMediaContainer(MediaContainerTypes.Show, gf.GroupFilterName, false, true, - info)); - if (!ret.Init(prov)) - { - return new MediaContainer(); - } - - var allGfs = - RepoFactory.GroupFilter.GetByParentID(groupFilterID) - .Where(a => !a.IsHidden && - ( - (a.GroupsIds.ContainsKey(userid) && a.GroupsIds[userid].Count > 0) - || a.IsDirectory) - ) - .ToList(); - var dirs = new List<Shoko.Models.PlexAndKodi.Directory>(); - foreach (var gg in allGfs) - { - var pp = Helper.DirectoryFromFilter(prov, gg, userid); - if (pp != null) - { - dirs.Add(prov, pp, info); - } - } - - if (dirs.Count > 0) - { - ret.Childrens = dirs.OrderBy(a => a.Title).Cast<Video>().ToList(); - return ret.GetStream(prov); - } - - var order = new Dictionary<CL_AnimeGroup_User, Video>(); - if (gf.GroupsIds.ContainsKey(userid)) - { - // NOTE: The ToList() in the below foreach is required to prevent enumerable was modified exception - foreach (var grp in gf.GroupsIds[userid] - .ToList() - .Select(a => RepoFactory.AnimeGroup.GetByID(a)) - .Where(a => a != null)) - { - Video v = grp.GetPlexContract(userid)?.Clone<Shoko.Models.PlexAndKodi.Directory>(prov); - if (v != null) - { - if (v.Group == null) - { - v.Group = grp.GetUserContract(userid); - } - - v.GenerateKey(prov, userid); - v.Type = "show"; - v.Art = Helper.GetRandomFanartFromVideo(v, prov) ?? v.Art; - v.Banner = Helper.GetRandomBannerFromVideo(v, prov) ?? v.Banner; - if (nocast) - { - v.Roles = null; - } - - order.Add(v.Group, v); - retGroups.Add(prov, v, info); - v.ParentThumb = v.GrandparentThumb = null; - } - } - } - - ret.MediaContainer.RandomizeArt(prov, retGroups); - var grps = retGroups.Select(a => a.Group); - grps = gf.SortCriteriaList.Count != 0 - ? GroupFilterHelper.Sort(grps, gf) - : grps.OrderBy(a => a.GroupName); - ret.Childrens = grps.Select(a => order[a]).ToList(); - FilterExtras(prov, ret.Childrens); - return ret.GetStream(prov); - } - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - return new MediaContainer { ErrorString = "System Error, see JMMServer logs for more information" }; - } - } - - public void UseDirectories(int userId, List<Directory> directories) - { - var settings = _settingsProvider.GetSettings(); - if (directories == null) - { - settings.Plex.Libraries = new List<int>(); - return; - } - - settings.Plex.Libraries = directories.Select(s => s.Key).ToList(); - _settingsProvider.SaveSettings(); - } - - public Directory[] Directories(int userId) - { - return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetDirectories(); - } - - public void UseDevice(int userId, MediaDevice server) - { - PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).UseServer(server); - } - - public MediaDevice[] AvailableDevices(int userId) - { - return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).GetPlexServers().ToArray(); - } - - public MediaDevice CurrentDevice(int userId) - { - return PlexHelper.GetForUser(RepoFactory.JMMUser.GetByID(userId)).ServerCache; - } -} diff --git a/Shoko.Server/PlexAndKodi/Helper.cs b/Shoko.Server/PlexAndKodi/Helper.cs index 3226b5aa2..932bedb3d 100644 --- a/Shoko.Server/PlexAndKodi/Helper.cs +++ b/Shoko.Server/PlexAndKodi/Helper.cs @@ -10,9 +10,6 @@ using Shoko.Models.Client; using Shoko.Models.Enums; using Shoko.Models.PlexAndKodi; -using Shoko.Models.Server; -using Shoko.Server.Extensions; -using Shoko.Server.ImageDownload; using Shoko.Server.Models; using Shoko.Server.Repositories; using Shoko.Server.Server; @@ -127,36 +124,6 @@ public static string Base64EncodeUrl(string plainText) return Convert.ToBase64String(plainTextBytes).Replace("+", "-").Replace("/", "_").Replace("=", ","); } - public static string Base64DecodeUrl(string url) - { - var data = Convert.FromBase64String(url.Replace("-", "+").Replace("_", "/").Replace(",", "=")); - return Encoding.UTF8.GetString(data); - } - - public static SVR_JMMUser GetUser(string userid) - { - var allusers = RepoFactory.JMMUser.GetAll(); - foreach (var n in allusers) - { - if (userid.FindIn(n.GetPlexUsers())) - { - return n; - } - } - - return allusers.FirstOrDefault(a => a.IsAdmin == 1) ?? - allusers.FirstOrDefault(a => a.Username == "Default") ?? allusers.First(); - } - - public static SVR_JMMUser GetJMMUser(string userid) - { - var allusers = RepoFactory.JMMUser.GetAll(); - int.TryParse(userid, out var id); - return allusers.FirstOrDefault(a => a.JMMUserID == id) ?? - allusers.FirstOrDefault(a => a.IsAdmin == 1) ?? - allusers.FirstOrDefault(a => a.Username == "Default") ?? allusers.First(); - } - public static void AddLinksToAnimeEpisodeVideo(IProvider prov, Video v, int userid) { @@ -198,105 +165,6 @@ public static void AddLinksToAnimeEpisodeVideo(IProvider prov, Video v, int user } } - public static Video VideoFromVideoLocal(IProvider prov, SVR_VideoLocal v, int userid) - { - var l = new Video - { - AnimeType = AnimeTypes.AnimeFile.ToString(), - Id = v.VideoLocalID, - Type = "episode", - Summary = "Episode Overview Not Available", //TODO Internationalization - Title = Path.GetFileNameWithoutExtension(v.FileName), - AddedAt = v.DateTimeCreated.ToUnixTime(), - UpdatedAt = v.DateTimeUpdated.ToUnixTime(), - OriginallyAvailableAt = v.DateTimeCreated.ToPlexDate(), - Year = v.DateTimeCreated.Year, - Medias = new List<Media>() - }; - var vlr = v.GetUserRecord(userid); - if (vlr?.WatchedDate != null) - { - l.LastViewedAt = vlr.WatchedDate.Value.ToUnixTime(); - } - - if (vlr?.ResumePosition > 0) - { - l.ViewOffset = vlr.ResumePosition; - } - - if (v.Media != null) - { - var m = new Media(v.VideoLocalID, v.Media); - l.Medias.Add(m); - l.Duration = m.Duration; - } - - AddLinksToAnimeEpisodeVideo(prov, l, userid); - return l; - } - - - public static Video VideoFromAnimeEpisode(IProvider prov, List<CrossRef_AniDB_TvDBV2> cross, - KeyValuePair<SVR_AnimeEpisode, CL_AnimeEpisode_User> e, int userid) - { - var v = GenerateVideoFromAnimeEpisode(e.Key, e.Value.JMMUserID); - if (v?.Thumb != null) - { - v.Thumb = prov.ReplaceSchemeHost(v.Thumb); - } - - if (v != null) - { - if (e.Key.AniDB_Episode == null) - { - return v; - } - - if (e.Value != null) - { - v.ViewCount = e.Value.WatchedCount; - if (e.Value.WatchedDate.HasValue) - { - v.LastViewedAt = e.Value.WatchedDate.Value.ToUnixTime(); - } - } - - v.ParentIndex = 1; - if (e.Key.EpisodeTypeEnum != EpisodeType.Episode) - { - v.ParentIndex = 0; - } - - if (e.Key.EpisodeTypeEnum == EpisodeType.Episode) - { - var client = prov.GetPlexClient().Product; - if (client == "Plex for Windows" || client == "Plex Home Theater") - { - v.Title = $"{v.EpisodeNumber}. {v.Title}"; - } - } - - if (cross != null && cross.Count > 0) - { - var c2 = - cross.FirstOrDefault( - a => - a.AniDBStartEpisodeType == v.EpisodeType && - a.AniDBStartEpisodeNumber <= v.EpisodeNumber); - if (c2?.TvDBSeasonNumber > 0) - { - v.ParentIndex = c2.TvDBSeasonNumber; - } - } - - AddLinksToAnimeEpisodeVideo(prov, v, userid); - } - - v.AddResumePosition(prov, userid); - - return v; - } - private static readonly Regex UrlSafe = new("[ \\$^`:<>\\[\\]\\{\\}\"“\\+%@/;=\\?\\\\\\^\\|~‘,]", RegexOptions.Compiled); @@ -428,102 +296,6 @@ public static Video GenerateVideoFromAnimeEpisode(SVR_AnimeEpisode ep, int userI return l; } - private static void GetValidVideoRecursive(IProvider prov, SVR_GroupFilter f, int userid, Directory pp) - { - var gfs = RepoFactory.GroupFilter.GetByParentID(f.GroupFilterID) - .Where(a => a.GroupsIds.ContainsKey(userid) && a.GroupsIds[userid].Count > 0) - .ToList(); - - foreach (var gg in gfs.Where(a => !a.IsDirectory)) - { - if (gg.GroupsIds.ContainsKey(userid)) - { - var groups = gg.GroupsIds[userid]; - if (groups.Count != 0) - { - foreach (var grp in groups.Randomize(f.GroupFilterID)) - { - var ag = RepoFactory.AnimeGroup.GetByID(grp); - var v = ag.GetPlexContract(userid); - if (v?.Art == null || v.Thumb == null) - { - continue; - } - - pp.Art = prov.ReplaceSchemeHost(v.Art); - pp.Thumb = prov.ReplaceSchemeHost(v.Thumb); - break; - } - } - } - - if (pp.Art != null) - { - break; - } - } - - if (pp.Art == null) - { - foreach (var gg in gfs - .Where(a => a.IsDirectory && !a.IsHidden) - .Randomize(f.GroupFilterID)) - { - GetValidVideoRecursive(prov, gg, userid, pp); - if (pp.Art != null) - { - break; - } - } - } - - pp.LeafCount = gfs.Count; - pp.ViewedLeafCount = 0; - } - - public static Directory DirectoryFromFilter(IProvider prov, SVR_GroupFilter gg, - int userid) - { - var pp = new Directory { Type = "show" }; - pp.Key = prov.ConstructFilterIdUrl(userid, gg.GroupFilterID); - pp.Title = gg.GroupFilterName; - pp.Id = gg.GroupFilterID; - pp.AnimeType = AnimeTypes.AnimeGroupFilter.ToString(); - if (gg.IsDirectory) - { - GetValidVideoRecursive(prov, gg, userid, pp); - } - else if (gg.GroupsIds.ContainsKey(userid)) - { - var groups = gg.GroupsIds[userid]; - if (groups.Count == 0) - { - return pp; - } - - pp.LeafCount = groups.Count; - pp.ViewedLeafCount = 0; - foreach (var grp in groups.Randomize()) - { - var ag = RepoFactory.AnimeGroup.GetByID(grp); - var v = ag.GetPlexContract(userid); - if (v?.Art == null || v.Thumb == null) - { - continue; - } - - pp.Art = prov.ReplaceSchemeHost(v.Art); - pp.Thumb = prov.ReplaceSchemeHost(v.Thumb); - break; - } - - return pp; - } - - return pp; - } - - public static void AddInformationFromMasterSeries(Video v, CL_AnimeSeries_User cserie, Video nv, bool omitExtraData = false) { @@ -609,38 +381,6 @@ public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source, int seed = return source.OrderBy(item => rnd.Next()); } - public static string GetRandomFanartFromVideo(Video v, IProvider prov) - { - return GetRandomArtFromList(v.Fanarts, prov); - } - - public static string GetRandomBannerFromVideo(Video v, IProvider prov) - { - return GetRandomArtFromList(v.Banners, prov); - } - - public static string GetRandomArtFromList(List<Contract_ImageDetails> list, IProvider prov) - { - if (list == null || list.Count == 0) - { - return null; - } - - Contract_ImageDetails art; - if (list.Count == 1) - { - art = list[0]; - } - else - { - var rand = new Random(); - art = list[rand.Next(0, list.Count)]; - } - - var details = new ImageDetails { ImageID = art.ImageID, ImageType = (ImageEntityType)art.ImageType }; - return details.GenArt(prov); - } - public static Video GenerateFromAnimeGroup(SVR_AnimeGroup grp, int userid, List<SVR_AnimeSeries> allSeries) { var cgrp = grp.GetUserContract(userid); @@ -760,29 +500,6 @@ public static Video GenerateFromAnimeGroup(SVR_AnimeGroup grp, int userid, List< } } - - public static List<Video> ConvertToDirectory(List<Video> n, IProvider prov) - { - var ks = new List<Video>(); - foreach (var n1 in n) - { - Video m; - if (n1 is Directory) - { - m = n1; - } - else - { - m = n1.Clone<Directory>(prov); - } - - m.ParentThumb = m.GrandparentThumb = null; - ks.Add(m); - } - - return ks; - } - public static Video MayReplaceVideo(Video v1, SVR_AnimeSeries ser, CL_AnimeSeries_User cserie, int userid, bool all = true, Video serie = null) { diff --git a/Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs b/Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs deleted file mode 100644 index 2dbd4dcc4..000000000 --- a/Shoko.Server/PlexAndKodi/Kodi/KodiProvider.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; -using Shoko.Models.PlexAndKodi; -using Shoko.Server.Server; -using Shoko.Server.Utilities; - -namespace Shoko.Server.PlexAndKodi.Kodi; - -public class KodiProvider : IProvider -{ - //public const string MediaTagVersion = "1420942002"; - - public string ServiceAddress => ShokoServer.PathAddressKodi; - public int ServicePort => Utils.SettingsProvider.GetSettings().ServerPort; - public bool UseBreadCrumbs => false; // turn off breadcrumbs navigation (plex) - public bool ConstructFakeIosParent => false; //turn off plex workaround for ios (plex) - public bool AutoWatch => false; //turn off marking watched on stream side (plex) - - public bool EnableRolesInLists { get; } = true; - public bool EnableAnimeTitlesInLists { get; } = true; - public bool EnableGenresInLists { get; } = true; - - - public string ExcludeTags => "Plex"; - - public string Proxyfy(string url) - { - return url; - } - - public string ShortUrl(string url) - { - // Faster and More accurate than regex - try - { - var uri = new Uri(url); - return uri.PathAndQuery; - } - catch - { - // if this fails, then there is a problem - return url; - } - } - - public MediaContainer NewMediaContainer(MediaContainerTypes type, string title = null, bool allowsync = false, - bool nocache = false, BreadCrumbs info = null) - { - var m = new MediaContainer(); - m.Title1 = m.Title2 = title; - // not needed - //m.AllowSync = allowsync ? "1" : "0"; - //m.NoCache = nocache ? "1" : "0"; - //m.ViewMode = "65592"; - //m.ViewGroup = "show"; - //m.MediaTagVersion = MediaTagVersion; - m.Identifier = "plugin.video.nakamori"; - return m; - } - - public bool AddPlexSearchItem { get; } = false; - public bool AddPlexPrefsItem { get; } = false; - public bool RemoveFileAttribute { get; } = false; - public bool AddEpisodeNumberToTitlesOnUnsupportedClients { get; } = false; - public HttpContext HttpContext { get; set; } -} diff --git a/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs b/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs index 136b4945f..b92b8c19b 100644 --- a/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs +++ b/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs @@ -429,10 +429,7 @@ public string LinkAniDBTrakt(int animeID, EpisodeType aniEpType, int aniEpNumber public string LinkAniDBTrakt(ISession session, int animeID, EpisodeType aniEpType, int aniEpNumber, string traktID, int seasonNumber, int traktEpNumber, bool excludeFromWebCache) { - var xrefTemps = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeIDEpTypeEpNumber( - session, animeID, - (int)aniEpType, - aniEpNumber); + var xrefTemps = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeIDEpTypeEpNumber(animeID, (int)aniEpType, aniEpNumber); if (xrefTemps is { Count: > 0 }) { foreach (var xrefTemp in xrefTemps) @@ -1321,7 +1318,7 @@ private List<TraktV2Comment> GetShowCommentsV2(ISession session, int animeID) } var traktXRefs = - RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(session, animeID); + RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(animeID); if (traktXRefs == null || traktXRefs.Count == 0) { return null; diff --git a/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs index 6a2bb6d8c..27998b5b0 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs @@ -146,11 +146,8 @@ public Dictionary<int, DefaultAnimeImages> GetDefaultImagesByAnime(ISessionWrapp } // treating cache as a global DB lock, as well - var results = Lock(() => - { - // TODO: Determine if joining on the correct columns - return session.CreateSQLQuery( - @"SELECT {defImg.*}, {tvWide.*}, {tvPoster.*}, {tvFanart.*}, {movPoster.*}, {movFanart.*} + var results = Lock(() => session.CreateSQLQuery( + @"SELECT {defImg.*}, {tvWide.*}, {tvPoster.*}, {tvFanart.*}, {movPoster.*}, {movFanart.*} FROM AniDB_Anime_DefaultImage defImg LEFT OUTER JOIN TvDB_ImageWideBanner AS tvWide ON tvWide.TvDB_ImageWideBannerID = defImg.ImageParentID AND defImg.ImageParentType = :tvdbBannerType @@ -163,21 +160,20 @@ LEFT OUTER JOIN MovieDB_Poster AS movPoster LEFT OUTER JOIN MovieDB_Fanart AS movFanart ON movFanart.MovieDB_FanartID = defImg.ImageParentID AND defImg.ImageParentType = :movdbFanartType WHERE defImg.AnimeID IN (:animeIds) AND defImg.ImageParentType IN (:tvdbBannerType, :tvdbCoverType, :tvdbFanartType, :movdbPosterType, :movdbFanartType)" - ) - .AddEntity("defImg", typeof(AniDB_Anime_DefaultImage)) - .AddEntity("tvWide", typeof(TvDB_ImageWideBanner)) - .AddEntity("tvPoster", typeof(TvDB_ImagePoster)) - .AddEntity("tvFanart", typeof(TvDB_ImageFanart)) - .AddEntity("movPoster", typeof(MovieDB_Poster)) - .AddEntity("movFanart", typeof(MovieDB_Fanart)) - .SetParameterList("animeIds", animeIds) - .SetInt32("tvdbBannerType", (int)ImageEntityType.TvDB_Banner) - .SetInt32("tvdbCoverType", (int)ImageEntityType.TvDB_Cover) - .SetInt32("tvdbFanartType", (int)ImageEntityType.TvDB_FanArt) - .SetInt32("movdbPosterType", (int)ImageEntityType.MovieDB_Poster) - .SetInt32("movdbFanartType", (int)ImageEntityType.MovieDB_FanArt) - .List<object[]>(); - }); + ) + .AddEntity("defImg", typeof(AniDB_Anime_DefaultImage)) + .AddEntity("tvWide", typeof(TvDB_ImageWideBanner)) + .AddEntity("tvPoster", typeof(TvDB_ImagePoster)) + .AddEntity("tvFanart", typeof(TvDB_ImageFanart)) + .AddEntity("movPoster", typeof(MovieDB_Poster)) + .AddEntity("movFanart", typeof(MovieDB_Fanart)) + .SetParameterList("animeIds", animeIds) + .SetInt32("tvdbBannerType", (int)ImageEntityType.TvDB_Banner) + .SetInt32("tvdbCoverType", (int)ImageEntityType.TvDB_Cover) + .SetInt32("tvdbFanartType", (int)ImageEntityType.TvDB_FanArt) + .SetInt32("movdbPosterType", (int)ImageEntityType.MovieDB_Poster) + .SetInt32("movdbFanartType", (int)ImageEntityType.MovieDB_FanArt) + .List<object[]>()); foreach (var result in results) { diff --git a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs index e66aae9d8..2a8c52f07 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using NLog; using NutzCode.InMemoryIndex; +using Shoko.Commons.Extensions; using Shoko.Commons.Properties; using Shoko.Models.Server; using Shoko.Server.Databases; @@ -25,7 +26,6 @@ public AnimeGroupRepository() BeginDeleteCallback = cr => { RepoFactory.AnimeGroup_User.Delete(RepoFactory.AnimeGroup_User.GetByGroupID(cr.AnimeGroupID)); - cr.DeleteFromFilters(); }; EndDeleteCallback = cr => { @@ -122,10 +122,8 @@ public void Save(SVR_AnimeGroup grp, bool updategrpcontractstats, bool recursive if (verifylockedFilters) { - RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, - grp.Contract?.Stat_AllYears, grp.Contract?.Stat_AllSeasons); - //This call will create extra years or tags if the Group have a new year or tag - grp.UpdateGroupFilters(types); + RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(false, grp.Contract?.Stat_AllTags, + grp.Contract?.Stat_AllYears, grp.Contract?.GetSeasons()); } if (grp.AnimeGroupParentID.HasValue && recursive) diff --git a/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs index 35c7d8ee7..4fc981965 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs @@ -23,8 +23,6 @@ public AnimeGroup_UserRepository() { Changes.TryAdd(cr.JMMUserID, new ChangeTracker<int>()); Changes[cr.JMMUserID].Remove(cr.AnimeGroupID); - - cr.DeleteFromFilters(); }; } @@ -52,49 +50,11 @@ public override void RegenerateDb() public override void Save(SVR_AnimeGroup_User obj) { - // Get The previous AnimeGroup_User from db for comparison; - var old = Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Get<SVR_AnimeGroup_User>(obj.AnimeGroup_UserID); - }); - obj.UpdatePlexKodiContracts(); - var types = GetConditionTypesChanged(old, obj); base.Save(obj); Changes.TryAdd(obj.JMMUserID, new ChangeTracker<int>()); Changes[obj.JMMUserID].AddOrUpdate(obj.AnimeGroupID); - obj.UpdateGroupFilters(types); - } - - private static HashSet<GroupFilterConditionType> GetConditionTypesChanged(SVR_AnimeGroup_User oldcontract, - SVR_AnimeGroup_User newcontract) - { - var h = new HashSet<GroupFilterConditionType>(); - - if (oldcontract == null || - oldcontract.UnwatchedEpisodeCount > 0 != newcontract.UnwatchedEpisodeCount > 0) - { - h.Add(GroupFilterConditionType.HasUnwatchedEpisodes); - } - - if (oldcontract == null || oldcontract.IsFave != newcontract.IsFave) - { - h.Add(GroupFilterConditionType.Favourite); - } - - if (oldcontract == null || oldcontract.WatchedDate != newcontract.WatchedDate) - { - h.Add(GroupFilterConditionType.EpisodeWatchedDate); - } - - if (oldcontract == null || oldcontract.WatchedEpisodeCount > 0 != newcontract.WatchedEpisodeCount > 0) - { - h.Add(GroupFilterConditionType.HasWatchedEpisodes); - } - - return h; } /// <summary> diff --git a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs index fdb108e99..3809f50ce 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeriesRepository.cs @@ -33,7 +33,6 @@ public AnimeSeriesRepository() }; EndDeleteCallback = cr => { - cr.DeleteFromFilters(); if (cr.AnimeGroupID <= 0) { return; @@ -212,7 +211,7 @@ public void Save(SVR_AnimeSeries obj, bool updateGroups, bool onlyupdatestats, b if (updateGroups && !isMigrating) UpdateGroups(obj, animeID, sw, oldGroup); - if (!skipgroupfilters && !isMigrating) UpdateGroupFilters(obj, sw, animeID, types); + if (false && !skipgroupfilters && !isMigrating) UpdateGroupFilters(obj, sw, animeID, types); Changes.AddOrUpdate(obj.AnimeSeriesID); @@ -283,11 +282,8 @@ private static void UpdateGroupFilters(SVR_AnimeSeries obj, Stopwatch sw, string //This call will create extra years or tags if the Group have a new year or tag logger.Trace( $"Saving Series {animeID} | Updating Group Filters for Years ({string.Join(",", (IEnumerable<int>)allyears?.OrderBy(a => a) ?? Array.Empty<int>())}) and Seasons ({string.Join(",", (IEnumerable<string>)seasons ?? Array.Empty<string>())})"); - RepoFactory.GroupFilter.CreateOrVerifyDirectoryFilters(false, - obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, seasons); - - // Update other existing filters - obj.UpdateGroupFilters(types); + RepoFactory.FilterPreset.CreateOrVerifyDirectoryFilters(false, + obj.Contract?.AniDBAnime?.AniDBAnime?.GetAllTags(), allyears, obj.Contract?.AniDBAnime?.AniDBAnime?.GetSeasons().ToHashSet()); sw.Stop(); logger.Trace($"Saving Series {animeID} | Updated Group Filters in {sw.Elapsed.TotalSeconds:0.00###}s"); sw.Restart(); diff --git a/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs index eae1d810e..e5d3913c7 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs @@ -21,8 +21,6 @@ public AnimeSeries_UserRepository() Changes.TryAdd(cr.JMMUserID, new ChangeTracker<int>()); Changes[cr.JMMUserID].Remove(cr.AnimeSeriesID); - - cr.DeleteFromFilters(); }; } @@ -46,19 +44,9 @@ public override void RegenerateDb() public override void Save(SVR_AnimeSeries_User obj) { UpdatePlexKodiContracts(obj); - SVR_AnimeSeries_User old; - old = Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Get<SVR_AnimeSeries_User>(obj.AnimeSeries_UserID); - }); - - var types = SVR_AnimeSeries_User.GetConditionTypesChanged(old, obj); base.Save(obj); Changes.TryAdd(obj.JMMUserID, new ChangeTracker<int>()); Changes[obj.JMMUserID].AddOrUpdate(obj.AnimeSeriesID); - - obj.UpdateGroupFilter(types); } private void UpdatePlexKodiContracts(SVR_AnimeSeries_User ugrp) diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs index c430ff12d..a47e03572 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs @@ -16,42 +16,12 @@ public class CrossRef_AniDB_TraktV2Repository : BaseCachedRepository<CrossRef_An public List<CrossRef_AniDB_TraktV2> GetByAnimeID(int id) { - return Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return GetByAnimeID(session, id); - }); - } - - public List<CrossRef_AniDB_TraktV2> GetByAnimeID(ISession session, int id) - { - return Lock(() => - { - var xrefs = session - .CreateCriteria(typeof(CrossRef_AniDB_TraktV2)) - .Add(Restrictions.Eq("AnimeID", id)) - .AddOrder(Order.Asc("AniDBStartEpisodeType")) - .AddOrder(Order.Asc("AniDBStartEpisodeNumber")) - .List<CrossRef_AniDB_TraktV2>(); - - return new List<CrossRef_AniDB_TraktV2>(xrefs); - }); + return AnimeIDs.GetMultiple(id).OrderBy(a => a.AniDBStartEpisodeType).ThenBy(a => a.AniDBStartEpisodeNumber).ToList(); } - public List<CrossRef_AniDB_TraktV2> GetByAnimeIDEpTypeEpNumber(ISession session, int id, int aniEpType, - int aniEpisodeNumber) + public List<CrossRef_AniDB_TraktV2> GetByAnimeIDEpTypeEpNumber(int id, int aniEpType, int aniEpisodeNumber) { - return Lock(() => - { - var xrefs = session - .CreateCriteria(typeof(CrossRef_AniDB_TraktV2)) - .Add(Restrictions.Eq("AnimeID", id)) - .Add(Restrictions.Eq("AniDBStartEpisodeType", aniEpType)) - .Add(Restrictions.Eq("AniDBStartEpisodeNumber", aniEpisodeNumber)) - .List<CrossRef_AniDB_TraktV2>(); - - return new List<CrossRef_AniDB_TraktV2>(xrefs); - }); + return AnimeIDs.GetMultiple(id).Where(a => a.AniDBStartEpisodeType == aniEpType && a.AniDBStartEpisodeNumber == aniEpisodeNumber).ToList(); } public CrossRef_AniDB_TraktV2 GetByTraktID(ISession session, string id, int season, int episodeNumber, diff --git a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs new file mode 100644 index 000000000..34cdaa743 --- /dev/null +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using NutzCode.InMemoryIndex; +using Shoko.Commons.Extensions; +using Shoko.Commons.Properties; +using Shoko.Models.Enums; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.SortingSelectors; +using Shoko.Server.Filters.User; +using Shoko.Server.Models; +using Shoko.Server.Server; +using Shoko.Server.Utilities; +using Constants = Shoko.Server.Server.Constants; + +namespace Shoko.Server.Repositories.Cached; + +public class FilterPresetRepository : BaseCachedRepository<FilterPreset, int> +{ + private PocoIndex<int, FilterPreset, int> Parents; + private readonly ChangeTracker<int> Changes = new(); + + public FilterPresetRepository() + { + EndSaveCallback = obj => + { + Changes.AddOrUpdate(obj.FilterPresetID); + }; + EndDeleteCallback = obj => + { + Changes.Remove(obj.FilterPresetID); + }; + } + + protected override int SelectKey(FilterPreset entity) + { + return entity.FilterPresetID; + } + + public override void PopulateIndexes() + { + Changes.AddOrUpdateRange(Cache.Keys); + Parents = Cache.CreateIndex(a => a.ParentFilterPresetID ?? 0); + } + + public override void RegenerateDb() { } + + + public override void PostProcess() + { + const string t = "FilterPreset"; + + // Clean up. This will populate empty conditions and remove duplicate filters + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, + t, + " " + Resources.GroupFilter_Cleanup); + var all = GetAll(); + var set = new HashSet<FilterPreset>(all); + var notin = all.Except(set).ToList(); + Delete(notin); + } + + public void CleanUpEmptyDirectoryFilters() + { + var evaluator = Utils.ServiceContainer.GetRequiredService<FilterEvaluator>(); + var filters = GetAll().Where(a => (a.FilterType & GroupFilterType.Directory) != 0) + .Where(gf => gf.Expression is not null && !gf.Expression.UserDependent).ToList(); + var toRemove = evaluator.BatchEvaluateFilters(filters, null, true).Where(a => !a.Value.Any()).Select(a => a.Key).ToList(); + if (toRemove.Count <= 0) return; + + Delete(toRemove); + } + + public void CreateOrVerifyLockedFilters() + { + const string t = "FilterPreset"; + + var lockedGFs = GetLockedGroupFilters(); + + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + " " + Resources.Filter_CreateContinueWatching); + + if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.ContinueWatching)) + { + var gf = new FilterPreset + { + Name = Constants.GroupFilterName.ContinueWatching, + Locked = true, + ApplyAtSeriesLevel = false, + FilterType = GroupFilterType.None, + Expression = new AndExpression{ Left = new HasWatchedEpisodesExpression(), Right = new HasUnwatchedEpisodesExpression() }, + SortingExpression = new LastWatchedDateSortingSelector { Descending = true } + }; + Save(gf); + } + + //Create All filter + if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.All)) + { + var gf = new FilterPreset + { + Name = Constants.GroupFilterName.All, + Locked = true, + FilterType = GroupFilterType.All, + SortingExpression = new NameSortingSelector() + }; + Save(gf); + } + + if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag))) + { + var gf = new FilterPreset + { + Name = Constants.GroupFilterName.Tags, + FilterType = (GroupFilterType.Directory | GroupFilterType.Tag), + Locked = true + }; + Save(gf); + } + + if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Year))) + { + var gf = new FilterPreset + { + Name = Constants.GroupFilterName.Years, + FilterType = (GroupFilterType.Directory | GroupFilterType.Year), + Locked = true + }; + Save(gf); + } + + if (!lockedGFs.Any(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Season))) + { + var gf = new FilterPreset + { + Name = Constants.GroupFilterName.Seasons, + FilterType = (GroupFilterType.Directory | GroupFilterType.Season), + Locked = true + }; + Save(gf); + } + + CreateOrVerifyDirectoryFilters(true); + } + + public void CreateOrVerifyDirectoryFilters(bool frominit = false, ISet<string> tags = null, + ISet<int> airdate = null, ISet<(int Year, AnimeSeason Season)> seasons = null) + { + const string t = "FilterPreset"; + + var lockedGFs = GetLockedGroupFilters(); + + var tagsdirec = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Tag)); + if (tagsdirec != null) + { + HashSet<string> alltags; + if (tags == null) + { + alltags = new HashSet<string>( + RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), + StringComparer.InvariantCultureIgnoreCase); + } + else + { + alltags = new HashSet<string>(tags, StringComparer.InvariantCultureIgnoreCase); + } + + var existingTags = + new HashSet<string>( + lockedGFs.Where(a => a.FilterType == GroupFilterType.Tag).Select(a => a.Expression).Cast<HasTagExpression>().Select(a => a.Parameter), + StringComparer.InvariantCultureIgnoreCase); + alltags.ExceptWith(existingTags); + + var max = alltags.Count; + var cnt = 0; + //AniDB Tags are in english so we use en-us culture + var tinfo = new CultureInfo("en-US", false).TextInfo; + foreach (var s in alltags) + { + cnt++; + if (frominit) + { + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + Resources.Filter_CreatingTag + " " + + Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); + } + + var yf = new FilterPreset + { + ParentFilterPresetID = tagsdirec.FilterPresetID, + FilterType = GroupFilterType.Tag, + ApplyAtSeriesLevel = true, + Name = tinfo.ToTitleCase(s), + Locked = true, + Expression = new HasTagExpression { Parameter = s }, + SortingExpression = new NameSortingSelector() + }; + Save(yf); + } + } + + var yearsdirec = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Year)); + if (yearsdirec != null) + { + HashSet<int> allyears; + if (airdate == null || airdate.Count == 0) + { + var grps = RepoFactory.AnimeSeries.GetAll().Select(a => a.GetAnime()); + + allyears = new HashSet<int>(); + foreach (var anime in grps) + { + var endyear = anime.EndYear; + var startyear = anime.BeginYear; + if (endyear <= 0) endyear = DateTime.Today.Year; + if (endyear < startyear || endyear - startyear + 1 >= int.MaxValue) endyear = startyear; + if (startyear != 0) allyears.UnionWith(Enumerable.Range(startyear, endyear - startyear + 1).Select(a => a)); + } + } + else + { + allyears = new HashSet<int>(airdate.Select(a => a)); + } + + var notin = new HashSet<int>(lockedGFs.Where(a => a.FilterType == GroupFilterType.Year).Select(a => a.Expression).Cast<InYearExpression>() + .Select(a => a.Parameter)); + allyears.ExceptWith(notin); + var max = allyears.Count; + var cnt = 0; + foreach (var s in allyears) + { + cnt++; + if (frominit) + { + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + Resources.Filter_CreatingYear + " " + + Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); + } + + var yf = new FilterPreset + { + ParentFilterPresetID = yearsdirec.FilterPresetID, + Name = s.ToString(), + FilterType = GroupFilterType.Year, + Locked = true, + ApplyAtSeriesLevel = true, + Expression = new InYearExpression { Parameter = s }, + SortingExpression = new NameSortingSelector() + }; + Save(yf); + } + } + + var seasonsdirectory = lockedGFs.FirstOrDefault(a => a.FilterType == (GroupFilterType.Directory | GroupFilterType.Season)); + if (seasonsdirectory != null) + { + ISet<(int Year, AnimeSeason Season)> allseasons; + if (seasons == null) + { + var grps = RepoFactory.AnimeSeries.GetAll().ToList(); + + allseasons = new SortedSet<(int Year, AnimeSeason Season)>(); + foreach (var ser in grps) + { + var seriesSeasons = ser?.GetAnime()?.GetSeasons().ToList(); + if ((seriesSeasons?.Count ?? 0) == 0) continue; + allseasons.UnionWith(seriesSeasons); + } + } + else + { + allseasons = seasons; + } + + var notin = new HashSet<(int Year, AnimeSeason Season)>(lockedGFs.Where(a => a.FilterType == GroupFilterType.Season).Select(a => a.Expression) + .Cast<InSeasonExpression>().Select(a => (a.Year, a.Season))); + allseasons.ExceptWith(notin); + + var max = allseasons.Count; + var cnt = 0; + foreach (var season in allseasons) + { + cnt++; + if (frominit) + { + ServerState.Instance.ServerStartingStatus = string.Format( + Resources.Database_Validating, t, + Resources.Filter_CreatingSeason + " " + + Resources.Filter_Filter + " " + cnt + "/" + max + " - " + season); + } + + var yf = new FilterPreset + { + ParentFilterPresetID = seasonsdirectory.FilterPresetID, + Name = season.Season + " " + season.Year, + Locked = true, + FilterType = GroupFilterType.Season, + ApplyAtSeriesLevel = true, + Expression = new InSeasonExpression { Season = season.Season, Year = season.Year }, + SortingExpression = new NameSortingSelector() + }; + Save(yf); + } + } + + CleanUpEmptyDirectoryFilters(); + } + + public void CreateInitialFilters() + { + // group filters + // Do to DatabaseFixes, some filters may be made, namely directory filters + // All, Continue Watching, Years, Seasons, Tags... 6 seems to be enough to tell for now + // We can't just check the existence of anything specific, as the user can delete most of these + if (GetTopLevel().Count > 6) return; + + // Favorites + var gf = new FilterPreset + { + Name = Constants.GroupFilterName.Favorites, + FilterType = GroupFilterType.UserDefined, + Expression = new IsFavoriteExpression(), + SortingExpression = new NameSortingSelector() + }; + Save(gf); + + // Missing Episodes + gf = new FilterPreset + { + Name = Constants.GroupFilterName.MissingEpisodes, + FilterType = GroupFilterType.UserDefined, + Expression = new HasMissingEpisodesCollectingExpression(), + SortingExpression = new MissingEpisodeCollectingCountSortingSelector{ Descending = true} + }; + Save(gf); + + + // Newly Added Series + gf = new FilterPreset + { + Name = Constants.GroupFilterName.NewlyAddedSeries, + FilterType = GroupFilterType.UserDefined, + Expression = new DateGreaterThanEqualsExpression + { + Left = new DateAddFunction(new LastAddedDateSelector(), TimeSpan.FromDays(10)), + Right = new TodayFunction() + }, + SortingExpression = new LastAddedDateSortingSelector { Descending = true} + }; + Save(gf); + + // Newly Airing Series + gf = new FilterPreset + { + Name = Constants.GroupFilterName.NewlyAiringSeries, + FilterType = GroupFilterType.UserDefined, + Expression = new DateGreaterThanEqualsExpression(new DateAddFunction(new LastAirDateSelector(), TimeSpan.FromDays(30)), new TodayFunction()), + SortingExpression = new LastAirDateSortingSelector { Descending = true } + }; + Save(gf); + + // Votes Needed + gf = new FilterPreset + { + Name = Constants.GroupFilterName.MissingVotes, + ApplyAtSeriesLevel = true, + FilterType = GroupFilterType.UserDefined, + Expression = new AndExpression + { + // all watched and none missing + Left = new AndExpression + { + // all watched, aka no episodes unwatched + Left = new NotExpression + { + Left = new HasUnwatchedEpisodesExpression() + }, + // no missing episodes + Right = new NotExpression + { + Left = new HasMissingEpisodesExpression() + } + }, + // does not have votes + Right = new NotExpression + { + Left = new HasUserVotesExpression() + } + }, + SortingExpression = new NameSortingSelector() + }; + Save(gf); + + // Recently Watched + gf = new FilterPreset + { + Name = Constants.GroupFilterName.RecentlyWatched, + FilterType = GroupFilterType.UserDefined, + Expression = new AndExpression(new HasWatchedEpisodesExpression(), new + DateGreaterThanEqualsExpression(new DateAddFunction(new LastWatchedDateSelector(), TimeSpan.FromDays(10)), new TodayFunction())), + SortingExpression = new LastWatchedDateSortingSelector + { + Descending = true + } + }; + Save(gf); + + // TvDB/MovieDB Link Missing + gf = new FilterPreset + { + Name = Constants.GroupFilterName.MissingLinks, + ApplyAtSeriesLevel = true, + FilterType = GroupFilterType.UserDefined, + Expression = new OrExpression(new MissingTvDBLinkExpression(), new MissingTMDbLinkExpression()), + SortingExpression = new NameSortingSelector() + }; + Save(gf); + } + + public override void Save(FilterPreset obj) + { + WriteLock(() => { base.Save(obj); }); + } + + public override void Save(IReadOnlyCollection<FilterPreset> objs) + { + foreach (var obj in objs) + { + Save(obj); + } + } + + public override void Delete(IReadOnlyCollection<FilterPreset> objs) + { + foreach (var cr in objs) + { + base.Delete(cr); + } + } + + public List<FilterPreset> GetByParentID(int parentid) + { + return ReadLock(() => Parents.GetMultiple(parentid)); + } + + public FilterPreset GetTopLevelFilter(int filterID) + { + var parent = GetByID(filterID); + if (parent == null || parent.ParentFilterPresetID is null or 0) + return parent; + + while (true) + { + if (parent.ParentFilterPresetID is null or 0) return parent; + var next = GetByID(parent.ParentFilterPresetID.Value); + // should never happen, but it's not completely impossible + if (next == null) return parent; + parent = next; + } + } + + public List<FilterPreset> GetTopLevel() + { + return GetByParentID(0); + } + + public List<FilterPreset> GetLockedGroupFilters() + { + return ReadLock(() => Cache.Values.Where(a => a.Locked).ToList()); + } + + public List<FilterPreset> GetTimeDependentFilters() + { + return ReadLock(() => GetAll().Where(a => a.Expression.TimeDependent).ToList()); + } + + public ChangeTracker<int> GetChangeTracker() + { + return Changes; + } +} diff --git a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs b/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs deleted file mode 100644 index 32653098c..000000000 --- a/Shoko.Server/Repositories/Cached/GroupFilterRepository.cs +++ /dev/null @@ -1,834 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; -using NutzCode.InMemoryIndex; -using Shoko.Commons.Extensions; -using Shoko.Commons.Properties; -using Shoko.Models; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Server.Extensions; -using Shoko.Server.Models; -using Shoko.Server.Repositories.NHibernate; -using Shoko.Server.Server; -using Constants = Shoko.Server.Server.Constants; - -namespace Shoko.Server.Repositories.Cached; - -public class GroupFilterRepository : BaseCachedRepository<SVR_GroupFilter, int> -{ - private PocoIndex<int, SVR_GroupFilter, int> Parents; - - private BiDictionaryManyToMany<int, GroupFilterConditionType> Types; - - private readonly ChangeTracker<int> Changes = new(); - - public List<SVR_GroupFilter> PostProcessFilters { get; set; } - - public GroupFilterRepository() - { - EndSaveCallback = obj => - { - Types[obj.GroupFilterID] = obj.Types; - Changes.AddOrUpdate(obj.GroupFilterID); - }; - EndDeleteCallback = obj => - { - Types.Remove(obj.GroupFilterID); - Changes.Remove(obj.GroupFilterID); - }; - } - - protected override int SelectKey(SVR_GroupFilter entity) - { - return entity.GroupFilterID; - } - - public override void PopulateIndexes() - { - Changes.AddOrUpdateRange(Cache.Keys); - Parents = Cache.CreateIndex(a => a.ParentGroupFilterID ?? 0); - Types = - new BiDictionaryManyToMany<int, GroupFilterConditionType>( - Cache.Values.ToDictionary(a => a.GroupFilterID, a => a.Types)); - PostProcessFilters = new List<SVR_GroupFilter>(); - } - - public override void RegenerateDb() - { - foreach (var g in Cache.Values.Where(g => - (g.GroupFilterID != 0 && g.GroupsIdsVersion < SVR_GroupFilter.GROUPFILTER_VERSION) || - g.GroupConditionsVersion < SVR_GroupFilter.GROUPCONDITIONS_VERSION).ToList()) - { - if (g.GroupConditionsVersion == 0) - { - g.Conditions = RepoFactory.GroupFilterCondition.GetByGroupFilterID(g.GroupFilterID); - } - - Save(g, true); - PostProcessFilters.Add(g); - } - } - - - public override void PostProcess() - { - const string t = "GroupFilter"; - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, - t, string.Empty); - foreach (var g in Cache.Values.ToList()) - { - if (g.GroupsIdsVersion < SVR_GroupFilter.GROUPFILTER_VERSION || - g.GroupConditionsVersion < SVR_GroupFilter.GROUPCONDITIONS_VERSION || - g.SeriesIdsVersion < SVR_GroupFilter.SERIEFILTER_VERSION) - { - if (!PostProcessFilters.Contains(g)) - { - PostProcessFilters.Add(g); - } - } - } - - var max = PostProcessFilters.Count; - var cnt = 0; - foreach (var gf in PostProcessFilters) - { - cnt++; - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_Recalc + " " + cnt + "/" + max + " - " + - gf.GroupFilterName); - if (gf.GroupsIdsVersion < SVR_GroupFilter.GROUPFILTER_VERSION || - gf.GroupConditionsVersion < SVR_GroupFilter.GROUPCONDITIONS_VERSION || - gf.SeriesIdsVersion < SVR_GroupFilter.SERIEFILTER_VERSION) - { - gf.CalculateGroupsAndSeries(); - } - - Save(gf); - } - - // Clean up. This will populate empty conditions and remove duplicate filters - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, - t, - " " + Resources.GroupFilter_Cleanup); - var all = GetAll(); - var set = new HashSet<SVR_GroupFilter>(all); - var notin = all.Except(set).ToList(); - Delete(notin); - - // Remove orphaned group filter conditions - var toremove = RepoFactory.GroupFilterCondition.GetAll().ToList() - .Where(condition => RepoFactory.GroupFilter.GetByID(condition.GroupFilterID) == null).ToList(); - RepoFactory.GroupFilterCondition.Delete(toremove); - - PostProcessFilters = null; - } - - public void CleanUpEmptyDirectoryFilters() - { - var toremove = GetAll().Where(a => (a.FilterType & (int)GroupFilterType.Directory) != 0) - .Where(gf => gf.GroupsIdsVersion == SVR_GroupFilter.GROUPFILTER_VERSION && - gf.SeriesIdsVersion == SVR_GroupFilter.SERIEFILTER_VERSION && gf.GroupsIds.Count == 0 && - string.IsNullOrEmpty(gf.GroupsIdsString) && gf.SeriesIds.Count == 0 && - string.IsNullOrEmpty(gf.SeriesIdsString)).ToList(); - if (toremove.Count > 0) - { - Delete(toremove); - } - } - - public void CreateOrVerifyLockedFilters() - { - const string t = "GroupFilter"; - - var lockedGFs = GetLockedGroupFilters(); - - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - " " + Resources.Filter_CreateContinueWatching); - - var cwatching = - lockedGFs.FirstOrDefault( - a => - a.FilterType == (int)GroupFilterType.ContinueWatching); - if (cwatching != null && cwatching.FilterType != (int)GroupFilterType.ContinueWatching) - { - cwatching.FilterType = (int)GroupFilterType.ContinueWatching; - Save(cwatching); - } - else if (cwatching == null) - { - var gf = new SVR_GroupFilter - { - GroupFilterName = Constants.GroupFilterName.ContinueWatching, - Locked = 1, - SortingCriteria = "4;2", // by last watched episode desc - ApplyToSeries = 0, - BaseCondition = 1, // all - FilterType = (int)GroupFilterType.ContinueWatching, - InvisibleInClients = 0, - Conditions = new List<GroupFilterCondition>() - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty, - GroupFilterID = gf.GroupFilterID - }; - gf.Conditions.Add(gfc); - gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = string.Empty, - GroupFilterID = gf.GroupFilterID - }; - gf.Conditions.Add(gfc); - gf.CalculateGroupsAndSeries(); - Save(gf); //Get ID - } - - //Create All filter - var allfilter = lockedGFs.FirstOrDefault(a => a.FilterType == (int)GroupFilterType.All); - if (allfilter == null) - { - var gf = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_All, - Locked = 1, - InvisibleInClients = 0, - FilterType = (int)GroupFilterType.All, - BaseCondition = 1, - SortingCriteria = "5;1" - }; - gf.CalculateGroupsAndSeries(); - Save(gf); - } - - var tagsdirec = - lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Tag)); - if (tagsdirec == null) - { - tagsdirec = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Tags, - InvisibleInClients = 0, - FilterType = (int)(GroupFilterType.Directory | GroupFilterType.Tag), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "13;1" - }; - Save(tagsdirec); - } - - var yearsdirec = - lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Year)); - if (yearsdirec == null) - { - yearsdirec = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Years, - InvisibleInClients = 0, - FilterType = (int)(GroupFilterType.Directory | GroupFilterType.Year), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "13;1" - }; - Save(yearsdirec); - } - - var seasonsdirec = - lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Season)); - if (seasonsdirec == null) - { - seasonsdirec = new SVR_GroupFilter - { - GroupFilterName = Resources.Filter_Seasons, - InvisibleInClients = 0, - FilterType = (int)(GroupFilterType.Directory | GroupFilterType.Season), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "13;1" - }; - Save(seasonsdirec); - } - - CreateOrVerifyDirectoryFilters(true); - } - - public void CreateOrVerifyDirectoryFilters(bool frominit = false, HashSet<string> tags = null, - HashSet<int> airdate = null, SortedSet<string> seasons = null) - { - const string t = "GroupFilter"; - - var lockedGFs = GetLockedGroupFilters(); - - - var tagsdirec = lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Tag)); - if (tagsdirec != null) - { - HashSet<string> alltags; - if (tags == null) - { - alltags = new HashSet<string>( - RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), - StringComparer.InvariantCultureIgnoreCase); - } - else - { - alltags = new HashSet<string>(tags, - StringComparer.InvariantCultureIgnoreCase); - } - - var existingTags = - new HashSet<string>( - lockedGFs.Where(a => a.FilterType == (int)GroupFilterType.Tag) - .Select(a => a.Conditions.FirstOrDefault()?.ConditionParameter), - StringComparer.InvariantCultureIgnoreCase); - alltags.ExceptWith(existingTags); - - var max = alltags.Count; - var cnt = 0; - //AniDB Tags are in english so we use en-us culture - var tinfo = new CultureInfo("en-US", false).TextInfo; - foreach (var s in alltags) - { - cnt++; - if (frominit) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_CreatingTag + " " + - Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); - } - - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = tagsdirec.GroupFilterID, - InvisibleInClients = 0, - ApplyToSeries = 1, - GroupFilterName = tinfo.ToTitleCase(s), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Tag - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Tag, - ConditionOperator = (int)GroupFilterOperator.In, - ConditionParameter = s, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.CalculateGroupsAndSeries(); - Save(yf); - } - } - - var yearsdirec = lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Year)); - if (yearsdirec != null) - { - HashSet<string> allyears; - if (airdate == null || airdate.Count == 0) - { - var grps = - RepoFactory.AnimeSeries.GetAll().Select(a => a.Contract).Where(a => a != null).ToList(); - - allyears = new HashSet<string>(StringComparer.Ordinal); - foreach (var ser in grps) - { - var endyear = ser.AniDBAnime.AniDBAnime.EndYear; - var startyear = ser.AniDBAnime.AniDBAnime.BeginYear; - if (endyear <= 0) - { - endyear = DateTime.Today.Year; - } - - if (endyear < startyear || endyear - startyear + 1 >= int.MaxValue) - { - endyear = startyear; - } - - if (startyear != 0) - { - allyears.UnionWith(Enumerable.Range(startyear, - endyear - startyear + 1) - .Select(a => a.ToString())); - } - } - } - else - { - allyears = new HashSet<string>(airdate.Select(a => a.ToString()), StringComparer.Ordinal); - } - - var notin = - new HashSet<string>( - lockedGFs.Where(a => a.FilterType == (int)GroupFilterType.Year) - .Select(a => a.Conditions.FirstOrDefault()?.ConditionParameter), - StringComparer.InvariantCultureIgnoreCase); - allyears.ExceptWith(notin); - var max = allyears.Count; - var cnt = 0; - foreach (var s in allyears) - { - cnt++; - if (frominit) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_CreatingYear + " " + - Resources.Filter_Filter + " " + cnt + "/" + max + " - " + s); - } - - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = yearsdirec.GroupFilterID, - InvisibleInClients = 0, - GroupFilterName = s, - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Year, - ApplyToSeries = 1 - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Year, - ConditionOperator = (int)GroupFilterOperator.Include, - ConditionParameter = s, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.CalculateGroupsAndSeries(); - Save(yf); - } - } - - var seasonsdirectory = lockedGFs.FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Season)); - if (seasonsdirectory != null) - { - SortedSet<string> allseasons; - if (seasons == null) - { - var grps = - RepoFactory.AnimeSeries.GetAll().ToList(); - - allseasons = new SortedSet<string>(new SeasonComparator()); - foreach (var ser in grps) - { - if ((ser?.Contract?.AniDBAnime?.Stat_AllSeasons?.Count ?? 0) == 0) - { - ser?.UpdateContract(); - } - - if ((ser?.Contract?.AniDBAnime?.Stat_AllSeasons?.Count ?? 0) == 0) - { - continue; - } - - allseasons.UnionWith(ser.Contract.AniDBAnime.Stat_AllSeasons); - } - } - else - { - allseasons = seasons; - } - - var notin = - new HashSet<string>( - lockedGFs.Where(a => a.FilterType == (int)GroupFilterType.Season) - .Select(a => a.Conditions.FirstOrDefault()?.ConditionParameter), - StringComparer.InvariantCultureIgnoreCase); - allseasons.ExceptWith(notin); - var max = allseasons.Count; - var cnt = 0; - foreach (var season in allseasons) - { - cnt++; - if (frominit) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - Resources.Filter_CreatingSeason + " " + - Resources.Filter_Filter + " " + cnt + "/" + max + " - " + season); - } - - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = seasonsdirectory.GroupFilterID, - InvisibleInClients = 0, - GroupFilterName = season, - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Season, - ApplyToSeries = 1 - }; - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Season, - ConditionOperator = (int)GroupFilterOperator.In, - ConditionParameter = season, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.CalculateGroupsAndSeries(); - Save(yf); - } - } - - CleanUpEmptyDirectoryFilters(); - } - - public override void Save(SVR_GroupFilter obj) - { - Save(obj, false); - } - - public void Save(SVR_GroupFilter obj, bool onlyconditions) - { - WriteLock( - () => - { - if (!onlyconditions) - { - obj.UpdateEntityReferenceStrings(); - } - - var resaveConditions = obj.GroupFilterID == 0; - obj.GroupConditions = JsonConvert.SerializeObject(obj._conditions); - obj.GroupConditionsVersion = SVR_GroupFilter.GROUPCONDITIONS_VERSION; - base.Save(obj); - if (resaveConditions) - { - obj.Conditions.ForEach(a => a.GroupFilterID = obj.GroupFilterID); - Save(obj, true); - } - } - ); - } - - public override void Save(IReadOnlyCollection<SVR_GroupFilter> objs) - { - foreach (var obj in objs) - { - Save(obj); - } - } - - public override void Delete(IReadOnlyCollection<SVR_GroupFilter> objs) - { - foreach (var cr in objs) - { - base.Delete(cr); - } - } - - /// <summary> - /// Updates a batch of <see cref="SVR_GroupFilter"/>s. - /// </summary> - /// <remarks> - /// This method ONLY updates existing <see cref="SVR_GroupFilter"/>s. It will not insert any that don't already exist. - /// </remarks> - /// <param name="session">The NHibernate session.</param> - /// <param name="groupFilters">The batch of <see cref="SVR_GroupFilter"/>s to update.</param> - /// <exception cref="ArgumentNullException"><paramref name="session"/> or <paramref name="groupFilters"/> is <c>null</c>.</exception> - public void BatchUpdate(ISessionWrapper session, IEnumerable<SVR_GroupFilter> groupFilters) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - if (groupFilters == null) - { - throw new ArgumentNullException(nameof(groupFilters)); - } - - foreach (var groupFilter in groupFilters) - { - session.Update(groupFilter); - UpdateCache(groupFilter); - Changes.AddOrUpdate(groupFilter.GroupFilterID); - } - } - - public List<SVR_GroupFilter> GetByParentID(int parentid) - { - return ReadLock(() => Parents.GetMultiple(parentid)); - } - - public List<SVR_GroupFilter> GetTopLevel() - { - return GetByParentID(0); - } - - /// <summary> - /// Calculates what groups should belong to tag related group filters. - /// </summary> - /// <param name="session">The NHibernate session.</param> - /// <returns>A <see cref="ILookup{TKey,TElement}"/> that maps group filter ID to anime group IDs.</returns> - /// <exception cref="ArgumentNullException"><paramref name="session"/> is <c>null</c>.</exception> - public void CalculateAnimeSeriesPerTagGroupFilter(ISessionWrapper session) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - var tagsdirec = GetAll(session).FirstOrDefault( - a => a.FilterType == (int)(GroupFilterType.Directory | GroupFilterType.Tag)); - - DropAllTagFilters(session); - - // user -> tag -> series - var somethingDictionary = - new ConcurrentDictionary<int, ConcurrentDictionary<string, HashSet<int>>>(); - var users = new List<SVR_JMMUser> { null }; - users.AddRange(RepoFactory.JMMUser.GetAll()); - var tags = RepoFactory.AniDB_Tag.GetAll().ToLookup(a => a?.TagName?.ToLowerInvariant()); - - Parallel.ForEach(tags, tag => - { - foreach (var series in tag.ToList().SelectMany(a => RepoFactory.AniDB_Anime_Tag.GetAnimeWithTag(a.TagID))) - { - var seriesTags = series.GetAnime()?.GetAllTags(); - foreach (var user in users) - { - if (user?.GetHideCategories().FindInEnumerable(seriesTags) ?? false) - { - continue; - } - - if (somethingDictionary.ContainsKey(user?.JMMUserID ?? 0)) - { - if (somethingDictionary[user?.JMMUserID ?? 0].ContainsKey(tag.Key)) - { - somethingDictionary[user?.JMMUserID ?? 0][tag.Key] - .Add(series.AnimeSeriesID); - } - else - { - somethingDictionary[user?.JMMUserID ?? 0].AddOrUpdate(tag.Key, - new HashSet<int> { series.AnimeSeriesID }, (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - } - } - else - { - somethingDictionary.AddOrUpdate(user?.JMMUserID ?? 0, id => - { - var newdict = new ConcurrentDictionary<string, HashSet<int>>(); - newdict.AddOrUpdate(tag.Key, new HashSet<int> { series.AnimeSeriesID }, - (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - return newdict; - }, - (i, value) => - { - if (value.ContainsKey(tag.Key)) - { - value[tag.Key] - .Add(series.AnimeSeriesID); - } - else - { - value.AddOrUpdate(tag.Key, - new HashSet<int> { series.AnimeSeriesID }, (oldTag, oldIDs) => - { - lock (oldIDs) - { - oldIDs.Add(series.AnimeSeriesID); - } - - return oldIDs; - }); - } - - return value; - }); - } - } - } - }); - - var lookup = somethingDictionary.Keys.Where(a => somethingDictionary[a] != null).ToDictionary(key => key, key => - somethingDictionary[key].Where(a => a.Value != null) - .SelectMany(p => p.Value.Select(a => Tuple.Create(p.Key, a))) - .ToLookup(p => p.Item1, p => p.Item2)); - - CreateAllTagFilters(session, tagsdirec, lookup); - } - - private void DropAllTagFilters(ISessionWrapper session) - { - ClearCache(); - Lock(() => - { - using var trans = session.BeginTransaction(); - session.CreateQuery($"DELETE FROM {nameof(SVR_GroupFilter)} WHERE FilterType = {(int)GroupFilterType.Tag};") - .ExecuteUpdate(); - trans.Commit(); - }); - } - - private void CreateAllTagFilters(ISessionWrapper session, SVR_GroupFilter tagsdirec, - Dictionary<int, ILookup<string, int>> lookup) - { - if (tagsdirec == null) - { - return; - } - - var alltags = new HashSet<string>( - RepoFactory.AniDB_Tag.GetAllForLocalSeries().Select(a => a.TagName.Replace('`', '\'')), - StringComparer.InvariantCultureIgnoreCase); - var toAdd = new List<SVR_GroupFilter>(alltags.Count); - - var users = RepoFactory.JMMUser.GetAll().ToList(); - - //AniDB Tags are in english so we use en-us culture - var tinfo = new CultureInfo("en-US", false).TextInfo; - foreach (var s in alltags) - { - // this is creating new tag filters, so locking isn't completely necessary. - // Ideally, we would create a blank filter to ensure an ID exists, but then it would be empty, - // and would have data inconsistencies, anyway - var yf = new SVR_GroupFilter - { - ParentGroupFilterID = tagsdirec.GroupFilterID, - InvisibleInClients = 0, - ApplyToSeries = 1, - GroupFilterName = tinfo.ToTitleCase(s), - BaseCondition = 1, - Locked = 1, - SortingCriteria = "5;1", - FilterType = (int)GroupFilterType.Tag - }; - yf.SeriesIds[0] = lookup[0][s.ToLowerInvariant()].ToHashSet(); - yf.GroupsIds[0] = yf.SeriesIds[0] - .Select(id => RepoFactory.AnimeSeries.GetByID(id).TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(id => id != -1).ToHashSet(); - foreach (var user in users) - { - var key = s.ToLowerInvariant(); - // this will happen if you only have porn or none the series you have are allowed by a user - if (!lookup.ContainsKey(user.JMMUserID) || !lookup[user.JMMUserID].Contains(key)) - { - continue; - } - - yf.SeriesIds[user.JMMUserID] = lookup[user.JMMUserID][key].ToHashSet(); - yf.GroupsIds[user.JMMUserID] = yf.SeriesIds[user.JMMUserID] - .Select(id => RepoFactory.AnimeSeries.GetByID(id).TopLevelAnimeGroup?.AnimeGroupID ?? -1) - .Where(id => id != -1).ToHashSet(); - } - - Lock(() => - { - using var trans = session.BeginTransaction(); - // get an ID - session.Insert(yf); - trans.Commit(); - }); - - var gfc = new GroupFilterCondition - { - ConditionType = (int)GroupFilterConditionType.Tag, - ConditionOperator = (int)GroupFilterOperator.In, - ConditionParameter = s, - GroupFilterID = yf.GroupFilterID - }; - yf.Conditions.Add(gfc); - yf.UpdateEntityReferenceStrings(); - yf.GroupConditions = JsonConvert.SerializeObject(yf._conditions); - yf.GroupConditionsVersion = SVR_GroupFilter.GROUPCONDITIONS_VERSION; - toAdd.Add(yf); - } - - Populate(session, false); - foreach (var filters in toAdd.Batch(50)) - { - Lock(() => - { - using var trans = session.BeginTransaction(); - BatchUpdate(session, filters); - trans.Commit(); - }); - } - } - - public List<SVR_GroupFilter> GetLockedGroupFilters() - { - return ReadLock(() => Cache.Values.Where(a => a.Locked == 1).ToList()); - } - - public List<SVR_GroupFilter> GetWithConditionTypesAndAll(HashSet<GroupFilterConditionType> types) - { - return ReadLock( - () => - { - var filters = new HashSet<int>( - Cache.Values - .Where(a => a.FilterType == (int)GroupFilterType.All) - .Select(a => a.GroupFilterID) - ); - foreach (var t in types) - { - filters.UnionWith(Types.FindInverse(t)); - } - - return filters.Select(a => Cache.Get(a)).ToList(); - } - ); - } - - public List<SVR_GroupFilter> GetWithConditionsTypes(HashSet<GroupFilterConditionType> types) - { - return ReadLock( - () => - { - var filters = new HashSet<int>(); - foreach (var t in types) - { - filters.UnionWith(Types.FindInverse(t)); - } - - return filters.Select(a => Cache.Get(a)).ToList(); - } - ); - } - - public ChangeTracker<int> GetChangeTracker() - { - return Changes; - } -} diff --git a/Shoko.Server/Repositories/Cached/JMMUserRepository.cs b/Shoko.Server/Repositories/Cached/JMMUserRepository.cs index cced968ad..354dbef9c 100644 --- a/Shoko.Server/Repositories/Cached/JMMUserRepository.cs +++ b/Shoko.Server/Repositories/Cached/JMMUserRepository.cs @@ -25,43 +25,6 @@ public override void RegenerateDb() { } - public override void Save(SVR_JMMUser obj) - { - Save(obj, true); - } - - public void Save(SVR_JMMUser obj, bool updateGroupFilters) - { - var isNew = false; - if (obj.JMMUserID == 0) - { - isNew = true; - base.Save(obj); - } - - if (updateGroupFilters) - { - SVR_JMMUser old = null; - if (!isNew) - { - old = Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Get<SVR_JMMUser>(obj.JMMUserID); - }); - } - - updateGroupFilters = SVR_JMMUser.CompareUser(old, obj); - } - - base.Save(obj); - if (updateGroupFilters) - { - logger.Trace("Updating group filter stats by user from JMMUserRepository.Save: {0}", obj.JMMUserID); - obj.UpdateGroupFilters(); - } - } - public SVR_JMMUser GetByUsername(string username) { if (string.IsNullOrWhiteSpace(username)) @@ -102,9 +65,6 @@ public bool RemoveUser(int userID, bool skipValidation = false) } } - var toSave = RepoFactory.GroupFilter.GetAll().AsParallel().Where(a => a.RemoveUser(userID)).ToList(); - RepoFactory.GroupFilter.Save(toSave); - RepoFactory.AnimeSeries_User.Delete(RepoFactory.AnimeSeries_User.GetByUserID(userID)); RepoFactory.AnimeGroup_User.Delete(RepoFactory.AnimeGroup_User.GetByUserID(userID)); RepoFactory.AnimeEpisode_User.Delete(RepoFactory.AnimeEpisode_User.GetByUserID(userID)); diff --git a/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs b/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs index ce6e91d1f..b2747c595 100644 --- a/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Direct/CrossRef_Languages_AniDB_FileRepository.cs @@ -2,21 +2,19 @@ using System.Collections.Generic; using System.Linq; using NHibernate; +using NutzCode.InMemoryIndex; using Shoko.Models.Server; using Shoko.Server.Databases; namespace Shoko.Server.Repositories.Direct; -public class CrossRef_Languages_AniDB_FileRepository : BaseDirectRepository<CrossRef_Languages_AniDB_File, int> +public class CrossRef_Languages_AniDB_FileRepository : BaseCachedRepository<CrossRef_Languages_AniDB_File, int> { + private PocoIndex<int, CrossRef_Languages_AniDB_File, int> FileIDs; + public List<CrossRef_Languages_AniDB_File> GetByFileID(int id) { - return Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session.Query<CrossRef_Languages_AniDB_File>() - .Where(a => a.FileID == id).ToList(); - }); + return FileIDs.GetMultiple(id); } public Dictionary<int, HashSet<string>> GetLanguagesByAnime(IEnumerable<int> animeIds) @@ -51,4 +49,13 @@ FROM CrossRef_File_Episode eps .List<string>().ToHashSet(StringComparer.InvariantCultureIgnoreCase); }); } + + public override void PopulateIndexes() + { + FileIDs = Cache.CreateIndex(a => a.FileID); + } + + public override void RegenerateDb() { } + + protected override int SelectKey(CrossRef_Languages_AniDB_File entity) => entity.CrossRef_Languages_AniDB_FileID; } diff --git a/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs b/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs index dbff47519..c4ddf1d9d 100644 --- a/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Direct/CrossRef_Subtitles_AniDB_FileRepository.cs @@ -2,23 +2,19 @@ using System.Collections.Generic; using System.Linq; using NHibernate; +using NutzCode.InMemoryIndex; using Shoko.Models.Server; using Shoko.Server.Databases; namespace Shoko.Server.Repositories.Direct; -public class CrossRef_Subtitles_AniDB_FileRepository : BaseDirectRepository<CrossRef_Subtitles_AniDB_File, int> +public class CrossRef_Subtitles_AniDB_FileRepository : BaseCachedRepository<CrossRef_Subtitles_AniDB_File, int> { + private PocoIndex<int, CrossRef_Subtitles_AniDB_File, int> FileIDs; + public List<CrossRef_Subtitles_AniDB_File> GetByFileID(int id) { - return Lock(() => - { - using var session = DatabaseFactory.SessionFactory.OpenSession(); - return session - .Query<CrossRef_Subtitles_AniDB_File>() - .Where(a => a.FileID == id) - .ToList(); - }); + return FileIDs.GetMultiple(id); } public Dictionary<int, HashSet<string>> GetLanguagesByAnime(IEnumerable<int> animeIds) @@ -51,4 +47,13 @@ public HashSet<string> GetLanguagesForAnime(int animeID) .List<string>().ToHashSet(StringComparer.InvariantCultureIgnoreCase); }); } + + public override void PopulateIndexes() + { + FileIDs = Cache.CreateIndex(a => a.FileID); + } + + public override void RegenerateDb() { } + + protected override int SelectKey(CrossRef_Subtitles_AniDB_File entity) => entity.CrossRef_Subtitles_AniDB_FileID; } diff --git a/Shoko.Server/Repositories/RepoFactory.cs b/Shoko.Server/Repositories/RepoFactory.cs index 5b21bcdf2..99fb55c61 100644 --- a/Shoko.Server/Repositories/RepoFactory.cs +++ b/Shoko.Server/Repositories/RepoFactory.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; using System.Runtime; -using System.Threading.Tasks; using NLog; using Shoko.Commons.Extensions; using Shoko.Commons.Properties; using Shoko.Server.Repositories.Cached; using Shoko.Server.Repositories.Direct; using Shoko.Server.Server; -using Shoko.Server.Settings; // ReSharper disable InconsistentNaming @@ -93,7 +91,7 @@ public static class RepoFactory public static AnimeCharacterRepository AnimeCharacter { get; } = new(); public static AnimeStaffRepository AnimeStaff { get; } = new(); public static CrossRef_Anime_StaffRepository CrossRef_Anime_Staff { get; } = new(); - public static GroupFilterRepository GroupFilter { get; } = new(); + public static FilterPresetRepository FilterPreset { get; } = new(); /************** DEPRECATED **************/ /* We need to delete them at some point */ @@ -123,7 +121,7 @@ public static void PostInit() CleanUpMemory(); } - public static async Task Init() + public static void Init() { try { diff --git a/Shoko.Server/Server/Constants.cs b/Shoko.Server/Server/Constants.cs index d89b69044..a595e48fa 100644 --- a/Shoko.Server/Server/Constants.cs +++ b/Shoko.Server/Server/Constants.cs @@ -24,7 +24,18 @@ public static class Constants public struct GroupFilterName { - public static readonly string ContinueWatching = Resources.Filter_Continue; + public const string All = "All"; + public const string ContinueWatching = "Continue Watching"; + public const string Favorites = "Favorites"; + public const string MissingEpisodes = "Missing Episodes"; + public const string NewlyAddedSeries = "Newly Added Series"; + public const string NewlyAiringSeries = "Newly Airing Series"; + public const string MissingVotes = "Missing Votes"; + public const string MissingLinks = "Missing Links"; + public const string RecentlyWatched = "Recently Watched"; + public const string Tags = "Tags"; + public const string Seasons = "Seasons"; + public const string Years = "Years"; } public struct DatabaseType diff --git a/Shoko.Server/Server/ShokoServer.cs b/Shoko.Server/Server/ShokoServer.cs index d56299756..580581f3d 100644 --- a/Shoko.Server/Server/ShokoServer.cs +++ b/Shoko.Server/Server/ShokoServer.cs @@ -545,7 +545,6 @@ private void ShutDown() private static void AutoUpdateTimer_Elapsed(object sender, ElapsedEventArgs e) { - Importer.CheckForDayFilters(); Importer.CheckForCalendarUpdate(false); Importer.CheckForAnimeUpdate(false); Importer.CheckForTvDBUpdates(false); diff --git a/Shoko.Server/Server/Startup.cs b/Shoko.Server/Server/Startup.cs index e5edabbe5..4b09a6c38 100644 --- a/Shoko.Server/Server/Startup.cs +++ b/Shoko.Server/Server/Startup.cs @@ -13,7 +13,8 @@ using Shoko.Plugin.Abstractions.Services; using Shoko.Server.API; using Shoko.Server.Commands; -using Shoko.Server.PlexAndKodi; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Legacy; using Shoko.Server.Plugin; using Shoko.Server.Providers.AniDB; using Shoko.Server.Providers.MovieDB; @@ -53,7 +54,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton<TraktTVHelper>(); services.AddSingleton<TvDBApiHelper>(); services.AddSingleton<MovieDBHelper>(); - services.AddScoped<CommonImplementation>(); + services.AddSingleton<FilterEvaluator>(); + services.AddSingleton<LegacyFilterConverter>(); services.AddSingleton<IShokoEventHandler>(ShokoEventHandler.Instance); services.AddSingleton<ICommandRequestFactory, CommandRequestFactory>(); services.AddSingleton<IConnectivityMonitor, CloudFlareConnectivityMonitor>(); diff --git a/Shoko.Server/Settings/ServerSettings.cs b/Shoko.Server/Settings/ServerSettings.cs index 8160e5d44..845cc034c 100644 --- a/Shoko.Server/Settings/ServerSettings.cs +++ b/Shoko.Server/Settings/ServerSettings.cs @@ -80,6 +80,7 @@ public List<string> LanguagePreference { _languagePreference = value; Languages.PreferredNamingLanguages = null; + Languages.PreferredNamingLanguageNames = null; } } diff --git a/Shoko.Server/Tasks/AnimeGroupCreator.cs b/Shoko.Server/Tasks/AnimeGroupCreator.cs index 4e5e86041..9daf32793 100644 --- a/Shoko.Server/Tasks/AnimeGroupCreator.cs +++ b/Shoko.Server/Tasks/AnimeGroupCreator.cs @@ -14,7 +14,6 @@ using Shoko.Server.Repositories.Cached; using Shoko.Server.Repositories.NHibernate; using Shoko.Server.Server; -using Shoko.Server.Settings; using Shoko.Server.Utilities; namespace Shoko.Server.Tasks; @@ -29,8 +28,7 @@ internal class AnimeGroupCreator private readonly AnimeSeriesRepository _animeSeriesRepo = RepoFactory.AnimeSeries; private readonly AnimeGroupRepository _animeGroupRepo = RepoFactory.AnimeGroup; private readonly AnimeGroup_UserRepository _animeGroupUserRepo = RepoFactory.AnimeGroup_User; - private readonly GroupFilterRepository _groupFilterRepo = RepoFactory.GroupFilter; - private readonly JMMUserRepository _userRepo = RepoFactory.JMMUser; + private readonly FilterPresetRepository _filterRepo = RepoFactory.FilterPreset; private readonly bool _autoGroupSeries; /// <summary> @@ -108,7 +106,6 @@ private void ClearGroupsAndDependencies(ISessionWrapper session, int tempGroupId // We've deleted/modified all AnimeSeries/GroupFilter records, so update caches to reflect that _animeSeriesRepo.ClearCache(); - _groupFilterRepo.ClearCache(); _log.Info("AnimeGroups have been removed and GroupFilters have been reset"); } @@ -200,35 +197,13 @@ private void UpdateAnimeGroupsAndTheirContracts(IReadOnlyCollection<SVR_AnimeGro /// <remarks> /// Assumes that all caches are up to date. /// </remarks> - private void UpdateGroupFilters(ISessionWrapper session) + private void UpdateGroupFilters() { _log.Info("Updating Group Filters"); _log.Info("Calculating Tag Filters"); ServerState.Instance.DatabaseBlocked = new ServerState.DatabaseBlockedInfo { Blocked = true, Status = "Calculating Tag Filters" }; - _groupFilterRepo.CalculateAnimeSeriesPerTagGroupFilter(session); - _log.Info("Calculating All Other Filters"); - ServerState.Instance.DatabaseBlocked = - new ServerState.DatabaseBlockedInfo { Blocked = true, Status = "Calculating Non-Tag Filters" }; - IEnumerable<SVR_GroupFilter> grpFilters = _groupFilterRepo.GetAll(session).Where(a => - a.FilterType != (int)GroupFilterType.Tag && !a.IsDirectory).ToList(); - - // The main reason for doing this in parallel is because UpdateEntityReferenceStrings does JSON encoding - // and is enough work that it can benefit from running in parallel - Parallel.ForEach( - grpFilters, filter => - { - filter.SeriesIds.Clear(); - filter.CalculateGroupsAndSeries(); - filter.UpdateEntityReferenceStrings(); - }); - - BaseRepository.Lock(() => - { - using var trans = session.BeginTransaction(); - _groupFilterRepo.BatchUpdate(session, grpFilters); - trans.Commit(); - }); + _filterRepo.CreateOrVerifyDirectoryFilters(); _log.Info("Group Filters updated"); } @@ -417,7 +392,10 @@ public SVR_AnimeGroup GetOrCreateSingleGroupForSeries(SVR_AnimeSeries series) { // Override the group name if the group is not manually named. if (animeGroup.IsManuallyNamed == 0) - animeGroup.GroupName = animeGroup.SortName = series.GetSeriesName(); + { + animeGroup.GroupName = series.GetSeriesName(); + animeGroup.SortName = animeGroup.GroupName.GetSortName(); + } // Override the group desc. if the group doesn't have an override. if (animeGroup.OverrideDescription == 0) animeGroup.Description = series.GetAnime().Description; @@ -505,9 +483,8 @@ public void RecreateAllGroups(ISessionWrapper session) // (Otherwise updating Group Filters won't get the correct results) _animeGroupRepo.Populate(session, false); _animeGroupUserRepo.Populate(session, false); - _groupFilterRepo.Populate(session, false); - UpdateGroupFilters(session); + UpdateGroupFilters(); _log.Info("Successfuly completed re-creating all groups"); } @@ -520,7 +497,6 @@ public void RecreateAllGroups(ISessionWrapper session) // If an error occurs then chances are the caches are in an inconsistent state. So re-populate them _animeSeriesRepo.Populate(); _animeGroupRepo.Populate(); - _groupFilterRepo.Populate(); _animeGroupUserRepo.Populate(); } catch (Exception ie) @@ -581,7 +557,7 @@ public void RecalculateStatsContractsForGroup(SVR_AnimeGroup group) // update filters _log.Info($"Recalculating Filters for Group: {group.GroupName} ({group.AnimeGroupID})"); - UpdateGroupFilters(session); + UpdateGroupFilters(); _log.Info($"Done Recalculating Stats and Contracts for Group: {group.GroupName} ({group.AnimeGroupID})"); } diff --git a/Shoko.Server/Utilities/GroupFilterHelper.cs b/Shoko.Server/Utilities/GroupFilterHelper.cs deleted file mode 100644 index e8f61a349..000000000 --- a/Shoko.Server/Utilities/GroupFilterHelper.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Server.Models; - -namespace Shoko.Server; - -public class GroupFilterHelper -{ - public static string GetTextForEnum_Sorting(GroupFilterSorting sort) - { - switch (sort) - { - case GroupFilterSorting.AniDBRating: - return "AniDB Rating"; - case GroupFilterSorting.EpisodeAddedDate: - return "Episode Added Date"; - case GroupFilterSorting.EpisodeAirDate: - return "Episode Air Date"; - case GroupFilterSorting.EpisodeWatchedDate: - return "Episode Watched Date"; - case GroupFilterSorting.GroupName: - return "Group Name"; - case GroupFilterSorting.SortName: - return "Sort Name"; - case GroupFilterSorting.MissingEpisodeCount: - return "Missing Episode Count"; - case GroupFilterSorting.SeriesAddedDate: - return "Series Added Date"; - case GroupFilterSorting.SeriesCount: - return "Series Count"; - case GroupFilterSorting.UnwatchedEpisodeCount: - return "Unwatched Episode Count"; - case GroupFilterSorting.UserRating: - return "User Rating"; - case GroupFilterSorting.Year: - return "Year"; - default: - return "AniDB Rating"; - } - } - - public static GroupFilterSorting GetEnumForText_Sorting(string enumDesc) - { - switch (enumDesc) - { - case "AniDB Rating": - return GroupFilterSorting.AniDBRating; - case "Episode Added Date": - return GroupFilterSorting.EpisodeAddedDate; - case "Episode Air Date": - return GroupFilterSorting.EpisodeAirDate; - case "Episode Watched Date": - return GroupFilterSorting.EpisodeWatchedDate; - case "Group Name": - return GroupFilterSorting.GroupName; - case "Sort Name": - return GroupFilterSorting.SortName; - case "Missing Episode Count": - return GroupFilterSorting.MissingEpisodeCount; - case "Series Added Date": - return GroupFilterSorting.SeriesAddedDate; - case "Series Count": - return GroupFilterSorting.SeriesCount; - case "Unwatched Episode Count": - return GroupFilterSorting.UnwatchedEpisodeCount; - case "User Rating": - return GroupFilterSorting.UserRating; - case "Year": - return GroupFilterSorting.Year; - default: - return GroupFilterSorting.AniDBRating; - } - } - - public static string GetTextForEnum_SortDirection(GroupFilterSortDirection sort) - { - switch (sort) - { - case GroupFilterSortDirection.Asc: - return "Asc"; - case GroupFilterSortDirection.Desc: - return "Desc"; - default: - return "Asc"; - } - } - - public static GroupFilterSortDirection GetEnumForText_SortDirection(string enumDesc) - { - switch (enumDesc) - { - case "Asc": - return GroupFilterSortDirection.Asc; - case "Desc": - return GroupFilterSortDirection.Desc; - default: - return GroupFilterSortDirection.Asc; - } - } - - public static List<string> GetAllSortTypes() - { - var cons = new List<string> - { - GetTextForEnum_Sorting(GroupFilterSorting.AniDBRating), - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAddedDate), - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAirDate), - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeWatchedDate), - GetTextForEnum_Sorting(GroupFilterSorting.GroupName), - GetTextForEnum_Sorting(GroupFilterSorting.MissingEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesAddedDate), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesCount), - GetTextForEnum_Sorting(GroupFilterSorting.SortName), - GetTextForEnum_Sorting(GroupFilterSorting.UnwatchedEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.UserRating), - GetTextForEnum_Sorting(GroupFilterSorting.Year) - }; - cons.Sort(); - - return cons; - } - - public static List<string> GetQuickSortTypes() - { - var cons = new List<string> - { - //GetTextForEnum_Sorting(GroupFilterSorting.AniDBRating); removed for performance reasons - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAddedDate), - //GetTextForEnum_Sorting(GroupFilterSorting.EpisodeAirDate); - GetTextForEnum_Sorting(GroupFilterSorting.EpisodeWatchedDate), - GetTextForEnum_Sorting(GroupFilterSorting.GroupName), - GetTextForEnum_Sorting(GroupFilterSorting.MissingEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesAddedDate), - GetTextForEnum_Sorting(GroupFilterSorting.SeriesCount), - GetTextForEnum_Sorting(GroupFilterSorting.SortName), - GetTextForEnum_Sorting(GroupFilterSorting.UnwatchedEpisodeCount), - GetTextForEnum_Sorting(GroupFilterSorting.UserRating), - GetTextForEnum_Sorting(GroupFilterSorting.Year) - }; - cons.Sort(); - - return cons; - } - - - public static string GetDateAsString(DateTime aDate) - { - return aDate.Year.ToString().PadLeft(4, '0') + - aDate.Month.ToString().PadLeft(2, '0') + - aDate.Day.ToString().PadLeft(2, '0'); - } - - public static DateTime GetDateFromString(string sDate) - { - try - { - var year = int.Parse(sDate.Substring(0, 4)); - var month = int.Parse(sDate.Substring(4, 2)); - var day = int.Parse(sDate.Substring(6, 2)); - - return new DateTime(year, month, day); - } - catch - { - return DateTime.Today; - } - } - - public static string GetDateAsFriendlyString(DateTime aDate) - { - return aDate.ToString("dd MMM yyyy", CultureInfo.CurrentCulture); - } - - public static IEnumerable<CL_AnimeGroup_User> Sort(IEnumerable<CL_AnimeGroup_User> groups, SVR_GroupFilter gf) - { - var isfirst = true; - var query = groups; - foreach (var gfsc in gf.SortCriteriaList) - { - query = Order(query, gfsc, isfirst); - isfirst = false; - } - - return query; - } - - public static IOrderedEnumerable<CL_AnimeGroup_User> Order(IEnumerable<CL_AnimeGroup_User> groups, - GroupFilterSortingCriteria gfsc, bool isfirst) - { - switch (gfsc.SortType) - { - case GroupFilterSorting.Year: - if (gfsc.SortDirection == GroupFilterSortDirection.Asc) - { - return Order(groups, a => a.Stat_AirDate_Min, gfsc.SortDirection, isfirst); - } - - return Order(groups, a => a.Stat_AirDate_Max, gfsc.SortDirection, isfirst); - case GroupFilterSorting.AniDBRating: - return Order(groups, a => a.Stat_AniDBRating, gfsc.SortDirection, isfirst); - case GroupFilterSorting.EpisodeAddedDate: - return Order(groups, a => a.EpisodeAddedDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.EpisodeAirDate: - return Order(groups, a => a.LatestEpisodeAirDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.EpisodeWatchedDate: - return Order(groups, a => a.WatchedDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.MissingEpisodeCount: - return Order(groups, a => a.MissingEpisodeCount, gfsc.SortDirection, isfirst); - case GroupFilterSorting.SeriesAddedDate: - return Order(groups, a => a.Stat_SeriesCreatedDate, gfsc.SortDirection, isfirst); - case GroupFilterSorting.SeriesCount: - return Order(groups, a => a.Stat_SeriesCount, gfsc.SortDirection, isfirst); - case GroupFilterSorting.SortName: - return Order(groups, a => a.SortName, gfsc.SortDirection, isfirst); - case GroupFilterSorting.UnwatchedEpisodeCount: - return Order(groups, a => a.UnwatchedEpisodeCount, gfsc.SortDirection, isfirst); - case GroupFilterSorting.UserRating: - return Order(groups, a => a.Stat_UserVoteOverall, gfsc.SortDirection, isfirst); - case GroupFilterSorting.GroupName: - case GroupFilterSorting.GroupFilterName: - return Order(groups, a => a.GroupName, gfsc.SortDirection, isfirst); - default: - return Order(groups, a => a.GroupName, gfsc.SortDirection, isfirst); - } - } - - private static IOrderedEnumerable<CL_AnimeGroup_User> Order<T>(IEnumerable<CL_AnimeGroup_User> groups, - Func<CL_AnimeGroup_User, T> o, - GroupFilterSortDirection direc, bool isfirst) - { - if (isfirst) - { - return direc == GroupFilterSortDirection.Asc - ? groups.OrderBy(o) - : groups.OrderByDescending(o); - } - - return direc == GroupFilterSortDirection.Asc - ? ((IOrderedEnumerable<CL_AnimeGroup_User>)groups).ThenBy(o) - : ((IOrderedEnumerable<CL_AnimeGroup_User>)groups).ThenByDescending(o); - } - - /* - public static List<SortPropOrFieldAndDirection> GetSortDescriptions(GroupFilter gf) - { - List<SortPropOrFieldAndDirection> sortlist = new List<SortPropOrFieldAndDirection>(); - - return sortlist; - } - - public static SortPropOrFieldAndDirection GetSortDescription(GroupFilterSorting sortType, - GroupFilterSortDirection sortDirection) - { - string sortColumn = string.Empty; - bool sortDescending = sortDirection == GroupFilterSortDirection.Desc; - SortType sortFieldType = SortType.eString; - - switch (sortType) - { - case GroupFilterSorting.AniDBRating: - sortColumn = "AniDBRating"; - sortFieldType = SortType.eDoubleOrFloat; - break; - case GroupFilterSorting.EpisodeAddedDate: - sortColumn = "EpisodeAddedDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.EpisodeAirDate: - sortColumn = "LatestEpisodeAirDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.EpisodeWatchedDate: - sortColumn = "WatchedDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.GroupName: - sortColumn = "GroupName"; - sortFieldType = SortType.eString; - break; - case GroupFilterSorting.SortName: - sortColumn = "SortName"; - sortFieldType = SortType.eString; - break; - case GroupFilterSorting.MissingEpisodeCount: - sortColumn = "MissingEpisodeCount"; - sortFieldType = SortType.eInteger; - break; - case GroupFilterSorting.SeriesAddedDate: - sortColumn = "Stat_SeriesCreatedDate"; - sortFieldType = SortType.eDateTime; - break; - case GroupFilterSorting.SeriesCount: - sortColumn = "AllSeriesCount"; - sortFieldType = SortType.eInteger; - break; - case GroupFilterSorting.UnwatchedEpisodeCount: - sortColumn = "UnwatchedEpisodeCount"; - sortFieldType = SortType.eInteger; - break; - case GroupFilterSorting.UserRating: - sortColumn = "Stat_UserVoteOverall"; - sortFieldType = SortType.eDoubleOrFloat; - break; - case GroupFilterSorting.Year: - if (sortDirection == GroupFilterSortDirection.Asc) - sortColumn = "Stat_AirDate_Min"; - else - sortColumn = "Stat_AirDate_Max"; - - sortFieldType = SortType.eDateTime; - break; - default: - sortColumn = "GroupName"; - sortFieldType = SortType.eString; - break; - } - - - return new SortPropOrFieldAndDirection(sortColumn, sortDescending, sortFieldType); - } - */ -} diff --git a/Shoko.Server/Utilities/Languages.cs b/Shoko.Server/Utilities/Languages.cs index f9c88f69c..2a47ad0cf 100644 --- a/Shoko.Server/Utilities/Languages.cs +++ b/Shoko.Server/Utilities/Languages.cs @@ -29,6 +29,18 @@ public static List<NamingLanguage> PreferredNamingLanguages set => _preferredNamingLanguages = value; } + private static List<TitleLanguage> _preferredNamingLanguageNames; + public static List<TitleLanguage> PreferredNamingLanguageNames + { + get + { + if (_preferredNamingLanguages != null) return _preferredNamingLanguageNames; + _preferredNamingLanguageNames = PreferredNamingLanguages.Select(a => a.Language).ToList(); + return _preferredNamingLanguageNames; + } + set => _preferredNamingLanguageNames = value; + } + private static List<NamingLanguage> _preferredEpisodeNamingLanguages; public static List<NamingLanguage> PreferredEpisodeNamingLanguages diff --git a/Shoko.Server/Utilities/PocoCache.cs b/Shoko.Server/Utilities/PocoCache.cs index 7fe901a85..da0d4c626 100644 --- a/Shoko.Server/Utilities/PocoCache.cs +++ b/Shoko.Server/Utilities/PocoCache.cs @@ -33,33 +33,32 @@ namespace NutzCode.InMemoryIndex; public class PocoCache<T, S> where S : class { - private Dictionary<T, S> _dict; - private Func<S, T> _func; - private List<IPocoCacheObserver<T, S>> _observers = new(); + private readonly Dictionary<T, S> _dict; + private readonly Func<S, T> _func; + private readonly List<IPocoCacheObserver<T, S>> _observers = new(); public Dictionary<T, S>.KeyCollection Keys => _dict.Keys; public Dictionary<T, S>.ValueCollection Values => _dict.Values; - public PocoIndex<T, S, U> CreateIndex<U>(Func<S, U> paramfunc1) + public PocoIndex<T, S, U> CreateIndex<U>(Func<S, U> func1) { - return new PocoIndex<T, S, U>(this, paramfunc1); + return new PocoIndex<T, S, U>(this, func1); } - public PocoIndex<T, S, N, U> CreateIndex<N, U>(Func<S, N> paramfunc1, Func<S, U> paramfunc2) + public PocoIndex<T, S, N, U> CreateIndex<N, U>(Func<S, N> func1, Func<S, U> func2) { - return new PocoIndex<T, S, N, U>(this, paramfunc1, paramfunc2); + return new PocoIndex<T, S, N, U>(this, func1, func2); } - public PocoIndex<T, S, N, R, U> CreateIndex<N, R, U>(Func<S, N> paramfunc1, Func<S, R> paramfunc2, - Func<S, U> paramfunc3) + public PocoIndex<T, S, N, R, U> CreateIndex<N, R, U>(Func<S, N> func1, Func<S, R> func2, Func<S, U> func3) { - return new PocoIndex<T, S, N, R, U>(this, paramfunc1, paramfunc2, paramfunc3); + return new PocoIndex<T, S, N, R, U>(this, func1, func2, func3); } - public PocoCache(IEnumerable<S> objectlist, Func<S, T> keyfunc) + public PocoCache(IEnumerable<S> objectList, Func<S, T> keyFunc) { - _func = keyfunc; - _dict = objectlist.ToDictionary(keyfunc, a => a); + _func = keyFunc; + _dict = objectList.ToDictionary(keyFunc, a => a); } internal void AddChain(IPocoCacheObserver<T, S> observer) @@ -69,9 +68,7 @@ internal void AddChain(IPocoCacheObserver<T, S> observer) public S Get(T key) { - return _dict.ContainsKey(key) - ? _dict[key] - : null; + return _dict.TryGetValue(key, out var value) ? value : null; } public void Update(S obj) @@ -110,7 +107,7 @@ public void Clear() } } -public interface IPocoCacheObserver<TKey, TEntity> where TEntity : class +public interface IPocoCacheObserver<in TKey, in TEntity> where TEntity : class { void Update(TKey key, TEntity entity); @@ -122,39 +119,33 @@ public interface IPocoCacheObserver<TKey, TEntity> where TEntity : class public class PocoIndex<T, S, U> : IPocoCacheObserver<T, S> where S : class { - internal PocoCache<T, S> _cache; - internal BiDictionaryOneToMany<T, U> _dict; - internal Func<S, U> _func; + private static readonly List<S> EmptyList = new (); + private readonly PocoCache<T, S> _cache; + private readonly BiDictionaryOneToMany<T, U> _dict; + private readonly Func<S, U> _func; - internal PocoIndex(PocoCache<T, S> cache, Func<S, U> paramfunc1) + internal PocoIndex(PocoCache<T, S> cache, Func<S, U> func1) { _cache = cache; - _dict = new BiDictionaryOneToMany<T, U>(_cache.Keys.ToDictionary(a => a, a => paramfunc1(_cache.Get(a)))); - _func = paramfunc1; + _dict = new BiDictionaryOneToMany<T, U>(_cache.Keys.ToDictionary(a => a, a => func1(_cache.Get(a)))); + _func = func1; cache.AddChain(this); } public S GetOne(U key) { - if (_cache == null || !_dict.ContainsInverseKey(key)) - { + if (_cache == null || !_dict.TryGetInverse(key, out var results)) return null; - } - var hashes = _dict.FindInverse(key); - return hashes.Count == 0 - ? null - : _cache.Get(hashes.First()); + return results.Count == 0 ? null : _cache.Get(results.FirstOrDefault()); } public List<S> GetMultiple(U key) { - if (_cache == null || !_dict.ContainsInverseKey(key)) - { - return new List<S>(); - } + if (_cache == null || !_dict.TryGetInverse(key, out var results)) + return EmptyList; - return _dict.FindInverse(key).Select(a => _cache.Get(a)).ToList(); + return results.Select(a => _cache.Get(a)).ToList(); } void IPocoCacheObserver<T, S>.Update(T key, S obj) @@ -175,11 +166,11 @@ void IPocoCacheObserver<T, S>.Clear() public class PocoIndex<T, S, N, U> where S : class { - public PocoIndex<T, S, Tuple<N, U>> _index; + private readonly PocoIndex<T, S, Tuple<N, U>> _index; - internal PocoIndex(PocoCache<T, S> cache, Func<S, N> paramfunc1, Func<S, U> paramfunc2) + internal PocoIndex(PocoCache<T, S> cache, Func<S, N> func1, Func<S, U> func2) { - _index = new PocoIndex<T, S, Tuple<N, U>>(cache, a => new Tuple<N, U>(paramfunc1(a), paramfunc2(a))); + _index = new PocoIndex<T, S, Tuple<N, U>>(cache, a => new Tuple<N, U>(func1(a), func2(a))); } public S GetOne(N key1, U key2) @@ -197,12 +188,12 @@ public List<S> GetMultiple(N key1, U key2) public class PocoIndex<T, S, N, R, U> where S : class { - public PocoIndex<T, S, Tuple<N, R, U>> _index; + private readonly PocoIndex<T, S, Tuple<N, R, U>> _index; - internal PocoIndex(PocoCache<T, S> cache, Func<S, N> paramfunc1, Func<S, R> paramfunc2, Func<S, U> paramfunc3) + internal PocoIndex(PocoCache<T, S> cache, Func<S, N> func1, Func<S, R> func2, Func<S, U> func3) { _index = new PocoIndex<T, S, Tuple<N, R, U>>(cache, - a => new Tuple<N, R, U>(paramfunc1(a), paramfunc2(a), paramfunc3(a))); + a => new Tuple<N, R, U>(func1(a), func2(a), func3(a))); } public S GetOne(N key1, R key2, U key3) @@ -220,8 +211,8 @@ public List<S> GetMultiple(N key1, R key2, U key3) public class BiDictionaryOneToOne<T, S> { - private Dictionary<T, S> direct = new(); - private Dictionary<S, T> inverse = new(); + private readonly Dictionary<T, S> _direct = new(); + private readonly Dictionary<S, T> _inverse = new(); public BiDictionaryOneToOne() { @@ -229,223 +220,175 @@ public BiDictionaryOneToOne() public BiDictionaryOneToOne(Dictionary<T, S> input) { - direct = input; - inverse = direct.ToDictionary(a => a.Value, a => a.Key); + _direct = input; + _inverse = _direct.ToDictionary(a => a.Value, a => a.Key); } public S this[T key] { - get => direct[key]; + get => _direct[key]; set { - if (direct.ContainsKey(key)) - { - var oldvalue = direct[key]; - if (oldvalue.Equals(value)) - { - return; - } - - if (inverse.ContainsKey(oldvalue)) - { - inverse.Remove(oldvalue); - } - } - - if (!inverse.ContainsKey(value)) + if (_direct.TryGetValue(key, out var oldValue)) { - inverse.Add(value, key); + if (oldValue.Equals(value)) return; + if (_inverse.ContainsKey(oldValue)) _inverse.Remove(oldValue); } - direct[key] = value; + _inverse.TryAdd(value, key); + _direct[key] = value; } } public T FindInverse(S k) { - return inverse[k]; + return _inverse[k]; } public bool ContainsKey(T key) { - return direct.ContainsKey(key); + return _direct.ContainsKey(key); } public bool ContainsInverseKey(S key) { - return inverse.ContainsKey(key); + return _inverse.ContainsKey(key); } public void Remove(T value) { - if (direct.ContainsKey(value)) - { - var n = direct[value]; - inverse.Remove(n); - direct.Remove(value); - } + if (!_direct.TryGetValue(value, out var oldValue)) return; + _inverse.Remove(oldValue); + _direct.Remove(value); } public void Clear() { - direct.Clear(); - inverse.Clear(); + _direct.Clear(); + _inverse.Clear(); } } public class BiDictionaryOneToMany<T, S> { - private Dictionary<T, S> direct = new(); - private Dictionary<S, HashSet<T>> inverse = new(); + private readonly Dictionary<T, S> _direct = new(); + private readonly Dictionary<S, HashSet<T>> _inverse = new(); - private readonly bool valueIsNullable; + private readonly bool _valueIsNullable; - private HashSet<T> inverseNullValueSet; + private HashSet<T> _inverseNullValueSet; public BiDictionaryOneToMany() { - valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; + _valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; } public BiDictionaryOneToMany(Dictionary<T, S> input) { - valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; - direct = input; - if (valueIsNullable) + _valueIsNullable = Nullable.GetUnderlyingType(typeof(S)) != null; + _direct = input; + if (_valueIsNullable) { var hashSet = input.Where(a => a.Value == null).Select(a => a.Key).ToHashSet(); // Only set the hash-set if the input contained a null value. See `ContainsInverseKey` at L348 as to why. if (hashSet.Count > 0) { - inverseNullValueSet = hashSet; + _inverseNullValueSet = hashSet; } - inverse = input.Where(a => a.Value != null).GroupBy(a => a.Value) + _inverse = input.Where(a => a.Value != null).GroupBy(a => a.Value) .ToDictionary(a => a.Key, a => a.Select(b => b.Key).ToHashSet()); } else { - inverse = input.GroupBy(a => a.Value).ToDictionary(a => a.Key, a => a.Select(b => b.Key).ToHashSet()); + _inverse = input.GroupBy(a => a.Value).ToDictionary(a => a.Key, a => a.Select(b => b.Key).ToHashSet()); } } public S this[T key] { - get => direct[key]; + get => _direct[key]; set { - if (direct.ContainsKey(key)) + if (_direct.TryGetValue(key, out var oldValue)) { - var oldvalue = direct[key]; - if (oldvalue.Equals(value)) + if (oldValue.Equals(value)) { return; } - if (valueIsNullable && oldvalue == null) - { - if (inverseNullValueSet != null && inverseNullValueSet.Contains(key)) - { - inverseNullValueSet.Remove(key); - } - } - else - { - if (inverse.ContainsKey(oldvalue) && inverse[oldvalue].Contains(key)) - { - inverse[oldvalue].Remove(key); - } - } + if (_inverse.TryGetValue(oldValue, out var inverseValue) && inverseValue.Contains(key)) + inverseValue.Remove(key); } - if (valueIsNullable && value == null) + if (_valueIsNullable && value == null) { - if (inverseNullValueSet == null) - { - inverseNullValueSet = new HashSet<T>(); - } - - if (!inverseNullValueSet.Contains(key)) - { - inverseNullValueSet.Add(key); - } + _inverseNullValueSet ??= new HashSet<T>(); + _inverseNullValueSet.Add(key); } else { - if (!inverse.ContainsKey(value)) - { - inverse[value] = new HashSet<T>(); - } - - if (!inverse[value].Contains(key)) - { - inverse[value].Add(key); - } + if (_inverse.TryGetValue(value, out var set)) + set.Add(key); + else + _inverse.Add(value, new HashSet<T>{key}); } - direct[key] = value; + _direct[key] = value; } } public bool ContainsKey(T key) { - return direct.ContainsKey(key); + return _direct.ContainsKey(key); } public bool ContainsInverseKey(S key) { - return valueIsNullable && key == null ? inverseNullValueSet != null : inverse.ContainsKey(key); + return _valueIsNullable && key == null ? _inverseNullValueSet != null : _inverse.ContainsKey(key); } + public bool TryGetValue(T key, out S value) => _direct.TryGetValue(key, out value); + public bool TryGetInverse(S key, out HashSet<T> value) => _inverse.TryGetValue(key, out value); + public HashSet<T> FindInverse(S key) { - if (valueIsNullable && key == null) + if (_valueIsNullable && key == null) { - return inverseNullValueSet ?? new HashSet<T>(); + return _inverseNullValueSet ?? new HashSet<T>(); } - return inverse.ContainsKey(key) - ? inverse[key] - : new HashSet<T>(); + return _inverse.TryGetValue(key, out var value) ? value : new HashSet<T>(); } public void Remove(T key) { - if (direct.ContainsKey(key)) + if (!_direct.TryGetValue(key, out var oldValue)) return; + if (_valueIsNullable && oldValue == null) { - var oldvalue = direct[key]; - if (valueIsNullable && oldvalue == null) - { - if (inverseNullValueSet != null && inverseNullValueSet.Contains(key)) - { - inverseNullValueSet.Remove(key); - } - } - else - { - if (inverse.ContainsKey(oldvalue) && inverse[oldvalue].Contains(key)) - { - inverse[oldvalue].Remove(key); - } - } - - direct.Remove(key); + if (_inverseNullValueSet != null && _inverseNullValueSet.Contains(key)) _inverseNullValueSet.Remove(key); + } + else + { + if (_inverse.TryGetValue(oldValue, out var inverseValue) && inverseValue.Contains(key)) inverseValue.Remove(key); } + + _direct.Remove(key); } public void Clear() { - direct.Clear(); - inverse.Clear(); - inverseNullValueSet = null; + _direct.Clear(); + _inverse.Clear(); + _inverseNullValueSet = null; } } public class BiDictionaryManyToMany<T, S> { - private Dictionary<T, HashSet<S>> direct = new(); - private Dictionary<S, HashSet<T>> inverse = new(); + private readonly Dictionary<T, HashSet<S>> _direct = new(); + private readonly Dictionary<S, HashSet<T>> _inverse = new(); public BiDictionaryManyToMany() { @@ -453,112 +396,82 @@ public BiDictionaryManyToMany() public BiDictionaryManyToMany(Dictionary<T, HashSet<S>> input) { - direct = input; - inverse = new Dictionary<S, HashSet<T>>(); + _direct = input; + _inverse = new Dictionary<S, HashSet<T>>(); foreach (var t in input.Keys) { foreach (var s in input[t]) { - if (!inverse.ContainsKey(s)) - { - inverse[s] = new HashSet<T>(); - } - - inverse[s].Add(t); + if (_inverse.TryGetValue(s, out var inverseValue)) + inverseValue.Add(t); + else + _inverse.Add(s, new HashSet<T> { t }); } } } public HashSet<S> this[T key] { - get => direct[key]; + get => _direct[key]; set { - if (direct.ContainsKey(key)) + if (_direct.TryGetValue(key, out var oldValue)) { - var oldvalue = direct[key]; - if (oldvalue.SetEquals(value)) - { - return; - } + if (oldValue.SetEquals(value)) return; - foreach (var s in oldvalue.ToList()) + foreach (var s in oldValue) { - if (inverse.ContainsKey(s)) - { - if (inverse[s].Contains(key)) - { - inverse[s].Remove(key); - } - - if (inverse[s].Count == 0) - { - inverse.Remove(s); - } - } + if (!_inverse.TryGetValue(s, out var inverseValue)) continue; + if (inverseValue.Contains(key)) inverseValue.Remove(key); + if (inverseValue.Count == 0) _inverse.Remove(s); } } foreach (var s in value) { - if (!inverse.ContainsKey(s)) - { - inverse[s] = new HashSet<T>(); - } - - inverse[s].Add(key); + if (_inverse.TryGetValue(s, out var inverseValue)) + inverseValue.Add(key); + else + _inverse.Add(s, new HashSet<T> { key }); } - direct[key] = value; + _direct[key] = value; } } public bool ContainsKey(T key) { - return direct.ContainsKey(key); + return _direct.ContainsKey(key); } public bool ContainsInverseKey(S key) { - return inverse.ContainsKey(key); + return _inverse.ContainsKey(key); } public HashSet<T> FindInverse(S k) { - return inverse.ContainsKey(k) - ? inverse[k] - : new HashSet<T>(); + return _inverse.TryGetValue(k, out var value) ? value : new HashSet<T>(); } public void Remove(T key) { - if (direct.ContainsKey(key)) - { - var oldvalue = direct[key]; - foreach (var s in oldvalue.ToList()) - { - if (inverse.ContainsKey(s)) - { - if (inverse[s].Contains(key)) - { - inverse[s].Remove(key); - } - - if (inverse[s].Count == 0) - { - inverse.Remove(s); - } - } - } + if (!_direct.TryGetValue(key, out var oldValue)) return; - direct.Remove(key); + foreach (var s in oldValue) + { + if (!_inverse.TryGetValue(s, out var inverseValue)) continue; + if (inverseValue.Contains(key)) inverseValue.Remove(key); + if (inverseValue.Count == 0) _inverse.Remove(s); } + + _direct.Remove(key); } public void Clear() { - direct.Clear(); - inverse.Clear(); + _direct.Clear(); + _inverse.Clear(); } } @@ -566,6 +479,6 @@ public static class Extensions { public static bool HasItems<T>(this List<T> org) { - return org != null && org.Count > 0; + return org is { Count: > 0 }; } } diff --git a/Shoko.Tests/Shoko.Tests/FilterTests.cs b/Shoko.Tests/Shoko.Tests/FilterTests.cs new file mode 100644 index 000000000..227c93644 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/FilterTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Files; +using Shoko.Server.Filters.Functions; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.Logic.DateTimes; +using Shoko.Server.Filters.Selectors; +using Shoko.Server.Filters.User; +using Xunit; + +namespace Shoko.Tests; + +public class FilterTests +{ + #region TestData + private const string GroupFilterableString = + "{\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"Earth\",\"Japan\",\"Asia\",\"friendship\",\"daily life\",\"high school\",\"school life\",\"comedy\",\"Kyoto\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"manga\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":true,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.4477733\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string GroupUserFilterableString = + "{\"IsFavorite\":false,\"WatchedEpisodes\":12,\"UnwatchedEpisodes\":0,\"HasVotes\":false,\"HasPermanentVotes\":false,\"MissingPermanentVotes\":false,\"WatchedDate\":\"2023-05-05T13:42:13.3933582\",\"LastWatchedDate\":\"2023-05-05T13:43:21.3729042\",\"LowestUserRating\":0.0,\"HighestUserRating\":0.0,\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"Earth\",\"Japan\",\"Asia\",\"friendship\",\"daily life\",\"high school\",\"school life\",\"comedy\",\"Kyoto\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"manga\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":true,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.4477733\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string SeriesFilterableString = + "{\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string SeriesUserFilterableString = + "{\"IsFavorite\":false,\"WatchedEpisodes\":12,\"UnwatchedEpisodes\":0,\"HasVotes\":false,\"HasPermanentVotes\":false,\"MissingPermanentVotes\":false,\"WatchedDate\":\"2023-05-05T13:42:13.3933582\",\"LastWatchedDate\":\"2023-05-05T13:43:21.3729042\",\"LowestUserRating\":0.0,\"HighestUserRating\":0.0,\"MissingEpisodes\":0,\"MissingEpisodesCollecting\":0,\"Tags\":[\"high school\",\"dynamic\",\"themes\",\"source material\",\"setting\",\"elements\",\"place\",\"Earth\",\"Japan\",\"Kyoto\",\"manga\",\"Asia\",\"comedy\",\"friendship\",\"daily life\",\"school life\",\"funny expressions\",\"storytelling\",\"character driven\",\"facial distortion\",\"narration\",\"origin\",\"episodic\",\"Japanese production\"],\"CustomTags\":[],\"Years\":[2022],\"Seasons\":[{\"Item1\":2022,\"Item2\":1}],\"HasTvDBLink\":true,\"HasMissingTvDbLink\":false,\"HasTMDbLink\":false,\"HasMissingTMDbLink\":false,\"HasTraktLink\":false,\"HasMissingTraktLink\":true,\"IsFinished\":true,\"AirDate\":\"2022-04-06T20:00:00\",\"LastAirDate\":\"2022-06-22T20:00:00\",\"AddedDate\":\"2023-05-05T14:42:24.3131538\",\"LastAddedDate\":\"2023-05-05T13:37:50.32298\",\"EpisodeCount\":12,\"TotalEpisodeCount\":12,\"LowestAniDBRating\":7.4,\"HighestAniDBRating\":7.4,\"VideoSources\":[\"Web\"],\"AnimeTypes\":[\"TVSeries\"],\"AudioLanguages\":[\"japanese\"],\"SubtitleLanguages\":[\"english\"]}"; + private const string AllSeries = + ""; + #endregion + + public static readonly IEnumerable<object[]> GroupFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestFilterable>(GroupFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> GroupUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestUserDependentFilterable>(GroupUserFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> SeriesFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestFilterable>(SeriesFilterableString, new IReadOnlySetConverter()) }}; + public static readonly IEnumerable<object[]> SeriesUserFilterable = new[] { new[] { JsonConvert.DeserializeObject<TestUserDependentFilterable>(SeriesUserFilterableString, new IReadOnlySetConverter()) }}; + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithUserFilter_ExpectsException(TestFilterable group) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), + new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + + Assert.True(top.UserDependent); + Assert.Throws<ArgumentException>(() => top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithoutUserFilter_ExpectsTrue(TestFilterable group) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasVideoSourceExpression("Web")); + + Assert.False(top.UserDependent); + Assert.True(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithDateFunctionFilter_ExpectsFalse(TestFilterable group) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), + new DateDiffFunction(new DateAddFunction(new TodayFunction(), TimeSpan.FromDays(1) - TimeSpan.FromMilliseconds(1)), TimeSpan.FromDays(30)))); + + Assert.False(top.UserDependent); + Assert.False(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupFilterable))] + public void GroupFilterable_WithDateFunctionFilter_ExpectsTrue(TestFilterable group) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new DateGreaterThanEqualsExpression(new LastAddedDateSelector(), DateTime.Parse("2023-4-15"))); + + Assert.False(top.UserDependent); + Assert.True(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(GroupUserFilterable))] + public void GroupUserFilterable_WithUserFilter_ExpectsTrue(TestUserDependentFilterable group) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + + Assert.True(top.UserDependent); + Assert.True(top.Evaluate(group)); + } + + [Theory, MemberData(nameof(SeriesFilterable))] + public void SeriesFilterable_WithUserFilter_ExpectsException(TestFilterable series) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + + Assert.True(top.UserDependent); + Assert.Throws<ArgumentException>(() => top.Evaluate(series)); + } + + [Theory, MemberData(nameof(SeriesUserFilterable))] + public void SeriesUserFilterable_WithUserFilter_ExpectsTrue(TestUserDependentFilterable series) + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + + Assert.True(top.UserDependent); + Assert.True(top.Evaluate(series)); + } +} diff --git a/Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs b/Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs new file mode 100644 index 000000000..28f26ccd8 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/IReadOnlySetConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Shoko.Tests; + +public class IReadOnlySetConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + if (!objectType.IsGenericType) return false; + var arguments = objectType.GetGenericArguments(); + if (arguments.Length > 1) return false; + return typeof(IReadOnlySet<>) == objectType.GetGenericTypeDefinition(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var argument = objectType.GetGenericArguments()[0]; + var newType = typeof(HashSet<>).MakeGenericType(argument); + return serializer.Deserialize(reader, newType); + } +} diff --git a/Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs b/Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs new file mode 100644 index 000000000..4a31130e1 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/LegacyFilterConditionTests.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Legacy; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Models; +using Xunit; + +namespace Shoko.Tests; + +public class LegacyFilterConditionTests +{ + [Fact] + public void ToConditions_Tags_NotIn_Single() + { + var top = new NotExpression(new HasTagExpression("comedy")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_Tags_In_Multiple() + { + var top = new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), new HasTagExpression("action")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_Tags_NotIn_Multiple() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), new HasTagExpression("action"))); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_In_Single() + { + var top = new HasAnimeTypeExpression("TVSeries"); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_NotIn_Single() + { + var top = new NotExpression(new HasAnimeTypeExpression("TVSeries")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_In_Multiple() + { + var top = new OrExpression(new OrExpression(new HasAnimeTypeExpression("TVSeries"), new HasAnimeTypeExpression("Movie")), new HasAnimeTypeExpression("OVA")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries,Movie,OVA" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToConditions_AnimeType_NotIn_Multiple() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasAnimeTypeExpression("TVSeries"), new HasAnimeTypeExpression("Movie")), new HasAnimeTypeExpression("OVA"))); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.AnimeType, + ConditionParameter = "TVSeries,Movie,OVA" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void ToExpression_Tags_In_Single() + { + var conditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + } + }; + var expression = LegacyConditionConverter.GetExpression(conditions, GroupFilterBaseCondition.Include); + var expected = new HasTagExpression("comedy"); + Assert.Equivalent(expected, expression); + } + + [Fact] + public void ToExpression_Tags_NotIn_Single() + { + var conditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + } + }; + var expression = LegacyConditionConverter.GetExpression(conditions, GroupFilterBaseCondition.Include); + var expected = new NotExpression(new HasTagExpression("comedy")); + Assert.Equivalent(expected, expression); + } +} diff --git a/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs b/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs new file mode 100644 index 000000000..1a2af710a --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/LegacyFilterTests.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Models.Server; +using Shoko.Server.Filters; +using Shoko.Server.Filters.Info; +using Shoko.Server.Filters.Legacy; +using Shoko.Server.Filters.Logic; +using Shoko.Server.Filters.User; +using Shoko.Server.Models; +using Xunit; + +namespace Shoko.Tests; + +public class LegacyFilterTests +{ + [Fact] + public void TryConvertToConditions_InvalidFilter_ExpectsNull() + { + var top = new OrExpression(new AndExpression(new HasTagExpression("comedy"), + new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + Assert.False(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Null(conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithTagIncludeAndExclude() + { + var top = new AndExpression(new AndExpression(new AndExpression(new HasTagExpression("comedy"), + new NotExpression(new HasTagExpression("18 restricted"))), + new HasWatchedEpisodesExpression()), new NotExpression(new HasUnwatchedEpisodesExpression())); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "18 restricted" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionType = (int)GroupFilterConditionType.HasWatchedEpisodes, + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Exclude, + ConditionType = (int)GroupFilterConditionType.HasUnwatchedEpisodes, + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithTagInOperator_IncludeBaseCondition() + { + var top = new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), new HasTagExpression("action")); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithTagInOperator_ExcludeBaseCondition() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasTagExpression("comedy"), new HasTagExpression("shounen")), + new HasTagExpression("action"))); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.NotIn, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy,shounen,action" + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithMultipleConditions_IncludeBaseCondition() + { + var top = new AndExpression(new AndExpression(new HasTagExpression("comedy"), new InSeasonExpression(2023, AnimeSeason.Winter)), + new IsFinishedExpression()); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Season, + ConditionParameter = "Winter 2023" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionType = (int)GroupFilterConditionType.FinishedAiring + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Include, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } + + [Fact] + public void TryConvertToConditions_FilterWithMultipleConditions_ExcludeBaseCondition() + { + var top = new NotExpression(new OrExpression(new OrExpression(new HasTagExpression("comedy"), new InSeasonExpression(2023, AnimeSeason.Winter)), + new IsFinishedExpression())); + var filter = new FilterPreset { Name = "Test", Expression = top }; + + var success = LegacyConditionConverter.TryConvertToConditions(filter, out var conditions, out var baseCondition); + var expectedConditions = new List<GroupFilterCondition> + { + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Tag, + ConditionParameter = "comedy" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.In, + ConditionType = (int)GroupFilterConditionType.Season, + ConditionParameter = "Winter 2023" + }, + new() + { + ConditionOperator = (int)GroupFilterOperator.Include, + ConditionType = (int)GroupFilterConditionType.FinishedAiring + } + }; + Assert.True(success); + Assert.Equal(GroupFilterBaseCondition.Exclude, baseCondition); + Assert.Equivalent(expectedConditions, conditions); + } +} diff --git a/Shoko.Tests/Shoko.Tests/TestFilterable.cs b/Shoko.Tests/Shoko.Tests/TestFilterable.cs new file mode 100644 index 000000000..5bb856f8c --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/TestFilterable.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Shoko.Models.Enums; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Tests; + +public class TestFilterable : IFilterable +{ + public string Name { get; init; } + public string SortingName { get; init; } + public int SeriesCount { get; init; } + public int MissingEpisodes { get; init; } + public int MissingEpisodesCollecting { get; init; } + public IReadOnlySet<string> Tags { get; init; } + public IReadOnlySet<string> CustomTags { get; init; } + public IReadOnlySet<int> Years { get; init; } + public IReadOnlySet<(int year, AnimeSeason season)> Seasons { get; init; } + public bool HasTvDBLink { get; init; } + public bool HasMissingTvDbLink { get; init; } + public bool HasTMDbLink { get; init; } + public bool HasMissingTMDbLink { get; init; } + public bool HasTraktLink { get; init; } + public bool HasMissingTraktLink { get; init; } + public bool IsFinished { get; init; } + public DateTime? AirDate { get; init; } + public DateTime? LastAirDate { get; init; } + public DateTime AddedDate { get; init; } + public DateTime LastAddedDate { get; init; } + public int EpisodeCount { get; init; } + public int TotalEpisodeCount { get; init; } + public decimal LowestAniDBRating { get; init; } + public decimal HighestAniDBRating { get; init; } + public IReadOnlySet<string> VideoSources { get; init; } + public IReadOnlySet<string> SharedVideoSources { get; init; } + public IReadOnlySet<string> AnimeTypes { get; init; } + public IReadOnlySet<string> AudioLanguages { get; init; } + public IReadOnlySet<string> SharedAudioLanguages { get; init; } + public IReadOnlySet<string> SubtitleLanguages { get; init; } + public IReadOnlySet<string> SharedSubtitleLanguages { get; init; } +} diff --git a/Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs b/Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs new file mode 100644 index 000000000..9a2c11df7 --- /dev/null +++ b/Shoko.Tests/Shoko.Tests/TestUserDependentFilterable.cs @@ -0,0 +1,18 @@ +using System; +using Shoko.Server.Filters.Interfaces; + +namespace Shoko.Tests; + +public class TestUserDependentFilterable : TestFilterable, IUserDependentFilterable +{ + public bool IsFavorite { get; init; } + public int WatchedEpisodes { get; init; } + public int UnwatchedEpisodes { get; init; } + public bool HasVotes { get; init; } + public bool HasPermanentVotes { get; init; } + public bool MissingPermanentVotes { get; init; } + public DateTime? WatchedDate { get; init; } + public DateTime? LastWatchedDate { get; init; } + public decimal LowestUserRating { get; init; } + public decimal HighestUserRating { get; init; } +}