From 1709eb316e33b8412285e04532828d66a082e925 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Mon, 30 Dec 2024 01:02:32 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20reimplement=20anime=E2=86=94charact?= =?UTF-8?q?er/creator=20mappings=20for=20AniDB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reimplemented the AniDB Character and Creator mappings for AniDB anime. Making sure we store all the mappings, and in the correct order they appear in the XMLs. This is done by dropping the existing tables, followed by creating new ones and restoring the data from the XMLs if possible, or queueing the anime for a forced online lookup if it's not in the local XML cache or is otherwise unusable. Dropped the `AnimeStaff` and `CrossRef_Anime_Staff` tables used by the API, which were almost a 1:1 mapping for `AniDB_Anime_Staff` combined with `AniDB_Anime_Character`, except sometimes out-of-sync data. All usage of the tables has been directed at the updated `AniDB_Character`, `AniDB_Anime_Staff`, `AniDB_Anime_Character`, or `AniDB_Anime_Character_Creator` tables where appropriate. Fixes #1183 Closes #1183 --- .vscode/settings.json | 1 + Shoko.Commons | 2 +- Shoko.Server/API/AuthenticationController.cs | 2 +- .../ShokoServiceImplementation.cs | 4 +- .../ShokoServiceImplementation_Entities.cs | 13 +- .../ShokoServiceImplementation_Providers.cs | 4 +- .../ShokoServiceImplementation_Utilities.cs | 14 +- .../ShokoServiceImplementationMetro.cs | 53 +- Shoko.Server/API/v2/Models/common/Group.cs | 19 +- Shoko.Server/API/v2/Models/common/RawFile.cs | 2 +- Shoko.Server/API/v2/Models/common/Serie.cs | 25 +- Shoko.Server/API/v2/Modules/Common.cs | 63 +- .../API/v3/Controllers/ActionController.cs | 4 +- .../API/v3/Controllers/DashboardController.cs | 4 +- .../API/v3/Controllers/FileController.cs | 20 +- .../ReleaseManagementController.cs | 4 +- ...aseManagementMultipleReleasesController.cs | 4 +- .../API/v3/Controllers/SeriesController.cs | 13 +- .../API/v3/Helpers/APIv3_Extensions.cs | 14 +- Shoko.Server/API/v3/Helpers/FilterFactory.cs | 2 +- Shoko.Server/API/v3/Helpers/ModelHelper.cs | 2 +- Shoko.Server/API/v3/Helpers/WebUIFactory.cs | 14 +- Shoko.Server/API/v3/Models/Common/Role.cs | 89 +-- Shoko.Server/API/v3/Models/Shoko/File.cs | 12 +- .../API/v3/Models/Shoko/FileCrossReference.cs | 8 +- .../API/v3/Models/Shoko/ImportFolder.cs | 7 +- Shoko.Server/API/v3/Models/Shoko/Series.cs | 36 +- Shoko.Server/API/v3/Models/Shoko/User.cs | 4 +- Shoko.Server/API/v3/Models/Shoko/WebUI.cs | 2 +- Shoko.Server/Databases/DatabaseFixes.cs | 178 +++--- Shoko.Server/Databases/MySQL.cs | 23 +- Shoko.Server/Databases/SQLServer.cs | 23 +- Shoko.Server/Databases/SQLite.cs | 22 +- Shoko.Server/Extensions/ImageResolvers.cs | 52 +- Shoko.Server/Extensions/ModelClients.cs | 12 +- Shoko.Server/Extensions/ModelDatabase.cs | 11 +- Shoko.Server/Extensions/ModelProviders.cs | 21 +- Shoko.Server/Extensions/StringExtensions.cs | 12 +- Shoko.Server/Filters/FilterEvaluator.cs | 2 +- .../Mappings/AniDB_Anime_CharacterMap.cs | 9 +- .../AniDB_Anime_Character_CreatorMap.cs | 20 + Shoko.Server/Mappings/AniDB_Anime_StaffMap.cs | 7 +- Shoko.Server/Mappings/AniDB_CharacterMap.cs | 14 +- .../Mappings/AniDB_Character_CreatorMap.cs | 17 - Shoko.Server/Mappings/AnimeCharacterMap.cs | 19 - Shoko.Server/Mappings/AnimeStaffMap.cs | 19 - .../Mappings/CrossRef_Anime_StaffMap.cs | 20 - .../Models/AniDB/AniDB_Anime_Character.cs | 44 ++ .../AniDB/AniDB_Anime_Character_Creator.cs | 34 ++ .../Models/AniDB/AniDB_Anime_Staff.cs | 30 + Shoko.Server/Models/AniDB/AniDB_Character.cs | 26 + .../Models/AniDB/AniDB_Character_Creator.cs | 16 - Shoko.Server/Models/AniDB/AniDB_Creator.cs | 8 + Shoko.Server/Models/SVR_AniDB_Anime.cs | 16 +- Shoko.Server/Models/SVR_AniDB_File.cs | 6 +- Shoko.Server/Models/SVR_AnimeEpisode.cs | 8 +- Shoko.Server/Models/SVR_AnimeSeries.cs | 7 +- .../Models/SVR_CrossRef_File_Episode.cs | 2 +- Shoko.Server/Models/SVR_ImportFolder.cs | 5 + Shoko.Server/Models/SVR_VideoLocal.cs | 38 +- Shoko.Server/Models/SVR_VideoLocal_Place.cs | 2 +- Shoko.Server/Models/SVR_VideoLocal_User.cs | 14 +- .../Providers/AniDB/HTTP/AnimeCreator.cs | 546 +++++++++--------- .../AniDB/HTTP/GetAnime/ResponseCharacter.cs | 1 + .../Providers/AniDB/HTTP/HttpAnimeParser.cs | 2 + .../Contracts/TraktSummaryContainer.cs | 3 +- .../Providers/TraktTV/TraktTVHelper.cs | 2 +- Shoko.Server/Renamer/RenameFileService.cs | 2 +- .../Repositories/BaseCachedRepository.cs | 36 +- .../Cached/AniDB_AnimeRepository.cs | 178 +----- .../Cached/AniDB_Anime_CharacterRepository.cs | 42 +- ...AniDB_Anime_Character_CreatorRepository.cs | 39 ++ .../AniDB_Anime_PreferredImageRepository.cs | 27 +- .../Cached/AniDB_Anime_TagRepository.cs | 77 +-- .../Cached/AniDB_Anime_TitleRepository.cs | 59 +- .../Cached/AniDB_CharacterRepository.cs | 35 +- .../AniDB_Character_CreatorRepository.cs | 47 -- .../Cached/AniDB_CreatorRepository.cs | 34 +- .../Cached/AniDB_EpisodeRepository.cs | 62 +- .../AniDB_Episode_PreferredImageRepository.cs | 23 +- .../Cached/AniDB_Episode_TitleRepository.cs | 33 +- .../Cached/AniDB_FileRepository.cs | 72 +-- .../Cached/AniDB_ReleaseGroupRepository.cs | 64 +- .../Cached/AniDB_TagRepository.cs | 105 +--- .../Cached/AniDB_VoteRepository.cs | 93 +-- .../Cached/AnimeCharacterRepository.cs | 34 -- .../Cached/AnimeEpisodeRepository.cs | 23 +- .../Cached/AnimeEpisode_UserRepository.cs | 108 ++-- .../Cached/AnimeGroupRepository.cs | 111 ++-- .../Cached/AnimeGroup_UserRepository.cs | 134 ++--- .../Cached/AnimeSeries_UserRepository.cs | 71 +-- .../Cached/AnimeStaffRepository.cs | 34 -- .../Cached/AuthTokensRepository.cs | 76 +-- .../Cached/CrossRef_AniDB_MALRepository.cs | 33 +- .../CrossRef_AniDB_TMDB_EpisodeRepository.cs | 40 +- .../CrossRef_AniDB_TMDB_MovieRepository.cs | 36 +- .../CrossRef_AniDB_TMDB_ShowRepository.cs | 36 +- .../CrossRef_AniDB_TraktV2Repository.cs | 80 +-- .../Cached/CrossRef_Anime_StaffRepository.cs | 95 --- .../Cached/CrossRef_CustomTagRepository.cs | 45 +- .../Cached/CrossRef_File_EpisodeRepository.cs | 68 +-- ...CrossRef_Languages_AniDB_FileRepository.cs | 56 +- ...CrossRef_Subtitles_AniDB_FileRepository.cs | 59 +- .../Cached/CustomTagRepository.cs | 32 +- .../Cached/FilterPresetRepository.cs | 128 ++-- .../Cached/ImportFolderRepository.cs | 71 +-- .../Repositories/Cached/JMMUserRepository.cs | 51 +- .../Cached/TMDB_ImageRepository.cs | 59 +- .../Cached/VideoLocalRepository.cs | 489 ++++++++-------- .../Cached/VideoLocal_PlaceRepository.cs | 74 ++- .../Cached/VideoLocal_UserRepository.cs | 48 +- .../Direct/AniDB_Anime_StaffRepository.cs | 6 +- Shoko.Server/Repositories/RepoFactory.cs | 15 +- .../Repositories/RepositoryStartup.cs | 5 +- .../Jobs/AniDB/AddFileToMyListJob.cs | 4 +- .../Jobs/AniDB/GetAniDBCreatorJob.cs | 22 +- .../Scheduling/Jobs/AniDB/GetAniDBFileJob.cs | 5 +- .../Jobs/AniDB/GetAniDBImagesJob.cs | 14 +- .../Jobs/AniDB/SyncAniDBMyListJob.cs | 4 +- .../Jobs/AniDB/UpdateMyListFileStatusJob.cs | 4 +- .../Scheduling/Jobs/Shoko/DiscoverFileJob.cs | 8 +- .../Scheduling/Jobs/Shoko/HashFileJob.cs | 8 +- .../Scheduling/Jobs/Shoko/ManualLinkJob.cs | 2 +- .../Scheduling/Jobs/Shoko/ProcessFileJob.cs | 18 +- .../Jobs/Shoko/ProcessFileMovedMessageJob.cs | 2 +- .../Jobs/Shoko/ValidateAllImagesJob.cs | 2 +- .../Jobs/Trakt/SearchTraktSeriesJob.cs | 2 +- Shoko.Server/Server/Enums.cs | 58 ++ Shoko.Server/Server/ShokoEventHandler.cs | 12 +- Shoko.Server/Services/ActionService.cs | 46 +- Shoko.Server/Services/AnimeEpisodeService.cs | 3 +- Shoko.Server/Services/AnimeGroupService.cs | 7 +- Shoko.Server/Services/AnimeSeriesService.cs | 82 ++- .../Services/GeneratedPlaylistService.cs | 6 +- Shoko.Server/Services/VideoLocalService.cs | 2 +- .../Services/VideoLocal_PlaceService.cs | 4 +- Shoko.Server/Services/WatchedStatusService.cs | 2 +- Shoko.Server/Utilities/ImageUtils.cs | 31 +- 138 files changed, 2133 insertions(+), 3092 deletions(-) create mode 100644 Shoko.Server/Mappings/AniDB_Anime_Character_CreatorMap.cs delete mode 100644 Shoko.Server/Mappings/AniDB_Character_CreatorMap.cs delete mode 100644 Shoko.Server/Mappings/AnimeCharacterMap.cs delete mode 100644 Shoko.Server/Mappings/AnimeStaffMap.cs delete mode 100644 Shoko.Server/Mappings/CrossRef_Anime_StaffMap.cs create mode 100644 Shoko.Server/Models/AniDB/AniDB_Anime_Character.cs create mode 100644 Shoko.Server/Models/AniDB/AniDB_Anime_Character_Creator.cs create mode 100644 Shoko.Server/Models/AniDB/AniDB_Anime_Staff.cs create mode 100644 Shoko.Server/Models/AniDB/AniDB_Character.cs delete mode 100644 Shoko.Server/Models/AniDB/AniDB_Character_Creator.cs create mode 100644 Shoko.Server/Repositories/Cached/AniDB_Anime_Character_CreatorRepository.cs delete mode 100644 Shoko.Server/Repositories/Cached/AniDB_Character_CreatorRepository.cs delete mode 100644 Shoko.Server/Repositories/Cached/AnimeCharacterRepository.cs delete mode 100644 Shoko.Server/Repositories/Cached/AnimeStaffRepository.cs delete mode 100644 Shoko.Server/Repositories/Cached/CrossRef_Anime_StaffRepository.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index e866024ef..2c08357f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,7 @@ "muxed", "muxing", "mylist", + "NutzCode", "outro", "ova", "ovas", diff --git a/Shoko.Commons b/Shoko.Commons index 258c2e38a..58ac73988 160000 --- a/Shoko.Commons +++ b/Shoko.Commons @@ -1 +1 @@ -Subproject commit 258c2e38a060b78df28947bc5f028419cfe521a8 +Subproject commit 58ac739881c6e3b190f9bcd1ab07394576e9063a diff --git a/Shoko.Server/API/AuthenticationController.cs b/Shoko.Server/API/AuthenticationController.cs index 38cf76b22..f11edc83e 100644 --- a/Shoko.Server/API/AuthenticationController.cs +++ b/Shoko.Server/API/AuthenticationController.cs @@ -48,7 +48,7 @@ public ActionResult> GetApikeys() public ActionResult GenerateApikey([FromBody]string device) { if (string.IsNullOrWhiteSpace(device)) return BadRequest("device cannot be empty"); - return RepoFactory.AuthTokens.CreateNewApikey(User, device); + return RepoFactory.AuthTokens.CreateNewApiKey(User, device); } /// diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs index 84e90504a..a98c01e3d 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs @@ -1278,7 +1278,7 @@ public List GetCharactersForSeiyuu(int seiyuuID) return chars; } - var links = RepoFactory.AniDB_Character_Creator.GetByCreatorID(seiyuu.CreatorID); + var links = RepoFactory.AniDB_Anime_Character_Creator.GetByCreatorID(seiyuu.CreatorID); foreach (var chrSei in links) { @@ -1286,7 +1286,7 @@ public List GetCharactersForSeiyuu(int seiyuuID) if (chr != null) { var aniChars = - RepoFactory.AniDB_Anime_Character.GetByCharID(chr.CharID); + RepoFactory.AniDB_Anime_Character.GetByCharacterID(chr.CharacterID); if (aniChars.Count > 0) { var anime = RepoFactory.AniDB_Anime.GetByAnimeID(aniChars[0].AnimeID); diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs index d74d056db..d343218fb 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs @@ -500,8 +500,7 @@ public string RemoveAssociationOnFile(int videoLocalID, int animeEpisodeID) } animeSeriesID = ep.AnimeSeriesID; - var xref = - RepoFactory.CrossRef_File_Episode.GetByHashAndEpisodeID(vid.Hash, ep.AniDB_EpisodeID); + var xref = vid.EpisodeCrossReferences.FirstOrDefault(x => x.EpisodeID == ep.AniDB_EpisodeID); if (xref != null) { if (xref.CrossRefSource == (int)CrossRefSource.AniDB) @@ -586,7 +585,7 @@ public string SetVariationStatusOnFile(int videoLocalID, bool isVariation) private static void RemoveXRefsForFile(int videoLocalID) { var vlocal = RepoFactory.VideoLocal.GetByID(videoLocalID); - var fileEps = RepoFactory.CrossRef_File_Episode.GetByHash(vlocal.Hash); + var fileEps = RepoFactory.CrossRef_File_Episode.GetByEd2k(vlocal.Hash); foreach (var fileEp in fileEps) { @@ -1193,7 +1192,7 @@ public List GetFilesForEpisode(int episodeID, int userID) var ep = RepoFactory.AnimeEpisode.GetByID(episodeID); if (ep != null) { - var files = ep.VideoLocals; + var files = ep.VideoLocals.ToList(); files.Sort(FileQualityFilter.CompareTo); return files.Select(a => _videoLocalService.GetV1DetailedContract(a, userID)).ToList(); } @@ -3348,10 +3347,8 @@ public string DeleteCustomTagCrossRef(int customTagID, int crossRefType, int cro { try { - var xrefs = - RepoFactory.CrossRef_CustomTag.GetByUniqueID(customTagID, crossRefType, crossRefID); - - if (xrefs is null || xrefs.Count == 0) + var xrefs = RepoFactory.CrossRef_CustomTag.GetByUniqueID(customTagID, (CustomTagCrossRefType)crossRefType, crossRefID); + if (xrefs.Count == 0) { return "Custom Tag not found"; } diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Providers.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Providers.cs index ee62de4cc..cdada73f4 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Providers.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Providers.cs @@ -57,7 +57,7 @@ public CL_AniDB_AnimeCrossRefs GetCrossRefDetails(int animeID) } // Trakt - foreach (var xref in anime.GetCrossRefTraktV2()) + foreach (var xref in anime.TraktShowCrossReferences) { result.CrossRef_AniDB_Trakt.Add(xref); @@ -81,7 +81,7 @@ public CL_AniDB_AnimeCrossRefs GetCrossRefDetails(int animeID) } // MAL - var xrefMAL = anime.GetCrossRefMAL(); + var xrefMAL = anime.MalCrossReferences; if (xrefMAL == null) { result.CrossRef_AniDB_MAL = null; diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Utilities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Utilities.cs index 53e245c9e..aa1300197 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Utilities.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Utilities.cs @@ -146,7 +146,7 @@ public bool DeleteMultipleFilesWithPreferences(int userID) foreach (var ep in eps) { - var videoLocals = ep.VideoLocals; + var videoLocals = ep.VideoLocals.ToList(); videoLocals.Sort(FileQualityFilter.CompareTo); var keep = videoLocals .Take(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) @@ -196,7 +196,7 @@ public List PreviewDeleteMultipleFilesWithPreferences(int userID) foreach (var ep in eps) { - var videoLocals = ep.VideoLocals; + var videoLocals = ep.VideoLocals.ToList(); videoLocals.Sort(FileQualityFilter.CompareTo); var keep = videoLocals .Take(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) @@ -222,7 +222,7 @@ public List GetMultipleFilesForDeletionByPreferences(int userI foreach (var ep in eps) { - var videoLocals = ep.VideoLocals; + var videoLocals = ep.VideoLocals.ToList(); videoLocals.Sort(FileQualityFilter.CompareTo); var keep = videoLocals .Take(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) @@ -275,7 +275,7 @@ public List SearchForFiles(int searchType, string searchCriteria, break; case FileSearchCriteria.ED2KHash: - var vidl = RepoFactory.VideoLocal.GetByHash(searchCriteria.Trim()); + var vidl = RepoFactory.VideoLocal.GetByEd2k(searchCriteria.Trim()); if (vidl != null) vids.Add(_videoLocalService.GetV1Contract(vidl, userID)); break; @@ -598,7 +598,7 @@ public List GetMyListFilesForRemoval(int userID) else { // now check if the file actually exists on disk - var v = RepoFactory.VideoLocal.GetByHash(hash); + var v = RepoFactory.VideoLocal.GetByEd2k(hash); fileMissing = true; if (v == null) break; foreach (var p in v.Places) @@ -850,7 +850,7 @@ public List GetAllDuplicateFiles() var vl = first.VideoLocal; SVR_AniDB_Anime anime = null; SVR_AniDB_Episode episode = null; - var xref = RepoFactory.CrossRef_File_Episode.GetByHash(vl.Hash); + var xref = RepoFactory.CrossRef_File_Episode.GetByEd2k(vl.Hash); if (xref.Count > 0) { if (xref.FirstOrDefault(x => x.AnimeID is not 0)?.AnimeID is { } animeId) @@ -1138,7 +1138,7 @@ public List GetGroupVideoQualitySummary(int animeID) { var vidQuals = new List(); - var files = RepoFactory.VideoLocal.GetByAniDBAnimeID(animeID); + var files = RepoFactory.VideoLocal.GetByAniDBAnimeID(animeID).ToList(); files.Sort(FileQualityFilter.CompareTo); var lookup = files.ToLookup(a => { diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs index a29ee889f..07e70bf94 100644 --- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs +++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementationMetro.cs @@ -136,7 +136,7 @@ public Metro_CommunityLinks GetCommunityLinks(int animeID) contract.AniDB_DiscussURL = string.Format(Constants.URLS.AniDB_SeriesDiscussion, animeID); // MAL - var malRef = anime.GetCrossRefMAL(); + var malRef = anime.MalCrossReferences; if (malRef is not null && malRef.Count > 0) { contract.MAL_ID = malRef[0].MALID.ToString(); @@ -145,7 +145,7 @@ public Metro_CommunityLinks GetCommunityLinks(int animeID) } // Trakt - var traktRef = anime.GetCrossRefTraktV2(); + var traktRef = anime.TraktShowCrossReferences; if (traktRef is not null && traktRef.Count > 0) { contract.Trakt_ID = traktRef[0].TraktID; @@ -644,46 +644,29 @@ public List SearchAnime(int userID, string queryText, int m return retAnime; } - - var allAnime = RepoFactory.AniDB_Anime.SearchByName(queryText); - foreach (var anidb_anime in allAnime) + var allAnime = SeriesSearch.SearchSeries(user, queryText, maxRecords, SeriesSearch.SearchFlags.Titles); + foreach (var result in allAnime) { - if (!user.AllowedAnime(anidb_anime)) - { + var ser = result.Result; + var anidb_anime = ser.AniDB_Anime!; + if (anidb_anime is null || !user.AllowedSeries(ser)) continue; - } - - var ser = RepoFactory.AnimeSeries.GetByAnimeID(anidb_anime.AnimeID); + var imgDet = anidb_anime.PreferredOrDefaultPoster; var summary = new Metro_Anime_Summary { AirDateAsSeconds = anidb_anime.GetAirDateAsSeconds(), - AnimeID = anidb_anime.AnimeID + AnimeID = anidb_anime.AnimeID, + AnimeName = ser.PreferredTitle, + AnimeSeriesID = ser.AnimeSeriesID, + BeginYear = anidb_anime.BeginYear, + EndYear = anidb_anime.EndYear, + PosterName = imgDet.LocalPath, + ImageType = (int)imgDet.ImageType.ToClient(imgDet.Source), + ImageID = imgDet.ID, }; - if (ser is not null) - { - summary.AnimeName = ser.PreferredTitle; - summary.AnimeSeriesID = ser.AnimeSeriesID; - } - else - { - summary.AnimeName = anidb_anime.MainTitle; - summary.AnimeSeriesID = 0; - } - - summary.BeginYear = anidb_anime.BeginYear; - summary.EndYear = anidb_anime.EndYear; - - var imgDet = anidb_anime.PreferredOrDefaultPoster; - summary.PosterName = imgDet.LocalPath; - summary.ImageType = (int)imgDet.ImageType.ToClient(imgDet.Source); - summary.ImageID = imgDet.ID; retAnime.Add(summary); - if (retAnime.Count == maxRecords) - { - break; - } } } catch (Exception ex) @@ -924,7 +907,7 @@ public List GetCharactersForAnime(int animeID, int maxRec try { var animeChars = RepoFactory.AniDB_Anime_Character.GetByAnimeID(animeID) - .OrderByDescending(item => item.CharType.Equals("main character in", StringComparison.InvariantCultureIgnoreCase)) + .OrderByDescending(item => item.AppearanceType is Server.CharacterAppearanceType.Main_Character) .ToList(); if (animeChars.Count == 0) { @@ -937,7 +920,7 @@ public List GetCharactersForAnime(int animeID, int maxRec foreach (var animeChar in animeChars) { index++; - var character = RepoFactory.AniDB_Character.GetByID(animeChar.CharID); + var character = RepoFactory.AniDB_Character.GetByID(animeChar.CharacterID); if (character is not null) { var contract = new Metro_AniDB_Character(); diff --git a/Shoko.Server/API/v2/Models/common/Group.cs b/Shoko.Server/API/v2/Models/common/Group.cs index 9dd38c356..5c4f89918 100644 --- a/Shoko.Server/API/v2/Models/common/Group.cs +++ b/Shoko.Server/API/v2/Models/common/Group.cs @@ -104,25 +104,26 @@ public static Group GenerateFromAnimeGroup(HttpContext ctx, SVR_AnimeGroup ag, i if (!noCast) { - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(anime.AnimeID, StaffRoleType.Seiyuu); + var xrefAnimeStaff = RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(anime.AnimeID); foreach (var xref in xrefAnimeStaff) { - if (xref.RoleID == null) continue; - - var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); + var character = RepoFactory.AniDB_Character.GetByID(xref.CharacterID); if (character == null) continue; - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); + var staff = RepoFactory.AniDB_Creator.GetByID(xref.CreatorID); if (staff == null) continue; + var xref2 = xref.CharacterCrossReference; + if (xref2 == null) continue; + var role = new Role { character = character.Name, - character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Character, DataSourceEnum.Shoko, xref.RoleID.Value), + character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Character, DataSourceEnum.Shoko, xref.CharacterID), staff = staff.Name, - staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Person, DataSourceEnum.Shoko, xref.StaffID), - role = xref.Role, - type = ((StaffRoleType)xref.RoleType).ToString() + staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Person, DataSourceEnum.Shoko, xref.CreatorID), + role = xref2.AppearanceType.ToString().Replace("_", " "), + type = "Seiyuu", }; g.roles ??= []; diff --git a/Shoko.Server/API/v2/Models/common/RawFile.cs b/Shoko.Server/API/v2/Models/common/RawFile.cs index fcc9cf995..5c34bea0b 100644 --- a/Shoko.Server/API/v2/Models/common/RawFile.cs +++ b/Shoko.Server/API/v2/Models/common/RawFile.cs @@ -159,7 +159,7 @@ public RawFile(HttpContext ctx, SVR_VideoLocal vl, int level, int uid, AnimeEpis url = APIV2Helper.ConstructVideoLocalStream(ctx, uid, vl.VideoLocalID.ToString(), "file" + Path.GetExtension(filename), false); - recognized = e != null || vl.EpisodeCrossRefs.Count != 0; + recognized = e != null || vl.EpisodeCrossReferences.Count != 0; if (vl.MediaInfo?.GeneralStream == null || level < 0) { diff --git a/Shoko.Server/API/v2/Models/common/Serie.cs b/Shoko.Server/API/v2/Models/common/Serie.cs index 7e200ff00..5e7d478e1 100644 --- a/Shoko.Server/API/v2/Models/common/Serie.cs +++ b/Shoko.Server/API/v2/Models/common/Serie.cs @@ -131,29 +131,26 @@ public static Serie GenerateFromAniDBAnime(HttpContext ctx, SVR_AniDB_Anime anim if (!noCast) { - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(anime.AnimeID, - StaffRoleType.Seiyuu); + var xrefAnimeStaff = RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(anime.AnimeID); foreach (var xref in xrefAnimeStaff) { - if (!xref.RoleID.HasValue) - continue; + var character = RepoFactory.AniDB_Character.GetByID(xref.CharacterID); + if (character == null) continue; - var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); - if (character is null) - continue; + var staff = RepoFactory.AniDB_Creator.GetByID(xref.CreatorID); + if (staff == null) continue; - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); - if (staff is null) - continue; + var xref2 = xref.CharacterCrossReference; + if (xref2 == null) continue; var role = new Role { character = character.Name, - character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Character, DataSourceEnum.Shoko, xref.RoleID.Value), + character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Character, DataSourceEnum.AniDB, xref.CharacterID), staff = staff.Name, - staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Person, DataSourceEnum.Shoko, xref.StaffID), - role = xref.Role, - type = ((StaffRoleType)xref.RoleType).ToString() + staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Person, DataSourceEnum.AniDB, xref.CreatorID), + role = xref2.AppearanceType.ToString().Replace("_", " "), + type = "Seiyuu", }; sr.roles ??= []; sr.roles.Add(role); diff --git a/Shoko.Server/API/v2/Modules/Common.cs b/Shoko.Server/API/v2/Modules/Common.cs index ff2c9cce9..88f629577 100644 --- a/Shoko.Server/API/v2/Modules/Common.cs +++ b/Shoko.Server/API/v2/Modules/Common.cs @@ -21,12 +21,14 @@ using Shoko.Server.Extensions; using Shoko.Server.Filters; using Shoko.Server.Models; +using Shoko.Server.Models.AniDB; using Shoko.Server.Repositories; using Shoko.Server.Repositories.Cached; using Shoko.Server.Scheduling; using Shoko.Server.Scheduling.Jobs.Actions; using Shoko.Server.Scheduling.Jobs.AniDB; using Shoko.Server.Scheduling.Jobs.Shoko; +using Shoko.Server.Server; using Shoko.Server.Services; using Shoko.Server.Settings; using Shoko.Server.Utilities; @@ -877,7 +879,7 @@ public object GetMultipleFiles([FromQuery] API_Call_Parameters para) para.notag == 1, 0, false, para.allpics != 0, para.pic, para.tagfilter); serie.eps ??= new List(); var episode = Episode.GenerateFromAnimeEpisode(HttpContext, ep, userID, 0); - var vls = ep.VideoLocals; + var vls = ep.VideoLocals.ToList(); if (vls.Count <= 0) { continue; @@ -3097,26 +3099,20 @@ public object GetCastFromSeries(int id) } var roles = new List(); - var xref_animestaff = RepoFactory.CrossRef_Anime_Staff.GetByAnimeIDAndRoleType(series.AniDB_ID, - StaffRoleType.Seiyuu); + var xref_animestaff = RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(series.AniDB_ID); foreach (var xref in xref_animestaff) { - if (xref.RoleID == null) - { - continue; - } - - var character = RepoFactory.AnimeCharacter.GetByID(xref.RoleID.Value); + var character = RepoFactory.AniDB_Character.GetByID(xref.CharacterID); if (character == null) - { continue; - } - var staff = RepoFactory.AnimeStaff.GetByID(xref.StaffID); + var staff = RepoFactory.AniDB_Creator.GetByID(xref.CreatorID); if (staff == null) - { continue; - } + + var xref2 = xref.CharacterCrossReference; + if (xref2 == null) + continue; var cdescription = character.Description; if (string.IsNullOrEmpty(cdescription)) @@ -3124,22 +3120,16 @@ public object GetCastFromSeries(int id) cdescription = null; } - var sdescription = staff.Description; - if (string.IsNullOrEmpty(sdescription)) - { - sdescription = null; - } - var role = new Role { character = character.Name, - character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Character, DataSourceEnum.Shoko, xref.RoleID.Value), + character_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Character, DataSourceEnum.AniDB, xref.CharacterID), character_description = cdescription, staff = staff.Name, - staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Person, DataSourceEnum.Shoko, xref.StaffID), - staff_description = sdescription, - role = xref.Role, - type = ((StaffRoleType)xref.RoleType).ToString() + staff_image = APIHelper.ConstructImageLinkFromTypeAndId(ctx, ImageEntityType.Person, DataSourceEnum.AniDB, xref.CreatorID), + staff_description = string.Empty, + role = xref2.AppearanceType.ToString().Replace("_", " "), + type = "Seiyuu", }; roles.Add(role); } @@ -3177,27 +3167,10 @@ private static int CompareRoleByImportance(Role role1, Role role2) } private static int CompareXRef_Anime_StaffByImportance( - KeyValuePair staff1, - KeyValuePair staff2) + KeyValuePair staff1, + KeyValuePair staff2) { - var succeeded1 = Enum.TryParse(staff1.Value.Role?.Replace(" ", "_"), out CharacterAppearanceType type1); - var succeeded2 = Enum.TryParse(staff2.Value.Role?.Replace(" ", "_"), out CharacterAppearanceType type2); - if (!succeeded1 && !succeeded2) - { - return 0; - } - - if (!succeeded1) - { - return 1; - } - - if (!succeeded2) - { - return -1; - } - - var result = ((int)type1).CompareTo((int)type2); + var result = ((int)staff1.Value.RoleType).CompareTo((int)staff2.Value.RoleType); if (result != 0) { return result; diff --git a/Shoko.Server/API/v3/Controllers/ActionController.cs b/Shoko.Server/API/v3/Controllers/ActionController.cs index 0cd406a59..d0117c327 100644 --- a/Shoko.Server/API/v3/Controllers/ActionController.cs +++ b/Shoko.Server/API/v3/Controllers/ActionController.cs @@ -374,7 +374,7 @@ public async Task UpdateMissingAnidbXml() index = 0; var videos = RepoFactory.VideoLocal.GetVideosWithMissingCrossReferenceData(); var unknownEpisodeDict = videos - .SelectMany(file => file.EpisodeCrossRefs) + .SelectMany(file => file.EpisodeCrossReferences) .Where(xref => xref.AnimeID is 0) .GroupBy(xref => xref.EpisodeID) .ToDictionary(groupBy => groupBy.Key, groupBy => groupBy.ToList()); @@ -422,7 +422,7 @@ public async Task UpdateMissingAnidbXml() .Select(episode => episode.EpisodeID) .ToHashSet(); var missingAnimeSet = videos - .SelectMany(file => file.EpisodeCrossRefs) + .SelectMany(file => file.EpisodeCrossReferences) .Where(xref => xref.AnimeID > 0 && !queuedAnimeSet.Contains(xref.AnimeID) && (!localAnimeSet.Contains(xref.AnimeID) || !localEpisodeSet.Contains(xref.EpisodeID))) .Select(xref => xref.AnimeID) .ToHashSet(); diff --git a/Shoko.Server/API/v3/Controllers/DashboardController.cs b/Shoko.Server/API/v3/Controllers/DashboardController.cs index 75812b941..71d18f6db 100644 --- a/Shoko.Server/API/v3/Controllers/DashboardController.cs +++ b/Shoko.Server/API/v3/Controllers/DashboardController.cs @@ -100,7 +100,7 @@ public Dashboard.CollectionStats GetStats() .ToList(); var duplicates = places .Where(a => !a.VideoLocal.IsVariation) - .SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByHash(a.VideoLocal.Hash)) + .SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByEd2k(a.VideoLocal.Hash)) .GroupBy(a => a.EpisodeID) .Count(a => a.Count() > 1); var percentDuplicates = places.Count == 0 @@ -494,7 +494,7 @@ public Dashboard.EpisodeDetails GetEpisodeDetailsForSeriesAndEpisode(SVR_JMMUser if (seriesDict.TryGetValue(episode.AnimeID, out var series)) { var xref = RepoFactory.CrossRef_File_Episode.GetByEpisodeID(episode.EpisodeID).MinBy(xref => xref.Percentage); - var file = xref != null ? RepoFactory.VideoLocal.GetByHash(xref.Hash) : null; + var file = xref?.VideoLocal; return new Dashboard.EpisodeDetails(episode, anime, series, file); } diff --git a/Shoko.Server/API/v3/Controllers/FileController.cs b/Shoko.Server/API/v3/Controllers/FileController.cs index b9cc750de..21f2a098b 100644 --- a/Shoko.Server/API/v3/Controllers/FileController.cs +++ b/Shoko.Server/API/v3/Controllers/FileController.cs @@ -192,7 +192,7 @@ public ActionResult GetFileByEd2k( if (string.IsNullOrEmpty(hash) || size <= 0) return NotFound(FileNotFoundWithHash); - var file = RepoFactory.VideoLocal.GetByHashAndSize(hash, size); + var file = RepoFactory.VideoLocal.GetByEd2kAndSize(hash, size); if (file == null) return NotFound(FileNotFoundWithHash); @@ -219,7 +219,7 @@ public ActionResult GetFileByCrc32( if (string.IsNullOrEmpty(hash) || size <= 0) return NotFound(FileNotFoundWithHash); - var file = RepoFactory.VideoLocal.GetByCRC32AndSize(hash, size); + var file = RepoFactory.VideoLocal.GetByCrc32AndSize(hash, size); if (file == null) return NotFound(FileNotFoundWithHash); @@ -246,7 +246,7 @@ public ActionResult GetFileByMd5( if (string.IsNullOrEmpty(hash) || size <= 0) return NotFound(FileNotFoundWithHash); - var file = RepoFactory.VideoLocal.GetByMD5AndSize(hash, size); + var file = RepoFactory.VideoLocal.GetByMd5AndSize(hash, size); if (file == null) return NotFound(FileNotFoundWithHash); @@ -273,7 +273,7 @@ public ActionResult GetFileBySha1( if (string.IsNullOrEmpty(hash) || size <= 0) return NotFound(FileNotFoundWithHash); - var file = RepoFactory.VideoLocal.GetBySHA1AndSize(hash, size); + var file = RepoFactory.VideoLocal.GetBySha1AndSize(hash, size); if (file == null) return NotFound(FileNotFoundWithHash); @@ -449,7 +449,7 @@ public ActionResult GetFileByAnidbFileID([FromRoute, Range(1, int.MaxValue if (anidb == null) return NotFound(FileNotFoundWithFileID); - var file = RepoFactory.VideoLocal.GetByHash(anidb.Hash); + var file = RepoFactory.VideoLocal.GetByEd2k(anidb.Hash); if (file == null) return NotFound(AnidbNotFoundForFileID); @@ -469,7 +469,7 @@ public async Task RescanFileByAniDBFileID([FromRoute, Range(1, int if (anidb == null) return NotFound(FileNotFoundWithFileID); - var file = RepoFactory.VideoLocal.GetByHash(anidb.Hash); + var file = RepoFactory.VideoLocal.GetByEd2k(anidb.Hash); if (file == null) return NotFound(AnidbNotFoundForFileID); @@ -1129,7 +1129,7 @@ public async Task UnlinkMultipleEpisodesFromFile([FromRoute, Range var seriesIDs = new HashSet(); var episodeList = file.AnimeEpisodes .Where(episode => all || episodeIdSet.Contains(episode.AniDB_EpisodeID)) - .Select(episode => (Episode: episode, XRef: RepoFactory.CrossRef_File_Episode.GetByHashAndEpisodeID(file.Hash, episode.AniDB_EpisodeID))) + .Select(episode => (Episode: episode, XRef: file.EpisodeCrossReferences.FirstOrDefault(x => x.EpisodeID == episode.AniDB_EpisodeID))) .Where(obj => obj.XRef != null) .ToList(); foreach (var (_, xref) in episodeList) @@ -1321,7 +1321,7 @@ await scheduler.StartJobNow(c => [NonAction] private static void RemoveXRefsForFile(SVR_VideoLocal file) { - foreach (var xref in RepoFactory.CrossRef_File_Episode.GetByHash(file.Hash)) + foreach (var xref in file.EpisodeCrossReferences) { if (xref.CrossRefSource == (int)CrossRefSource.AniDB) return; @@ -1340,7 +1340,7 @@ private static void RemoveXRefsForFile(SVR_VideoLocal file) [NonAction] private static void CheckXRefsForFile(SVR_VideoLocal file, ModelStateDictionary modelState) { - foreach (var xref in RepoFactory.CrossRef_File_Episode.GetByHash(file.Hash)) + foreach (var xref in file.EpisodeCrossReferences) if (xref.CrossRefSource == (int)CrossRefSource.AniDB) modelState.AddModelError("CrossReferences", $"Unable to remove AniDB cross-reference to anidb episode with id {xref.EpisodeID} for file with id {file.VideoLocalID}."); } @@ -1406,7 +1406,7 @@ private ActionResult> PathEndsWithInternal(string path, bool includeX if (file == null) return false; - var xrefs = file.EpisodeCrossRefs; + var xrefs = file.EpisodeCrossReferences; var series = xrefs.FirstOrDefault(xref => xref.AnimeID is not 0)?.AnimeSeries; return series == null || User.AllowedSeries(series); }) diff --git a/Shoko.Server/API/v3/Controllers/ReleaseManagementController.cs b/Shoko.Server/API/v3/Controllers/ReleaseManagementController.cs index 8b7838b81..e8ce61af6 100644 --- a/Shoko.Server/API/v3/Controllers/ReleaseManagementController.cs +++ b/Shoko.Server/API/v3/Controllers/ReleaseManagementController.cs @@ -141,7 +141,7 @@ public ActionResult> GetFileIdsWithPreference( return enumerable .SelectMany(episode => { - var files = episode.VideoLocals; + var files = episode.VideoLocals.ToList(); files.Sort(FileQualityFilter.CompareTo); return files .Skip(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) @@ -168,7 +168,7 @@ public ActionResult> GetFileIdsWithPreference( return enumerable .SelectMany(episode => { - var files = episode.VideoLocals; + var files = episode.VideoLocals.ToList(); files.Sort(FileQualityFilter.CompareTo); return files .Skip(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) diff --git a/Shoko.Server/API/v3/Controllers/ReleaseManagementMultipleReleasesController.cs b/Shoko.Server/API/v3/Controllers/ReleaseManagementMultipleReleasesController.cs index 7da0cce5c..8d303a6be 100644 --- a/Shoko.Server/API/v3/Controllers/ReleaseManagementMultipleReleasesController.cs +++ b/Shoko.Server/API/v3/Controllers/ReleaseManagementMultipleReleasesController.cs @@ -66,7 +66,7 @@ public ActionResult> GetFileIdsWithPreference( return enumerable .SelectMany(episode => { - var files = episode.VideoLocals; + var files = episode.VideoLocals.ToList(); files.Sort(FileQualityFilter.CompareTo); return files .Skip(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) @@ -167,7 +167,7 @@ public ActionResult> GetFileIdsWithPreference( return enumerable .SelectMany(episode => { - var files = episode.VideoLocals; + var files = episode.VideoLocals.ToList(); files.Sort(FileQualityFilter.CompareTo); return files .Skip(FileQualityFilter.Settings.MaxNumberOfFilesToKeep) diff --git a/Shoko.Server/API/v3/Controllers/SeriesController.cs b/Shoko.Server/API/v3/Controllers/SeriesController.cs index d1701bbb3..e736f2eaa 100644 --- a/Shoko.Server/API/v3/Controllers/SeriesController.cs +++ b/Shoko.Server/API/v3/Controllers/SeriesController.cs @@ -32,6 +32,7 @@ using Shoko.Server.Scheduling; using Shoko.Server.Scheduling.Jobs.Shoko; using Shoko.Server.Scheduling.Jobs.Trakt; +using Shoko.Server.Server; using Shoko.Server.Services; using Shoko.Server.Settings; using Shoko.Server.Utilities; @@ -703,12 +704,11 @@ private static List GetWatchedAnimeForPeriod( return userDataQuery .OrderByDescending(userData => userData.LastUpdated) - .Select(userData => RepoFactory.VideoLocal.GetByID(userData.VideoLocalID)) + .Select(userData => userData.VideoLocal) .WhereNotNull() - .Select(file => file.EpisodeCrossRefs.OrderBy(xref => xref.EpisodeOrder).ThenBy(xref => xref.Percentage) - .FirstOrDefault()) + .Select(file => file.EpisodeCrossReferences.OrderBy(xref => xref.EpisodeOrder).ThenBy(xref => xref.Percentage).FirstOrDefault()) .WhereNotNull() - .Select(xref => RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(xref.EpisodeID)) + .Select(xref => xref.AnimeEpisode) .WhereNotNull() .DistinctBy(episode => episode.AnimeSeriesID) .Select(episode => episode.AnimeSeries?.AniDB_Anime) @@ -2198,7 +2198,8 @@ public ParallelQuery GetEpisodesInternal( ep => RepoFactory.AniDB_Episode_Title.GetByEpisodeID(ep.AniDB.EpisodeID) .Where(title => title != null && languages.Contains(title.Language)) .Select(title => title.Title) - .Append(ep.Shoko.PreferredTitle) + .Append(ep.Shoko?.PreferredTitle) + .WhereNotDefault() .Distinct() .ToList(), fuzzy @@ -2685,7 +2686,7 @@ public ActionResult RemoveSeriesUserTags( /// [HttpGet("{seriesID}/Cast")] public ActionResult> GetSeriesCast([FromRoute, Range(1, int.MaxValue)] int seriesID, - [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet? roleType = null) + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet? roleType = null) { var series = RepoFactory.AnimeSeries.GetByID(seriesID); if (series == null) diff --git a/Shoko.Server/API/v3/Helpers/APIv3_Extensions.cs b/Shoko.Server/API/v3/Helpers/APIv3_Extensions.cs index 25758b0db..54c543766 100644 --- a/Shoko.Server/API/v3/Helpers/APIv3_Extensions.cs +++ b/Shoko.Server/API/v3/Helpers/APIv3_Extensions.cs @@ -5,7 +5,7 @@ using Shoko.Server.API.v3.Models.Common; using Shoko.Server.Models.TMDB; using Shoko.Server.Providers.TMDB; - +using Shoko.Server.Server; using ImageEntityType = Shoko.Plugin.Abstractions.Enums.ImageEntityType; using TitleLanguage = Shoko.Plugin.Abstractions.DataModels.TitleLanguage; @@ -14,23 +14,23 @@ namespace Shoko.Server.API.v3.Helpers; public static class APIv3_Extensions { - public static Role.CreatorRoleType ToCreatorRole(this TMDB_Movie_Crew crew) + public static CreatorRoleType ToCreatorRole(this TMDB_Movie_Crew crew) => ToCreatorRole(crew.Department, crew.Job); - public static Role.CreatorRoleType ToCreatorRole(this TMDB_Show_Crew crew) + public static CreatorRoleType ToCreatorRole(this TMDB_Show_Crew crew) => ToCreatorRole(crew.Department, crew.Job); - public static Role.CreatorRoleType ToCreatorRole(this TMDB_Season_Crew crew) + public static CreatorRoleType ToCreatorRole(this TMDB_Season_Crew crew) => ToCreatorRole(crew.Department, crew.Job); - public static Role.CreatorRoleType ToCreatorRole(this TMDB_Episode_Crew crew) + public static CreatorRoleType ToCreatorRole(this TMDB_Episode_Crew crew) => ToCreatorRole(crew.Department, crew.Job); - private static Role.CreatorRoleType ToCreatorRole(string department, string job) + private static CreatorRoleType ToCreatorRole(string department, string job) => department switch { // TODO: Implement this. - _ => Role.CreatorRoleType.Staff, + _ => CreatorRoleType.Staff, }; public static IEnumerable InLanguage(this IEnumerable imageList, IReadOnlySet? language = null) diff --git a/Shoko.Server/API/v3/Helpers/FilterFactory.cs b/Shoko.Server/API/v3/Helpers/FilterFactory.cs index 7340274d3..e0ea52852 100644 --- a/Shoko.Server/API/v3/Helpers/FilterFactory.cs +++ b/Shoko.Server/API/v3/Helpers/FilterFactory.cs @@ -332,7 +332,7 @@ public FilterPreset GetFilterPreset(Filter.Input.CreateOrUpdateFilterBody filter } else { - var subFilters = existing.FilterPresetID != 0 ? RepoFactory.FilterPreset.GetByParentID(existing.FilterPresetID) : new(); + var subFilters = existing.FilterPresetID != 0 ? RepoFactory.FilterPreset.GetByParentID(existing.FilterPresetID) : []; if (subFilters.Count > 0) modelState?.AddModelError(nameof(filter.IsDirectory), "Cannot turn a directory filter with sub-filters into a normal filter without first removing the sub-filters"); } diff --git a/Shoko.Server/API/v3/Helpers/ModelHelper.cs b/Shoko.Server/API/v3/Helpers/ModelHelper.cs index 8188ee6aa..e53af6719 100644 --- a/Shoko.Server/API/v3/Helpers/ModelHelper.cs +++ b/Shoko.Server/API/v3/Helpers/ModelHelper.cs @@ -531,7 +531,7 @@ public static ListResult FilterFiles(IEnumerable input, SV .Where(tuple => { var (video, _, locations, userRecord) = tuple; - var xrefs = video.EpisodeCrossRefs; + var xrefs = video.EpisodeCrossReferences; var isAnimeAllowed = xrefs .DistinctBy(xref => xref.AnimeID) .Select(xref => xref.AniDBAnime) diff --git a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs index 9d2599f98..172040ebb 100644 --- a/Shoko.Server/API/v3/Helpers/WebUIFactory.cs +++ b/Shoko.Server/API/v3/Helpers/WebUIFactory.cs @@ -5,32 +5,26 @@ using Shoko.Server.API.v3.Models.Common; using Shoko.Server.API.v3.Models.Shoko; using Shoko.Server.Models; +using Shoko.Server.Server; namespace Shoko.Server.API.v3.Helpers; public class WebUIFactory { - private readonly FilterFactory _filterFactory; - - public WebUIFactory(FilterFactory filterFactory) - { - _filterFactory = filterFactory; - } - public Models.Shoko.WebUI.WebUISeriesExtra GetWebUISeriesExtra(SVR_AnimeSeries series) { var anime = series.AniDB_Anime; var animeEpisodes = anime.AniDBEpisodes; var runtimeLength = GuessCorrectRuntimeLength(animeEpisodes); - var cast = Series.GetCast(anime.AnimeID, [Role.CreatorRoleType.Studio, Role.CreatorRoleType.Producer]); + var cast = Series.GetCast(anime.AnimeID, [CreatorRoleType.Studio, CreatorRoleType.Producer]); var season = GetFirstAiringSeason(anime); var result = new Models.Shoko.WebUI.WebUISeriesExtra { RuntimeLength = runtimeLength, FirstAirSeason = season, - 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(), + Studios = cast.Where(role => role.RoleName == CreatorRoleType.Studio).Select(role => role.Staff).ToList(), + Producers = cast.Where(role => role.RoleName == CreatorRoleType.Producer).Select(role => role.Staff).ToList(), SourceMaterial = Series.GetTags(anime, TagFilter.Filter.Invert | TagFilter.Filter.Source, excludeDescriptions: true).FirstOrDefault()?.Name ?? "Original Work", }; return result; diff --git a/Shoko.Server/API/v3/Models/Common/Role.cs b/Shoko.Server/API/v3/Models/Common/Role.cs index 8da8b5a6c..0736407c6 100644 --- a/Shoko.Server/API/v3/Models/Common/Role.cs +++ b/Shoko.Server/API/v3/Models/Common/Role.cs @@ -3,7 +3,9 @@ using Shoko.Models.Server; using Shoko.Server.API.v3.Helpers; using Shoko.Server.Extensions; +using Shoko.Server.Models.AniDB; using Shoko.Server.Models.TMDB; +using Shoko.Server.Server; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -39,25 +41,39 @@ public class Role [Required] public string RoleDetails { get; set; } = string.Empty; - public Role(CrossRef_Anime_Staff xref, AnimeStaff staff, AnimeCharacter? character) + public Role(AniDB_Anime_Character xref, AniDB_Creator staff, AniDB_Character? character) { Character = character == null ? null : new() { - ID = character.AniDBID, + ID = character.CharacterID, Name = character.Name, - AlternateName = character.AlternateName, - Description = character.Description, + AlternateName = character.OriginalName ?? string.Empty, + Description = character.Description ?? string.Empty, Image = character.GetImageMetadata() is { } characterImage ? new Image(characterImage) : null, }; Staff = new() { - ID = staff.AniDBID, + ID = staff.CreatorID, Name = staff.Name, - AlternateName = staff.AlternateName, - Description = staff.Description, + AlternateName = staff.OriginalName ?? string.Empty, + Description = string.Empty, Image = staff.GetImageMetadata() is { } staffImage ? new Image(staffImage) : null, }; - RoleName = (CreatorRoleType)xref.RoleType; + RoleName = CreatorRoleType.Actor; + RoleDetails = xref.AppearanceType.ToString().Replace("_", " "); + } + + public Role(AniDB_Anime_Staff xref, AniDB_Creator staff) + { + Staff = new() + { + ID = staff.CreatorID, + Name = staff.Name, + AlternateName = staff.OriginalName ?? string.Empty, + Description = string.Empty, + Image = staff.GetImageMetadata() is { } staffImage ? new Image(staffImage) : null, + }; + RoleName = xref.RoleType; RoleDetails = xref.Role; } @@ -77,7 +93,7 @@ public Role(TMDB_Movie_Cast cast) Description = person.EnglishBiography, Image = personImages.Count > 0 ? new Image(personImages[0]) : null, }; - RoleName = CreatorRoleType.Seiyuu; + RoleName = CreatorRoleType.Actor; RoleDetails = "Character"; } @@ -97,7 +113,7 @@ public Role(TMDB_Show_Cast cast) Description = person.EnglishBiography, Image = personImages.Count > 0 ? new Image(personImages[0]) : null, }; - RoleName = CreatorRoleType.Seiyuu; + RoleName = CreatorRoleType.Actor; RoleDetails = "Character"; } @@ -117,7 +133,7 @@ public Role(TMDB_Season_Cast cast) Description = person.EnglishBiography, Image = personImages.Count > 0 ? new Image(personImages[0]) : null, }; - RoleName = CreatorRoleType.Seiyuu; + RoleName = CreatorRoleType.Actor; RoleDetails = "Character"; } @@ -137,7 +153,7 @@ public Role(TMDB_Episode_Cast cast) Description = person.EnglishBiography, Image = personImages.Count > 0 ? new Image(personImages[0]) : null, }; - RoleName = CreatorRoleType.Seiyuu; + RoleName = CreatorRoleType.Actor; RoleDetails = "Character"; } @@ -239,53 +255,4 @@ public class Person /// public Image? Image { get; set; } } - - [JsonConverter(typeof(StringEnumConverter))] - public enum CreatorRoleType - { - /// - /// Voice actor or voice actress. - /// - Seiyuu, - - /// - /// This can be anything involved in writing the show. - /// - Staff, - - /// - /// The studio responsible for publishing the show. - /// - Studio, - - /// - /// The main producer(s) for the show. - /// - Producer, - - /// - /// Direction. - /// - Director, - - /// - /// Series Composition. - /// - SeriesComposer, - - /// - /// Character Design. - /// - CharacterDesign, - - /// - /// Music composer. - /// - Music, - - /// - /// Responsible for the creation of the source work this show is detrived from. - /// - SourceWork, - } } diff --git a/Shoko.Server/API/v3/Models/Shoko/File.cs b/Shoko.Server/API/v3/Models/Shoko/File.cs index 108a3a3e0..154666bbe 100644 --- a/Shoko.Server/API/v3/Models/Shoko/File.cs +++ b/Shoko.Server/API/v3/Models/Shoko/File.cs @@ -145,7 +145,7 @@ public File(SVR_VideoLocal_User userRecord, SVR_VideoLocal file, bool withXRefs Created = file.DateTimeCreated.ToUniversalTime(); Updated = file.DateTimeUpdated.ToUniversalTime(); if (withXRefs) - SeriesIDs = FileCrossReference.From(file.EpisodeCrossRefs); + SeriesIDs = FileCrossReference.From(file.EpisodeCrossReferences); if (includeDataFrom?.Contains(DataSource.AniDB) ?? false) { @@ -369,8 +369,8 @@ public FileUserStats(SVR_VideoLocal_User userStats) public FileUserStats MergeWithExisting(SVR_VideoLocal_User existing, SVR_VideoLocal file = null) { - // Get the file assosiated with the user entry. - file ??= existing.GetVideoLocal(); + // Get the file associated with the user entry. + file ??= existing.VideoLocal; // Sync the watch date and aggregate the data up to the episode if needed. var watchedService = Utils.ServiceContainer.GetRequiredService(); @@ -581,7 +581,7 @@ public enum FileSortCriteria FileID = 16, } - private static Func<(SVR_VideoLocal Video, SVR_VideoLocal_Place Location, List Locations, SVR_VideoLocal_User UserRecord), object> GetOrderFunction(FileSortCriteria criteria, bool isInverted) => + private static Func<(SVR_VideoLocal Video, SVR_VideoLocal_Place Location, IReadOnlyList Locations, SVR_VideoLocal_User UserRecord), object> GetOrderFunction(FileSortCriteria criteria, bool isInverted) => criteria switch { FileSortCriteria.ImportFolderName => (tuple) => tuple.Location?.ImportFolder?.ImportFolderName ?? string.Empty, @@ -603,7 +603,7 @@ public enum FileSortCriteria _ => null, }; - public static IEnumerable<(SVR_VideoLocal, SVR_VideoLocal_Place, List, SVR_VideoLocal_User)> OrderBy(IEnumerable<(SVR_VideoLocal, SVR_VideoLocal_Place, List, SVR_VideoLocal_User)> enumerable, List sortCriterias) + public static IEnumerable<(SVR_VideoLocal, SVR_VideoLocal_Place, IReadOnlyList, SVR_VideoLocal_User)> OrderBy(IEnumerable<(SVR_VideoLocal, SVR_VideoLocal_Place, IReadOnlyList, SVR_VideoLocal_User)> enumerable, List sortCriterias) { var first = true; return sortCriterias.Aggregate(enumerable, (current, rawSortCriteria) => @@ -622,7 +622,7 @@ public enum FileSortCriteria } // All other criterias in the list. - var ordered = current as IOrderedEnumerable<(SVR_VideoLocal, SVR_VideoLocal_Place, List, SVR_VideoLocal_User)>; + var ordered = current as IOrderedEnumerable<(SVR_VideoLocal, SVR_VideoLocal_Place, IReadOnlyList, SVR_VideoLocal_User)>; return isInverted ? ordered.ThenByDescending(orderFunc) : ordered.ThenBy(orderFunc); }); } diff --git a/Shoko.Server/API/v3/Models/Shoko/FileCrossReference.cs b/Shoko.Server/API/v3/Models/Shoko/FileCrossReference.cs index bfee149e3..42addc404 100644 --- a/Shoko.Server/API/v3/Models/Shoko/FileCrossReference.cs +++ b/Shoko.Server/API/v3/Models/Shoko/FileCrossReference.cs @@ -145,20 +145,20 @@ public static List From(IEnumerable cr { // Percentages. Tuple percentage = new(0, 100); - int? releaseGroup = xref.Source == DataSourceEnum.AniDB ? RepoFactory.AniDB_File.GetByHashAndFileSize(xref.ED2K, xref.Size)?.GroupID ?? 0 : null; + int? releaseGroup = xref.Source == DataSourceEnum.AniDB ? RepoFactory.AniDB_File.GetByEd2kAndFileSize(xref.ED2K, xref.Size)?.GroupID ?? 0 : null; var assumedFileCount = PercentageToFileCount(xref.Percentage); if (assumedFileCount > 1) { var xrefs = RepoFactory.CrossRef_File_Episode.GetByEpisodeID(xref.AnidbEpisodeID) // Filter to only cross-references which are partially linked in the same number of parts to the episode, and from the same group as the current cross-reference. - .Where(xref2 => PercentageToFileCount(xref2.Percentage) == assumedFileCount && (xref2.CrossRefSource == (int)CrossRefSource.AniDB ? RepoFactory.AniDB_File.GetByHashAndFileSize(xref2.Hash, xref2.FileSize)?.GroupID ?? -1 : null) == releaseGroup) + .Where(xref2 => PercentageToFileCount(xref2.Percentage) == assumedFileCount && (xref2.CrossRefSource == (int)CrossRefSource.AniDB ? RepoFactory.AniDB_File.GetByEd2kAndFileSize(xref2.Hash, xref2.FileSize)?.GroupID ?? -1 : null) == releaseGroup) // This will order by the "full" episode if the xref is linked to both a "full" episode and "part" episode, // then fall back on the episode order if either a "full" episode is not available, or if it's for cross-references // for a single-file-multiple-episodes file. .Select(xref2 => ( xref: xref2, - episode: RepoFactory.CrossRef_File_Episode.GetByHash(xref2.Hash) - .FirstOrDefault(xref3 => xref3.Percentage == 100 && (xref3.CrossRefSource == (int)CrossRefSource.AniDB ? RepoFactory.AniDB_File.GetByHashAndFileSize(xref3.Hash, xref3.FileSize)?.GroupID ?? -1 : null) == releaseGroup) + episode: RepoFactory.CrossRef_File_Episode.GetByEd2k(xref2.Hash) + .FirstOrDefault(xref3 => xref3.Percentage == 100 && (xref3.CrossRefSource == (int)CrossRefSource.AniDB ? RepoFactory.AniDB_File.GetByEd2kAndFileSize(xref3.Hash, xref3.FileSize)?.GroupID ?? -1 : null) == releaseGroup) ?.AniDBEpisode )) .OrderBy(tuple => tuple.episode?.EpisodeTypeEnum) diff --git a/Shoko.Server/API/v3/Models/Shoko/ImportFolder.cs b/Shoko.Server/API/v3/Models/Shoko/ImportFolder.cs index b33d5c1e4..6f36eff51 100644 --- a/Shoko.Server/API/v3/Models/Shoko/ImportFolder.cs +++ b/Shoko.Server/API/v3/Models/Shoko/ImportFolder.cs @@ -40,14 +40,15 @@ public ImportFolder() { } public ImportFolder(SVR_ImportFolder folder) { - var series = RepoFactory.VideoLocalPlace.GetByImportFolder(folder.ImportFolderID) + var places = folder.Places; + var series = places .Select(a => a?.VideoLocal?.Hash) .Where(a => !string.IsNullOrEmpty(a)) .Distinct() - .SelectMany(RepoFactory.CrossRef_File_Episode.GetByHash) + .SelectMany(RepoFactory.CrossRef_File_Episode.GetByEd2k) .DistinctBy(a => a.AnimeID) .Count(); - var size = RepoFactory.VideoLocalPlace.GetByImportFolder(folder.ImportFolderID) + var size = places .Select(a => a.VideoLocal) .WhereNotNull() .Sum(b => b.FileSize); diff --git a/Shoko.Server/API/v3/Models/Shoko/Series.cs b/Shoko.Server/API/v3/Models/Shoko/Series.cs index 15106248d..c03d109d8 100644 --- a/Shoko.Server/API/v3/Models/Shoko/Series.cs +++ b/Shoko.Server/API/v3/Models/Shoko/Series.cs @@ -14,16 +14,15 @@ using Shoko.Server.API.Converters; using Shoko.Server.API.v3.Helpers; using Shoko.Server.API.v3.Models.Common; -using Shoko.Server.Extensions; using Shoko.Server.Models; using Shoko.Server.Providers.AniDB.Titles; using Shoko.Server.Repositories; +using Shoko.Server.Server; using Shoko.Server.Utilities; using AniDBAnimeType = Shoko.Models.Enums.AnimeType; using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType; using DataSource = Shoko.Server.API.v3.Models.Common.DataSource; -using InternalEpisodeType = Shoko.Models.Enums.EpisodeType; using RelationType = Shoko.Plugin.Abstractions.DataModels.RelationType; using TmdbMovie = Shoko.Server.API.v3.Models.TMDB.Movie; using TmdbShow = Shoko.Server.API.v3.Models.TMDB.Show; @@ -145,7 +144,7 @@ public Series(SVR_AnimeSeries ser, int userId = 0, bool randomizeImages = false, Show = tmdbShowXRefs.Select(a => a.TmdbShowID).Distinct().ToList(), }, TraktTv = ser.TraktShowCrossReferences.Select(a => a.TraktID).Distinct().ToList(), - MAL = ser.MALCrossReferences.Select(a => a.MALID).Distinct().ToList() + MAL = ser.MalCrossReferences.Select(a => a.MALID).Distinct().ToList() }; Links = anime.Resources .Select(tuple => new Resource(tuple)) @@ -235,23 +234,36 @@ private static List GetAirsOnDaysOfWeek(IEnumerable /// /// /// - public static List GetCast(int animeID, HashSet? roleTypes = null) + public static List GetCast(int animeID, HashSet? roleTypes = null) { var roles = new List(); - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByAnimeID(animeID); - foreach (var xref in xrefAnimeStaff) + if (roleTypes == null || roleTypes.Contains(CreatorRoleType.Actor)) + { + var characterXrefs = RepoFactory.AniDB_Anime_Character.GetByAnimeID(animeID); + foreach (var xref in characterXrefs.OrderBy(x => x.Ordering)) + { + if (xref.Creators is not { Count: > 0 } creators) + continue; + + if (xref.Character is not { } character) + continue; + + foreach (var creator in creators) + roles.Add(new(xref, creator, character)); + } + } + + var staff = RepoFactory.AniDB_Anime_Staff.GetByAnimeID(animeID); + foreach (var xref in staff.OrderBy(x => x.Ordering)) { // Filter out any roles that are not of the desired type. - if (roleTypes != null && !roleTypes.Contains((Role.CreatorRoleType)xref.RoleType)) + if (roleTypes != null && !roleTypes.Contains(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) + if (xref.Creator is not { } creator) continue; - var role = new Role(xref, staff, character); - roles.Add(role); + roles.Add(new(xref, creator)); } return roles; diff --git a/Shoko.Server/API/v3/Models/Shoko/User.cs b/Shoko.Server/API/v3/Models/Shoko/User.cs index 8349eb9b6..34f1962fb 100644 --- a/Shoko.Server/API/v3/Models/Shoko/User.cs +++ b/Shoko.Server/API/v3/Models/Shoko/User.cs @@ -230,8 +230,8 @@ public CreateOrUpdateUserBody() { } { user.InvalidateHideCategoriesCache(); var tags = RestrictedTags - .Select(tagID => RepoFactory.AniDB_Tag.GetByTagID(tagID)) - .Where(tag => tag != null) + .Select(RepoFactory.AniDB_Tag.GetByTagID) + .WhereNotNull() .Select(tag => tag.TagName); user.HideCategories = string.Join(',', tags); } diff --git a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs index 327cc8491..1fd5b2729 100644 --- a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs +++ b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs @@ -203,7 +203,7 @@ public WebUISeriesFileSummary( .Where(xref => episodes.ContainsKey(xref.EpisodeID)) .Select(xref => { - var file = RepoFactory.VideoLocal.GetByHash(xref.Hash); + var file = RepoFactory.VideoLocal.GetByEd2k(xref.Hash); var location = file?.FirstValidPlace; return (file, xref, location); diff --git a/Shoko.Server/Databases/DatabaseFixes.cs b/Shoko.Server/Databases/DatabaseFixes.cs index 02b52083f..2226645a4 100644 --- a/Shoko.Server/Databases/DatabaseFixes.cs +++ b/Shoko.Server/Databases/DatabaseFixes.cs @@ -174,103 +174,6 @@ public static void FixHashes() } } - public static void PopulateCharactersAndStaff() - { - var allCharacters = RepoFactory.AniDB_Character.GetAll(); - var allStaff = RepoFactory.AniDB_Creator.GetAll(); - var allAnimeCharacters = RepoFactory.AniDB_Anime_Character.GetAll().ToLookup(a => a.CharID, b => b); - var allCharacterStaff = RepoFactory.AniDB_Character_Creator.GetAll(); - var charBasePath = ImageUtils.GetBaseAniDBCharacterImagesPath() + Path.DirectorySeparatorChar; - var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath() + Path.DirectorySeparatorChar; - - var charsToSave = allCharacters.Select(character => new AnimeCharacter - { - Name = character.CharName?.Replace("`", "'"), - AniDBID = character.CharID, - Description = character.CharDescription?.Replace("`", "'"), - ImagePath = character.GetFullImagePath()?.Replace(charBasePath, ""), - }).ToList(); - RepoFactory.AnimeCharacter.Save(charsToSave); - - var staffToSave = allStaff.Select(a => new AnimeStaff - { - Name = a.Name?.Replace("`", "'"), - AniDBID = a.CreatorID, - ImagePath = a.GetFullImagePath()?.Replace(creatorBasePath, ""), - }).ToList(); - RepoFactory.AnimeStaff.Save(staffToSave); - - // This is not accurate. There was a mistake in DB design - var xrefsToSave = ( - from xref in allCharacterStaff - let animeList = allAnimeCharacters[xref.CharacterID].ToList() - from anime in animeList - select new CrossRef_Anime_Staff - { - AniDB_AnimeID = anime.AnimeID, - Language = "Japanese", - RoleType = (int)StaffRoleType.Seiyuu, - Role = anime.CharType, - RoleID = RepoFactory.AnimeCharacter.GetByAniDBID(xref.CharacterID).CharacterID, - StaffID = RepoFactory.AnimeStaff.GetByAniDBID(xref.CreatorID).StaffID - } - ).ToList(); - RepoFactory.CrossRef_Anime_Staff.Save(xrefsToSave); - } - - public static void FixCharactersWithGrave() - { - var list = RepoFactory.AnimeCharacter.GetAll() - .Where(character => character.Description != null && character.Description.Contains('`')).ToList(); - foreach (var character in list) - { - character.Description = character.Description.Replace('`', '\''); - RepoFactory.AnimeCharacter.Save(character); - } - } - - public static void RemoveBasePathsFromStaffAndCharacters() - { - var charBasePath = ImageUtils.GetBaseAniDBCharacterImagesPath(); - var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath(); - var charactersList = RepoFactory.AnimeCharacter.GetAll() - .Where(a => a.ImagePath.StartsWith(charBasePath)).ToList(); - foreach (var character in charactersList) - { - character.ImagePath = character.ImagePath.Replace(charBasePath, ""); - while (character.ImagePath.StartsWith("" + Path.DirectorySeparatorChar)) - { - character.ImagePath = character.ImagePath[1..]; - } - - while (character.ImagePath.StartsWith("" + Path.AltDirectorySeparatorChar)) - { - character.ImagePath = character.ImagePath[1..]; - } - - RepoFactory.AnimeCharacter.Save(character); - } - - var creatorsList = RepoFactory.AnimeStaff.GetAll() - .Where(a => a.ImagePath.StartsWith(creatorBasePath)).ToList(); - foreach (var creator in creatorsList) - { - creator.ImagePath = creator.ImagePath.Replace(creatorBasePath, ""); - creator.ImagePath = creator.ImagePath.Replace(charBasePath, ""); - while (creator.ImagePath.StartsWith("" + Path.DirectorySeparatorChar)) - { - creator.ImagePath = creator.ImagePath[1..]; - } - - while (creator.ImagePath.StartsWith("" + Path.AltDirectorySeparatorChar)) - { - creator.ImagePath = creator.ImagePath[1..]; - } - - RepoFactory.AnimeStaff.Save(creator); - } - } - public static void RefreshAniDBInfoFromXML() { var i = 0; @@ -333,7 +236,7 @@ public static void MigrateAniDB_FileUpdates() .ToList(); updates.AddRange(RepoFactory.CrossRef_File_Episode.GetAll().Where(a => RepoFactory.AniDB_File.GetByHash(a.Hash) == null) - .Select(a => (xref: a, vl: RepoFactory.VideoLocal.GetByHash(a.Hash))).Where(a => a.vl != null).Select(a => new AniDB_FileUpdate + .Select(a => (xref: a, vl: RepoFactory.VideoLocal.GetByEd2k(a.Hash))).Where(a => a.vl != null).Select(a => new AniDB_FileUpdate { FileSize = a.xref.FileSize, Hash = a.xref.Hash, @@ -742,12 +645,12 @@ public static void FixOrphanedShokoEpisodes() { var xrefs = RepoFactory.CrossRef_File_Episode.GetByEpisodeID(shokoEpisode.AniDB_EpisodeID); var videos = xrefs - .Select(xref => RepoFactory.VideoLocal.GetByHashAndSize(xref.Hash, xref.FileSize)) + .Select(xref => RepoFactory.VideoLocal.GetByEd2kAndSize(xref.Hash, xref.FileSize)) .Where(video => video != null) .ToList(); var anidbFiles = xrefs .Where(xref => xref.CrossRefSource == (int)CrossRefSource.AniDB) - .Select(xref => RepoFactory.AniDB_File.GetByHashAndFileSize(xref.Hash, xref.FileSize)) + .Select(xref => RepoFactory.AniDB_File.GetByEd2kAndFileSize(xref.Hash, xref.FileSize)) .Where(anidbFile => anidbFile != null) .ToList(); var tmdbXrefs = RepoFactory.CrossRef_AniDB_TMDB_Episode.GetByAnidbEpisodeID(shokoEpisode.AniDB_EpisodeID); @@ -805,13 +708,13 @@ public static void CreateDefaultRenamerConfig() var existingRenamer = RepoFactory.RenamerConfig.GetByName("Default"); if (existingRenamer != null) return; - + var renamerService = Utils.ServiceContainer.GetRequiredService(); renamerService.RenamersByKey.TryGetValue("WebAOM", out var renamer); - + if (renamer == null) return; - + var defaultSettings = renamer.GetType().GetInterfaces().FirstOrDefault(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(IRenamer<>)) ?.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(a => a.Name == "DefaultSettings")?.GetMethod?.Invoke(renamer, null); @@ -843,4 +746,73 @@ public static void RepairMissingTMDBPersons() var service = Utils.ServiceContainer.GetRequiredService(); service.RepairMissingPeople().ConfigureAwait(false).GetAwaiter().GetResult(); } + + public static void RecreateAnimeCharactersAndCreators() + { + var xmlUtils = Utils.ServiceContainer.GetRequiredService(); + var animeParser = Utils.ServiceContainer.GetRequiredService(); + var animeCreator = Utils.ServiceContainer.GetRequiredService(); + var schedulerFactory = Utils.ServiceContainer.GetRequiredService(); + var scheduler = schedulerFactory.GetScheduler().ConfigureAwait(false).GetAwaiter().GetResult(); + var animeList = RepoFactory.AniDB_Anime.GetAll(); + var str = ServerState.Instance.ServerStartingStatus; + ServerState.Instance.ServerStartingStatus = $"{str} - 0 / {animeList.Count}"; + _logger.Info($"Recreating characters and creator relations for {animeList.Count} anidb anime entries..."); + + var count = 0; + foreach (var anime in animeList) + { + if (++count % 10 == 0) + { + _logger.Info($"Recreating characters and creator relations for anidb anime entries... ({count}/{animeList.Count})"); + ServerState.Instance.ServerStartingStatus = $"{str} - {count} / {animeList.Count}"; + } + + var xml = xmlUtils.LoadAnimeHTTPFromFile(anime.AnimeID).Result; + if (string.IsNullOrEmpty(xml)) + { + _logger.Warn($"Unable to load cached Anime_HTTP xml dump for anime: {anime.AnimeID}/{anime.MainTitle}"); + scheduler.StartJob(c => + { + c.AnimeID = anime.AnimeID; + c.CacheOnly = false; + c.ForceRefresh = true; + c.DownloadRelations = false; + c.CreateSeriesEntry = false; + c.RelDepth = 0; + }).ConfigureAwait(false).GetAwaiter().GetResult(); + continue; + } + + ResponseGetAnime response; + try + { + response = animeParser.Parse(anime.AnimeID, xml); + if (response == null) throw new NullReferenceException(nameof(response)); + } + catch (Exception e) + { + _logger.Error(e, $"Unable to parse cached Anime_HTTP xml dump for anime: {anime.AnimeID}/{anime.MainTitle}"); + scheduler.StartJob(c => + { + c.AnimeID = anime.AnimeID; + c.CacheOnly = false; + c.ForceRefresh = true; + c.DownloadRelations = false; + c.CreateSeriesEntry = false; + c.RelDepth = 0; + }).ConfigureAwait(false).GetAwaiter().GetResult(); + continue; + } + + animeCreator.CreateCharacters(response.Characters, anime); + + animeCreator.CreateStaff(response.Staff, anime); + + RepoFactory.AniDB_Anime.Save(anime); + } + + + _logger.Info($"Done recreating characters and creator relations for {animeList.Count} anidb anime entries."); + } } diff --git a/Shoko.Server/Databases/MySQL.cs b/Shoko.Server/Databases/MySQL.cs index 3ee5ce02e..791942a62 100644 --- a/Shoko.Server/Databases/MySQL.cs +++ b/Shoko.Server/Databases/MySQL.cs @@ -27,7 +27,7 @@ namespace Shoko.Server.Databases; public class MySQL : BaseDatabase { public override string Name { get; } = "MySQL"; - public override int RequiredVersion { get; } = 146; + public override int RequiredVersion { get; } = 147; private List createVersionTable = new() { @@ -596,20 +596,20 @@ public class MySQL : BaseDatabase "CREATE TABLE `AnimeStaff` ( `StaffID` INT NOT NULL AUTO_INCREMENT, `AniDBID` INT NOT NULL, `Name` text character set utf8 NOT NULL, `AlternateName` text character set utf8 NULL, `Description` text character set utf8 NULL, `ImagePath` text character set utf8 NULL, PRIMARY KEY (`StaffID`) )"), new(70, 3, "CREATE TABLE `CrossRef_Anime_Staff` ( `CrossRef_Anime_StaffID` INT NOT NULL AUTO_INCREMENT, `AniDB_AnimeID` INT NOT NULL, `StaffID` INT NOT NULL, `Role` text character set utf8 NULL, `RoleID` INT, `RoleType` INT NOT NULL, `Language` text character set utf8 NOT NULL, PRIMARY KEY (`CrossRef_Anime_StaffID`) )"), - new(70, 4, DatabaseFixes.PopulateCharactersAndStaff), + new(70, 4, DatabaseFixes.NoOperation), new(71, 1, "ALTER TABLE `MovieDB_Movie` ADD `Rating` INT NOT NULL DEFAULT 0"), new(71, 2, "ALTER TABLE `TvDB_Series` ADD `Rating` INT NULL"), new(72, 1, "ALTER TABLE `AniDB_Episode` ADD `Description` text character set utf8 NOT NULL"), - new(72, 2, DatabaseFixes.FixCharactersWithGrave), + new(72, 2, DatabaseFixes.NoOperation), new(73, 1, DatabaseFixes.RefreshAniDBInfoFromXML), new(74, 1, DatabaseFixes.NoOperation), new(74, 2, DatabaseFixes.UpdateAllStats), - new(75, 1, DatabaseFixes.RemoveBasePathsFromStaffAndCharacters), + new(75, 1, DatabaseFixes.NoOperation), new(76, 1, "CREATE TABLE `AniDB_AnimeUpdate` ( `AniDB_AnimeUpdateID` INT NOT NULL AUTO_INCREMENT, `AnimeID` INT NOT NULL, `UpdatedAt` datetime NOT NULL, PRIMARY KEY (`AniDB_AnimeUpdateID`) );"), new(76, 2, "ALTER TABLE `AniDB_AnimeUpdate` ADD INDEX `UIX_AniDB_AnimeUpdate` (`AnimeID` ASC) ;"), new(76, 3, DatabaseFixes.MigrateAniDB_AnimeUpdates), - new(77, 1, DatabaseFixes.RemoveBasePathsFromStaffAndCharacters), + new(77, 1, DatabaseFixes.NoOperation), new(78, 1, DatabaseFixes.NoOperation), new(79, 1, DatabaseFixes.NoOperation), new(80, 1, "ALTER TABLE `CrossRef_AniDB_MAL` DROP INDEX `UIX_CrossRef_AniDB_MAL_Anime` ;"), @@ -908,6 +908,19 @@ public class MySQL : BaseDatabase new(146, 41, "CREATE UNIQUE INDEX UIX_TMDB_Season_TmdbSeasonID ON TMDB_Season(TmdbSeasonID);"), new(146, 42, "CREATE INDEX IX_TMDB_Season_TmdbShowID ON TMDB_Season(TmdbShowID);"), new(146, 43, "CREATE UNIQUE INDEX UIX_TMDB_Network_TmdbNetworkID ON TMDB_Network(TmdbNetworkID);"), + new(147, 01, "DROP TABLE IF EXISTS `AnimeStaff`;"), + new(147, 02, "DROP TABLE IF EXISTS `CrossRef_Anime_Staff`;"), + new(147, 03, "DROP TABLE IF EXISTS `AniDB_Character`;"), + new(147, 04, "DROP TABLE IF EXISTS `AniDB_Anime_Staff`;"), + new(147, 05, "DROP TABLE IF EXISTS `AniDB_Anime_Character`;"), + new(147, 06, "DROP TABLE IF EXISTS `AniDB_Character_Creator`;"), + // One character's name is 502 characters long, so 512 it is. Blame Gintama. + new(147, 07, "CREATE TABLE `AniDB_Character` (`AniDB_CharacterID` INT NOT NULL AUTO_INCREMENT, `CharacterID` INT NOT NULL, `Name` VARCHAR(512) CHARACTER SET UTF8 NOT NULL, `OriginalName` VARCHAR(512) CHARACTER SET UTF8 NOT NULL, `Description` TEXT CHARACTER SET UTF8 NOT NULL, `ImagePath` VARCHAR(20) CHARACTER SET UTF8 NOT NULL, Gender INT NOT NULL, PRIMARY KEY (`AniDB_CharacterID`));"), + new(147, 08, "CREATE TABLE `AniDB_Anime_Staff` (`AniDB_Anime_StaffID` INT NOT NULL AUTO_INCREMENT, `AnimeID` INT NOT NULL, `CreatorID` INT NOT NULL, `Role` VARCHAR(64) CHARACTER SET UTF8 NOT NULL, `RoleType` INT NOT NULL, `Ordering` INT NOT NULL, PRIMARY KEY (`AniDB_Anime_StaffID`));"), + new(147, 09, "CREATE TABLE `AniDB_Anime_Character` (`AniDB_Anime_CharacterID` INT NOT NULL AUTO_INCREMENT, `AnimeID` INT NOT NULL, `CharacterID` INT NOT NULL, `Appearance` VARCHAR(20) CHARACTER SET UTF8 NOT NULL, `AppearanceType` INT NOT NULL, `Ordering` INT NOT NULL, PRIMARY KEY (`AniDB_Anime_CharacterID`));"), + new(147, 10, "CREATE TABLE `AniDB_Anime_Character_Creator` (`AniDB_Anime_Character_CreatorID` INT NOT NULL AUTO_INCREMENT, `AnimeID` INT NOT NULL, `CharacterID` INT NOT NULL, `CreatorID` INT NOT NULL, `Ordering` INT NOT NULL, PRIMARY KEY (`AniDB_Anime_Character_CreatorID`));"), + new(147, 11, "CREATE INDEX IX_AniDB_Anime_Staff_CreatorID ON AniDB_Anime_Staff(CreatorID);"), + new(147, 12, DatabaseFixes.RecreateAnimeCharactersAndCreators), }; private DatabaseCommand linuxTableVersionsFix = new("RENAME TABLE versions TO Versions;"); diff --git a/Shoko.Server/Databases/SQLServer.cs b/Shoko.Server/Databases/SQLServer.cs index 0afd65d84..d0df15409 100644 --- a/Shoko.Server/Databases/SQLServer.cs +++ b/Shoko.Server/Databases/SQLServer.cs @@ -28,7 +28,7 @@ namespace Shoko.Server.Databases; public class SQLServer : BaseDatabase { public override string Name { get; } = "SQLServer"; - public override int RequiredVersion { get; } = 139; + public override int RequiredVersion { get; } = 140; public override void BackupDatabase(string fullfilename) { @@ -556,20 +556,20 @@ public override bool HasVersionsTable() new DatabaseCommand(64, 1, "CREATE TABLE AnimeCharacter ( CharacterID INT IDENTITY(1,1) NOT NULL, AniDBID INT NOT NULL, Name NVARCHAR(MAX) NOT NULL, AlternateName NVARCHAR(MAX), Description NVARCHAR(MAX), ImagePath NVARCHAR(MAX) )"), new DatabaseCommand(64, 2, "CREATE TABLE AnimeStaff ( StaffID INT IDENTITY(1,1) NOT NULL, AniDBID INT NOT NULL, Name NVARCHAR(MAX) NOT NULL, AlternateName NVARCHAR(MAX), Description NVARCHAR(MAX), ImagePath NVARCHAR(MAX) )"), new DatabaseCommand(64, 3, "CREATE TABLE CrossRef_Anime_Staff ( CrossRef_Anime_StaffID INT IDENTITY(1,1) NOT NULL, AniDB_AnimeID INT NOT NULL, StaffID INT NOT NULL, Role NVARCHAR(MAX), RoleID INT, RoleType INT NOT NULL, Language NVARCHAR(MAX) NOT NULL )"), - new DatabaseCommand(64, 4, DatabaseFixes.PopulateCharactersAndStaff), + new DatabaseCommand(64, 4, DatabaseFixes.NoOperation), new DatabaseCommand(65, 1, "ALTER TABLE MovieDB_Movie ADD Rating INT NOT NULL DEFAULT(0)"), new DatabaseCommand(65, 2, "ALTER TABLE TvDB_Series ADD Rating INT NULL"), new DatabaseCommand(66, 1, "ALTER TABLE AniDB_Episode ADD Description nvarchar(max) NOT NULL DEFAULT('')"), - new DatabaseCommand(66, 2, DatabaseFixes.FixCharactersWithGrave), + new DatabaseCommand(66, 2, DatabaseFixes.NoOperation), new DatabaseCommand(67, 1, DatabaseFixes.RefreshAniDBInfoFromXML), new DatabaseCommand(68, 1, DatabaseFixes.NoOperation), new DatabaseCommand(68, 2, DatabaseFixes.UpdateAllStats), - new DatabaseCommand(69, 1, DatabaseFixes.RemoveBasePathsFromStaffAndCharacters), + new DatabaseCommand(69, 1, DatabaseFixes.NoOperation), new DatabaseCommand(70, 1, "ALTER TABLE AniDB_Character ALTER COLUMN CharName nvarchar(max) NOT NULL"), new DatabaseCommand(71, 1, "CREATE TABLE AniDB_AnimeUpdate ( AniDB_AnimeUpdateID INT IDENTITY(1,1) NOT NULL, AnimeID INT NOT NULL, UpdatedAt datetime NOT NULL )"), new DatabaseCommand(71, 2, "CREATE UNIQUE INDEX UIX_AniDB_AnimeUpdate ON AniDB_AnimeUpdate(AnimeID)"), new DatabaseCommand(71, 3, DatabaseFixes.MigrateAniDB_AnimeUpdates), - new DatabaseCommand(72, 1, DatabaseFixes.RemoveBasePathsFromStaffAndCharacters), + new DatabaseCommand(72, 1, DatabaseFixes.NoOperation), new DatabaseCommand(73, 1, DatabaseFixes.NoOperation), new DatabaseCommand(74, 1, DatabaseFixes.NoOperation), new DatabaseCommand(75, 1, "DROP INDEX UIX_CrossRef_AniDB_MAL_Anime ON CrossRef_AniDB_MAL;"), @@ -857,6 +857,19 @@ public override bool HasVersionsTable() new DatabaseCommand(139, 41, "CREATE UNIQUE INDEX UIX_TMDB_Season_TmdbSeasonID ON TMDB_Season(TmdbSeasonID);"), new DatabaseCommand(139, 42, "CREATE INDEX IX_TMDB_Season_TmdbShowID ON TMDB_Season(TmdbShowID);"), new DatabaseCommand(139, 43, "CREATE UNIQUE INDEX UIX_TMDB_Network_TmdbNetworkID ON TMDB_Network(TmdbNetworkID);"), + new DatabaseCommand(140, 01, "DROP TABLE IF EXISTS `AnimeStaff`;"), + new DatabaseCommand(140, 02, "DROP TABLE IF EXISTS `CrossRef_Anime_Staff`;"), + new DatabaseCommand(140, 03, "DROP TABLE IF EXISTS `AniDB_Character`;"), + new DatabaseCommand(140, 04, "DROP TABLE IF EXISTS `AniDB_Anime_Staff`;"), + new DatabaseCommand(140, 05, "DROP TABLE IF EXISTS `AniDB_Anime_Character`;"), + new DatabaseCommand(140, 06, "DROP TABLE IF EXISTS `AniDB_Character_Creator`;"), + // One character's name is 502 characters long, so 512 it is. Blame Gintama. + new DatabaseCommand(140, 07, "CREATE TABLE AniDB_Character (AniDB_CharacterID INT IDENTITY(1,1), CharacterID INT NOT NULL, Name NVARCHAR(512) NOT NULL, OriginalName NVARCHAR(512) NOT NULL, Description TEXT NOT NULL, ImagePath NVARCHAR(20) NOT NULL, Gender INT NOT NULL);"), + new DatabaseCommand(140, 08, "CREATE TABLE AniDB_Anime_Staff (AniDB_Anime_StaffID INT IDENTITY(1,1), AnimeID INT NOT NULL, CreatorID INT NOT NULL, Role NVARCHAR(64) NOT NULL, RoleType INT NOT NULL, Ordering INT NOT NULL);"), + new DatabaseCommand(140, 09, "CREATE TABLE AniDB_Anime_Character (AniDB_Anime_CharacterID INT IDENTITY(1,1), AnimeID INT NOT NULL, CharacterID INT NOT NULL, Appearance NVARCHAR(20) NOT NULL, AppearanceType INT NOT NULL, Ordering INT NOT NULL);"), + new DatabaseCommand(140, 10, "CREATE TABLE AniDB_Anime_Character_Creator (AniDB_Anime_Character_CreatorID INT IDENTITY(1,1), AnimeID INT NOT NULL, CharacterID INT NOT NULL, CreatorID INT NOT NULL, Ordering INT NOT NULL);"), + new DatabaseCommand(140, 11, "CREATE INDEX IX_AniDB_Anime_Staff_CreatorID ON AniDB_Anime_Staff(CreatorID);"), + new DatabaseCommand(140, 12, DatabaseFixes.RecreateAnimeCharactersAndCreators), }; private static void AlterImdbMovieIDType() diff --git a/Shoko.Server/Databases/SQLite.cs b/Shoko.Server/Databases/SQLite.cs index a26009750..be01ae714 100644 --- a/Shoko.Server/Databases/SQLite.cs +++ b/Shoko.Server/Databases/SQLite.cs @@ -28,7 +28,7 @@ public class SQLite : BaseDatabase { public override string Name => "SQLite"; - public override int RequiredVersion => 129; + public override int RequiredVersion => 130; public override void BackupDatabase(string fullfilename) { @@ -537,20 +537,20 @@ public override void CreateDatabase() "CREATE TABLE AnimeStaff ( StaffID INTEGER PRIMARY KEY AUTOINCREMENT, AniDBID INTEGER NOT NULL, Name TEXT NOT NULL, AlternateName TEXT NULL, Description TEXT NULL, ImagePath TEXT NULL )"), new(60, 3, "CREATE TABLE CrossRef_Anime_Staff ( CrossRef_Anime_StaffID INTEGER PRIMARY KEY AUTOINCREMENT, AniDB_AnimeID INTEGER NOT NULL, StaffID INTEGER NOT NULL, Role TEXT NULL, RoleID INTEGER, RoleType INTEGER NOT NULL, Language TEXT NOT NULL )"), - new(60, 4, DatabaseFixes.PopulateCharactersAndStaff), + new(60, 4, DatabaseFixes.NoOperation), new(61, 1, "ALTER TABLE MovieDB_Movie ADD Rating INT NOT NULL DEFAULT 0"), new(61, 2, "ALTER TABLE TvDB_Series ADD Rating INT NULL"), new(62, 1, "ALTER TABLE AniDB_Episode ADD Description TEXT NOT NULL DEFAULT ''"), - new(62, 2, DatabaseFixes.FixCharactersWithGrave), + new(62, 2, DatabaseFixes.NoOperation), new(63, 1, DatabaseFixes.RefreshAniDBInfoFromXML), new(64, 1, DatabaseFixes.NoOperation), new(64, 2, DatabaseFixes.UpdateAllStats), - new(65, 1, DatabaseFixes.RemoveBasePathsFromStaffAndCharacters), + new(65, 1, DatabaseFixes.NoOperation), new(66, 1, "CREATE TABLE AniDB_AnimeUpdate ( AniDB_AnimeUpdateID INTEGER PRIMARY KEY AUTOINCREMENT, AnimeID INTEGER NOT NULL, UpdatedAt timestamp NOT NULL )"), new(66, 2, "CREATE UNIQUE INDEX UIX_AniDB_AnimeUpdate ON AniDB_AnimeUpdate(AnimeID)"), new(66, 3, DatabaseFixes.MigrateAniDB_AnimeUpdates), - new(67, 1, DatabaseFixes.RemoveBasePathsFromStaffAndCharacters), + new(67, 1, DatabaseFixes.NoOperation), new(68, 1, DatabaseFixes.NoOperation), new(69, 1, DatabaseFixes.NoOperation), new(70, 1, "DROP INDEX UIX_CrossRef_AniDB_MAL_Anime;"), @@ -830,6 +830,18 @@ public override void CreateDatabase() new(129, 41, "CREATE UNIQUE INDEX UIX_TMDB_Season_TmdbSeasonID ON TMDB_Season(TmdbSeasonID);"), new(129, 42, "CREATE INDEX IX_TMDB_Season_TmdbShowID ON TMDB_Season(TmdbShowID);"), new(129, 43, "CREATE UNIQUE INDEX UIX_TMDB_Network_TmdbNetworkID ON TMDB_Network(TmdbNetworkID);"), + new(130, 01, "DROP TABLE IF EXISTS AnimeStaff"), + new(130, 02, "DROP TABLE IF EXISTS CrossRef_Anime_Staff"), + new(130, 03, "DROP TABLE IF EXISTS AniDB_Character"), + new(130, 04, "DROP TABLE IF EXISTS AniDB_Anime_Staff"), + new(130, 05, "DROP TABLE IF EXISTS AniDB_Anime_Character"), + new(130, 06, "DROP TABLE IF EXISTS AniDB_Character_Creator"), + new(130, 07, "CREATE TABLE AniDB_Character (AniDB_CharacterID INTEGER PRIMARY KEY AUTOINCREMENT, CharacterID INTEGER NOT NULL, Name TEXT NOT NULL, OriginalName TEXT NOT NULL, Description TEXT NOT NULL, ImagePath TEXT NOT NULL, Gender INTEGER NOT NULL);"), + new(130, 08, "CREATE TABLE AniDB_Anime_Staff (AniDB_Anime_StaffID INTEGER PRIMARY KEY AUTOINCREMENT, AnimeID INTEGER NOT NULL, CreatorID INTEGER NOT NULL, Role TEXT NOT NULL, RoleType INTEGER NOT NULL, Ordering INTEGER NOT NULL);"), + new(130, 09, "CREATE TABLE AniDB_Anime_Character (AniDB_Anime_CharacterID INTEGER PRIMARY KEY AUTOINCREMENT, AnimeID INTEGER NOT NULL, CharacterID INTEGER NOT NULL, Appearance TEXT NOT NULL, AppearanceType INTEGER NOT NULL, Ordering INTEGER NOT NULL);"), + new(130, 10, "CREATE TABLE AniDB_Anime_Character_Creator (AniDB_Anime_Character_CreatorID INTEGER PRIMARY KEY AUTOINCREMENT, AnimeID INTEGER NOT NULL, CharacterID INTEGER NOT NULL, CreatorID INTEGER NOT NULL, Ordering INTEGER NOT NULL);"), + new(130, 11, "CREATE INDEX IX_AniDB_Anime_Staff_CreatorID ON AniDB_Anime_Staff(CreatorID);"), + new(130, 12, DatabaseFixes.RecreateAnimeCharactersAndCreators), }; private static Tuple MigrateRenamers(object connection) diff --git a/Shoko.Server/Extensions/ImageResolvers.cs b/Shoko.Server/Extensions/ImageResolvers.cs index 2c7c6bfd6..8985e5de2 100644 --- a/Shoko.Server/Extensions/ImageResolvers.cs +++ b/Shoko.Server/Extensions/ImageResolvers.cs @@ -21,8 +21,8 @@ private static string ResolveAnidbImageUrl(string relativePath) => string.Format(string.Format(Constants.URLS.AniDB_Images, Constants.URLS.AniDB_Images_Domain), relativePath.Split(Path.DirectorySeparatorChar).LastOrDefault()); public static IImageMetadata? GetImageMetadata(this AniDB_Character character, bool preferred = false) - => !string.IsNullOrEmpty(character.PicName) - ? new Image_Base(DataSourceEnum.AniDB, ImageEntityType.Character, character.CharID, character.GetFullImagePath(), ResolveAnidbImageUrl(character.PicName)) + => !string.IsNullOrEmpty(character.ImagePath) + ? new Image_Base(DataSourceEnum.AniDB, ImageEntityType.Character, character.CharacterID, character.GetFullImagePath(), ResolveAnidbImageUrl(character.ImagePath)) { IsEnabled = true, IsPreferred = preferred, @@ -49,61 +49,27 @@ public static string GetFullImagePath(this AniDB_Anime anime) } public static string GetFullImagePath(this AniDB_Character character) - { - if (string.IsNullOrEmpty(character.PicName)) - return string.Empty; - - return Path.Combine(ImageUtils.GetAniDBCharacterImagePath(character.CharID), character.PicName); - } - - public static IImageMetadata? GetImageMetadata(this AniDB_Creator seiyuu, bool preferred = false) - => !string.IsNullOrEmpty(seiyuu.ImagePath) - ? new Image_Base(DataSourceEnum.AniDB, ImageEntityType.Person, seiyuu.CreatorID, seiyuu.GetFullImagePath(), ResolveAnidbImageUrl(seiyuu.ImagePath)) - { - IsEnabled = true, - IsPreferred = preferred, - } - : null; - - public static string GetFullImagePath(this AniDB_Creator seiyuu) - { - if (string.IsNullOrEmpty(seiyuu.ImagePath)) - return string.Empty; - - return Path.Combine(ImageUtils.GetAniDBCreatorImagePath(seiyuu.CreatorID), seiyuu.ImagePath); - } - - public static IImageMetadata? GetImageMetadata(this AnimeCharacter character, bool preferred = false) - => !string.IsNullOrEmpty(character.ImagePath) - ? new Image_Base(DataSourceEnum.Shoko, ImageEntityType.Character, character.CharacterID, character.GetFullImagePath(), ResolveAnidbImageUrl(character.ImagePath)) - { - IsEnabled = true, - IsPreferred = preferred, - } - : null; - - public static string GetFullImagePath(this AnimeCharacter character) { if (string.IsNullOrEmpty(character.ImagePath)) return string.Empty; - return Path.Combine(ImageUtils.GetBaseAniDBCharacterImagesPath(), character.ImagePath); + return Path.Combine(ImageUtils.GetAniDBCharacterImagePath(character.CharacterID), character.ImagePath); } - public static IImageMetadata? GetImageMetadata(this AnimeStaff staff, bool preferred = false) - => !string.IsNullOrEmpty(staff.ImagePath) - ? new Image_Base(DataSourceEnum.Shoko, ImageEntityType.Person, staff.StaffID, staff.GetFullImagePath(), ResolveAnidbImageUrl(staff.ImagePath)) + public static IImageMetadata? GetImageMetadata(this AniDB_Creator creator, bool preferred = false) + => !string.IsNullOrEmpty(creator.ImagePath) + ? new Image_Base(DataSourceEnum.AniDB, ImageEntityType.Person, creator.CreatorID, creator.GetFullImagePath(), ResolveAnidbImageUrl(creator.ImagePath)) { IsEnabled = true, IsPreferred = preferred, } : null; - public static string GetFullImagePath(this AnimeStaff staff) + public static string GetFullImagePath(this AniDB_Creator creator) { - if (string.IsNullOrEmpty(staff.ImagePath)) + if (string.IsNullOrEmpty(creator.ImagePath)) return string.Empty; - return Path.Combine(ImageUtils.GetBaseAniDBCreatorImagesPath(), staff.ImagePath); + return Path.Combine(ImageUtils.GetAniDBCreatorImagePath(creator.CreatorID), creator.ImagePath); } } diff --git a/Shoko.Server/Extensions/ModelClients.cs b/Shoko.Server/Extensions/ModelClients.cs index cf19f6f8a..dc635edb9 100644 --- a/Shoko.Server/Extensions/ModelClients.cs +++ b/Shoko.Server/Extensions/ModelClients.cs @@ -413,12 +413,12 @@ public static CL_AniDB_Character ToClient(this AniDB_Character character) => new() { AniDB_CharacterID = character.AniDB_CharacterID, - CharID = character.CharID, - PicName = character.PicName, - CreatorListRaw = character.CreatorListRaw ?? "", - CharName = character.CharName, - CharKanjiName = character.CharKanjiName, - CharDescription = character.CharDescription, + CharID = character.CharacterID, + PicName = character.ImagePath, + CreatorListRaw = string.Empty, + CharName = character.Name, + CharKanjiName = character.OriginalName, + CharDescription = character.Description, Seiyuu = character.GetCreator()?.ToClient(), }; diff --git a/Shoko.Server/Extensions/ModelDatabase.cs b/Shoko.Server/Extensions/ModelDatabase.cs index 4ebcded67..4eb87e8d4 100644 --- a/Shoko.Server/Extensions/ModelDatabase.cs +++ b/Shoko.Server/Extensions/ModelDatabase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using NHibernate; using Shoko.Models.Server; using Shoko.Server.Models.AniDB; @@ -10,14 +11,14 @@ namespace Shoko.Server.Extensions; public static class ModelDatabase { - public static AniDB_Character? GetCharacter(this AniDB_Anime_Character character) - => RepoFactory.AniDB_Character.GetByCharID(character.CharID); - public static AniDB_Creator? GetCreator(this AniDB_Character character) - => RepoFactory.AniDB_Character_Creator.GetByCharacterID(character.CharID) is { Count: > 0 } characterVAs - ? RepoFactory.AniDB_Creator.GetByCreatorID(characterVAs[0].CreatorID) + => RepoFactory.AniDB_Anime_Character_Creator.GetByCharacterID(character.CharacterID) is { Count: > 0 } characterVAs + ? characterVAs.OrderBy(x => x.AnimeID).ThenBy(x => x.Ordering).First().Creator : null; + public static IReadOnlyList GetRoles(this AniDB_Character character) + => RepoFactory.AniDB_Anime_Character.GetByCharacterID(character.CharacterID); + public static Trakt_Show? GetByTraktShow(this CrossRef_AniDB_TraktV2 cross, ISession session) => RepoFactory.Trakt_Show.GetByTraktSlug(session, cross.TraktID); diff --git a/Shoko.Server/Extensions/ModelProviders.cs b/Shoko.Server/Extensions/ModelProviders.cs index aa1b251f4..7906926a7 100644 --- a/Shoko.Server/Extensions/ModelProviders.cs +++ b/Shoko.Server/Extensions/ModelProviders.cs @@ -3,6 +3,7 @@ using Shoko.Models.Metro; using Shoko.Models.Server; using Shoko.Server.Models; +using Shoko.Server.Models.AniDB; using Shoko.Server.Models.Trakt; using Shoko.Server.Providers.TraktTV.Contracts; @@ -26,21 +27,21 @@ public static Metro_AniDB_Character ToContractMetro(this AniDB_Character charact var contract = new Metro_AniDB_Character { AniDB_CharacterID = character.AniDB_CharacterID, - CharID = character.CharID, - CharName = character.CharName, - CharKanjiName = character.CharKanjiName, - CharDescription = character.CharDescription, - CharType = charRel.CharType, + CharID = character.CharacterID, + CharName = character.Name, + CharKanjiName = character.OriginalName, + CharDescription = character.Description, + CharType = charRel.Appearance, ImageType = (int)CL_ImageEntityType.AniDB_Character, ImageID = character.AniDB_CharacterID }; - var seiyuu = character.GetCreator(); - if (seiyuu != null) + var creator = charRel.Creators is { Count: > 0 } ? charRel.Creators[0] : null; + if (creator != null) { - contract.SeiyuuID = seiyuu.AniDB_CreatorID; - contract.SeiyuuName = seiyuu.Name; + contract.SeiyuuID = creator.AniDB_CreatorID; + contract.SeiyuuName = creator.Name; contract.SeiyuuImageType = (int)CL_ImageEntityType.AniDB_Creator; - contract.SeiyuuImageID = seiyuu.CreatorID; + contract.SeiyuuImageID = creator.CreatorID; } return contract; diff --git a/Shoko.Server/Extensions/StringExtensions.cs b/Shoko.Server/Extensions/StringExtensions.cs index 1efd09e93..ee322b42b 100644 --- a/Shoko.Server/Extensions/StringExtensions.cs +++ b/Shoko.Server/Extensions/StringExtensions.cs @@ -8,29 +8,33 @@ namespace Shoko.Server.Extensions; public static class StringExtensions { - public static void Deconstruct(this IReadOnlyList list, out T first, out T second) + public static void Deconstruct(this IEnumerable enumerable, out T first, out T second) { + var list = enumerable is IReadOnlyList readonlyList ? readonlyList : enumerable.ToList(); first = list.Count > 0 ? list[0] : default; second = list.Count > 1 ? list[1] : default; } - public static void Deconstruct(this IReadOnlyList list, out T first, out T second, out T third) + public static void Deconstruct(this IEnumerable enumerable, out T first, out T second, out T third) { + var list = enumerable is IReadOnlyList readonlyList ? readonlyList : enumerable.ToList(); first = list.Count > 0 ? list[0] : default; second = list.Count > 1 ? list[1] : default; third = list.Count > 2 ? list[2] : default; } - public static void Deconstruct(this IReadOnlyList list, out T first, out T second, out T third, out T forth) + public static void Deconstruct(this IEnumerable enumerable, out T first, out T second, out T third, out T forth) { + var list = enumerable is IReadOnlyList readonlyList ? readonlyList : enumerable.ToList(); first = list.Count > 0 ? list[0] : default; second = list.Count > 1 ? list[1] : default; third = list.Count > 2 ? list[2] : default; forth = list.Count > 3 ? list[3] : default; } - public static void Deconstruct(this IReadOnlyList list, out T first, out T second, out T third, out T forth, out T fifth) + public static void Deconstruct(this IEnumerable enumerable, out T first, out T second, out T third, out T forth, out T fifth) { + var list = enumerable is IReadOnlyList readonlyList ? readonlyList : enumerable.ToList(); first = list.Count > 0 ? list[0] : default; second = list.Count > 1 ? list[1] : default; third = list.Count > 2 ? list[2] : default; diff --git a/Shoko.Server/Filters/FilterEvaluator.cs b/Shoko.Server/Filters/FilterEvaluator.cs index cb3d43606..c3ebe61a6 100644 --- a/Shoko.Server/Filters/FilterEvaluator.cs +++ b/Shoko.Server/Filters/FilterEvaluator.cs @@ -98,7 +98,7 @@ public IEnumerable> EvaluateFilter(FilterPreset filter, int? /// /// SeriesIDs, grouped by the direct parent GroupID /// - public Dictionary>> BatchEvaluateFilters(List filters, int? userID, bool skipSorting = false) + public Dictionary>> BatchEvaluateFilters(IReadOnlyList filters, int? userID, bool skipSorting = false) { ArgumentNullException.ThrowIfNull(filters); if (filters.Count == 0) return []; diff --git a/Shoko.Server/Mappings/AniDB_Anime_CharacterMap.cs b/Shoko.Server/Mappings/AniDB_Anime_CharacterMap.cs index 1be09ff3d..98c9a8001 100644 --- a/Shoko.Server/Mappings/AniDB_Anime_CharacterMap.cs +++ b/Shoko.Server/Mappings/AniDB_Anime_CharacterMap.cs @@ -1,5 +1,6 @@ using FluentNHibernate.Mapping; -using Shoko.Models.Server; +using Shoko.Server.Models.AniDB; +using Shoko.Server.Server; namespace Shoko.Server.Mappings; @@ -11,7 +12,9 @@ public AniDB_Anime_CharacterMap() Not.LazyLoad(); Id(x => x.AniDB_Anime_CharacterID); Map(x => x.AnimeID).Not.Nullable(); - Map(x => x.CharID).Not.Nullable(); - Map(x => x.CharType).Not.Nullable(); + Map(x => x.CharacterID).Not.Nullable(); + Map(x => x.Appearance).Not.Nullable(); + Map(x => x.AppearanceType).CustomType().Not.Nullable(); + Map(x => x.Ordering).Not.Nullable(); } } diff --git a/Shoko.Server/Mappings/AniDB_Anime_Character_CreatorMap.cs b/Shoko.Server/Mappings/AniDB_Anime_Character_CreatorMap.cs new file mode 100644 index 000000000..d1ba6ada2 --- /dev/null +++ b/Shoko.Server/Mappings/AniDB_Anime_Character_CreatorMap.cs @@ -0,0 +1,20 @@ +using FluentNHibernate.Mapping; +using Shoko.Models.Server; +using Shoko.Server.Models.AniDB; + +namespace Shoko.Server.Mappings; + +public class AniDB_Anime_Character_CreatorMap : ClassMap +{ + public AniDB_Anime_Character_CreatorMap() + { + Table("AniDB_Anime_Character_Creator"); + Not.LazyLoad(); + Id(x => x.AniDB_Anime_Character_CreatorID); + + Map(x => x.AnimeID).Not.Nullable(); + Map(x => x.CharacterID).Not.Nullable(); + Map(x => x.CreatorID).Not.Nullable(); + Map(x => x.Ordering).Not.Nullable(); + } +} diff --git a/Shoko.Server/Mappings/AniDB_Anime_StaffMap.cs b/Shoko.Server/Mappings/AniDB_Anime_StaffMap.cs index 6dffc8811..683a55395 100644 --- a/Shoko.Server/Mappings/AniDB_Anime_StaffMap.cs +++ b/Shoko.Server/Mappings/AniDB_Anime_StaffMap.cs @@ -1,5 +1,6 @@ using FluentNHibernate.Mapping; -using Shoko.Models.Server; +using Shoko.Server.Models.AniDB; +using Shoko.Server.Server; namespace Shoko.Server.Mappings; @@ -12,6 +13,8 @@ public AniDB_Anime_StaffMap() Id(x => x.AniDB_Anime_StaffID); Map(x => x.AnimeID).Not.Nullable(); Map(x => x.CreatorID).Not.Nullable(); - Map(x => x.CreatorType).Not.Nullable(); + Map(x => x.RoleType).CustomType().Not.Nullable(); + Map(x => x.Role).Not.Nullable(); + Map(x => x.Ordering).Not.Nullable(); } } diff --git a/Shoko.Server/Mappings/AniDB_CharacterMap.cs b/Shoko.Server/Mappings/AniDB_CharacterMap.cs index d7f152ae0..8f7f548b6 100644 --- a/Shoko.Server/Mappings/AniDB_CharacterMap.cs +++ b/Shoko.Server/Mappings/AniDB_CharacterMap.cs @@ -1,5 +1,6 @@ using FluentNHibernate.Mapping; -using Shoko.Models.Server; +using Shoko.Server.Models.AniDB; +using Shoko.Server.Providers.TMDB; namespace Shoko.Server.Mappings; @@ -11,10 +12,11 @@ public AniDB_CharacterMap() Not.LazyLoad(); Id(x => x.AniDB_CharacterID); - Map(x => x.CharDescription).Not.Nullable().CustomType("StringClob"); - Map(x => x.CharID).Not.Nullable(); - Map(x => x.PicName).Not.Nullable(); - Map(x => x.CharKanjiName).Not.Nullable(); - Map(x => x.CharName).Not.Nullable(); + Map(x => x.Description).Not.Nullable().CustomType("StringClob"); + Map(x => x.CharacterID).Not.Nullable(); + Map(x => x.ImagePath).Not.Nullable(); + Map(x => x.OriginalName).Not.Nullable(); + Map(x => x.Name).Not.Nullable(); + Map(x => x.Gender).CustomType().Not.Nullable(); } } diff --git a/Shoko.Server/Mappings/AniDB_Character_CreatorMap.cs b/Shoko.Server/Mappings/AniDB_Character_CreatorMap.cs deleted file mode 100644 index 0c31136dd..000000000 --- a/Shoko.Server/Mappings/AniDB_Character_CreatorMap.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Models.Server; -using Shoko.Server.Models.AniDB; - -namespace Shoko.Server.Mappings; - -public class AniDB_Character_CreatorMap : ClassMap -{ - public AniDB_Character_CreatorMap() - { - Not.LazyLoad(); - Id(x => x.AniDB_Character_CreatorID); - - Map(x => x.CharacterID).Not.Nullable(); - Map(x => x.CreatorID).Not.Nullable(); - } -} diff --git a/Shoko.Server/Mappings/AnimeCharacterMap.cs b/Shoko.Server/Mappings/AnimeCharacterMap.cs deleted file mode 100644 index 106e8223d..000000000 --- a/Shoko.Server/Mappings/AnimeCharacterMap.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Models.Server; - -namespace Shoko.Server.Mappings; - -public class AnimeCharacterMap : ClassMap -{ - public AnimeCharacterMap() - { - Table("AnimeCharacter"); - Not.LazyLoad(); - Id(x => x.CharacterID); - Map(x => x.AniDBID).Not.Nullable(); - Map(x => x.Name).Not.Nullable(); - Map(x => x.AlternateName); - Map(x => x.Description).CustomType("StringClob").CustomSqlType("nvarchar(max)"); - Map(x => x.ImagePath); - } -} diff --git a/Shoko.Server/Mappings/AnimeStaffMap.cs b/Shoko.Server/Mappings/AnimeStaffMap.cs deleted file mode 100644 index e9d8331f1..000000000 --- a/Shoko.Server/Mappings/AnimeStaffMap.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Models.Server; - -namespace Shoko.Server.Mappings; - -public class AnimeStaffMap : ClassMap -{ - public AnimeStaffMap() - { - Table("AnimeStaff"); - Not.LazyLoad(); - Id(x => x.StaffID); - Map(x => x.AniDBID).Not.Nullable(); - Map(x => x.Name).Not.Nullable(); - Map(x => x.AlternateName); - Map(x => x.Description).CustomType("StringClob").CustomSqlType("nvarchar(max)"); - Map(x => x.ImagePath); - } -} diff --git a/Shoko.Server/Mappings/CrossRef_Anime_StaffMap.cs b/Shoko.Server/Mappings/CrossRef_Anime_StaffMap.cs deleted file mode 100644 index 2987ee89e..000000000 --- a/Shoko.Server/Mappings/CrossRef_Anime_StaffMap.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentNHibernate.Mapping; -using Shoko.Models.Server; - -namespace Shoko.Server.Mappings; - -public class CrossRef_Anime_StaffMap : ClassMap -{ - public CrossRef_Anime_StaffMap() - { - Table("CrossRef_Anime_Staff"); - Not.LazyLoad(); - Id(x => x.CrossRef_Anime_StaffID); - Map(x => x.AniDB_AnimeID).Not.Nullable(); - Map(x => x.Language).Not.Nullable(); - Map(x => x.StaffID).Not.Nullable(); - Map(x => x.RoleType).Not.Nullable(); - Map(x => x.Role); - Map(x => x.RoleID); - } -} diff --git a/Shoko.Server/Models/AniDB/AniDB_Anime_Character.cs b/Shoko.Server/Models/AniDB/AniDB_Anime_Character.cs new file mode 100644 index 000000000..943ca1bb2 --- /dev/null +++ b/Shoko.Server/Models/AniDB/AniDB_Anime_Character.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using Shoko.Commons.Extensions; +using Shoko.Models.Server; +using Shoko.Server.Repositories; +using Shoko.Server.Server; + +#nullable enable +namespace Shoko.Server.Models.AniDB; + +public class AniDB_Anime_Character +{ + #region Server DB columns + + public int AniDB_Anime_CharacterID { get; set; } + + public int AnimeID { get; set; } + + public int CharacterID { get; set; } + + public string Appearance { get; set; } = string.Empty; + + public CharacterAppearanceType AppearanceType { get; set; } + + public int Ordering { get; set; } + + public SVR_AniDB_Anime? Anime + => RepoFactory.AniDB_Anime.GetByAnimeID(AnimeID); + + public IReadOnlyList CreatorCrossReferences + => RepoFactory.AniDB_Anime_Character_Creator.GetByCharacterIDAndAnimeID(CharacterID, AnimeID); + + public IReadOnlyList Creators + => CreatorCrossReferences + .OrderBy(a => a.Ordering) + .Select(a => a.Creator) + .WhereNotNull() + .ToList(); + + public AniDB_Character? Character + => RepoFactory.AniDB_Character.GetByCharacterID(CharacterID); + + #endregion +} diff --git a/Shoko.Server/Models/AniDB/AniDB_Anime_Character_Creator.cs b/Shoko.Server/Models/AniDB/AniDB_Anime_Character_Creator.cs new file mode 100644 index 000000000..7ac88d394 --- /dev/null +++ b/Shoko.Server/Models/AniDB/AniDB_Anime_Character_Creator.cs @@ -0,0 +1,34 @@ +using Shoko.Models.Server; +using Shoko.Server.Repositories; + +#nullable enable +namespace Shoko.Server.Models.AniDB; + +public class AniDB_Anime_Character_Creator +{ + #region DB columns + + public int AniDB_Anime_Character_CreatorID { get; set; } + + public int AnimeID { get; set; } + + public int CharacterID { get; set; } + + public int CreatorID { get; set; } + + public int Ordering { get; set; } + + #endregion + + public SVR_AniDB_Anime? Anime + => RepoFactory.AniDB_Anime.GetByAnimeID(AnimeID); + + public AniDB_Creator? Creator + => RepoFactory.AniDB_Creator.GetByCreatorID(CreatorID); + + public AniDB_Character? Character + => RepoFactory.AniDB_Character.GetByCharacterID(CharacterID); + + public AniDB_Anime_Character? CharacterCrossReference + => RepoFactory.AniDB_Anime_Character.GetByAnimeIDAndCharacterID(AnimeID, CharacterID); +} diff --git a/Shoko.Server/Models/AniDB/AniDB_Anime_Staff.cs b/Shoko.Server/Models/AniDB/AniDB_Anime_Staff.cs new file mode 100644 index 000000000..b76b0fe00 --- /dev/null +++ b/Shoko.Server/Models/AniDB/AniDB_Anime_Staff.cs @@ -0,0 +1,30 @@ +using Shoko.Server.Repositories; +using Shoko.Server.Server; + +#nullable enable +namespace Shoko.Server.Models.AniDB; + +public class AniDB_Anime_Staff +{ + #region Server DB columns + + public int AniDB_Anime_StaffID { get; set; } + + public int AnimeID { get; set; } + + public int CreatorID { get; set; } + + public string Role { get; set; } = string.Empty; + + public CreatorRoleType RoleType { get; set; } + + public int Ordering { get; set; } + + #endregion + + public SVR_AniDB_Anime? Anime + => RepoFactory.AniDB_Anime.GetByAnimeID(AnimeID); + + public AniDB_Creator? Creator + => RepoFactory.AniDB_Creator.GetByCreatorID(CreatorID); +} diff --git a/Shoko.Server/Models/AniDB/AniDB_Character.cs b/Shoko.Server/Models/AniDB/AniDB_Character.cs new file mode 100644 index 000000000..249c60fd9 --- /dev/null +++ b/Shoko.Server/Models/AniDB/AniDB_Character.cs @@ -0,0 +1,26 @@ + +#nullable enable +using Shoko.Server.Providers.TMDB; + +namespace Shoko.Server.Models.AniDB; + +public class AniDB_Character +{ + #region Server DB columns + + public int AniDB_CharacterID { get; set; } + + public int CharacterID { get; set; } + + public string Name { get; set; } = string.Empty; + + public string OriginalName { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public string ImagePath { get; set; } = string.Empty; + + public PersonGender Gender { get; set; } + + #endregion +} diff --git a/Shoko.Server/Models/AniDB/AniDB_Character_Creator.cs b/Shoko.Server/Models/AniDB/AniDB_Character_Creator.cs deleted file mode 100644 index b830a74ce..000000000 --- a/Shoko.Server/Models/AniDB/AniDB_Character_Creator.cs +++ /dev/null @@ -1,16 +0,0 @@ - -#nullable enable -namespace Shoko.Server.Models.AniDB; - -public class AniDB_Character_Creator -{ - #region DB columns - - public int AniDB_Character_CreatorID { get; set; } - - public int CharacterID { get; set; } - - public int CreatorID { get; set; } - - #endregion -} diff --git a/Shoko.Server/Models/AniDB/AniDB_Creator.cs b/Shoko.Server/Models/AniDB/AniDB_Creator.cs index e69d16228..2dd5fa2b0 100644 --- a/Shoko.Server/Models/AniDB/AniDB_Creator.cs +++ b/Shoko.Server/Models/AniDB/AniDB_Creator.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Shoko.Server.Providers.AniDB; +using Shoko.Server.Repositories; #nullable enable namespace Shoko.Server.Models.AniDB; @@ -64,4 +66,10 @@ public class AniDB_Creator public DateTime LastUpdatedAt { get; set; } #endregion + + public IReadOnlyList Characters + => RepoFactory.AniDB_Anime_Character_Creator.GetByCreatorID(CreatorID); + + public IReadOnlyList Staff + => RepoFactory.AniDB_Anime_Staff.GetByCreatorID(CreatorID); } diff --git a/Shoko.Server/Models/SVR_AniDB_Anime.cs b/Shoko.Server/Models/SVR_AniDB_Anime.cs index ca1fd06f1..8cbd7b88a 100644 --- a/Shoko.Server/Models/SVR_AniDB_Anime.cs +++ b/Shoko.Server/Models/SVR_AniDB_Anime.cs @@ -147,7 +147,7 @@ public List RelatedAnime public List SimilarAnime => RepoFactory.AniDB_Anime_Similar.GetByAnimeID(AnimeID); - public List Characters + public IReadOnlyList Characters => RepoFactory.AniDB_Anime_Character.GetByAnimeID(AnimeID); #endregion @@ -274,16 +274,14 @@ public IReadOnlyList GetImages(ImageEntityType? entityType = nul #region AniDB - public List AniDBEpisodes => RepoFactory.AniDB_Episode.GetByAnimeID(AnimeID); + public IReadOnlyList AniDBEpisodes => RepoFactory.AniDB_Episode.GetByAnimeID(AnimeID); #endregion #region Trakt - public List GetCrossRefTraktV2() - { - return RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AnimeID); - } + public IReadOnlyList TraktShowCrossReferences + => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AnimeID); #endregion @@ -346,10 +344,8 @@ public IReadOnlyList TmdbMovieBackdrops #region MAL - public List GetCrossRefMAL() - { - return RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(AnimeID); - } + public IReadOnlyList MalCrossReferences + => RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(AnimeID); #endregion diff --git a/Shoko.Server/Models/SVR_AniDB_File.cs b/Shoko.Server/Models/SVR_AniDB_File.cs index 331c26901..9c8471f2f 100644 --- a/Shoko.Server/Models/SVR_AniDB_File.cs +++ b/Shoko.Server/Models/SVR_AniDB_File.cs @@ -21,11 +21,11 @@ public class SVR_AniDB_File : AniDB_File, IAniDBFile RepoFactory.CrossRef_Subtitles_AniDB_File.GetByFileID(FileID).ToList(); [XmlIgnore] - public List EpisodeIDs => RepoFactory.CrossRef_File_Episode.GetByHash(Hash) + public List EpisodeIDs => RepoFactory.CrossRef_File_Episode.GetByEd2k(Hash) .Select(crossref => crossref.EpisodeID).ToList(); [XmlIgnore] - public List Episodes => RepoFactory.CrossRef_File_Episode.GetByHash(Hash) + public List Episodes => RepoFactory.CrossRef_File_Episode.GetByEd2k(Hash) .Select(crossref => crossref.AniDBEpisode) .WhereNotNull() .OrderBy(ep => ep.EpisodeTypeEnum) @@ -33,7 +33,7 @@ public class SVR_AniDB_File : AniDB_File, IAniDBFile .ToList(); [XmlIgnore] - public List EpisodeCrossRefs => RepoFactory.CrossRef_File_Episode.GetByHash(Hash); + public IReadOnlyList EpisodeCrossReferences => RepoFactory.CrossRef_File_Episode.GetByEd2k(Hash); // NOTE: I want to cache it, but i won't for now. not until the anidb files and release groups are stored in a non-cached repo. public AniDB_ReleaseGroup ReleaseGroup => diff --git a/Shoko.Server/Models/SVR_AnimeEpisode.cs b/Shoko.Server/Models/SVR_AnimeEpisode.cs index 4a60fe2e3..bc93081f8 100644 --- a/Shoko.Server/Models/SVR_AnimeEpisode.cs +++ b/Shoko.Server/Models/SVR_AnimeEpisode.cs @@ -72,8 +72,8 @@ public string PreferredTitle var languageOrder = Languages.PreferredEpisodeNamingLanguages; // Lazy load AniDB titles if needed. - List? anidbTitles = null; - List GetAnidbTitles() + IReadOnlyList? anidbTitles = null; + IReadOnlyList GetAnidbTitles() => anidbTitles ??= RepoFactory.AniDB_Episode_Title.GetByEpisodeID(AniDB_EpisodeID); // Lazy load TMDB titles if needed. @@ -199,10 +199,10 @@ public IReadOnlyList GetImages(ImageEntityType? entityType = nul public SVR_AnimeSeries? AnimeSeries => RepoFactory.AnimeSeries.GetByID(AnimeSeriesID); - public List VideoLocals + public IReadOnlyList VideoLocals => RepoFactory.VideoLocal.GetByAniDBEpisodeID(AniDB_EpisodeID); - public List FileCrossReferences + public IReadOnlyList FileCrossReferences => RepoFactory.CrossRef_File_Episode.GetByEpisodeID(AniDB_EpisodeID); #endregion diff --git a/Shoko.Server/Models/SVR_AnimeSeries.cs b/Shoko.Server/Models/SVR_AnimeSeries.cs index 8a9471601..f548304c6 100644 --- a/Shoko.Server/Models/SVR_AnimeSeries.cs +++ b/Shoko.Server/Models/SVR_AnimeSeries.cs @@ -310,7 +310,7 @@ IReadOnlyList GetTmdbOverviews() #endregion - public List VideoLocals => RepoFactory.VideoLocal.GetByAniDBAnimeID(AniDB_ID); + public IReadOnlyList VideoLocals => RepoFactory.VideoLocal.GetByAniDBAnimeID(AniDB_ID); public IReadOnlyList AnimeEpisodes => RepoFactory.AnimeEpisode.GetBySeriesID(AnimeSeriesID) .Where(episode => !episode.IsHidden) @@ -444,7 +444,8 @@ public IReadOnlyList GetTmdbSeasonCrossReferences(in #region Trakt - public List TraktShowCrossReferences => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AniDB_ID); + public IReadOnlyList TraktShowCrossReferences + => RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AniDB_ID); public List TraktShow { @@ -467,7 +468,7 @@ public List TraktShow #region MAL - public List MALCrossReferences + public IReadOnlyList MalCrossReferences => RepoFactory.CrossRef_AniDB_MAL.GetByAnimeID(AniDB_ID); #endregion diff --git a/Shoko.Server/Models/SVR_CrossRef_File_Episode.cs b/Shoko.Server/Models/SVR_CrossRef_File_Episode.cs index 817fdb25e..86558c463 100644 --- a/Shoko.Server/Models/SVR_CrossRef_File_Episode.cs +++ b/Shoko.Server/Models/SVR_CrossRef_File_Episode.cs @@ -11,7 +11,7 @@ namespace Shoko.Server.Models; public class SVR_CrossRef_File_Episode : CrossRef_File_Episode, IVideoCrossReference { - public SVR_VideoLocal? VideoLocal => RepoFactory.VideoLocal.GetByHash(Hash); + public SVR_VideoLocal? VideoLocal => RepoFactory.VideoLocal.GetByEd2k(Hash); public SVR_AniDB_Episode? AniDBEpisode => RepoFactory.AniDB_Episode.GetByEpisodeID(EpisodeID); diff --git a/Shoko.Server/Models/SVR_ImportFolder.cs b/Shoko.Server/Models/SVR_ImportFolder.cs index 6ba6a552e..74c983a59 100755 --- a/Shoko.Server/Models/SVR_ImportFolder.cs +++ b/Shoko.Server/Models/SVR_ImportFolder.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; using System.Xml.Serialization; using Newtonsoft.Json; using Shoko.Models.Server; using Shoko.Plugin.Abstractions.DataModels; +using Shoko.Server.Repositories; namespace Shoko.Server.Models; @@ -88,6 +90,9 @@ public override string ToString() public bool CanAcceptFile(IVideoFile file) => file is not null && (file.ImportFolderID == ImportFolderID || file.Size < AvailableFreeSpace); + public IReadOnlyList Places + => RepoFactory.VideoLocalPlace.GetByImportFolder(ImportFolderID); + #region IImportFolder Implementation int IImportFolder.ID => ImportFolderID; diff --git a/Shoko.Server/Models/SVR_VideoLocal.cs b/Shoko.Server/Models/SVR_VideoLocal.cs index f99bc90b1..73186f50f 100644 --- a/Shoko.Server/Models/SVR_VideoLocal.cs +++ b/Shoko.Server/Models/SVR_VideoLocal.cs @@ -76,8 +76,7 @@ public TimeSpan DurationTimeSpan public MediaContainer? MediaInfo { get; set; } - - public List Places => VideoLocalID == 0 ? new List() : RepoFactory.VideoLocalPlace.GetByVideoLocal(VideoLocalID); + public IReadOnlyList Places => VideoLocalID is 0 ? [] : RepoFactory.VideoLocalPlace.GetByVideoLocal(VideoLocalID); public SVR_AniDB_File? AniDBFile => RepoFactory.AniDB_File.GetByHash(Hash); @@ -85,23 +84,30 @@ internal AniDB_ReleaseGroup? ReleaseGroup { get { - var anifile = AniDBFile; - if (anifile == null) return null; + if (AniDBFile is not { } anidbFile) + return null; - return RepoFactory.AniDB_ReleaseGroup.GetByGroupID(anifile.GroupID); + return RepoFactory.AniDB_ReleaseGroup.GetByGroupID(anidbFile.GroupID); } } - public List AnimeEpisodes => RepoFactory.AnimeEpisode.GetByHash(Hash); - + public IReadOnlyList AnimeEpisodes + => RepoFactory.AnimeEpisode.GetByHash(Hash); - public List EpisodeCrossRefs => - string.IsNullOrEmpty(Hash) ? [] : RepoFactory.CrossRef_File_Episode.GetByHash(Hash); + public IReadOnlyList EpisodeCrossReferences => + string.IsNullOrEmpty(Hash) ? [] : RepoFactory.CrossRef_File_Episode.GetByEd2k(Hash); - public SVR_VideoLocal_Place? FirstValidPlace => Places.Where(p => !string.IsNullOrEmpty(p?.FullServerPath)).MinBy(a => a.ImportFolderType); + public SVR_VideoLocal_Place? FirstValidPlace + => Places + .Where(p => !string.IsNullOrEmpty(p?.FullServerPath)) + .MinBy(a => a.ImportFolderType); - public SVR_VideoLocal_Place? FirstResolvedPlace => Places.Where(p => !string.IsNullOrEmpty(p?.FullServerPath)).OrderBy(a => a.ImportFolderType) - .FirstOrDefault(p => File.Exists(p.FullServerPath)); + public SVR_VideoLocal_Place? FirstResolvedPlace + => Places + .Select(location => (location, importFolder: location.ImportFolder, fullPath: location.FullServerPath)) + .Where(tuple => tuple.importFolder is not null && !string.IsNullOrEmpty(tuple.fullPath)) + .OrderBy(a => a.importFolder!.ImportFolderType) + .FirstOrDefault(p => File.Exists(p.fullPath)).location; public override string ToString() { @@ -163,16 +169,16 @@ public bool HasAnyEmptyHashes() IMediaInfo? IVideo.MediaInfo => MediaInfo; - IReadOnlyList IVideo.CrossReferences => EpisodeCrossRefs; + IReadOnlyList IVideo.CrossReferences => EpisodeCrossReferences; IReadOnlyList IVideo.Episodes => - EpisodeCrossRefs + EpisodeCrossReferences .Select(x => x.AnimeEpisode) .WhereNotNull() .ToArray(); IReadOnlyList IVideo.Series => - EpisodeCrossRefs + EpisodeCrossReferences .DistinctBy(x => x.AnimeID) .Select(x => x.AnimeSeries) .WhereNotNull() @@ -180,7 +186,7 @@ public bool HasAnyEmptyHashes() .ToArray(); IReadOnlyList IVideo.Groups => - EpisodeCrossRefs + EpisodeCrossReferences .DistinctBy(x => x.AnimeID) .Select(x => x.AnimeSeries) .WhereNotNull() diff --git a/Shoko.Server/Models/SVR_VideoLocal_Place.cs b/Shoko.Server/Models/SVR_VideoLocal_Place.cs index 3057f41c1..e12134f76 100644 --- a/Shoko.Server/Models/SVR_VideoLocal_Place.cs +++ b/Shoko.Server/Models/SVR_VideoLocal_Place.cs @@ -19,7 +19,7 @@ public string? FullServerPath if (string.IsNullOrEmpty(importFolderLocation) || string.IsNullOrEmpty(FilePath)) return null; - return Path.Combine(importFolderLocation, FilePath); + return Path.Join(importFolderLocation, FilePath); } } diff --git a/Shoko.Server/Models/SVR_VideoLocal_User.cs b/Shoko.Server/Models/SVR_VideoLocal_User.cs index 5e0d6a319..93596ab1c 100644 --- a/Shoko.Server/Models/SVR_VideoLocal_User.cs +++ b/Shoko.Server/Models/SVR_VideoLocal_User.cs @@ -2,6 +2,7 @@ using Shoko.Models.Server; using Shoko.Server.Repositories; +#nullable enable namespace Shoko.Server.Models; public class SVR_VideoLocal_User : VideoLocal_User @@ -28,16 +29,17 @@ public TimeSpan? ResumePositionTimeSpan /// /// Get the related . /// - public SVR_VideoLocal GetVideoLocal() - { - return RepoFactory.VideoLocal.GetByID(VideoLocalID); - } + public SVR_VideoLocal? VideoLocal + => RepoFactory.VideoLocal.GetByID(VideoLocalID); public override string ToString() { - var file = GetVideoLocal(); + var video = VideoLocal; + if (video == null) + return $"{VideoLocalID} -- User {JMMUserID}"; + #pragma warning disable CS0618 - return $"{file.FileName} --- {file.Hash} --- User {JMMUserID}"; + return $"{video.FileName} --- {video.Hash} --- User {JMMUserID}"; #pragma warning restore CS0618 } } diff --git a/Shoko.Server/Providers/AniDB/HTTP/AnimeCreator.cs b/Shoko.Server/Providers/AniDB/HTTP/AnimeCreator.cs index 59a924d8d..d4ba3fbb5 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/AnimeCreator.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/AnimeCreator.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Quartz; +using Shoko.Commons.Extensions; using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Plugin.Abstractions.Enums; @@ -17,11 +18,12 @@ using Shoko.Server.Models.AniDB; using Shoko.Server.Models.CrossReference; using Shoko.Server.Providers.AniDB.HTTP.GetAnime; +using Shoko.Server.Providers.TMDB; using Shoko.Server.Repositories; -using Shoko.Server.Repositories.Cached; using Shoko.Server.Scheduling; using Shoko.Server.Scheduling.Jobs.AniDB; using Shoko.Server.Scheduling.Jobs.Shoko; +using Shoko.Server.Server; using Shoko.Server.Settings; using Shoko.Server.Utilities; @@ -527,12 +529,12 @@ private static (bool animeUpdated, bool descriptionUpdated) PopulateAnime(Respon shokoEpisodesToRemove.Add(shokoEpisode); var xrefs = RepoFactory.CrossRef_File_Episode.GetByEpisodeID(episode.EpisodeID); var videos = xrefs - .Select(xref => RepoFactory.VideoLocal.GetByHashAndSize(xref.Hash, xref.FileSize)) + .Select(xref => RepoFactory.VideoLocal.GetByEd2kAndSize(xref.Hash, xref.FileSize)) .Where(video => video != null) .ToList(); var anidbFiles = xrefs .Where(xref => xref.CrossRefSource == (int)CrossRefSource.AniDB) - .Select(xref => RepoFactory.AniDB_File.GetByHashAndFileSize(xref.Hash, xref.FileSize)) + .Select(xref => RepoFactory.AniDB_File.GetByEd2kAndFileSize(xref.Hash, xref.FileSize)) .Where(anidbFile => anidbFile != null) .ToList(); var tmdbXRefs = RepoFactory.CrossRef_AniDB_TMDB_Episode.GetByAnidbEpisodeID(episode.EpisodeID); @@ -553,12 +555,12 @@ private static (bool animeUpdated, bool descriptionUpdated) PopulateAnime(Respon shokoEpisodesToRemove.Add(shokoEpisode); var xrefs = RepoFactory.CrossRef_File_Episode.GetByEpisodeID(episode.EpisodeID); var videos = xrefs - .Select(xref => RepoFactory.VideoLocal.GetByHashAndSize(xref.Hash, xref.FileSize)) + .Select(xref => RepoFactory.VideoLocal.GetByEd2kAndSize(xref.Hash, xref.FileSize)) .Where(video => video != null) .ToList(); var anidbFiles = xrefs .Where(xref => xref.CrossRefSource == (int)CrossRefSource.AniDB) - .Select(xref => RepoFactory.AniDB_File.GetByHashAndFileSize(xref.Hash, xref.FileSize)) + .Select(xref => RepoFactory.AniDB_File.GetByEd2kAndFileSize(xref.Hash, xref.FileSize)) .Where(anidbFile => anidbFile != null) .ToList(); var tmdbXRefs = RepoFactory.CrossRef_AniDB_TMDB_Episode.GetByAnidbEpisodeID(episode.EpisodeID); @@ -779,398 +781,386 @@ public static bool CreateTags(List tags, SVR_AniDB_Anime anime) return xrefsToSave.Count > 0 || xrefsToDelete.Count > 0; } - private void CreateCharacters(List chars, SVR_AniDB_Anime anime) + public void CreateCharacters(List chars, SVR_AniDB_Anime anime) { if (chars == null) return; - // delete all the existing cross-references just in case one has been removed - var animeChars = RepoFactory.AniDB_Anime_Character.GetByAnimeID(anime.AnimeID); - - try - { - RepoFactory.AniDB_Anime_Character.Delete(animeChars); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to Remove Characters for {MainTitle}", anime.MainTitle); - } - - // delete existing relationships to seiyuu's - var charSeiyuusToDelete = chars.SelectMany(rawchar => RepoFactory.AniDB_Character_Creator.GetByCharacterID(rawchar.CharacterID)).ToList(); + var charBasePath = ImageUtils.GetBaseAniDBCharacterImagesPath() + Path.DirectorySeparatorChar; + var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath() + Path.DirectorySeparatorChar; + var settings = _settingsProvider.GetSettings(); - try - { - RepoFactory.AniDB_Character_Creator.Delete(charSeiyuusToDelete); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to Remove Seiyuus for {MainTitle}", anime.MainTitle); - } + var existingCreators = new Dictionary(); + var existingXrefs = RepoFactory.AniDB_Anime_Character.GetByAnimeID(anime.AnimeID) + .ToLookup(a => a.CharacterID); + var existingCreatorXrefs = RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(anime.AnimeID) + .ToLookup(a => (a.CharacterID, a.CreatorID)); - var chrsToSave = new List(); - var xrefsToSave = new List(); + var charactersToKeep = new HashSet(); + var charactersToSave = new List(); + var characterXrefsToKeep = new HashSet(); + var characterXrefsToSave = new List(); var creatorsToSchedule = new HashSet(); - var creatorsToSave = new Dictionary(); - var creatorXrefToSave = new List(); - - var charBasePath = ImageUtils.GetBaseAniDBCharacterImagesPath() + Path.DirectorySeparatorChar; - var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath() + Path.DirectorySeparatorChar; - var charLookup = chars.ToLookup(a => a.CharacterID); + var creatorsToSave = new List(); + var creatorXrefsToKeep = new HashSet(); + var creatorXrefsToSave = new List(); - foreach (var groupings in charLookup.Where(a => a.Count() > 1)) + try { - _logger.LogWarning("Anime had a duplicate character listing for CharacterID: {CharID}", groupings.Key); - } + var charLookup = chars.ToLookup(a => a.CharacterID); + foreach (var groupings in charLookup.Where(a => a.Count() > 1)) + _logger.LogWarning("Anime had a duplicate character listing for CharacterID: {CharID}", groupings.Key); - foreach (var grouping in charLookup) - { - try + var characterOrdering = 0; + foreach (var (rawCharacter, _) in charLookup) { - var rawchar = grouping.FirstOrDefault(); - var chr = RepoFactory.AniDB_Character.GetByCharID(rawchar.CharacterID) ?? new AniDB_Character(); + var characterIndex = characterOrdering++; + if (rawCharacter.AnimeID != anime.AnimeID || rawCharacter.CharacterID <= 0 || string.IsNullOrEmpty(rawCharacter.CharacterType)) + continue; - if (chr.CharID != 0) + var gender = rawCharacter.Gender switch { - // only update the fields that come from HTTP API - if (string.IsNullOrEmpty(rawchar?.CharacterName)) continue; - - chr.CharDescription = rawchar.CharacterDescription ?? string.Empty; - chr.CharName = rawchar.CharacterName; - chr.PicName = rawchar.PicName ?? string.Empty; - } - else + null => PersonGender.Unknown, + _ => Enum.TryParse(rawCharacter.Gender, true, out var result) ? result : PersonGender.Unknown + }; + var character = RepoFactory.AniDB_Character.GetByCharacterID(rawCharacter.CharacterID) ?? new() { - if (rawchar == null) continue; - if (rawchar.CharacterID <= 0 || string.IsNullOrEmpty(rawchar.CharacterName)) continue; - - chr.CharID = rawchar.CharacterID; - chr.CharDescription = rawchar.CharacterDescription ?? string.Empty; - chr.CharKanjiName = rawchar.CharacterKanjiName ?? string.Empty; - chr.CharName = rawchar.CharacterName; - chr.PicName = rawchar.PicName ?? string.Empty; - } - - chrsToSave.Add(chr); - - var character = RepoFactory.AnimeCharacter.GetByAniDBID(chr.CharID); - if (character == null) + CharacterID = rawCharacter.CharacterID, + }; + if (character.AniDB_CharacterID is 0) { - character = new AnimeCharacter - { - AniDBID = chr.CharID, - Name = chr.CharName, - AlternateName = rawchar.CharacterKanjiName, - Description = chr.CharDescription, - ImagePath = chr.GetFullImagePath()?.Replace(charBasePath, "") - }; - // we need an ID for xref - RepoFactory.AnimeCharacter.Save(character); + if (rawCharacter == null) continue; + if (rawCharacter.CharacterID <= 0 || string.IsNullOrEmpty(rawCharacter.CharacterName)) continue; + + character.Description = rawCharacter.CharacterDescription ?? string.Empty; + character.OriginalName = rawCharacter.CharacterKanjiName ?? string.Empty; + character.Name = rawCharacter.CharacterName; + character.ImagePath = rawCharacter.PicName ?? string.Empty; + character.Gender = gender; + charactersToSave.Add(character); } else { + if (string.IsNullOrEmpty(rawCharacter?.CharacterName)) continue; + var updated = false; - if (character.Name != chr.CharName) + if (character.Description != (rawCharacter.CharacterDescription ?? string.Empty)) + { + character.Description = rawCharacter.CharacterDescription ?? string.Empty; + updated = true; + } + if (character.Name != rawCharacter.CharacterName) { - character.Name = chr.CharName; + character.Name = rawCharacter.CharacterName; updated = true; } - if (character.AlternateName != chr.CharKanjiName) + if (character.OriginalName != (rawCharacter.CharacterKanjiName ?? string.Empty)) { - character.AlternateName = chr.CharKanjiName; + character.OriginalName = rawCharacter.CharacterKanjiName ?? string.Empty; updated = true; } - if (character.Description != chr.CharDescription) + if (character.ImagePath != (rawCharacter.PicName ?? string.Empty)) { - character.Description = chr.CharDescription; + character.ImagePath = rawCharacter.PicName ?? string.Empty; updated = true; } - var imagePath = chr.GetFullImagePath()?.Replace(charBasePath, ""); - if (character.ImagePath != imagePath) + if (character.Gender != gender) { - character.ImagePath = imagePath; + character.Gender = gender; updated = true; } if (updated) - RepoFactory.AnimeCharacter.Save(character); + charactersToSave.Add(character); + charactersToKeep.Add(character.AniDB_CharacterID); } - // create cross ref's between anime and character, but don't actually download anything - var animeChar = new AniDB_Anime_Character(); - if (rawchar.AnimeID <= 0 || rawchar.CharacterID <= 0 || string.IsNullOrEmpty(rawchar.CharacterType)) continue; - - animeChar.CharID = rawchar.CharacterID; - animeChar.AnimeID = rawchar.AnimeID; - animeChar.CharType = rawchar.CharacterType; - xrefsToSave.Add(animeChar); - - var seiyuuLookup = rawchar.Seiyuus.ToLookup(a => (rawchar.CharacterID, a.SeiyuuID)); - if (seiyuuLookup.Any(a => a.Count() > 1)) + var appearance = rawCharacter.CharacterType; + var appearanceType = appearance switch + { + "main character in" => CharacterAppearanceType.Main_Character, + "secondary cast in" => CharacterAppearanceType.Minor_Character, + "appears in" => CharacterAppearanceType.Background_Character, + "cameo appearance in" => CharacterAppearanceType.Cameo, + _ => CharacterAppearanceType.Unknown, + }; + var xref = existingXrefs.Contains(rawCharacter.CharacterID) + ? existingXrefs[rawCharacter.CharacterID].First() + : new AniDB_Anime_Character() + { + AnimeID = anime.AnimeID, + CharacterID = rawCharacter.CharacterID, + }; + if (xref.AniDB_Anime_CharacterID == 0) { - foreach (var groupings in seiyuuLookup.Where(a => a.Count() > 1)) + xref.Ordering = characterIndex; + xref.Appearance = appearance; + xref.AppearanceType = appearanceType; + characterXrefsToSave.Add(xref); + } + else + { + var updated = false; + if (xref.Ordering != characterIndex) { - _logger.LogWarning("Anime had a duplicate seiyuu listing for SeiyuuID: {SeiyuuID} and CharacterID: {CharID}", groupings.Key.SeiyuuID, groupings.Key.CharacterID); + xref.Ordering = characterIndex; + updated = true; } + if (xref.Appearance != appearance) + { + xref.Appearance = appearance; + updated = true; + } + if (xref.AppearanceType != appearanceType) + { + xref.AppearanceType = appearanceType; + updated = true; + } + if (updated) + characterXrefsToSave.Add(xref); + characterXrefsToKeep.Add(xref.AniDB_Anime_CharacterID); } - var settings = _settingsProvider.GetSettings(); - foreach (var seiyuuGrouping in seiyuuLookup) + var creatorLookup = rawCharacter.Seiyuus.ToLookup(a => a.SeiyuuID); + foreach (var groupings in creatorLookup.Where(a => a.Count() > 1)) + _logger.LogWarning("Anime had a duplicate voice actor listing for SeiyuuID: {SeiyuuID} and CharacterID: {CharID}", groupings.Key, rawCharacter.CharacterID); + + var actorOrdering = 0; + foreach (var (rawSeiyuu, _) in creatorLookup) { - try + var actorIndex = actorOrdering++; + if (!existingCreators.TryGetValue(rawSeiyuu.SeiyuuID, out var creator)) { - var rawSeiyuu = seiyuuGrouping.FirstOrDefault(); - // save the link between character and seiyuu - // this should always be null - creatorXrefToSave.Add(new() { CharacterID = chr.CharID, CreatorID = rawSeiyuu.SeiyuuID }); - - // save the seiyuu - var creator = RepoFactory.AniDB_Creator.GetByCreatorID(rawSeiyuu.SeiyuuID) ?? new() + creator = RepoFactory.AniDB_Creator.GetByCreatorID(rawSeiyuu.SeiyuuID) ?? new() { CreatorID = rawSeiyuu.SeiyuuID, - Name = rawSeiyuu.SeiyuuName, Type = CreatorType.Unknown, - ImagePath = rawSeiyuu.PicName, }; - if (string.IsNullOrEmpty(creator.Name) && !string.IsNullOrEmpty(rawSeiyuu.SeiyuuName)) + if (creator.AniDB_CreatorID == 0) + { creator.Name = rawSeiyuu.SeiyuuName; - if (string.IsNullOrEmpty(creator.ImagePath) && !string.IsNullOrEmpty(rawSeiyuu.PicName)) creator.ImagePath = rawSeiyuu.PicName; - - creatorsToSave[creator.CreatorID] = creator; - if (settings.AniDb.DownloadCreators && creator.Type is CreatorType.Unknown) - creatorsToSchedule.Add(creator.CreatorID); - - var staff = RepoFactory.AnimeStaff.GetByAniDBID(creator.CreatorID); - if (staff == null) - { - staff = new AnimeStaff - { - // Unfortunately, most of the info is not provided - AniDBID = creator.CreatorID, - Name = rawSeiyuu.SeiyuuName, - ImagePath = creator.GetFullImagePath()?.Replace(creatorBasePath, ""), - }; - // we need an ID for xref - RepoFactory.AnimeStaff.Save(staff); + creatorsToSave.Add(creator); } else { var updated = false; - if (!string.IsNullOrEmpty(creator.Name) && !string.Equals(staff.Name, creator.Name)) + if (string.IsNullOrEmpty(creator.Name) && !string.IsNullOrEmpty(rawSeiyuu.SeiyuuName)) { - staff.Name = creator.Name; + creator.Name = rawSeiyuu.SeiyuuName; updated = true; } - if (!string.IsNullOrEmpty(creator.OriginalName) && !string.Equals(staff.AlternateName, creator.OriginalName)) + if (string.IsNullOrEmpty(creator.ImagePath) && !string.IsNullOrEmpty(rawSeiyuu.PicName)) { - staff.AlternateName = creator.OriginalName; - updated = true; - } - var imagePath = creator.GetFullImagePath().Replace(creatorBasePath, ""); - if (!string.IsNullOrEmpty(imagePath) && !string.Equals(staff.ImagePath, imagePath)) - { - staff.ImagePath = imagePath; + creator.ImagePath = rawSeiyuu.PicName; updated = true; } if (updated) - RepoFactory.AnimeStaff.Save(staff); - } - - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByParts(anime.AnimeID, - character.CharacterID, - staff.StaffID, StaffRoleType.Seiyuu); - if (xrefAnimeStaff != null) - { - continue; + creatorsToSave.Add(creator); } - var role = rawchar.CharacterType; - if (CrossRef_Anime_StaffRepository.Roles.TryGetValue(role, out var role1)) - { - role = role1.ToString().Replace("_", " "); - } + if (settings.AniDb.DownloadCreators && creator.Type is CreatorType.Unknown) + creatorsToSchedule.Add(creator.CreatorID); + existingCreators[rawSeiyuu.SeiyuuID] = creator; + } - xrefAnimeStaff = new CrossRef_Anime_Staff + var creatorXref = existingCreatorXrefs.Contains((rawCharacter.CharacterID, rawSeiyuu.SeiyuuID)) + ? existingCreatorXrefs[(rawCharacter.CharacterID, rawSeiyuu.SeiyuuID)].First() + : new AniDB_Anime_Character_Creator() { - AniDB_AnimeID = anime.AnimeID, - Language = "Japanese", - RoleType = (int)StaffRoleType.Seiyuu, - Role = role, - RoleID = character.CharacterID, - StaffID = staff.StaffID + AnimeID = anime.AnimeID, + CharacterID = rawCharacter.CharacterID, + CreatorID = rawSeiyuu.SeiyuuID, }; - RepoFactory.CrossRef_Anime_Staff.Save(xrefAnimeStaff); + if (creatorXref.AniDB_Anime_Character_CreatorID == 0) + { + creatorXref.Ordering = actorIndex; + creatorXrefsToSave.Add(creatorXref); } - catch (Exception e) + else { - _logger.LogError(e, "Unable to Populate and Save Seiyuus for {MainTitle}", anime.AnimeID); + var updated = false; + if (creatorXref.Ordering != actorIndex) + { + creatorXref.Ordering = actorIndex; + updated = true; + } + if (updated) + creatorXrefsToSave.Add(creatorXref); + creatorXrefsToKeep.Add(creatorXref.AniDB_Anime_Character_CreatorID); } } } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to Populate and Save Characters for {MainTitle}", anime.AnimeID); - } } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to Populate and Save Characters for {MainTitle} (Anime={AnimeID})", anime.MainTitle, anime.AnimeID); + // If we continue we may be doing potential damage to otherwise existing entries, so abort for now. + return; + } + + var xrefsToDelete = existingXrefs + .SelectMany(x => x) + .ExceptBy(characterXrefsToKeep, x => x.AniDB_Anime_CharacterID) + .ToList(); + var xrefsCreatorToDelete = existingCreatorXrefs + .SelectMany(x => x) + .ExceptBy(creatorXrefsToKeep, x => x.AniDB_Anime_Character_CreatorID) + .ToList(); + var charactersToRemove = xrefsToDelete + .Select(x => x.Character) + .WhereNotNull() + .Where(x => !x.GetRoles().Concat(characterXrefsToSave.Where(y => y.CharacterID == x.CharacterID)).ExceptBy(xrefsToDelete.Select(y => y.AniDB_Anime_CharacterID), y => y.AniDB_Anime_CharacterID).Any()) + .ToList(); + var creatorsToRemove = xrefsCreatorToDelete + .Select(x => x.Creator) + .WhereNotNull() + .Where(x => x.Staff.Count == 0 && !x.Characters.Concat(creatorXrefsToSave.Where(y => y.CreatorID == x.CreatorID)).ExceptBy(xrefsCreatorToDelete.Select(y => y.AniDB_Anime_Character_CreatorID), y => y.AniDB_Anime_Character_CreatorID).Any()) + .ToList(); try { - RepoFactory.AniDB_Character.Save(chrsToSave); - RepoFactory.AniDB_Anime_Character.Save(xrefsToSave); - RepoFactory.AniDB_Creator.Save(creatorsToSave.Values.ToList()); - RepoFactory.AniDB_Character_Creator.Save(creatorXrefToSave); + RepoFactory.AniDB_Creator.Save(creatorsToSave); + RepoFactory.AniDB_Creator.Delete(creatorsToRemove); + + RepoFactory.AniDB_Character.Save(charactersToSave); + RepoFactory.AniDB_Character.Delete(charactersToRemove); + + RepoFactory.AniDB_Anime_Character.Save(characterXrefsToSave); + RepoFactory.AniDB_Anime_Character.Delete(xrefsToDelete); + + RepoFactory.AniDB_Anime_Character_Creator.Save(creatorXrefsToSave); + RepoFactory.AniDB_Anime_Character_Creator.Delete(xrefsCreatorToDelete); } catch (Exception ex) { - _logger.LogError(ex, "Unable to Save Characters and Seiyuus for {MainTitle}", anime.MainTitle); + _logger.LogError(ex, "Unable to save characters and creators for {MainTitle}", anime.MainTitle); } ScheduleCreators(creatorsToSchedule, anime.MainTitle); } - private void CreateStaff(List staffList, SVR_AniDB_Anime anime) + public void CreateStaff(List staffList, SVR_AniDB_Anime anime) { if (staffList == null) return; - // delete all the existing cross-references just in case one has been removed - var animeStaff = RepoFactory.AniDB_Anime_Staff.GetByAnimeID(anime.AnimeID); + var settings = _settingsProvider.GetSettings(); + var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath() + Path.DirectorySeparatorChar; - try - { - RepoFactory.AniDB_Anime_Staff.Delete(animeStaff); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to Remove Staff for {MainTitle}", anime.MainTitle); - } + var existingCreators = new Dictionary(); + var existingXrefs = RepoFactory.AniDB_Anime_Staff.GetByAnimeID(anime.AnimeID) + .ToLookup(x => (x.AnimeID, x.CreatorID, x.Role)); - var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath() + Path.DirectorySeparatorChar; var creatorsToSchedule = new HashSet(); var creatorsToSave = new List(); - var animeStaffToSave = new List(); - var xRefToSave = new List(); - - var staffLookup = staffList.ToLookup(a => (a.AnimeID, a.CreatorID, a.CreatorType)); - if (staffLookup.Any(a => a.Count() > 1)) + var creatorXrefsToKeep = new HashSet(); + var creatorXrefsToSave = new List(); + try { + var staffLookup = staffList.ToLookup(a => (a.AnimeID, a.CreatorID, a.CreatorType)); foreach (var groupings in staffLookup.Where(a => a.Count() > 1)) - { _logger.LogWarning("Anime had a duplicate staff listing for CreatorID: {CreatorID} and CreatorType: {CreatorType}", groupings.Key.CreatorID, groupings.Key.CreatorType); - } - } - var settings = _settingsProvider.GetSettings(); - foreach (var grouping in staffLookup) - { - try + var staffOrdering = 0; + foreach (var (rawStaff, _) in staffLookup) { - var rawStaff = grouping.FirstOrDefault(); - animeStaffToSave.Add(new AniDB_Anime_Staff - { - AnimeID = rawStaff.AnimeID, - CreatorID = rawStaff.CreatorID, - CreatorType = rawStaff.CreatorType - }); - - var creator = RepoFactory.AniDB_Creator.GetByCreatorID(rawStaff.CreatorID); - if (creator is null) + var staffIndex = staffOrdering++; + if (!existingCreators.TryGetValue(rawStaff.CreatorID, out var creator)) { - creator = new() + creator = RepoFactory.AniDB_Creator.GetByCreatorID(rawStaff.CreatorID) ?? new() { CreatorID = rawStaff.CreatorID, - Name = rawStaff.CreatorName, Type = CreatorType.Unknown, }; - creatorsToSave.Add(creator); - } - else if (string.IsNullOrEmpty(creator.Name) && !string.IsNullOrEmpty(rawStaff.CreatorName)) - { - creator.Name = rawStaff.CreatorName; - creatorsToSave.Add(creator); + if (creator.AniDB_CreatorID == 0) + { + creator.Name = rawStaff.CreatorName; + creatorsToSave.Add(creator); + } + else + { + var updated = false; + if (string.IsNullOrEmpty(creator.Name) && !string.IsNullOrEmpty(rawStaff.CreatorName)) + { + creator.Name = rawStaff.CreatorName; + updated = true; + } + if (updated) + creatorsToSave.Add(creator); + } + + if (settings.AniDb.DownloadCreators && creator.Type is CreatorType.Unknown) + creatorsToSchedule.Add(creator.CreatorID); + existingCreators[rawStaff.CreatorID] = creator; } - if (settings.AniDb.DownloadCreators && creator.Type is CreatorType.Unknown) - creatorsToSchedule.Add(creator.CreatorID); - var staff = RepoFactory.AnimeStaff.GetByAniDBID(creator.CreatorID); - if (staff == null) + var role = rawStaff.CreatorType; + var roleType = role switch { - staff = new AnimeStaff + "Animation Work" => CreatorRoleType.Studio, + "Work" => CreatorRoleType.Studio, + "Original Work" => CreatorRoleType.SourceWork, + "Music" => CreatorRoleType.Music, + "Character Design" => CreatorRoleType.CharacterDesign, + "Direction" => CreatorRoleType.Director, + "Series Composition" => CreatorRoleType.SeriesComposer, + "Chief Animation Direction" => CreatorRoleType.Producer, + _ => CreatorRoleType.Staff + }; + var staff = existingXrefs.Contains((anime.AnimeID, rawStaff.CreatorID, role)) + ? existingXrefs[(anime.AnimeID, rawStaff.CreatorID, role)].First() + : new AniDB_Anime_Staff() { - AniDBID = creator.CreatorID, - Name = creator.Name, + AnimeID = anime.AnimeID, + CreatorID = rawStaff.CreatorID, + Role = role, }; - // we need an ID for xref - RepoFactory.AnimeStaff.Save(staff); + if (staff.AniDB_Anime_StaffID == 0) + { + staff.Ordering = staffIndex; + staff.RoleType = roleType; } else { var updated = false; - if (!string.IsNullOrEmpty(creator.Name) && !string.Equals(staff.Name, creator.Name)) - { - staff.Name = creator.Name; - updated = true; - } - if (!string.IsNullOrEmpty(creator.OriginalName) && !string.Equals(staff.AlternateName, creator.OriginalName)) + if (staff.Ordering != staffIndex) { - staff.AlternateName = creator.OriginalName; + staff.Ordering = staffIndex; updated = true; } - var imagePath = creator.GetFullImagePath().Replace(creatorBasePath, ""); - if (!string.IsNullOrEmpty(imagePath) && !string.Equals(staff.ImagePath, imagePath)) + if (staff.RoleType != roleType) { - staff.ImagePath = imagePath; + staff.RoleType = roleType; updated = true; } if (updated) - RepoFactory.AnimeStaff.Save(staff); - } - - var roleType = rawStaff.CreatorType switch - { - "Animation Work" => StaffRoleType.Studio, - "Work" => StaffRoleType.Studio, - "Original Work" => StaffRoleType.SourceWork, - "Music" => StaffRoleType.Music, - "Character Design" => StaffRoleType.CharacterDesign, - "Direction" => StaffRoleType.Director, - "Series Composition" => StaffRoleType.SeriesComposer, - "Chief Animation Direction" => StaffRoleType.Producer, - _ => StaffRoleType.Staff - }; - - var xrefAnimeStaff = RepoFactory.CrossRef_Anime_Staff.GetByParts(anime.AnimeID, null, staff.StaffID, roleType); - if (xrefAnimeStaff != null) - { - continue; - } - - var role = rawStaff.CreatorType; - if (CrossRef_Anime_StaffRepository.Roles.TryGetValue(role, out var role1)) - { - role = role1.ToString().Replace("_", " "); + creatorXrefsToSave.Add(staff); + creatorXrefsToKeep.Add(staff.AniDB_Anime_StaffID); } - - xrefAnimeStaff = new CrossRef_Anime_Staff - { - AniDB_AnimeID = anime.AnimeID, - Language = "Japanese", - RoleType = (int)roleType, - Role = role, - RoleID = null, - StaffID = staff.StaffID - }; - xRefToSave.Add(xrefAnimeStaff); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to Populate and Save Staff for {MainTitle}", anime.MainTitle); } } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to Populate and Save Staff for {MainTitle}", anime.MainTitle); + // If we continue we may be doing potential damage to otherwise existing entries, so abort for now. + return; + } + var xrefsCreatorToDelete = existingXrefs + .SelectMany(x => x) + .ExceptBy(creatorXrefsToKeep, x => x.AniDB_Anime_StaffID) + .ToList(); + var creatorsToRemove = xrefsCreatorToDelete + .Select(x => x.Creator) + .WhereNotNull() + .Where(x => x.Characters.Count == 0 && !x.Staff.Concat(creatorXrefsToSave.Where(y => y.CreatorID == x.CreatorID)).ExceptBy(xrefsCreatorToDelete.Select(y => y.AniDB_Anime_StaffID), y => y.AniDB_Anime_StaffID).Any()) + .ToList(); try { - RepoFactory.AniDB_Anime_Staff.Save(animeStaffToSave); - RepoFactory.CrossRef_Anime_Staff.Save(xRefToSave); + RepoFactory.AniDB_Creator.Save(creatorsToSave); + RepoFactory.AniDB_Creator.Delete(creatorsToRemove); + + RepoFactory.AniDB_Anime_Staff.Save(creatorXrefsToSave); + RepoFactory.AniDB_Anime_Staff.Delete(xrefsCreatorToDelete); } catch (Exception ex) { diff --git a/Shoko.Server/Providers/AniDB/HTTP/GetAnime/ResponseCharacter.cs b/Shoko.Server/Providers/AniDB/HTTP/GetAnime/ResponseCharacter.cs index fe466da46..bfd877007 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/GetAnime/ResponseCharacter.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/GetAnime/ResponseCharacter.cs @@ -11,5 +11,6 @@ public class ResponseCharacter public string CharacterKanjiName { get; set; } public string CharacterDescription { get; set; } public string CharacterType { get; set; } + public string Gender { get; set; } public List Seiyuus { get; set; } } diff --git a/Shoko.Server/Providers/AniDB/HTTP/HttpAnimeParser.cs b/Shoko.Server/Providers/AniDB/HTTP/HttpAnimeParser.cs index b7c046b36..98f1513ec 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/HttpAnimeParser.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/HttpAnimeParser.cs @@ -601,6 +601,7 @@ private static ResponseCharacter ParseCharacter(int animeID, XmlNode node) var charType = TryGetAttribute(node, "type"); var charName = TryGetProperty(node, "name")?.Replace('`', '\''); + var charGender = TryGetProperty(node, "gender")?.Replace('`', '\''); var charDescription = TryGetProperty(node, "description")?.Replace('`', '\''); var picName = TryGetProperty(node, "picture"); @@ -631,6 +632,7 @@ private static ResponseCharacter ParseCharacter(int animeID, XmlNode node) CharacterName = charName, CharacterDescription = charDescription, PicName = picName, + Gender = charGender, Seiyuus = seiyuus }; } diff --git a/Shoko.Server/Providers/TraktTV/Contracts/TraktSummaryContainer.cs b/Shoko.Server/Providers/TraktTV/Contracts/TraktSummaryContainer.cs index 7b3d44640..7f20640eb 100644 --- a/Shoko.Server/Providers/TraktTV/Contracts/TraktSummaryContainer.cs +++ b/Shoko.Server/Providers/TraktTV/Contracts/TraktSummaryContainer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using NLog; using Shoko.Models.Server; using Shoko.Server.Repositories; @@ -35,7 +36,7 @@ private void PopulateCrossRefs() { try { - crossRefTraktV2 = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AnimeID); + crossRefTraktV2 = RepoFactory.CrossRef_AniDB_TraktV2.GetByAnimeID(AnimeID).ToList(); } catch (Exception ex) { diff --git a/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs b/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs index da9af2d8a..6da9f1418 100644 --- a/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs +++ b/Shoko.Server/Providers/TraktTV/TraktTVHelper.cs @@ -1765,7 +1765,7 @@ private void RemoveTraktDBEntries(Trakt_Show show) } private EpisodeSyncDetails ReconSyncTraktEpisode(SVR_AnimeSeries ser, SVR_AnimeEpisode ep, - List traktUsers, List collected, + IReadOnlyList traktUsers, List collected, List watched, bool sendNow) { try diff --git a/Shoko.Server/Renamer/RenameFileService.cs b/Shoko.Server/Renamer/RenameFileService.cs index 522ce4486..526b3ed9c 100644 --- a/Shoko.Server/Renamer/RenameFileService.cs +++ b/Shoko.Server/Renamer/RenameFileService.cs @@ -78,7 +78,7 @@ public RelocationResult GetNewPath(SVR_VideoLocal_Place place, RenamerConfig? re var videoLocal = place.VideoLocal ?? throw new NullReferenceException(nameof(place.VideoLocal)); - var xrefs = videoLocal.EpisodeCrossRefs; + var xrefs = videoLocal.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() diff --git a/Shoko.Server/Repositories/BaseCachedRepository.cs b/Shoko.Server/Repositories/BaseCachedRepository.cs index cbe77648a..793f71182 100644 --- a/Shoko.Server/Repositories/BaseCachedRepository.cs +++ b/Shoko.Server/Repositories/BaseCachedRepository.cs @@ -181,12 +181,9 @@ public virtual async Task DeleteWithOpenTransactionAsync(ISession session, T cr) } //This function do not run the BeginDeleteCallback and the EndDeleteCallback - public void DeleteWithOpenTransaction(ISession session, List objs) + public void DeleteWithOpenTransaction(ISession session, IReadOnlyList objs) { - if (objs.Count == 0) - { - return; - } + if (objs.Count == 0) return; foreach (var cr in objs) { @@ -194,10 +191,14 @@ public void DeleteWithOpenTransaction(ISession session, List objs) Lock(() => session.Delete(cr)); } - WriteLock(() => objs.ForEach(DeleteFromCacheUnsafe)); + WriteLock(() => + { + foreach (var obj in objs) + DeleteFromCacheUnsafe(obj); + }); } - - public async Task DeleteWithOpenTransactionAsync(ISession session, List objs) + + public async Task DeleteWithOpenTransactionAsync(ISession session, IReadOnlyList objs) { if (objs.Count == 0) return; @@ -207,7 +208,11 @@ public async Task DeleteWithOpenTransactionAsync(ISession session, List objs) await Lock(async () => await session.DeleteAsync(cr)); } - WriteLock(() => objs.ForEach(DeleteFromCacheUnsafe)); + WriteLock(() => + { + foreach (var obj in objs) + DeleteFromCacheUnsafe(obj); + }); } public virtual void Save(T obj) @@ -331,8 +336,8 @@ public void SaveWithOpenTransaction(ISession session, List objs) WriteLock(() => UpdateCacheUnsafe(obj)); } } - - public async Task SaveWithOpenTransactionAsync(ISession session, List objs) + + public async Task SaveWithOpenTransactionAsync(ISession session, IReadOnlyList objs) { if (objs.Count == 0) return; @@ -453,12 +458,11 @@ private void DeleteFromDatabaseUnsafe(IReadOnlyCollection objs) #region abstract - public abstract void PopulateIndexes(); - public abstract void RegenerateDb(); + public virtual void PopulateIndexes() { } - public virtual void PostProcess() - { - } + public virtual void RegenerateDb() { } + + public virtual void PostProcess() { } protected abstract S SelectKey(T entity); diff --git a/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs index 2c534a8b0..1894076e8 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_AnimeRepository.cs @@ -2,185 +2,43 @@ using System.Collections.Generic; using System.Linq; using NutzCode.InMemoryIndex; -using Shoko.Models.Client; -using Shoko.Models.Enums; -using Shoko.Models.Interfaces; -using Shoko.Models.Server; -using Shoko.Plugin.Abstractions.Enums; using Shoko.Server.Databases; -using Shoko.Server.Extensions; using Shoko.Server.Models; -using Shoko.Server.Models.AniDB; -using Shoko.Server.Models.TMDB; -using Shoko.Server.Repositories.NHibernate; -using Shoko.Server.Server; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_AnimeRepository : BaseCachedRepository +public class AniDB_AnimeRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private static PocoIndex Animes; - - public AniDB_AnimeRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { } + private static PocoIndex? _animeIDs; protected override int SelectKey(SVR_AniDB_Anime entity) - { - return entity.AniDB_AnimeID; - } + => entity.AniDB_AnimeID; public override void PopulateIndexes() { - Animes = new PocoIndex(Cache, a => a.AnimeID); + _animeIDs = new PocoIndex(Cache, a => a.AnimeID); } public override void RegenerateDb() { foreach (var anime in Cache.Values.ToList()) - { anime.ResetPreferredTitle(); - } } - public SVR_AniDB_Anime GetByAnimeID(int id) - { - return ReadLock(() => Animes.GetOne(id)); - } - - public SVR_AniDB_Anime GetByAnimeID(ISessionWrapper session, int id) - { - return GetByAnimeID(id); - } + public SVR_AniDB_Anime GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetOne(animeID)); public List GetForDate(DateTime startDate, DateTime endDate) - { - return ReadLock(() => - Cache.Values.Where(a => a.AirDate.HasValue && a.AirDate.Value >= startDate && a.AirDate.Value <= endDate) - .ToList()); - } - - public List SearchByName(string queryText) - { - return ReadLock(() => - Cache.Values.Where(a => a.AllTitles.Contains(queryText, StringComparison.InvariantCultureIgnoreCase)) - .ToList()); - } - - public Dictionary GetDefaultImagesByAnime(ISessionWrapper session, int[] animeIds) - { - ArgumentNullException.ThrowIfNull(session, nameof(session)); - ArgumentNullException.ThrowIfNull(animeIds, nameof(animeIds)); - var defImagesByAnime = new Dictionary(); - - if (animeIds is { Length: 0 }) - return defImagesByAnime; - - // treating cache as a global DB lock, as well - var results = Lock(() => - { - // TODO: Determine if joining on the correct columns - return session.CreateSQLQuery( - @"SELECT {prefImg.*}, {tmdbPoster.*}, {tmdbBackdrop.*} - FROM AniDB_Anime_PreferredImage prefImg - LEFT OUTER JOIN TMDB_Image AS tmdbPoster - ON tmdbPoster.ImageType = :imagePosterType AND tmdbPoster.TMDB_ImageID = prefImg.ImageID AND prefImg.ImageSource = :tmdbSourceType AND prefImg.ImageParentType = :imagePosterType - LEFT OUTER JOIN TMDB_Image AS tmdbBackdrop - ON tmdbBackdrop.ImageType = :imageBackdropType AND tmdbBackdrop.TMDB_ImageID = prefImg.ImageID AND prefImg.ImageSource = :tmdbSourceType AND prefImg.ImageType = :imageBackdropType - WHERE prefImg.AnimeID IN (:animeIds) AND prefImg.ImageType IN (:imagePosterType, :imageBackdropType)" - ) - .AddEntity("prefImg", typeof(AniDB_Anime_PreferredImage)) - .AddEntity("tmdbPoster", typeof(TMDB_Image)) - .AddEntity("tmdbBackdrop", typeof(TMDB_Image)) - .SetParameterList("animeIds", animeIds) - .SetInt32("tmdbSourceType", (int)DataSourceType.TMDB) - .SetInt32("imageBackdropType", (int)ImageEntityType.Backdrop) - .SetInt32("imagePosterType", (int)ImageEntityType.Poster) - .List(); - }); - - foreach (var result in results) - { - var preferredImage = (AniDB_Anime_PreferredImage)result[0]; - IImageEntity image = null; - switch (preferredImage.ImageType.ToClient(preferredImage.ImageSource)) - { - case CL_ImageEntityType.MovieDB_Poster: - image = ((TMDB_Image)result[1]).ToClientPoster(); - break; - case CL_ImageEntityType.MovieDB_FanArt: - image = ((TMDB_Image)result[2]).ToClientFanart(); - break; - } - - if (image == null) - continue; - - if (!defImagesByAnime.TryGetValue(preferredImage.AnidbAnimeID, out var defImages)) - { - defImages = new DefaultAnimeImages { AnimeID = preferredImage.AnidbAnimeID }; - defImagesByAnime.Add(defImages.AnimeID, defImages); - } - - switch (preferredImage.ImageType) - { - case ImageEntityType.Poster: - defImages.Poster = preferredImage.ToClient(image); - break; - case ImageEntityType.Banner: - defImages.Banner = preferredImage.ToClient(image); - break; - case ImageEntityType.Backdrop: - defImages.Backdrop = preferredImage.ToClient(image); - break; - } - } - - return defImagesByAnime; - } -} - -public class DefaultAnimeImages -{ - public CL_AniDB_Anime_DefaultImage GetPosterContractNoBlanks() - { - if (Poster != null) - { - return Poster; - } - - return new() { AnimeID = AnimeID, ImageType = (int)CL_ImageEntityType.AniDB_Cover }; - } - - public CL_AniDB_Anime_DefaultImage GetFanartContractNoBlanks(CL_AniDB_Anime anime) - { - ArgumentNullException.ThrowIfNull(anime, nameof(anime)); - - if (Backdrop != null) - { - return Backdrop; - } - - var fanarts = anime.Fanarts; - - if (fanarts == null || fanarts.Count == 0) - { - return null; - } - - if (fanarts.Count == 1) - { - return fanarts[0]; - } - - var random = new Random(); - - return fanarts[random.Next(0, fanarts.Count - 1)]; - } - - public int AnimeID { get; set; } - - public CL_AniDB_Anime_DefaultImage Poster { get; set; } - - public CL_AniDB_Anime_DefaultImage Backdrop { get; set; } - - public CL_AniDB_Anime_DefaultImage Banner { get; set; } + => ReadLock(() => + Cache.Values + .Where(a => a.AirDate.HasValue && a.AirDate.Value >= startDate && a.AirDate.Value <= endDate) + .ToList() + ); + + public List SearchByName(string name) + => ReadLock(() => + Cache.Values.Where(a => a.AllTitles.Contains(name, StringComparison.InvariantCultureIgnoreCase)) + .ToList() + ); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Anime_CharacterRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Anime_CharacterRepository.cs index c9fdad1e4..95968e0e4 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_Anime_CharacterRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_Anime_CharacterRepository.cs @@ -1,45 +1,33 @@ using System.Collections.Generic; using System.Linq; using NutzCode.InMemoryIndex; -using Shoko.Models.Server; using Shoko.Server.Databases; +using Shoko.Server.Models.AniDB; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_Anime_CharacterRepository : BaseCachedRepository +public class AniDB_Anime_CharacterRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex _animeIDs; - public List GetByAnimeID(int id) - { - return ReadLock(() => _animeIDs.GetMultiple(id)); - } + private PocoIndex? _animeIDs; - public List GetByCharID(int id) - { - return Lock(() => - { - using var session = _databaseFactory.SessionFactory.OpenStatelessSession(); - return session.Query() - .Where(a => a.CharID == id) - .ToList(); - }); - } + private PocoIndex? _characterIDs; + + protected override int SelectKey(AniDB_Anime_Character entity) + => entity.AniDB_Anime_CharacterID; public override void PopulateIndexes() { _animeIDs = new PocoIndex(Cache, a => a.AnimeID); + _characterIDs = new PocoIndex(Cache, a => a.CharacterID); } - public override void RegenerateDb() - { - } + public IReadOnlyList GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID)); - protected override int SelectKey(AniDB_Anime_Character entity) - { - return entity.AniDB_Anime_CharacterID; - } + public IReadOnlyList GetByCharacterID(int characterID) + => ReadLock(() => _characterIDs!.GetMultiple(characterID)); - public AniDB_Anime_CharacterRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public AniDB_Anime_Character? GetByAnimeIDAndCharacterID(int animeID, int characterID) + => GetByCharacterID(characterID).FirstOrDefault(a => a.AnimeID == animeID); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Anime_Character_CreatorRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Anime_Character_CreatorRepository.cs new file mode 100644 index 000000000..302be2459 --- /dev/null +++ b/Shoko.Server/Repositories/Cached/AniDB_Anime_Character_CreatorRepository.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using NutzCode.InMemoryIndex; +using Shoko.Server.Databases; +using Shoko.Server.Models.AniDB; + +#nullable enable +namespace Shoko.Server.Repositories.Cached; + +public class AniDB_Anime_Character_CreatorRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) +{ + private PocoIndex? _animeIDs; + + private PocoIndex? _characterIDs; + + private PocoIndex? _creatorIDs; + + protected override int SelectKey(AniDB_Anime_Character_Creator entity) + => entity.AniDB_Anime_Character_CreatorID; + + public override void PopulateIndexes() + { + _animeIDs = new PocoIndex(Cache, a => a.AnimeID); + _characterIDs = new PocoIndex(Cache, a => a.CharacterID); + _creatorIDs = new PocoIndex(Cache, a => a.CreatorID); + } + + public IReadOnlyList GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID)); + + public IReadOnlyList GetByCharacterID(int characterID) + => ReadLock(() => _characterIDs!.GetMultiple(characterID)); + + public IReadOnlyList GetByCharacterIDAndAnimeID(int characterID, int animeID) + => GetByCharacterID(characterID).Where(a => a.AnimeID == animeID).ToList(); + + public IReadOnlyList GetByCreatorID(int creatorID) + => ReadLock(() => _creatorIDs!.GetMultiple(creatorID)); +} diff --git a/Shoko.Server/Repositories/Cached/AniDB_Anime_PreferredImageRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Anime_PreferredImageRepository.cs index 92bc8c6a5..3b3e59c88 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_Anime_PreferredImageRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_Anime_PreferredImageRepository.cs @@ -9,31 +9,24 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_Anime_PreferredImageRepository : BaseCachedRepository +public class AniDB_Anime_PreferredImageRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex? AnimeIDs; - - public AniDB_Anime_PreferredImage? GetByAnidbAnimeIDAndType(int animeId, ImageEntityType imageType) - => GetByAnimeID(animeId).FirstOrDefault(a => a.ImageType == imageType); - - public AniDB_Anime_PreferredImage? GetByAnidbAnimeIDAndTypeAndSource(int animeId, ImageEntityType imageType, DataSourceType imageSource) - => GetByAnimeID(animeId).FirstOrDefault(a => a.ImageType == imageType && a.ImageSource == imageSource); - - public List GetByAnimeID(int animeId) - => ReadLock(() => AnimeIDs!.GetMultiple(animeId)); + private PocoIndex? _animeIDs; protected override int SelectKey(AniDB_Anime_PreferredImage entity) => entity.AniDB_Anime_PreferredImageID; public override void PopulateIndexes() { - AnimeIDs = new(Cache, a => a.AnidbAnimeID); + _animeIDs = new(Cache, a => a.AnidbAnimeID); } - public override void RegenerateDb() - { - } + public AniDB_Anime_PreferredImage? GetByAnidbAnimeIDAndType(int animeID, ImageEntityType imageType) + => GetByAnimeID(animeID).FirstOrDefault(a => a.ImageType == imageType); + + public AniDB_Anime_PreferredImage? GetByAnidbAnimeIDAndTypeAndSource(int animeID, ImageEntityType imageType, DataSourceType imageSource) + => GetByAnimeID(animeID).FirstOrDefault(a => a.ImageType == imageType && a.ImageSource == imageSource); - public AniDB_Anime_PreferredImageRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { } + public List GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID)); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Anime_TagRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Anime_TagRepository.cs index 6443f5aae..d3f503c5d 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_Anime_TagRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_Anime_TagRepository.cs @@ -1,93 +1,44 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NutzCode.InMemoryIndex; -using Shoko.Commons.Collections; using Shoko.Models.Server; using Shoko.Server.Databases; -using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_Anime_TagRepository : BaseCachedRepository +public class AniDB_Anime_TagRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Animes; - private PocoIndex TagIDs; + private PocoIndex? _animeIDs; - public override void RegenerateDb() - { - } + private PocoIndex? _tagIDs; protected override int SelectKey(AniDB_Anime_Tag entity) - { - return entity.AniDB_Anime_TagID; - } + => entity.AniDB_Anime_TagID; public override void PopulateIndexes() { - Animes = new PocoIndex(Cache, a => a.AnimeID); - TagIDs = new PocoIndex(Cache, a => a.TagID); - } - - public AniDB_Anime_Tag GetByAnimeIDAndTagID(int animeid, int tagid) - { - return ReadLock(() => Animes.GetMultiple(animeid).FirstOrDefault(a => a.TagID == tagid)); - } - - public List GetByAnimeID(int id) - { - return ReadLock(() => Animes.GetMultiple(id)); - } - - public ILookup GetByAnimeIDs(ICollection ids) - { - if (ids == null) - { - throw new ArgumentNullException(nameof(ids)); - } - - if (ids.Count == 0) - { - return EmptyLookup.Instance; - } - - return ReadLock(() => ids.SelectMany(Animes.GetMultiple).ToLookup(t => t.AnimeID)); + _animeIDs = new PocoIndex(Cache, a => a.AnimeID); + _tagIDs = new PocoIndex(Cache, a => a.TagID); } - public List GetAnimeWithTag(string tagName) - { - return GetAll().AsParallel() - .Where(a => RepoFactory.AniDB_Tag.GetByName(tagName).Select(b => b.TagID).Contains(a.TagID)) - .Select(a => RepoFactory.AnimeSeries.GetByAnimeID(a.AnimeID)) - .Where(a => a != null) - .ToList(); - } + public AniDB_Anime_Tag? GetByAnimeIDAndTagID(int animeID, int tagID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID).FirstOrDefault(a => a.TagID == tagID)); - public List GetAnimeWithTag(int tagID) - { - return GetByTagID(tagID).Select(a => RepoFactory.AnimeSeries.GetByAnimeID(a.AnimeID)) - .Where(a => a != null).ToList(); - } + public List GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID)); public List GetByTagID(int tagID) - { - return ReadLock(() => TagIDs.GetMultiple(tagID)); - } + => ReadLock(() => _tagIDs!.GetMultiple(tagID)); /// /// Gets all the anime tags, but only if we have the anime locally /// /// public List GetAllForLocalSeries() - { - return RepoFactory.AnimeSeries.GetAll() + => RepoFactory.AnimeSeries.GetAll() .SelectMany(a => GetByAnimeID(a.AniDB_ID)) .Where(a => a != null) .Distinct() .ToList(); - } - - public AniDB_Anime_TagRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Anime_TitleRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Anime_TitleRepository.cs index 28903eaab..3f48af66c 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_Anime_TitleRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_Anime_TitleRepository.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NHibernate.Criterion; using NutzCode.InMemoryIndex; -using Shoko.Commons.Collections; using Shoko.Commons.Properties; using Shoko.Server.Databases; using Shoko.Server.Models; -using Shoko.Server.Repositories.NHibernate; using Shoko.Server.Server; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_Anime_TitleRepository : BaseCachedRepository +public class AniDB_Anime_TitleRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Animes; - - public override void PopulateIndexes() - { - Animes = new PocoIndex(Cache, a => a.AnimeID); - } + private PocoIndex? _animeIDs; protected override int SelectKey(SVR_AniDB_Anime_Title entity) + => entity.AniDB_Anime_TitleID; + + public override void PopulateIndexes() { - return entity.AniDB_Anime_TitleID; + _animeIDs = new PocoIndex(Cache, a => a.AnimeID); } public override void RegenerateDb() @@ -39,40 +34,6 @@ public override void RegenerateDb() } } - public List GetByAnimeID(int id) - { - return ReadLock(() => Animes.GetMultiple(id)); - } - - public ILookup GetByAnimeIDs(ISessionWrapper session, ICollection ids) - { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - if (ids == null) - { - throw new ArgumentNullException(nameof(ids)); - } - - if (ids.Count == 0) - { - return EmptyLookup.Instance; - } - - return Lock(session, s => - { - var titles = s.CreateCriteria() - .Add(Restrictions.InG(nameof(SVR_AniDB_Anime_Title.AnimeID), ids)) - .List() - .ToLookup(t => t.AnimeID); - - return titles; - }); - } - - public AniDB_Anime_TitleRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public List GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID)); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_CharacterRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_CharacterRepository.cs index a934bd0a4..09ab4f880 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_CharacterRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_CharacterRepository.cs @@ -2,40 +2,27 @@ using System.Linq; using NutzCode.InMemoryIndex; using Shoko.Commons.Extensions; -using Shoko.Models.Server; using Shoko.Server.Databases; +using Shoko.Server.Models.AniDB; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_CharacterRepository : BaseCachedRepository +public class AniDB_CharacterRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex _charIDs; + private PocoIndex? _characterIDs; - public AniDB_Character GetByCharID(int id) - { - return ReadLock(() => _charIDs.GetOne(id)); - } - - public List GetCharactersForAnime(int animeID) - { - return ReadLock(() => RepoFactory.AniDB_Anime_Character.GetByAnimeID(animeID).Select(a => GetByCharID(a.CharID)).WhereNotNull().ToList()); - } - - public AniDB_CharacterRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + protected override int SelectKey(AniDB_Character entity) + => entity.AniDB_CharacterID; public override void PopulateIndexes() { - _charIDs = new PocoIndex(Cache, a => a.CharID); + _characterIDs = new PocoIndex(Cache, a => a.CharacterID); } - public override void RegenerateDb() - { - } + public AniDB_Character GetByCharacterID(int characterID) + => ReadLock(() => _characterIDs!.GetOne(characterID)); - protected override int SelectKey(AniDB_Character entity) - { - return entity.AniDB_CharacterID; - } + public List GetCharactersForAnime(int animeID) + => ReadLock(() => RepoFactory.AniDB_Anime_Character.GetByAnimeID(animeID).Select(xref => GetByCharacterID(xref.CharacterID)).WhereNotNull().ToList()); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Character_CreatorRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Character_CreatorRepository.cs deleted file mode 100644 index e289da372..000000000 --- a/Shoko.Server/Repositories/Cached/AniDB_Character_CreatorRepository.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NutzCode.InMemoryIndex; -using Shoko.Server.Databases; -using Shoko.Server.Models.AniDB; - -#nullable enable -namespace Shoko.Server.Repositories.Cached; - -public class AniDB_Character_CreatorRepository : BaseCachedRepository -{ - private PocoIndex? _charIDs; - - public List GetByCharacterID(int characterID) - { - return ReadLock(() => _charIDs!.GetMultiple(characterID)); - } - - public List GetByCreatorID(int creatorID) - { - return Lock(() => - { - using var session = _databaseFactory.SessionFactory.OpenSession(); - return session.Query() - .Where(a => a.CreatorID == creatorID) - .ToList(); - }); - } - - public override void PopulateIndexes() - { - _charIDs = new PocoIndex(Cache, a => a.CharacterID); - } - - public override void RegenerateDb() - { - } - - protected override int SelectKey(AniDB_Character_Creator entity) - { - return entity.AniDB_Character_CreatorID; - } - - public AniDB_Character_CreatorRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } -} diff --git a/Shoko.Server/Repositories/Cached/AniDB_CreatorRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_CreatorRepository.cs index 6bcdc70bc..b1cc05037 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_CreatorRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_CreatorRepository.cs @@ -6,13 +6,21 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_CreatorRepository : BaseCachedRepository +public class AniDB_CreatorRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex? _seiyuuIDs; + private PocoIndex? _creatorIDs; - public AniDB_Creator? GetByCreatorID(int id) + protected override int SelectKey(AniDB_Creator entity) + => entity.AniDB_CreatorID; + + public override void PopulateIndexes() + { + _creatorIDs = new PocoIndex(Cache, a => a.CreatorID); + } + + public AniDB_Creator? GetByCreatorID(int creatorID) { - return ReadLock(() => _seiyuuIDs!.GetOne(id)); + return ReadLock(() => _creatorIDs!.GetOne(creatorID)); } public AniDB_Creator? GetByName(string creatorName) @@ -26,22 +34,4 @@ public class AniDB_CreatorRepository : BaseCachedRepository .SingleOrDefault(); }); } - - public override void PopulateIndexes() - { - _seiyuuIDs = new PocoIndex(Cache, a => a.CreatorID); - } - - public override void RegenerateDb() - { - } - - protected override int SelectKey(AniDB_Creator entity) - { - return entity.AniDB_CreatorID; - } - - public AniDB_CreatorRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/AniDB_EpisodeRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_EpisodeRepository.cs index a451f9411..740f5e232 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_EpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_EpisodeRepository.cs @@ -7,62 +7,40 @@ using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_EpisodeRepository : BaseCachedRepository +public class AniDB_EpisodeRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex EpisodesIds; - private PocoIndex Animes; + private PocoIndex? _episodesIDs; - public override void PopulateIndexes() - { - EpisodesIds = new PocoIndex(Cache, a => a.EpisodeID); - Animes = new PocoIndex(Cache, a => a.AnimeID); - } + private PocoIndex? _animeIDs; protected override int SelectKey(SVR_AniDB_Episode entity) - { - return entity.AniDB_EpisodeID; - } + => entity.AniDB_EpisodeID; - public override void RegenerateDb() + public override void PopulateIndexes() { + _episodesIDs = new PocoIndex(Cache, a => a.EpisodeID); + _animeIDs = new PocoIndex(Cache, a => a.AnimeID); } - public SVR_AniDB_Episode GetByEpisodeID(int id) - { - return ReadLock(() => EpisodesIds.GetOne(id)); - } + public SVR_AniDB_Episode? GetByEpisodeID(int episodeID) + => ReadLock(() => _episodesIDs!.GetOne(episodeID)); - public List GetByAnimeID(int id) - { - return ReadLock(() => Animes.GetMultiple(id)); - } + public IReadOnlyList GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID)); - public List GetForDate(DateTime startDate, DateTime endDate) - { - return ReadLock(() => Cache.Values.Where(a => - { - var date = a.GetAirDateAsDate(); - return date.HasValue && date.Value >= startDate && date.Value <= endDate; - }).ToList()); - } + public IReadOnlyList GetForDate(DateTime startDate, DateTime endDate) + => ReadLock(() => Cache.Values.Where(a => a.GetAirDateAsDate() is { } date && date >= startDate && date <= endDate).ToList()); - public List GetByAnimeIDAndEpisodeNumber(int animeid, int epnumber) - { - return GetByAnimeID(animeid) - .Where(a => a.EpisodeNumber == epnumber && a.EpisodeTypeEnum == EpisodeType.Episode) + public IReadOnlyList GetByAnimeIDAndEpisodeNumber(int animeID, int episodeNumber) + => GetByAnimeID(animeID) + .Where(a => a.EpisodeNumber == episodeNumber && a.EpisodeTypeEnum == EpisodeType.Episode) .ToList(); - } - public List GetByAnimeIDAndEpisodeTypeNumber(int animeid, EpisodeType epType, int epnumber) - { - return GetByAnimeID(animeid) - .Where(a => a.EpisodeNumber == epnumber && a.EpisodeTypeEnum == epType) + public IReadOnlyList GetByAnimeIDAndEpisodeTypeNumber(int animeID, EpisodeType episodeType, int episodeNumber) + => GetByAnimeID(animeID) + .Where(a => a.EpisodeNumber == episodeNumber && a.EpisodeTypeEnum == episodeType) .ToList(); - } - - public AniDB_EpisodeRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Episode_PreferredImageRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Episode_PreferredImageRepository.cs index 546e19ac4..8e3a66de3 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_Episode_PreferredImageRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_Episode_PreferredImageRepository.cs @@ -9,19 +9,10 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_Episode_PreferredImageRepository : BaseCachedRepository +public class AniDB_Episode_PreferredImageRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { private PocoIndex? _episodeIDs; - public AniDB_Episode_PreferredImage? GetByAnidbEpisodeIDAndType(int episodeId, ImageEntityType imageType) - => GetByEpisodeID(episodeId).FirstOrDefault(a => a.ImageType == imageType); - - public AniDB_Episode_PreferredImage? GetByAnidbEpisodeIDAndTypeAndSource(int episodeId, ImageEntityType imageType, DataSourceType imageSource) - => GetByEpisodeID(episodeId).FirstOrDefault(a => a.ImageType == imageType && a.ImageSource == imageSource); - - public List GetByEpisodeID(int episodeId) - => ReadLock(() => _episodeIDs!.GetMultiple(episodeId)); - protected override int SelectKey(AniDB_Episode_PreferredImage entity) => entity.AniDB_Episode_PreferredImageID; @@ -30,10 +21,12 @@ public override void PopulateIndexes() _episodeIDs = new(Cache, a => a.AnidbEpisodeID); } - public override void RegenerateDb() - { - } + public AniDB_Episode_PreferredImage? GetByAnidbEpisodeIDAndType(int episodeID, ImageEntityType imageType) + => GetByEpisodeID(episodeID).FirstOrDefault(a => a.ImageType == imageType); + + public AniDB_Episode_PreferredImage? GetByAnidbEpisodeIDAndTypeAndSource(int episodeID, ImageEntityType imageType, DataSourceType imageSource) + => GetByEpisodeID(episodeID).FirstOrDefault(a => a.ImageType == imageType && a.ImageSource == imageSource); - public AniDB_Episode_PreferredImageRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { } + public List GetByEpisodeID(int episodeId) + => ReadLock(() => _episodeIDs!.GetMultiple(episodeId)); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_Episode_TitleRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_Episode_TitleRepository.cs index c68430de1..1cef937f9 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_Episode_TitleRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_Episode_TitleRepository.cs @@ -5,37 +5,24 @@ using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_Episode_TitleRepository : BaseCachedRepository +public class AniDB_Episode_TitleRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Episodes; - - public override void PopulateIndexes() - { - Episodes = new PocoIndex(Cache, a => a.AniDB_EpisodeID); - } + private PocoIndex? _episodeIDs; protected override int SelectKey(SVR_AniDB_Episode_Title entity) - { - return entity.AniDB_Episode_TitleID; - } - - public override void RegenerateDb() - { - } + => entity.AniDB_Episode_TitleID; - public List GetByEpisodeIDAndLanguage(int id, TitleLanguage language) + public override void PopulateIndexes() { - return GetByEpisodeID(id).Where(a => a.Language == language).ToList(); + _episodeIDs = new PocoIndex(Cache, a => a.AniDB_EpisodeID); } - public List GetByEpisodeID(int id) - { - return ReadLock(() => Episodes.GetMultiple(id)); - } + public IReadOnlyList GetByEpisodeIDAndLanguage(int episodeID, TitleLanguage language) + => GetByEpisodeID(episodeID).Where(a => a.Language == language).ToList(); - public AniDB_Episode_TitleRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public IReadOnlyList GetByEpisodeID(int episodeID) + => ReadLock(() => _episodeIDs!.GetMultiple(episodeID)); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_FileRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_FileRepository.cs index 324e8c677..9dbc5d614 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_FileRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using NLog; using NutzCode.InMemoryIndex; using Shoko.Server.Databases; @@ -8,76 +9,55 @@ using Shoko.Server.Scheduling; using Shoko.Server.Scheduling.Jobs.Actions; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_FileRepository : BaseCachedRepository +public class AniDB_FileRepository(ILogger logger, JobFactory jobFactory, DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - private readonly JobFactory _jobFactory; + private readonly ILogger _logger = logger; - private PocoIndex Hashes; - private PocoIndex FileIds; - private PocoIndex InternalVersions; + private readonly JobFactory _jobFactory = jobFactory; + + private PocoIndex? _ed2k; + + private PocoIndex? _fileIDs; + + private PocoIndex? _internalVersions; protected override int SelectKey(SVR_AniDB_File entity) - { - return entity.AniDB_FileID; - } + => entity.AniDB_FileID; public override void PopulateIndexes() { // Only populated from main thread before these are accessible, so no lock - Hashes = new PocoIndex(Cache, a => a.Hash); - FileIds = new PocoIndex(Cache, a => a.FileID); - InternalVersions = new PocoIndex(Cache, a => a.InternalVersion); - } - - public override void RegenerateDb() - { + _ed2k = new PocoIndex(Cache, a => a.Hash); + _fileIDs = new PocoIndex(Cache, a => a.FileID); + _internalVersions = new PocoIndex(Cache, a => a.InternalVersion); } public override void Save(SVR_AniDB_File obj) - { - Save(obj, true); - } + => Save(obj, true); public void Save(SVR_AniDB_File obj, bool updateStats) { base.Save(obj); if (!updateStats) - { return; - } - Logger.Trace("Updating group stats by file from AniDB_FileRepository.Save: {Hash}", obj.Hash); - var anime = RepoFactory.CrossRef_File_Episode.GetByHash(obj.Hash).Select(a => a.AnimeID).Except([0]).Distinct(); + _logger.LogTrace("Updating group stats by file from AniDB_FileRepository.Save: {Hash}", obj.Hash); + var anime = RepoFactory.CrossRef_File_Episode.GetByEd2k(obj.Hash).Select(a => a.AnimeID).Except([0]).Distinct(); Task.WhenAll(anime.Select(a => _jobFactory.CreateJob(b => b.AnimeID = a).Process())).GetAwaiter().GetResult(); } + public SVR_AniDB_File? GetByHash(string hash) + => ReadLock(() => _ed2k!.GetOne(hash)); - public SVR_AniDB_File GetByHash(string hash) - { - return ReadLock(() => Hashes.GetOne(hash)); - } - - public List GetByInternalVersion(int version) - { - return ReadLock(() => InternalVersions.GetMultiple(version)); - } - - public SVR_AniDB_File GetByHashAndFileSize(string hash, long fsize) - { - var list = ReadLock(() => Hashes.GetMultiple(hash)); - return list.Count == 1 ? list.FirstOrDefault() : list.FirstOrDefault(a => a.FileSize == fsize); - } + public IReadOnlyList GetByInternalVersion(int version) + => ReadLock(() => _internalVersions!.GetMultiple(version)); - public SVR_AniDB_File GetByFileID(int fileID) - { - return ReadLock(() => FileIds.GetOne(fileID)); - } + public SVR_AniDB_File? GetByEd2kAndFileSize(string ed2k, long fileSize) + => ReadLock(() => _ed2k!.GetMultiple(ed2k)).FirstOrDefault(a => a.FileSize == fileSize); - public AniDB_FileRepository(DatabaseFactory databaseFactory, JobFactory jobFactory) : base(databaseFactory) - { - _jobFactory = jobFactory; - } + public SVR_AniDB_File? GetByFileID(int fileID) + => ReadLock(() => _fileIDs!.GetOne(fileID)); } diff --git a/Shoko.Server/Repositories/Cached/AniDB_ReleaseGroupRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_ReleaseGroupRepository.cs index 5e083c096..79df6115c 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_ReleaseGroupRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_ReleaseGroupRepository.cs @@ -11,52 +11,48 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_ReleaseGroupRepository : BaseCachedRepository +public class AniDB_ReleaseGroupRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex _groupIDs; + private PocoIndex? _groupIDs; - public AniDB_ReleaseGroup? GetByGroupID(int id) + protected override int SelectKey(AniDB_ReleaseGroup entity) + => entity.AniDB_ReleaseGroupID; + + public override void PopulateIndexes() { - return ReadLock(() => _groupIDs.GetOne(id)); + _groupIDs = Cache.CreateIndex(a => a.GroupID); } + public AniDB_ReleaseGroup? GetByGroupID(int groupID) + => ReadLock(() => _groupIDs!.GetOne(groupID)); + public IReadOnlyList GetUsedReleaseGroups() - { - var results = Lock(() => + => Lock(() => { using var session = _databaseFactory.SessionFactory.OpenSession(); - return session.Query().Where(a => a.GroupName != "raw/unknown").Join(session.Query(), a => a.GroupID, a => a.GroupID, (a, b) => new { Group = a, File = b }) - .Join(session.Query(), a => a.File.Hash, a => a.Hash, (a, b) => a.Group).OrderBy(a => a.GroupName).ToList().Distinct().ToList(); + return session.Query() + .Where(a => a.GroupName != "raw/unknown") + .Join(session.Query(), a => a.GroupID, a => a.GroupID, (a, b) => new { Group = a, File = b }) + .Join(session.Query(), a => a.File.Hash, a => a.Hash, (a, b) => a.Group) + .OrderBy(a => a.GroupName) + .ToList() + .Distinct() + .ToList(); }); - return results; - } public IReadOnlyList GetUnusedReleaseGroups() - { - var results = Lock(() => + => Lock(() => { using var session = _databaseFactory.SessionFactory.OpenSession(); - return session.Query().Where(a => a.GroupName != "raw/unknown").LeftJoin(session.Query(), a => a.GroupID, a => a.GroupID, - (a, b) => new { Group = a, File = b }).Where(a => a.File == null).Select(a => a.Group).OrderBy(a => a.GroupName).ToList().Distinct().ToList(); + return session.Query() + .Where(a => a.GroupName != "raw/unknown") + .LeftJoin(session.Query(), a => a.GroupID, a => a.GroupID, + (a, b) => new { Group = a, File = b }) + .Where(a => a.File == null) + .Select(a => a.Group) + .OrderBy(a => a.GroupName) + .ToList() + .Distinct() + .ToList(); }); - return results; - } - - public override void PopulateIndexes() - { - _groupIDs = Cache.CreateIndex(a => a.GroupID); - } - - public override void RegenerateDb() - { - } - - protected override int SelectKey(AniDB_ReleaseGroup entity) - { - return entity.AniDB_ReleaseGroupID; - } - - public AniDB_ReleaseGroupRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/AniDB_TagRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_TagRepository.cs index cf8528347..872f1cdaf 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_TagRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_TagRepository.cs @@ -1,39 +1,35 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using NutzCode.InMemoryIndex; -using Shoko.Commons.Collections; +using Shoko.Commons.Extensions; using Shoko.Models.Server; -using Shoko.Server.API; using Shoko.Server.Databases; -using Shoko.Server.Models; -using Shoko.Server.Utilities; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AniDB_TagRepository : BaseCachedRepository +public class AniDB_TagRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Tags; - private PocoIndex Names; - private PocoIndex SourceNames; + private PocoIndex? _tagIDs; - public override void PopulateIndexes() - { - Tags = new PocoIndex(Cache, a => a.TagID); - Names = new PocoIndex(Cache, a => a.TagName); - SourceNames = new PocoIndex(Cache, a => a.TagNameSource); - } + private PocoIndex? _names; + + private PocoIndex? _sourceNames; protected override int SelectKey(AniDB_Tag entity) + => entity.AniDB_TagID; + + public override void PopulateIndexes() { - return entity.AniDB_TagID; + _tagIDs = new PocoIndex(Cache, a => a.TagID); + _names = new PocoIndex(Cache, a => a.TagName); + _sourceNames = new PocoIndex(Cache, a => a.TagNameSource); } public override void RegenerateDb() { - var tags = Cache.Values.Where(tag => (tag.TagDescription?.Contains('`') ?? false) || tag.TagName.Contains('`')) + var tags = Cache.Values + .Where(tag => (tag.TagDescription?.Contains('`') ?? false) || tag.TagName.Contains('`')) .ToList(); foreach (var tag in tags) { @@ -44,74 +40,25 @@ public override void RegenerateDb() } } - public List GetByAnimeID(int animeID) - { - return RepoFactory.AniDB_Anime_Tag.GetByAnimeID(animeID) - .Select(a => GetByTagID(a.TagID)) - .Where(a => a != null) - .ToList(); - } - - - public ILookup GetByAnimeIDs(int[] ids) - { - if (ids == null) - { - throw new ArgumentNullException(nameof(ids)); - } - - if (ids.Length == 0) - { - return EmptyLookup.Instance; - } - - return RepoFactory.AniDB_Anime_Tag.GetByAnimeIDs(ids).SelectMany(a => a.ToList()) - .ToLookup(t => t.AnimeID, t => GetByTagID(t.TagID)); - } - - - public AniDB_Tag GetByTagID(int id) - { - return ReadLock(() => Tags.GetOne(id)); - } + public AniDB_Tag? GetByTagID(int tagID) + => ReadLock(() => _tagIDs!.GetOne(tagID)); - public List GetByName(string name) - { - return ReadLock(() => Names.GetMultiple(name)); - } + public IReadOnlyList GetByName(string name) + => ReadLock(() => _names!.GetMultiple(name)); - public List GetBySourceName(string sourceName) - { - return ReadLock(() => SourceNames.GetMultiple(sourceName)); - } + public IReadOnlyList GetBySourceName(string sourceName) + => ReadLock(() => _sourceNames!.GetMultiple(sourceName)); /// /// Gets all the tags, but only if we have the anime locally /// /// - public List GetAllForLocalSeries(bool useUser = false) - { - IEnumerable series; - if (useUser) - { - var user = Utils.ServiceContainer.GetService()?.HttpContext?.GetUser(); - series = RepoFactory.AnimeSeries.GetAll().Where(a => user?.AllowedSeries(a) ?? true); - } - else - { - series = RepoFactory.AnimeSeries.GetAll(); - } - - return series + public IReadOnlyList GetAllForLocalSeries() + => RepoFactory.AnimeSeries.GetAll() .SelectMany(a => RepoFactory.AniDB_Anime_Tag.GetByAnimeID(a.AniDB_ID)) - .Where(a => a != null) + .WhereNotNull() .Select(a => GetByTagID(a.TagID)) - .Where(a => a != null) + .WhereNotNull() .DistinctBy(a => a.TagID) .ToList(); - } - - public AniDB_TagRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/AniDB_VoteRepository.cs b/Shoko.Server/Repositories/Cached/AniDB_VoteRepository.cs index 26df4180c..40000c0fa 100644 --- a/Shoko.Server/Repositories/Cached/AniDB_VoteRepository.cs +++ b/Shoko.Server/Repositories/Cached/AniDB_VoteRepository.cs @@ -8,109 +8,74 @@ using Shoko.Server.Scheduling; using Shoko.Server.Scheduling.Jobs.Actions; +#nullable enable namespace Shoko.Server.Repositories.Cached; public class AniDB_VoteRepository : BaseCachedRepository { - private PocoIndex EntityIDs; - private PocoIndex EntityIDAndTypes; + private PocoIndex? _entityIDs; - public AniDB_VoteRepository(DatabaseFactory databaseFactory, JobFactory jobFactory) : base(databaseFactory) + private PocoIndex? _entityIDAndTypes; + + public AniDB_VoteRepository(JobFactory jobFactory, DatabaseFactory databaseFactory) : base(databaseFactory) { EndSaveCallback = cr => { - switch (cr.VoteType) + switch ((AniDBVoteType)cr.VoteType) { - case (int)AniDBVoteType.Anime: - case (int)AniDBVoteType.AnimeTemp: + case AniDBVoteType.Anime: + case AniDBVoteType.AnimeTemp: jobFactory.CreateJob(a => a.AnimeID = cr.EntityID).Process().GetAwaiter().GetResult(); break; - case (int)AniDBVoteType.Episode: - var ep = RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(cr.EntityID); - RepoFactory.AnimeEpisode.Save(ep); - break; } }; EndDeleteCallback = cr => { - switch (cr.VoteType) + switch ((AniDBVoteType)cr.VoteType) { - case (int)AniDBVoteType.Anime: - case (int)AniDBVoteType.AnimeTemp: + case AniDBVoteType.Anime: + case AniDBVoteType.AnimeTemp: jobFactory.CreateJob(a => a.AnimeID = cr.EntityID).Process().GetAwaiter().GetResult(); break; - case (int)AniDBVoteType.Episode: - var ep = RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(cr.EntityID); - RepoFactory.AnimeEpisode.Save(ep); - break; } }; } - public AniDB_Vote GetByEntityAndType(int entID, AniDBVoteType voteType) + protected override int SelectKey(AniDB_Vote entity) + => entity.AniDB_VoteID; + + public override void PopulateIndexes() { - var cr = ReadLock(() => EntityIDAndTypes.GetMultiple((entID, voteType))); + _entityIDs = new PocoIndex(Cache, a => a.EntityID); + _entityIDAndTypes = new PocoIndex(Cache, a => (a.EntityID, (AniDBVoteType)a.VoteType)); + } - if (cr == null) - { + public AniDB_Vote? GetByEntityAndType(int entityID, AniDBVoteType voteType) + { + if (ReadLock(() => _entityIDAndTypes!.GetMultiple((entityID, voteType))) is not { } cr) return null; - } if (cr.Count <= 1) - { return cr.FirstOrDefault(); - } return Lock(() => { using var session = _databaseFactory.SessionFactory.OpenSession(); foreach (var dbVote in cr.Skip(1)) { - using var transact = session.BeginTransaction(); - RepoFactory.AniDB_Vote.DeleteWithOpenTransaction(session, dbVote); - transact.Commit(); + using var transaction = session.BeginTransaction(); + DeleteWithOpenTransaction(session, dbVote); + transaction.Commit(); } return cr.FirstOrDefault(); }); } - public List GetByEntity(int entID) - { - return ReadLock(() => EntityIDs.GetMultiple(entID)?.ToList()); - } - - public AniDB_Vote GetByAnimeID(int animeID) - { - return GetByEntityAndType(animeID, AniDBVoteType.Anime) ?? - GetByEntityAndType(animeID, AniDBVoteType.AnimeTemp); - } - - public Dictionary GetByAnimeIDs(IReadOnlyCollection animeIDs) - { - if (animeIDs == null) - { - throw new ArgumentNullException(nameof(animeIDs)); - } - - var votesByAnime = animeIDs.Select(a => new { AnimeID = a, Vote = GetByAnimeID(a) }).Where(a => a.Vote != null) - .ToDictionary(a => a.AnimeID, a => a.Vote); - return votesByAnime; - } + public IReadOnlyList GetByEntity(int entityID) + => ReadLock(() => _entityIDs!.GetMultiple(entityID)); - protected override int SelectKey(AniDB_Vote entity) - { - return entity.AniDB_VoteID; - } - - public override void PopulateIndexes() - { - EntityIDs = new PocoIndex(Cache, a => a.EntityID); - EntityIDAndTypes = - new PocoIndex(Cache, a => (a.EntityID, (AniDBVoteType)a.VoteType)); - } - - public override void RegenerateDb() - { - } + public AniDB_Vote? GetByAnimeID(int animeID) + => GetByEntityAndType(animeID, AniDBVoteType.Anime) ?? + GetByEntityAndType(animeID, AniDBVoteType.AnimeTemp); } diff --git a/Shoko.Server/Repositories/Cached/AnimeCharacterRepository.cs b/Shoko.Server/Repositories/Cached/AnimeCharacterRepository.cs deleted file mode 100644 index bb8bc22fe..000000000 --- a/Shoko.Server/Repositories/Cached/AnimeCharacterRepository.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NutzCode.InMemoryIndex; -using Shoko.Models.Server; -using Shoko.Server.Databases; - -namespace Shoko.Server.Repositories.Cached; - -public class AnimeCharacterRepository : BaseCachedRepository -{ - private PocoIndex AniDBIDs; - - public override void RegenerateDb() - { - } - - protected override int SelectKey(AnimeCharacter entity) - { - return entity.CharacterID; - } - - public override void PopulateIndexes() - { - AniDBIDs = new PocoIndex(Cache, a => a.AniDBID); - } - - - public AnimeCharacter GetByAniDBID(int id) - { - return ReadLock(() => AniDBIDs.GetOne(id)); - } - - public AnimeCharacterRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } -} diff --git a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs index 98739ef18..9316d79a1 100644 --- a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs @@ -18,8 +18,9 @@ namespace Shoko.Server.Repositories.Cached; public class AnimeEpisodeRepository : BaseCachedRepository { - private PocoIndex? Series; - private PocoIndex? EpisodeIDs; + private PocoIndex? _seriesIDs; + + private PocoIndex? _anidbEpisodeIDs; public AnimeEpisodeRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { @@ -35,19 +36,15 @@ protected override int SelectKey(SVR_AnimeEpisode entity) public override void PopulateIndexes() { - Series = Cache.CreateIndex(a => a.AnimeSeriesID); - EpisodeIDs = Cache.CreateIndex(a => a.AniDB_EpisodeID); + _seriesIDs = Cache.CreateIndex(a => a.AnimeSeriesID); + _anidbEpisodeIDs = Cache.CreateIndex(a => a.AniDB_EpisodeID); } - public override void RegenerateDb() { } - - public List GetBySeriesID(int seriesid) - => ReadLock(() => Series!.GetMultiple(seriesid)); - - - public SVR_AnimeEpisode? GetByAniDBEpisodeID(int epid) - => ReadLock(() => EpisodeIDs!.GetOne(epid)); + public List GetBySeriesID(int seriesID) + => ReadLock(() => _seriesIDs!.GetMultiple(seriesID)); + public SVR_AnimeEpisode? GetByAniDBEpisodeID(int episodeID) + => ReadLock(() => _anidbEpisodeIDs!.GetOne(episodeID)); /// /// Get the AnimeEpisode @@ -84,7 +81,7 @@ public List GetByHash(string hash) if (string.IsNullOrEmpty(hash)) return []; - return RepoFactory.CrossRef_File_Episode.GetByHash(hash) + return RepoFactory.CrossRef_File_Episode.GetByEd2k(hash) .Select(a => GetByAniDBEpisodeID(a.EpisodeID)) .WhereNotNull() .ToList(); diff --git a/Shoko.Server/Repositories/Cached/AnimeEpisode_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeEpisode_UserRepository.cs index 174a079a8..641980a03 100644 --- a/Shoko.Server/Repositories/Cached/AnimeEpisode_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeEpisode_UserRepository.cs @@ -7,93 +7,71 @@ using Shoko.Server.Models; using Shoko.Server.Server; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class AnimeEpisode_UserRepository : BaseCachedRepository +public class AnimeEpisode_UserRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex UsersEpisodes; - private PocoIndex Users; - private PocoIndex Episodes; - private PocoIndex UsersSeries; + private PocoIndex? _userIDs; + + private PocoIndex? _episodeIDs; + + private PocoIndex? _userEpisodeIDs; + + private PocoIndex? _userSeriesIDs; protected override int SelectKey(SVR_AnimeEpisode_User entity) - { - return entity.AnimeEpisode_UserID; - } + => entity.AnimeEpisode_UserID; public override void PopulateIndexes() { - UsersEpisodes = Cache.CreateIndex(a => (a.JMMUserID, a.AnimeEpisodeID)); - Users = Cache.CreateIndex(a => a.JMMUserID); - Episodes = Cache.CreateIndex(a => a.AnimeEpisodeID); - UsersSeries = Cache.CreateIndex(a => (a.JMMUserID, a.AnimeSeriesID)); + _userIDs = Cache.CreateIndex(a => a.JMMUserID); + _episodeIDs = Cache.CreateIndex(a => a.AnimeEpisodeID); + _userEpisodeIDs = Cache.CreateIndex(a => (a.JMMUserID, a.AnimeEpisodeID)); + _userSeriesIDs = Cache.CreateIndex(a => (a.JMMUserID, a.AnimeSeriesID)); } public override void RegenerateDb() { - var cnt = 0; - var sers = - Cache.Values.Where(a => a.AnimeEpisode_UserID == 0) - .ToList(); - var max = sers.Count; - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, - typeof(AnimeEpisode_User).Name, " DbRegen"); - if (max <= 0) - { + var current = 0; + var records = Cache.Values.Where(a => a.AnimeEpisode_UserID == 0).ToList(); + var total = records.Count; + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, typeof(AnimeEpisode_User).Name, " Database Regeneration"); + if (total is 0) return; - } - foreach (var g in sers) + foreach (var record in records) { - Save(g); - cnt++; - if (cnt % 10 == 0) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, typeof(AnimeEpisode_User).Name, - " DbRegen - " + cnt + "/" + max); - } + Save(record); + current++; + if (current % 10 == 0) + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, typeof(AnimeEpisode_User).Name, " Database Regeneration - " + current + "/" + total); } - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, - typeof(AnimeEpisode_User).Name, - " DbRegen - " + max + "/" + max); + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, typeof(AnimeEpisode_User).Name, " Database Regeneration - " + total + "/" + total); } - public SVR_AnimeEpisode_User GetByUserIDAndEpisodeID(int userid, int epid) - { - return ReadLock(() => UsersEpisodes.GetOne((userid, epid))); - } + public SVR_AnimeEpisode_User? GetByUserIDAndEpisodeID(int userID, int episodeID) + => ReadLock(() => _userEpisodeIDs!.GetOne((userID, episodeID))); + public IReadOnlyList GetByUserID(int userid) + => ReadLock(() => _userIDs!.GetMultiple(userid)); - public List GetByUserID(int userid) - { - return ReadLock(() => Users.GetMultiple(userid)); - } + public IReadOnlyList GetMostRecentlyWatched(int userid, int limit = 100) + => GetByUserID(userid).Where(a => a.WatchedCount > 0) + .OrderByDescending(a => a.WatchedDate) + .Take(limit) + .ToList(); - public List GetMostRecentlyWatched(int userid, int maxresults = 100) - { - return GetByUserID(userid).Where(a => a.WatchedCount > 0).OrderByDescending(a => a.WatchedDate) - .Take(maxresults).ToList(); - } - - public SVR_AnimeEpisode_User GetLastWatchedEpisodeForSeries(int seriesid, int userid) - { - return GetByUserIDAndSeriesID(userid, seriesid).Where(a => a.WatchedCount > 0) - .OrderByDescending(a => a.WatchedDate).FirstOrDefault(); - } + public SVR_AnimeEpisode_User? GetLastWatchedEpisodeForSeries(int seriesID, int userID) + => GetByUserIDAndSeriesID(userID, seriesID) + .Where(a => a.WatchedCount > 0) + .OrderByDescending(a => a.WatchedDate) + .FirstOrDefault(); - public List GetByEpisodeID(int epid) - { - return ReadLock(() => Episodes.GetMultiple(epid)); - } + public IReadOnlyList GetByEpisodeID(int episodeID) + => ReadLock(() => _episodeIDs!.GetMultiple(episodeID)); - public List GetByUserIDAndSeriesID(int userid, int seriesid) - { - return ReadLock(() => UsersSeries.GetMultiple((userid, seriesid))); - } - - public AnimeEpisode_UserRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public IReadOnlyList GetByUserIDAndSeriesID(int userID, int seriesID) + => ReadLock(() => _userSeriesIDs!.GetMultiple((userID, seriesID))); } diff --git a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs index 3a230d6ac..88e693fa0 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroupRepository.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using NLog; +using Microsoft.Extensions.Logging; using NutzCode.InMemoryIndex; using Shoko.Server.Databases; using Shoko.Server.Models; @@ -12,14 +12,15 @@ namespace Shoko.Server.Repositories.Cached; public class AnimeGroupRepository : BaseCachedRepository { - private static Logger logger = LogManager.GetCurrentClassLogger(); + private readonly ILogger _logger; - private PocoIndex Parents; + private PocoIndex _parentIDs; - private ChangeTracker Changes = new(); + private readonly ChangeTracker _changes = new(); - public AnimeGroupRepository(DatabaseFactory databaseFactory) : base(databaseFactory) + public AnimeGroupRepository(ILogger logger, DatabaseFactory databaseFactory) : base(databaseFactory) { + _logger = logger; BeginDeleteCallback = cr => { RepoFactory.AnimeGroup_User.Delete(RepoFactory.AnimeGroup_User.GetByGroupID(cr.AnimeGroupID)); @@ -28,84 +29,68 @@ public AnimeGroupRepository(DatabaseFactory databaseFactory) : base(databaseFact { if (cr.AnimeGroupParentID.HasValue && cr.AnimeGroupParentID.Value > 0) { - logger.Trace("Updating group stats by group from AnimeGroupRepository.Delete: {0}", - cr.AnimeGroupParentID.Value); - var ngrp = GetByID(cr.AnimeGroupParentID.Value); - if (ngrp != null) + _logger.LogTrace("Updating group stats by group from AnimeGroupRepository.Delete: {Count}", cr.AnimeGroupParentID.Value); + var parentGroup = GetByID(cr.AnimeGroupParentID.Value); + if (parentGroup != null) { - Save(ngrp, true); + Save(parentGroup, true); } } }; } protected override int SelectKey(SVR_AnimeGroup entity) - { - return entity.AnimeGroupID; - } + => entity.AnimeGroupID; public override void PopulateIndexes() { - Changes.AddOrUpdateRange(Cache.Keys); - Parents = Cache.CreateIndex(a => a.AnimeGroupParentID ?? 0); - } - - public override void RegenerateDb() - { + _changes.AddOrUpdateRange(Cache.Keys); + _parentIDs = Cache.CreateIndex(a => a.AnimeGroupParentID ?? 0); } public override void Save(SVR_AnimeGroup obj) - { - Save(obj, true); - } + => Save(obj, true); - public void Save(SVR_AnimeGroup grp, bool recursive) + public void Save(SVR_AnimeGroup group, bool recursive) { using var session = _databaseFactory.SessionFactory.OpenSession(); Lock(session, s => { //We are creating one, and we need the AnimeGroupID before Update the contracts - if (grp.AnimeGroupID == 0) + if (group.AnimeGroupID == 0) { using var transaction = s.BeginTransaction(); - s.SaveOrUpdate(grp); + s.SaveOrUpdate(group); transaction.Commit(); } }); - UpdateCache(grp); + UpdateCache(group); Lock(session, s => { using var transaction = s.BeginTransaction(); - SaveWithOpenTransaction(s, grp); + SaveWithOpenTransaction(s, group); transaction.Commit(); }); - Changes.AddOrUpdate(grp.AnimeGroupID); + _changes.AddOrUpdate(group.AnimeGroupID); - if (grp.AnimeGroupParentID.HasValue && recursive) + if (group.AnimeGroupParentID.HasValue && recursive) { - var pgroup = GetByID(grp.AnimeGroupParentID.Value); + var parentGroup = GetByID(group.AnimeGroupParentID.Value); // This will avoid the recursive error that would be possible, it won't update it, but that would be // the least of the issues - if (pgroup != null && pgroup.AnimeGroupParentID == grp.AnimeGroupID) + if (parentGroup != null && parentGroup.AnimeGroupParentID == group.AnimeGroupID) { - Save(pgroup, true); + Save(parentGroup, true); } } } public async Task InsertBatch(ISessionWrapper session, IReadOnlyCollection groups) { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - if (groups == null) - { - throw new ArgumentNullException(nameof(groups)); - } + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(groups); using var trans = session.BeginTransaction(); foreach (var group in groups) @@ -113,22 +98,16 @@ public async Task InsertBatch(ISessionWrapper session, IReadOnlyCollection g.AnimeGroupID)); + _changes.AddOrUpdateRange(groups.Select(g => g.AnimeGroupID)); } public async Task UpdateBatch(ISessionWrapper session, IReadOnlyCollection groups) { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - - if (groups == null) - { - throw new ArgumentNullException(nameof(groups)); - } + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(groups); using var trans = session.BeginTransaction(); foreach (var group in groups) @@ -136,9 +115,10 @@ public async Task UpdateBatch(ISessionWrapper session, IReadOnlyCollection g.AnimeGroupID)); + _changes.AddOrUpdateRange(groups.Select(g => g.AnimeGroupID)); } /// @@ -152,13 +132,10 @@ public async Task UpdateBatch(ISessionWrapper session, IReadOnlyCollection is null. public async Task DeleteAll(ISessionWrapper session, int? excludeGroupId = null) { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } + ArgumentNullException.ThrowIfNull(session); // First, get all of the current groups so that we can inform the change tracker that they have been removed later - var allGrps = GetAll(); + var allGroups = GetAll(); await Lock(async () => { @@ -178,12 +155,12 @@ await session.CreateSQLQuery("DELETE FROM AnimeGroup WHERE AnimeGroupID > 0") if (excludeGroupId != null) { - Changes.RemoveRange(allGrps.Select(g => g.AnimeGroupID) + _changes.RemoveRange(allGroups.Select(g => g.AnimeGroupID) .Where(id => id != excludeGroupId.Value)); } else { - Changes.RemoveRange(allGrps.Select(g => g.AnimeGroupID)); + _changes.RemoveRange(allGroups.Select(g => g.AnimeGroupID)); } // Finally, we need to clear the cache so that it is in sync with the database @@ -192,7 +169,7 @@ await session.CreateSQLQuery("DELETE FROM AnimeGroup WHERE AnimeGroupID > 0") // If we're excluding a group from deletion, and it was in the cache originally, then re-add it back in if (excludeGroupId != null) { - var excludedGroup = allGrps.FirstOrDefault(g => g.AnimeGroupID == excludeGroupId.Value); + var excludedGroup = allGroups.FirstOrDefault(g => g.AnimeGroupID == excludeGroupId.Value); if (excludedGroup != null) { @@ -201,18 +178,12 @@ await session.CreateSQLQuery("DELETE FROM AnimeGroup WHERE AnimeGroupID > 0") } } - public List GetByParentID(int parentid) - { - return ReadLock(() => Parents.GetMultiple(parentid)); - } + public List GetByParentID(int parentID) + => ReadLock(() => _parentIDs.GetMultiple(parentID)); public List GetAllTopLevelGroups() - { - return GetByParentID(0); - } + => GetByParentID(0); public ChangeTracker GetChangeTracker() - { - return Changes; - } + => _changes; } diff --git a/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs index c27a2cfd6..3a4c8bf9b 100644 --- a/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeGroup_UserRepository.cs @@ -7,93 +7,82 @@ using Shoko.Server.Databases; using Shoko.Server.Repositories.NHibernate; +#nullable enable namespace Shoko.Server.Repositories.Cached; public class AnimeGroup_UserRepository : BaseCachedRepository { - private PocoIndex Groups; - private PocoIndex Users; - private PocoIndex UsersGroups; - private Dictionary> Changes = new(); + private PocoIndex? _groupIDs; + private PocoIndex? _userIDs; + + private PocoIndex? _userGroupIDs; + + private readonly Dictionary> _changes = []; public AnimeGroup_UserRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { EndDeleteCallback = cr => { - Changes.TryAdd(cr.JMMUserID, new ChangeTracker()); - Changes[cr.JMMUserID].Remove(cr.AnimeGroupID); + _changes.TryAdd(cr.JMMUserID, new()); + _changes[cr.JMMUserID].Remove(cr.AnimeGroupID); }; } protected override int SelectKey(AnimeGroup_User entity) - { - return entity.AnimeGroup_UserID; - } + => entity.AnimeGroup_UserID; public override void PopulateIndexes() { - Groups = Cache.CreateIndex(a => a.AnimeGroupID); - Users = Cache.CreateIndex(a => a.JMMUserID); - UsersGroups = Cache.CreateIndex(a => a.JMMUserID, a => a.AnimeGroupID); + _groupIDs = Cache.CreateIndex(a => a.AnimeGroupID); + _userIDs = Cache.CreateIndex(a => a.JMMUserID); + _userGroupIDs = Cache.CreateIndex(a => a.JMMUserID, a => a.AnimeGroupID); foreach (var n in Cache.Values.Select(a => a.JMMUserID).Distinct()) { - Changes[n] = new ChangeTracker(); - Changes[n].AddOrUpdateRange(Users.GetMultiple(n).Select(a => a.AnimeGroupID)); + _changes[n] = new(); + _changes[n].AddOrUpdateRange(_userIDs.GetMultiple(n).Select(a => a.AnimeGroupID)); } } - public override void RegenerateDb() - { - } - public override void Save(AnimeGroup_User obj) { base.Save(obj); - Changes.TryAdd(obj.JMMUserID, new ChangeTracker()); - - Changes[obj.JMMUserID].AddOrUpdate(obj.AnimeGroupID); + _changes.TryAdd(obj.JMMUserID, new()); + _changes[obj.JMMUserID].AddOrUpdate(obj.AnimeGroupID); } /// /// Inserts a batch of into the database. /// /// - /// This method should NOT be used for updating existing entities. - /// It is up to the caller of this method to manage transactions, etc. - /// Group Filters, etc. will not be updated by this method. + /// This method should NOT be used for updating existing entities. + /// It is up to the caller of this method to manage transactions, etc. + /// Group Filters, etc. will not be updated by this method. /// /// The NHibernate session. /// The batch of to insert into the database. /// or is null. public async Task InsertBatch(ISessionWrapper session, IEnumerable groupUsers) { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(groupUsers); - if (groupUsers == null) - { - throw new ArgumentNullException(nameof(groupUsers)); - } - - using var trans = session.BeginTransaction(); + using var transaction = session.BeginTransaction(); foreach (var groupUser in groupUsers) { await session.InsertAsync(groupUser); - UpdateCache(groupUser); - if (!Changes.TryGetValue(groupUser.JMMUserID, out var changeTracker)) + if (!_changes.TryGetValue(groupUser.JMMUserID, out var changeTracker)) { - changeTracker = new ChangeTracker(); - Changes[groupUser.JMMUserID] = changeTracker; + changeTracker = new(); + _changes[groupUser.JMMUserID] = changeTracker; } changeTracker.AddOrUpdate(groupUser.AnimeGroupID); } - await trans.CommitAsync(); + + await transaction.CommitAsync(); } /// @@ -108,31 +97,24 @@ public async Task InsertBatch(ISessionWrapper session, IEnumerable or is null. public async Task UpdateBatch(ISessionWrapper session, IEnumerable groupUsers) { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(groupUsers); - if (groupUsers == null) - { - throw new ArgumentNullException(nameof(groupUsers)); - } - - using var trans = session.BeginTransaction(); + using var transaction = session.BeginTransaction(); foreach (var groupUser in groupUsers) { await session.UpdateAsync(groupUser); UpdateCache(groupUser); - - if (!Changes.TryGetValue(groupUser.JMMUserID, out var changeTracker)) + if (!_changes.TryGetValue(groupUser.JMMUserID, out var changeTracker)) { - changeTracker = new ChangeTracker(); - Changes[groupUser.JMMUserID] = changeTracker; + changeTracker = new(); + _changes[groupUser.JMMUserID] = changeTracker; } changeTracker.AddOrUpdate(groupUser.AnimeGroupID); } - await trans.CommitAsync(); + + await transaction.CommitAsync(); } /// @@ -145,52 +127,40 @@ public async Task UpdateBatch(ISessionWrapper session, IEnumerable is null. public async Task DeleteAll(ISessionWrapper session) { - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } + ArgumentNullException.ThrowIfNull(session); // First, get all of the current user/groups so that we can inform the change tracker that they have been removed later - var usrGrpMap = GetAll().GroupBy(g => g.JMMUserID, g => g.AnimeGroupID); + var groupUsers = GetAll().GroupBy(g => g.JMMUserID, g => g.AnimeGroupID); // Then, actually delete the AnimeGroup_Users await Lock(async () => await session.CreateSQLQuery("DELETE FROM AnimeGroup_User WHERE AnimeGroup_UserID > 0").ExecuteUpdateAsync()); // Now, update the change trackers with all removed records - foreach (var grp in usrGrpMap) + foreach (var groupUser in groupUsers) { - var jmmUserId = grp.Key; - - if (!Changes.TryGetValue(jmmUserId, out var changeTracker)) + var userId = groupUser.Key; + if (!_changes.TryGetValue(userId, out var changeTracker)) { - changeTracker = new ChangeTracker(); - Changes[jmmUserId] = changeTracker; + changeTracker = new(); + _changes[userId] = changeTracker; } - changeTracker.RemoveRange(grp); + changeTracker.RemoveRange(groupUser); } // Finally, we need to clear the cache so that it is in sync with the database ClearCache(); } - public AnimeGroup_User GetByUserAndGroupID(int userid, int groupid) - { - return ReadLock(() => UsersGroups.GetOne(userid, groupid)); - } + public AnimeGroup_User? GetByUserAndGroupID(int userID, int groupID) + => ReadLock(() => _userGroupIDs!.GetOne(userID, groupID)); - public List GetByUserID(int userid) - { - return ReadLock(() => Users.GetMultiple(userid)); - } + public List GetByUserID(int userID) + => ReadLock(() => _userIDs!.GetMultiple(userID)); - public List GetByGroupID(int groupid) - { - return ReadLock(() => Groups.GetMultiple(groupid)); - } + public List GetByGroupID(int groupID) + => ReadLock(() => _groupIDs!.GetMultiple(groupID)); - public ChangeTracker GetChangeTracker(int userid) - { - return ReadLock(() => Changes.TryGetValue(userid, out var change) ? change : new ChangeTracker()); - } + public ChangeTracker GetChangeTracker(int userID) + => ReadLock(() => _changes.TryGetValue(userID, out var change) ? change : new()); } diff --git a/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs b/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs index 051cb87b1..8dcb840b5 100644 --- a/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeSeries_UserRepository.cs @@ -8,73 +8,56 @@ namespace Shoko.Server.Repositories.Cached; public class AnimeSeries_UserRepository : BaseCachedRepository { - private PocoIndex Users; - private PocoIndex Series; - private PocoIndex UsersSeries; - private Dictionary> Changes = new(); + private PocoIndex _userIDs; + + private PocoIndex _seriesIDs; + + private PocoIndex _userSeriesIDs; + + private readonly Dictionary> _changes = []; public AnimeSeries_UserRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { EndDeleteCallback = cr => { - Changes.TryAdd(cr.JMMUserID, new ChangeTracker()); + _changes.TryAdd(cr.JMMUserID, new ChangeTracker()); - Changes[cr.JMMUserID].Remove(cr.AnimeSeriesID); + _changes[cr.JMMUserID].Remove(cr.AnimeSeriesID); }; } protected override int SelectKey(AnimeSeries_User entity) - { - return entity.AnimeSeries_UserID; - } + => entity.AnimeSeries_UserID; public override void PopulateIndexes() { - Users = Cache.CreateIndex(a => a.JMMUserID); - Series = Cache.CreateIndex(a => a.AnimeSeriesID); - UsersSeries = Cache.CreateIndex(a => (a.JMMUserID, a.AnimeSeriesID)); + _userIDs = Cache.CreateIndex(a => a.JMMUserID); + _seriesIDs = Cache.CreateIndex(a => a.AnimeSeriesID); + _userSeriesIDs = Cache.CreateIndex(a => (a.JMMUserID, a.AnimeSeriesID)); } - public override void RegenerateDb() - { - } - - public override void Save(AnimeSeries_User obj) { base.Save(obj); - Changes.TryAdd(obj.JMMUserID, new ChangeTracker()); - Changes[obj.JMMUserID].AddOrUpdate(obj.AnimeSeriesID); + _changes.TryAdd(obj.JMMUserID, new()); + _changes[obj.JMMUserID].AddOrUpdate(obj.AnimeSeriesID); } - public AnimeSeries_User GetByUserAndSeriesID(int userid, int seriesid) - { - return ReadLock(() => UsersSeries.GetOne((userid, seriesid))); - } + public AnimeSeries_User GetByUserAndSeriesID(int userID, int seriesID) + => ReadLock(() => _userSeriesIDs.GetOne((userID, seriesID))); - public List GetByUserID(int userid) - { - return ReadLock(() => Users.GetMultiple(userid)); - } - - public List GetBySeriesID(int seriesid) - { - return ReadLock(() => Series.GetMultiple(seriesid)); - } + public List GetByUserID(int userID) + => ReadLock(() => _userIDs.GetMultiple(userID)); + public List GetBySeriesID(int seriesID) + => ReadLock(() => _seriesIDs.GetMultiple(seriesID)); public List GetMostRecentlyWatched(int userID) - { - return - GetByUserID(userID) - .Where(a => a.UnwatchedEpisodeCount > 0) - .OrderByDescending(a => a.WatchedDate) - .ToList(); - } - + => GetByUserID(userID) + .Where(a => a.UnwatchedEpisodeCount > 0) + .OrderByDescending(a => a.WatchedDate) + .ToList(); - public ChangeTracker GetChangeTracker(int userid) - { - return Changes.TryGetValue(userid, out var change) ? change : new ChangeTracker(); - } + public ChangeTracker GetChangeTracker(int userID) + => _changes.TryGetValue(userID, out var change) ? change : new ChangeTracker(); } diff --git a/Shoko.Server/Repositories/Cached/AnimeStaffRepository.cs b/Shoko.Server/Repositories/Cached/AnimeStaffRepository.cs deleted file mode 100644 index 4bb95b902..000000000 --- a/Shoko.Server/Repositories/Cached/AnimeStaffRepository.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NutzCode.InMemoryIndex; -using Shoko.Models.Server; -using Shoko.Server.Databases; - -namespace Shoko.Server.Repositories.Cached; - -public class AnimeStaffRepository : BaseCachedRepository -{ - private PocoIndex AniDBIDs; - - public override void RegenerateDb() - { - } - - protected override int SelectKey(AnimeStaff entity) - { - return entity.StaffID; - } - - public override void PopulateIndexes() - { - AniDBIDs = new PocoIndex(Cache, a => a.AniDBID); - } - - - public AnimeStaff GetByAniDBID(int id) - { - return ReadLock(() => AniDBIDs.GetOne(id)); - } - - public AnimeStaffRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } -} diff --git a/Shoko.Server/Repositories/Cached/AuthTokensRepository.cs b/Shoko.Server/Repositories/Cached/AuthTokensRepository.cs index 179737e52..cf634166f 100644 --- a/Shoko.Server/Repositories/Cached/AuthTokensRepository.cs +++ b/Shoko.Server/Repositories/Cached/AuthTokensRepository.cs @@ -7,24 +7,30 @@ namespace Shoko.Server.Repositories.Cached; -public class AuthTokensRepository : BaseCachedRepository +public class AuthTokensRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Tokens; - private PocoIndex UserIDs; + private PocoIndex _tokens; + + private PocoIndex _userIDs; + + protected override int SelectKey(AuthTokens entity) + => entity.AuthID; + + public override void PopulateIndexes() + { + _tokens = new PocoIndex(Cache, a => a.Token); + _userIDs = new PocoIndex(Cache, a => a.UserID); + } public AuthTokens GetByToken(string token) { if (string.IsNullOrEmpty(token)) - { return null; - } - var tokens = ReadLock(Tokens.GetMultiple(token.ToLowerInvariant().Trim()).ToList); + var tokens = ReadLock(_tokens.GetMultiple(token.ToLowerInvariant().Trim()).ToList); var auth = tokens.FirstOrDefault(); if (tokens.Count <= 1) - { return auth; - } tokens.Remove(auth); tokens.ForEach(Delete); @@ -33,70 +39,53 @@ public AuthTokens GetByToken(string token) public void DeleteAllWithUserID(int id) { - var ids = ReadLock(() => UserIDs.GetMultiple(id)); + var ids = ReadLock(() => _userIDs.GetMultiple(id)); ids.ForEach(Delete); } public void DeleteWithToken(string token) { if (string.IsNullOrEmpty(token)) - { return; - } - var tokens = ReadLock(() => Tokens.GetMultiple(token)); + var tokens = ReadLock(() => _tokens.GetMultiple(token)); tokens.ForEach(Delete); } - public List GetByUserID(int userID) - { - return ReadLock(() => UserIDs.GetMultiple(userID)); - } - - protected override int SelectKey(AuthTokens entity) - { - return entity.AuthID; - } - - public override void PopulateIndexes() - { - Tokens = new PocoIndex(Cache, a => a.Token); - UserIDs = new PocoIndex(Cache, a => a.UserID); - } - - public override void RegenerateDb() - { - } + public IReadOnlyList GetByUserID(int userID) + => ReadLock(() => _userIDs.GetMultiple(userID)); public string ValidateUser(string username, string password, string device) - { - JMMUser user = RepoFactory.JMMUser.AuthenticateUser(username, password); - return CreateNewApikey(user, device); - } + => CreateNewApiKey(RepoFactory.JMMUser.AuthenticateUser(username, password), device); - public string CreateNewApikey(JMMUser user, string device) + public string CreateNewApiKey(JMMUser user, string device) { - if (user == null) return string.Empty; + if (user == null) + return string.Empty; // get tokens that are invalid var uid = user.JMMUserID; - var ids = ReadLock(() => UserIDs.GetMultiple(uid)); + var ids = ReadLock(() => _userIDs.GetMultiple(uid)); var tokens = ids.Where(a => !string.IsNullOrEmpty(a.Token) && a.DeviceName.Trim().Equals(device.Trim(), StringComparison.InvariantCultureIgnoreCase)) .ToList(); var auth = tokens.FirstOrDefault(); if (tokens.Count > 1) { - if (auth != null) tokens.Remove(auth); + if (auth != null) + tokens.Remove(auth); Delete(tokens); } - var invalidTokens = ids.Where(a => string.IsNullOrEmpty(a.Token)).ToList(); + var invalidTokens = ids + .Where(a => string.IsNullOrEmpty(a.Token)) + .ToList(); Delete(invalidTokens); var apiKey = auth?.Token.ToLowerInvariant().Trim() ?? string.Empty; - if (!string.IsNullOrEmpty(apiKey)) return apiKey; + if (!string.IsNullOrEmpty(apiKey)) + return apiKey; apiKey = Guid.NewGuid().ToString().ToLowerInvariant().Trim(); var newToken = new AuthTokens { UserID = uid, DeviceName = device.Trim().ToLowerInvariant(), Token = apiKey }; @@ -104,9 +93,4 @@ public string CreateNewApikey(JMMUser user, string device) return apiKey; } - - - public AuthTokensRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_MALRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_MALRepository.cs index 280920e90..562022ca0 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_MALRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_MALRepository.cs @@ -4,40 +4,27 @@ using Shoko.Models.Server; using Shoko.Server.Databases; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_AniDB_MALRepository : BaseCachedRepository +public class CrossRef_AniDB_MALRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex _animeIDs; - private PocoIndex _MALIDs; + private PocoIndex? _animeIDs; - public List GetByAnimeID(int id) - { - return ReadLock(() => - _animeIDs.GetMultiple(id).OrderBy(a => a.StartEpisodeType).ThenBy(a => a.StartEpisodeNumber).ToList()); - } - - public List GetByMALID(int id) - { - return ReadLock(() => _MALIDs.GetMultiple(id)); - } + private PocoIndex? _malIDs; protected override int SelectKey(CrossRef_AniDB_MAL entity) - { - return entity.CrossRef_AniDB_MALID; - } + => entity.CrossRef_AniDB_MALID; public override void PopulateIndexes() { - _MALIDs = new PocoIndex(Cache, a => a.MALID); + _malIDs = new PocoIndex(Cache, a => a.MALID); _animeIDs = new PocoIndex(Cache, a => a.AnimeID); } - public override void RegenerateDb() - { - } + public IReadOnlyList GetByAnimeID(int animeID) + => ReadLock(() => _animeIDs!.GetMultiple(animeID).OrderBy(a => a.StartEpisodeType).ThenBy(a => a.StartEpisodeNumber).ToList()); - public CrossRef_AniDB_MALRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public IReadOnlyList GetByMALID(int malID) + => ReadLock(() => _malIDs!.GetMultiple(malID)); } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_EpisodeRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_EpisodeRepository.cs index c3dec6209..96472430f 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_EpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_EpisodeRepository.cs @@ -7,14 +7,30 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_AniDB_TMDB_EpisodeRepository : BaseCachedRepository +public class CrossRef_AniDB_TMDB_EpisodeRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { private PocoIndex? _anidbAnimeIDs; + private PocoIndex? _anidbEpisodeIDs; + private PocoIndex? _tmdbShowIDs; + private PocoIndex? _tmdbEpisodeIDs; + private PocoIndex? _pairedIDs; + protected override int SelectKey(CrossRef_AniDB_TMDB_Episode entity) + => entity.CrossRef_AniDB_TMDB_EpisodeID; + + public override void PopulateIndexes() + { + _anidbAnimeIDs = new(Cache, a => a.AnidbAnimeID); + _anidbEpisodeIDs = new(Cache, a => a.AnidbEpisodeID); + _tmdbShowIDs = new(Cache, a => a.TmdbShowID); + _tmdbEpisodeIDs = new(Cache, a => a.TmdbEpisodeID); + _pairedIDs = new(Cache, a => (a.AnidbAnimeID, a.TmdbShowID)); + } + public IReadOnlyList GetByAnidbAnimeID(int animeId) => ReadLock(() => _anidbAnimeIDs!.GetMultiple(animeId).ToList()); @@ -31,28 +47,8 @@ public IReadOnlyList GetByTmdbEpisodeID(int episode => ReadLock(() => _tmdbEpisodeIDs!.GetMultiple(episodeId).OrderBy(a => a.Ordering).ToList()); public IReadOnlyList GetAllByAnidbAnimeAndTmdbShowIDs(int anidbId, int tmdbId) - => ReadLock(() => _tmdbShowIDs!.GetMultiple(tmdbId).Concat(_anidbAnimeIDs!.GetMultiple(anidbId)).ToList()); + => GetByTmdbShowID(tmdbId).Concat(GetByAnidbAnimeID(anidbId)).ToList(); public IReadOnlyList GetOnlyByAnidbAnimeAndTmdbShowIDs(int anidbId, int tmdbId) => ReadLock(() => _pairedIDs!.GetMultiple((anidbId, tmdbId))); - - protected override int SelectKey(CrossRef_AniDB_TMDB_Episode entity) - => entity.CrossRef_AniDB_TMDB_EpisodeID; - - public override void PopulateIndexes() - { - _anidbAnimeIDs = new(Cache, a => a.AnidbAnimeID); - _anidbEpisodeIDs = new(Cache, a => a.AnidbEpisodeID); - _tmdbShowIDs = new(Cache, a => a.TmdbShowID); - _tmdbEpisodeIDs = new(Cache, a => a.TmdbEpisodeID); - _pairedIDs = new(Cache, a => (a.AnidbAnimeID, a.TmdbShowID)); - } - - public override void RegenerateDb() - { - } - - public CrossRef_AniDB_TMDB_EpisodeRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_MovieRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_MovieRepository.cs index 292225a5b..e47688761 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_MovieRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_MovieRepository.cs @@ -8,12 +8,24 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_AniDB_TMDB_MovieRepository : BaseCachedRepository +public class CrossRef_AniDB_TMDB_MovieRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { private PocoIndex? _anidbAnimeIDs; + private PocoIndex? _anidbEpisodeIDs; + private PocoIndex? _tmdbMovieIDs; + protected override int SelectKey(CrossRef_AniDB_TMDB_Movie entity) + => entity.CrossRef_AniDB_TMDB_MovieID; + + public override void PopulateIndexes() + { + _tmdbMovieIDs = new(Cache, a => a.TmdbMovieID); + _anidbAnimeIDs = new(Cache, a => a.AnidbAnimeID); + _anidbEpisodeIDs = new(Cache, a => a.AnidbEpisodeID); + } + public IReadOnlyList GetByAnidbAnimeID(int animeId) => ReadLock(() => _anidbAnimeIDs!.GetMultiple(animeId)); @@ -31,26 +43,6 @@ public ILookup GetByAnimeIDsAndType(IReadOnlyCol if (animeIds == null || animeIds?.Count == 0) return EmptyLookup.Instance; - return Lock( - () => animeIds!.SelectMany(animeId => _anidbAnimeIDs!.GetMultiple(animeId)).ToLookup(xref => xref.AnidbAnimeID) - ); - } - - protected override int SelectKey(CrossRef_AniDB_TMDB_Movie entity) - => entity.CrossRef_AniDB_TMDB_MovieID; - - public override void PopulateIndexes() - { - _tmdbMovieIDs = new(Cache, a => a.TmdbMovieID); - _anidbAnimeIDs = new(Cache, a => a.AnidbAnimeID); - _anidbEpisodeIDs = new(Cache, a => a.AnidbEpisodeID); - } - - public override void RegenerateDb() - { - } - - public CrossRef_AniDB_TMDB_MovieRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { + return Lock(() => animeIds!.SelectMany(animeId => _anidbAnimeIDs!.GetMultiple(animeId)).ToLookup(xref => xref.AnidbAnimeID)); } } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_ShowRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_ShowRepository.cs index 2aed7bb69..fa1afb346 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_ShowRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TMDB_ShowRepository.cs @@ -8,12 +8,24 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_AniDB_TMDB_ShowRepository : BaseCachedRepository +public class CrossRef_AniDB_TMDB_ShowRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { private PocoIndex? _anidbAnimeIDs; + private PocoIndex? _tmdbShowIDs; + private PocoIndex? _pairedIDs; + protected override int SelectKey(CrossRef_AniDB_TMDB_Show entity) + => entity.CrossRef_AniDB_TMDB_ShowID; + + public override void PopulateIndexes() + { + _tmdbShowIDs = new(Cache, a => a.TmdbShowID); + _anidbAnimeIDs = new(Cache, a => a.AnidbAnimeID); + _pairedIDs = new(Cache, a => (a.AnidbAnimeID, a.TmdbShowID)); + } + public IReadOnlyList GetByAnidbAnimeID(int animeId) => ReadLock(() => _anidbAnimeIDs!.GetMultiple(animeId)); @@ -28,26 +40,6 @@ public ILookup GetByAnimeIDsAndType(IReadOnlyColl if (animeIds == null || animeIds?.Count == 0) return EmptyLookup.Instance; - return Lock( - () => animeIds!.SelectMany(animeId => _anidbAnimeIDs!.GetMultiple(animeId)).ToLookup(xref => xref.AnidbAnimeID) - ); - } - - protected override int SelectKey(CrossRef_AniDB_TMDB_Show entity) - => entity.CrossRef_AniDB_TMDB_ShowID; - - public override void PopulateIndexes() - { - _tmdbShowIDs = new(Cache, a => a.TmdbShowID); - _anidbAnimeIDs = new(Cache, a => a.AnidbAnimeID); - _pairedIDs = new(Cache, a => (a.AnidbAnimeID, a.TmdbShowID)); - } - - public override void RegenerateDb() - { - } - - public CrossRef_AniDB_TMDB_ShowRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { + return Lock(() => animeIds!.SelectMany(animeId => _anidbAnimeIDs!.GetMultiple(animeId)).ToLookup(xref => xref.AnidbAnimeID)); } } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs index 372449dd6..41b663605 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_AniDB_TraktV2Repository.cs @@ -1,34 +1,35 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NHibernate; using NHibernate.Criterion; using NutzCode.InMemoryIndex; -using Shoko.Commons.Collections; using Shoko.Models.Server; using Shoko.Server.Databases; +#nullable enable +#pragma warning disable CA1822 namespace Shoko.Server.Repositories.Cached; -public class CrossRef_AniDB_TraktV2Repository : BaseCachedRepository +public class CrossRef_AniDB_TraktV2Repository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex AnimeIDs; + private PocoIndex? _animeIDs; - public List GetByAnimeID(int id) - { - return AnimeIDs.GetMultiple(id).OrderBy(a => a.AniDBStartEpisodeType).ThenBy(a => a.AniDBStartEpisodeNumber).ToList(); - } + protected override int SelectKey(CrossRef_AniDB_TraktV2 entity) + => entity.CrossRef_AniDB_TraktV2ID; - public List GetByAnimeIDEpTypeEpNumber(int id, int aniEpType, int aniEpisodeNumber) + public override void PopulateIndexes() { - return AnimeIDs.GetMultiple(id).Where(a => a.AniDBStartEpisodeType == aniEpType && a.AniDBStartEpisodeNumber == aniEpisodeNumber).ToList(); + _animeIDs = new PocoIndex(Cache, a => a.AnimeID); } - public CrossRef_AniDB_TraktV2 GetByTraktID(ISession session, string id, int season, int episodeNumber, - int animeID, - int aniEpType, int aniEpisodeNumber) - { - return Lock(() => + public IReadOnlyList GetByAnimeID(int id) + => _animeIDs!.GetMultiple(id).OrderBy(a => a.AniDBStartEpisodeType).ThenBy(a => a.AniDBStartEpisodeNumber).ToList(); + + public IReadOnlyList GetByAnimeIDEpTypeEpNumber(int id, int aniEpType, int aniEpisodeNumber) + => _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, int animeID, int aniEpType, int aniEpisodeNumber) + => Lock(() => { var cr = session .CreateCriteria(typeof(CrossRef_AniDB_TraktV2)) @@ -41,21 +42,16 @@ public CrossRef_AniDB_TraktV2 GetByTraktID(ISession session, string id, int seas .UniqueResult(); return cr; }); - } - public CrossRef_AniDB_TraktV2 GetByTraktID(string id, int season, int episodeNumber, int animeID, int aniEpType, - int aniEpisodeNumber) - { - return Lock(() => + public CrossRef_AniDB_TraktV2? GetByTraktID(string id, int season, int episodeNumber, int animeID, int aniEpType, int aniEpisodeNumber) + => Lock(() => { using var session = _databaseFactory.SessionFactory.OpenSession(); return GetByTraktID(session, id, season, episodeNumber, animeID, aniEpType, aniEpisodeNumber); }); - } - public List GetByTraktID(string traktID) - { - return Lock(() => + public IReadOnlyList GetByTraktID(string traktID) + => Lock(() => { using var session = _databaseFactory.SessionFactory.OpenSession(); var xrefs = session @@ -65,39 +61,7 @@ public List GetByTraktID(string traktID) return new List(xrefs); }); - } internal ILookup GetByAnimeIDs(IReadOnlyCollection animeIds) - { - if (animeIds == null) - { - throw new ArgumentNullException(nameof(animeIds)); - } - - if (animeIds.Count == 0) - { - return EmptyLookup.Instance; - } - - return ReadLock(() => animeIds.SelectMany(id => AnimeIDs.GetMultiple(id)) - .ToLookup(xref => xref.AnimeID)); - } - - protected override int SelectKey(CrossRef_AniDB_TraktV2 entity) - { - return entity.CrossRef_AniDB_TraktV2ID; - } - - public override void PopulateIndexes() - { - AnimeIDs = new PocoIndex(Cache, a => a.AnimeID); - } - - public override void RegenerateDb() - { - } - - public CrossRef_AniDB_TraktV2Repository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + => ReadLock(() => animeIds.SelectMany(id => _animeIDs!.GetMultiple(id)).ToLookup(xref => xref.AnimeID)); } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_Anime_StaffRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_Anime_StaffRepository.cs deleted file mode 100644 index 4e9a50db1..000000000 --- a/Shoko.Server/Repositories/Cached/CrossRef_Anime_StaffRepository.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NutzCode.InMemoryIndex; -using Shoko.Commons.Properties; -using Shoko.Models.Enums; -using Shoko.Models.Server; -using Shoko.Server.Databases; -using Shoko.Server.Server; - -namespace Shoko.Server.Repositories.Cached; - -public class CrossRef_Anime_StaffRepository : BaseCachedRepository -{ - private PocoIndex AnimeIDs; - private PocoIndex StaffIDs; - private PocoIndex RoleIDs; - private PocoIndex RoleTypes; - - public static readonly Dictionary Roles = - new() - { - { "main character in", CharacterAppearanceType.Main_Character }, - { "secondary cast in", CharacterAppearanceType.Minor_Character }, - { "appears in", CharacterAppearanceType.Background_Character }, - { "cameo appearance in", CharacterAppearanceType.Cameo } - }; - - public override void PopulateIndexes() - { - AnimeIDs = new PocoIndex(Cache, a => a.AniDB_AnimeID); - StaffIDs = new PocoIndex(Cache, a => a.StaffID); - RoleIDs = new PocoIndex(Cache, a => a.RoleID); - RoleTypes = new PocoIndex(Cache, a => (StaffRoleType)a.RoleType); - } - - public override void RegenerateDb() - { - var list = Cache.Values.Where(animeStaff => - animeStaff.RoleID != null && !string.IsNullOrEmpty(animeStaff.Role) && Roles.ContainsKey(animeStaff.Role) && - Roles[animeStaff.Role].ToString().Contains("_")).ToList(); - for (var index = 0; index < list.Count; index++) - { - var animeStaff = list[index]; - animeStaff.Role = Roles[animeStaff.Role].ToString().Replace("_", " "); - Save(animeStaff); - if (index % 10 == 0) - { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, nameof(CrossRef_Anime_Staff), - $" DbRegen - {index}/{list.Count}" - ); - } - } - } - - protected override int SelectKey(CrossRef_Anime_Staff entity) - { - return entity.CrossRef_Anime_StaffID; - } - - public List GetByStaffID(int id) - { - return ReadLock(() => StaffIDs.GetMultiple(id)); - } - - public List GetByRoleID(int id) - { - return ReadLock(() => RoleIDs.GetMultiple(id)); - } - - public List GetByRoleType(StaffRoleType type) - { - return ReadLock(() => RoleTypes.GetMultiple(type)); - } - - public List GetByAnimeID(int id) - { - return ReadLock(() => AnimeIDs.GetMultiple(id)); - } - - public List GetByAnimeIDAndRoleType(int id, StaffRoleType type) - { - return GetByAnimeID(id).Where(xref => xref.RoleType == (int)type).ToList(); - } - - public CrossRef_Anime_Staff GetByParts(int AnimeID, int? RoleID, int StaffID, StaffRoleType RoleType) - { - return GetByAnimeID(AnimeID).FirstOrDefault(a => - a.RoleID == RoleID && a.StaffID == StaffID && a.RoleType == (int)RoleType); - } - - public CrossRef_Anime_StaffRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } -} diff --git a/Shoko.Server/Repositories/Cached/CrossRef_CustomTagRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_CustomTagRepository.cs index fc45438bf..32e8c9569 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_CustomTagRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_CustomTagRepository.cs @@ -5,45 +5,34 @@ using Shoko.Models.Server; using Shoko.Server.Databases; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_CustomTagRepository : BaseCachedRepository +public class CrossRef_CustomTagRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Tags; - private PocoIndex Refs; + private PocoIndex? _customTagIDs; + private PocoIndex? _entityIDandType; protected override int SelectKey(CrossRef_CustomTag entity) - { - return entity.CrossRef_CustomTagID; - } + => entity.CrossRef_CustomTagID; public override void PopulateIndexes() { - Tags = new PocoIndex(Cache, a => a.CustomTagID); - Refs = new PocoIndex(Cache, a => a.CrossRefID, a => a.CrossRefType); + _customTagIDs = new PocoIndex(Cache, a => a.CustomTagID); + _entityIDandType = new PocoIndex(Cache, a => (a.CrossRefID, (CustomTagCrossRefType)a.CrossRefType)); } - public override void RegenerateDb() - { - } + public IReadOnlyList GetByCustomTagID(int customTagID) + => ReadLock(() => _customTagIDs!.GetMultiple(customTagID)); - public List GetByAnimeID(int id) - { - return ReadLock(() => Refs.GetMultiple(id, (int)CustomTagCrossRefType.Anime)); - } + public IReadOnlyList GetByEntityIDAndType(int entityID, CustomTagCrossRefType entityType) + => ReadLock(() => _entityIDandType!.GetMultiple((entityID, entityType))); - public List GetByCustomTagID(int id) - { - return ReadLock(() => Tags.GetMultiple(id)); - } - - public List GetByUniqueID(int customTagID, int crossRefType, int crossRefID) - { - return ReadLock(() => - Refs.GetMultiple(crossRefID, crossRefType).Where(a => a.CustomTagID == customTagID).ToList()); - } + public IReadOnlyList GetByAnimeID(int animeID) + => GetByEntityIDAndType(animeID, CustomTagCrossRefType.Anime); - public CrossRef_CustomTagRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public IReadOnlyList GetByUniqueID(int customTagID, CustomTagCrossRefType entityType, int entityID) + => GetByEntityIDAndType(entityID, entityType) + .Where(a => a.CustomTagID == customTagID) + .ToList(); } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_File_EpisodeRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_File_EpisodeRepository.cs index 5ffaa0709..f9f2dcb98 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_File_EpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_File_EpisodeRepository.cs @@ -7,34 +7,27 @@ using Shoko.Server.Scheduling; using Shoko.Server.Scheduling.Jobs.Actions; +#nullable enable namespace Shoko.Server.Repositories.Cached; public class CrossRef_File_EpisodeRepository : BaseCachedRepository { private readonly ILogger _logger; + private readonly JobFactory _jobFactory; - private PocoIndex Hashes; - private PocoIndex Animes; - private PocoIndex Episodes; - private PocoIndex Filenames; + private PocoIndex? _ed2k; - public override void PopulateIndexes() - { - Hashes = new PocoIndex(Cache, a => a.Hash); - Animes = new PocoIndex(Cache, a => a.AnimeID); - Episodes = new PocoIndex(Cache, a => a.EpisodeID); - Filenames = new PocoIndex(Cache, a => a.FileName); - } + private PocoIndex? _anidbAnimeIDs; - public override void RegenerateDb() - { - } + private PocoIndex? _anidbEpisodeIDs; - public CrossRef_File_EpisodeRepository(DatabaseFactory databaseFactory, JobFactory jobFactory, ILogger logger) : base(databaseFactory) + private PocoIndex? _fileNames; + + public CrossRef_File_EpisodeRepository(ILogger logger, JobFactory jobFactory, DatabaseFactory databaseFactory) : base(databaseFactory) { - _jobFactory = jobFactory; _logger = logger; + _jobFactory = jobFactory; EndSaveCallback = obj => { var job = _jobFactory.CreateJob(a => a.AnimeID = obj.AnimeID); @@ -51,40 +44,25 @@ public CrossRef_File_EpisodeRepository(DatabaseFactory databaseFactory, JobFacto } protected override int SelectKey(SVR_CrossRef_File_Episode entity) - { - return entity.CrossRef_File_EpisodeID; - } + => entity.CrossRef_File_EpisodeID; - public List GetByHash(string hash) + public override void PopulateIndexes() { - return ReadLock(() => Hashes.GetMultiple(hash).OrderBy(a => a.EpisodeOrder).ToList()); + _ed2k = new PocoIndex(Cache, a => a.Hash); + _anidbAnimeIDs = new PocoIndex(Cache, a => a.AnimeID); + _anidbEpisodeIDs = new PocoIndex(Cache, a => a.EpisodeID); + _fileNames = new PocoIndex(Cache, a => (a.FileName, a.FileSize)); } + public IReadOnlyList GetByEd2k(string ed2k) + => ReadLock(() => _ed2k!.GetMultiple(ed2k).OrderBy(a => a.EpisodeOrder).ToList()); - public List GetByAnimeID(int animeID) - { - return ReadLock(() => Animes.GetMultiple(animeID)); - } - + public IReadOnlyList GetByAnimeID(int animeID) + => ReadLock(() => _anidbAnimeIDs!.GetMultiple(animeID)); - public List GetByFileNameAndSize(string filename, long filesize) - { - return ReadLock(() => Filenames.GetMultiple(filename).Where(a => a.FileSize == filesize).ToList()); - } + public IReadOnlyList GetByFileNameAndSize(string fileName, long fileSize) + => ReadLock(() => _fileNames!.GetMultiple((fileName, fileSize))); - /// - /// This is the only way to uniquely identify the record other than the IDENTITY - /// - /// - /// - /// - public SVR_CrossRef_File_Episode GetByHashAndEpisodeID(string hash, int episodeID) - { - return ReadLock(() => Hashes.GetMultiple(hash).FirstOrDefault(a => a.EpisodeID == episodeID)); - } - - public List GetByEpisodeID(int episodeID) - { - return ReadLock(() => Episodes.GetMultiple(episodeID)); - } + public IReadOnlyList GetByEpisodeID(int episodeID) + => ReadLock(() => _anidbEpisodeIDs!.GetMultiple(episodeID)); } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_Languages_AniDB_FileRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_Languages_AniDB_FileRepository.cs index 4e2ca39b6..f7a51672d 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_Languages_AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_Languages_AniDB_FileRepository.cs @@ -7,46 +7,42 @@ using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_Languages_AniDB_FileRepository : BaseCachedRepository +public class CrossRef_Languages_AniDB_FileRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex _fileIDs; + private PocoIndex? _fileIDs; - public List GetByFileID(int id) - { - return ReadLock(() => _fileIDs.GetMultiple(id)); - } - - public HashSet GetLanguagesForGroup(SVR_AnimeGroup group) - { - return ReadLock(() => - { - return group.AllSeries.Select(a => a.AniDB_Anime).WhereNotNull().SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByAnimeID(a.AnimeID)) - .Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)).WhereNotNull().SelectMany(a => GetByFileID(a.FileID)).Select(a => a.LanguageName) - .ToHashSet(StringComparer.InvariantCultureIgnoreCase); - }); - } - - public HashSet GetLanguagesForAnime(int animeID) - { - return ReadLock(() => - { - return RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID)?.Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)).WhereNotNull() - .SelectMany(a => GetByFileID(a.FileID)).Select(a => a.LanguageName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? []; - }); - } + protected override int SelectKey(CrossRef_Languages_AniDB_File entity) + => entity.CrossRef_Languages_AniDB_FileID; public override void PopulateIndexes() { _fileIDs = Cache.CreateIndex(a => a.FileID); } - public override void RegenerateDb() { } + public List GetByFileID(int id) + => ReadLock(() => _fileIDs!.GetMultiple(id)); - protected override int SelectKey(CrossRef_Languages_AniDB_File entity) => entity.CrossRef_Languages_AniDB_FileID; + public HashSet GetLanguagesForGroup(SVR_AnimeGroup group) + => ReadLock(() => group.AllSeries + .Select(a => a.AniDB_Anime) + .WhereNotNull() + .SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByAnimeID(a.AnimeID)) + .Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)) + .WhereNotNull() + .SelectMany(a => GetByFileID(a.FileID)) + .Select(a => a.LanguageName) + .ToHashSet(StringComparer.InvariantCultureIgnoreCase) + ); - public CrossRef_Languages_AniDB_FileRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public HashSet GetLanguagesForAnime(int animeID) + => ReadLock(() => RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) + .Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)) + .WhereNotNull() + .SelectMany(a => GetByFileID(a.FileID)) + .Select(a => a.LanguageName) + .ToHashSet(StringComparer.InvariantCultureIgnoreCase) + ); } diff --git a/Shoko.Server/Repositories/Cached/CrossRef_Subtitles_AniDB_FileRepository.cs b/Shoko.Server/Repositories/Cached/CrossRef_Subtitles_AniDB_FileRepository.cs index 715102885..417d3f88d 100644 --- a/Shoko.Server/Repositories/Cached/CrossRef_Subtitles_AniDB_FileRepository.cs +++ b/Shoko.Server/Repositories/Cached/CrossRef_Subtitles_AniDB_FileRepository.cs @@ -1,53 +1,48 @@ using System; using System.Collections.Generic; using System.Linq; -using NHibernate; using NutzCode.InMemoryIndex; using Shoko.Commons.Extensions; using Shoko.Models.Server; using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class CrossRef_Subtitles_AniDB_FileRepository : BaseCachedRepository +public class CrossRef_Subtitles_AniDB_FileRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex FileIDs; + private PocoIndex? _fileIDs; - public List GetByFileID(int id) - { - return FileIDs.GetMultiple(id); - } - - public HashSet GetLanguagesForGroup(SVR_AnimeGroup group) - { - return ReadLock(() => - { - return group.AllSeries.Select(a => a.AniDB_Anime).WhereNotNull().SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByAnimeID(a.AnimeID)) - .Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)).WhereNotNull().SelectMany(a => GetByFileID(a.FileID)).Select(a => a.LanguageName) - .ToHashSet(StringComparer.InvariantCultureIgnoreCase); - }); - } - - public HashSet GetLanguagesForAnime(int animeID) - { - return ReadLock(() => - { - return RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID)?.Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)).WhereNotNull() - .SelectMany(a => GetByFileID(a.FileID)).Select(a => a.LanguageName).ToHashSet(StringComparer.InvariantCultureIgnoreCase) ?? []; - }); - } + protected override int SelectKey(CrossRef_Subtitles_AniDB_File entity) + => entity.CrossRef_Subtitles_AniDB_FileID; public override void PopulateIndexes() { - FileIDs = Cache.CreateIndex(a => a.FileID); + _fileIDs = Cache.CreateIndex(a => a.FileID); } - public override void RegenerateDb() { } + public IReadOnlyList GetByFileID(int id) + => _fileIDs!.GetMultiple(id); - protected override int SelectKey(CrossRef_Subtitles_AniDB_File entity) => entity.CrossRef_Subtitles_AniDB_FileID; + public HashSet GetLanguagesForGroup(SVR_AnimeGroup group) + => ReadLock(() => group.AllSeries + .Select(a => a.AniDB_Anime) + .WhereNotNull() + .SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByAnimeID(a.AnimeID)) + .Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)) + .WhereNotNull() + .SelectMany(a => GetByFileID(a.FileID)) + .Select(a => a.LanguageName) + .ToHashSet(StringComparer.InvariantCultureIgnoreCase) + ); - public CrossRef_Subtitles_AniDB_FileRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public HashSet GetLanguagesForAnime(int animeID) + => ReadLock(() => RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) + .Select(a => RepoFactory.AniDB_File.GetByHash(a.Hash)) + .WhereNotNull() + .SelectMany(a => GetByFileID(a.FileID)) + .Select(a => a.LanguageName) + .ToHashSet(StringComparer.InvariantCultureIgnoreCase) + ); } diff --git a/Shoko.Server/Repositories/Cached/CustomTagRepository.cs b/Shoko.Server/Repositories/Cached/CustomTagRepository.cs index 106638376..9845af302 100644 --- a/Shoko.Server/Repositories/Cached/CustomTagRepository.cs +++ b/Shoko.Server/Repositories/Cached/CustomTagRepository.cs @@ -12,18 +12,13 @@ public class CustomTagRepository : BaseCachedRepository { private PocoIndex? _names; + protected override int SelectKey(CustomTag entity) + => entity.CustomTagID; + public CustomTagRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { DeleteWithOpenTransactionCallback = (ses, obj) => - { - RepoFactory.CrossRef_CustomTag.DeleteWithOpenTransaction(ses, - RepoFactory.CrossRef_CustomTag.GetByCustomTagID(obj.CustomTagID)); - }; - } - - protected override int SelectKey(CustomTag entity) - { - return entity.CustomTagID; + RepoFactory.CrossRef_CustomTag.DeleteWithOpenTransaction(ses, RepoFactory.CrossRef_CustomTag.GetByCustomTagID(obj.CustomTagID)); } public override void PopulateIndexes() @@ -31,29 +26,14 @@ public override void PopulateIndexes() _names = new PocoIndex(Cache, a => a.TagName); } - public override void RegenerateDb() - { - } - public List GetByAnimeID(int animeID) - { - return RepoFactory.CrossRef_CustomTag.GetByAnimeID(animeID) + => RepoFactory.CrossRef_CustomTag.GetByAnimeID(animeID) .Select(a => GetByID(a.CustomTagID)) .Where(a => a != null) .ToList(); - } public CustomTag? GetByTagName(string? tagName) - => !string.IsNullOrEmpty(tagName?.Trim()) + => !string.IsNullOrWhiteSpace(tagName) ? ReadLock(() => _names!.GetOne(tagName)) : null; - - public Dictionary> GetByAnimeIDs(ISessionWrapper session, int[] animeIDs) - { - return animeIDs.ToDictionary(a => a, - a => RepoFactory.CrossRef_CustomTag.GetByAnimeID(a) - .Select(b => GetByID(b.CustomTagID)) - .Where(b => b != null) - .ToList()); - } } diff --git a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs index fc1823177..b0d15e79c 100644 --- a/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs +++ b/Shoko.Server/Repositories/Cached/FilterPresetRepository.cs @@ -6,7 +6,6 @@ using Shoko.Commons.Properties; using Shoko.Models.Enums; using Shoko.Server.Databases; -using Shoko.Server.Filters; using Shoko.Server.Filters.Functions; using Shoko.Server.Filters.Info; using Shoko.Server.Filters.Logic.DateTimes; @@ -16,64 +15,56 @@ using Shoko.Server.Filters.User; using Shoko.Server.Models; using Shoko.Server.Server; + using Constants = Shoko.Server.Server.Constants; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class FilterPresetRepository : BaseCachedRepository +public class FilterPresetRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex Parents; public static readonly FilterPreset[] DirectoryFilters = [ - new FilterPreset + new() { Name = "Seasons", Locked = true, FilterType = GroupFilterType.Season | GroupFilterType.Directory, FilterPresetID = -1 }, - new FilterPreset + new() { Name = "Tags", Locked = true, FilterType = GroupFilterType.Tag | GroupFilterType.Directory, FilterPresetID = -2 }, - new FilterPreset + new() { Name = "Years", Locked = true, FilterType = GroupFilterType.Season | GroupFilterType.Directory, FilterPresetID = -3 } ]; + private PocoIndex? _parentIDs; + protected override int SelectKey(FilterPreset entity) - { - return entity.FilterPresetID; - } + => entity.FilterPresetID; public override void PopulateIndexes() { - Parents = Cache.CreateIndex(a => a.ParentFilterPresetID ?? 0); + _parentIDs = Cache.CreateIndex(a => a.ParentFilterPresetID ?? 0); } - public override void RegenerateDb() { } - + private const string Template = "FilterPreset"; 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); + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, Template, " " + Resources.GroupFilter_Cleanup); var all = GetAll(); var set = new HashSet(all); - var notin = all.Except(set).ToList(); - Delete(notin); + var notIn = all.Except(set).ToList(); + Delete(notIn); } public void CreateOrVerifyLockedFilters() { - const string t = "FilterPreset"; - var lockedGFs = GetLockedGroupFilters(); - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, t, - " " + Resources.Filter_CreateContinueWatching); + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, Template, " " + Resources.Filter_CreateContinueWatching); if (!lockedGFs.Any(a => a.Name == Constants.GroupFilterName.ContinueWatching)) { @@ -235,12 +226,10 @@ public override void Delete(IReadOnlyCollection objs) } } - public List GetByParentID(int parentid) - { - return ReadLock(() => Parents.GetMultiple(parentid)); - } + public IReadOnlyList GetByParentID(int parentID) + => ReadLock(() => _parentIDs!.GetMultiple(parentID)); - public FilterPreset GetTopLevelFilter(int filterID) + public FilterPreset? GetTopLevelFilter(int filterID) { var parent = GetByID(filterID); if (parent == null || parent.ParentFilterPresetID is null or 0) @@ -256,55 +245,47 @@ public FilterPreset GetTopLevelFilter(int filterID) } } - public List GetTopLevel() - { - return GetByParentID(0); - } + public IReadOnlyList GetTopLevel() + => GetByParentID(0); - public List GetLockedGroupFilters() - { - return ReadLock(() => Cache.Values.Where(a => a.Locked).ToList()); - } + public IReadOnlyList GetLockedGroupFilters() + => ReadLock(() => Cache.Values.Where(a => a.Locked).ToList()); - public List GetTimeDependentFilters() - { - return ReadLock(() => GetAll().Where(a => a.Expression.TimeDependent).ToList()); - } + public IReadOnlyList GetTimeDependentFilters() + => ReadLock(() => GetAll().Where(a => a.Expression.TimeDependent).ToList()); public static IReadOnlyList GetAllYearFilters(int offset = 0) - { - var years = RepoFactory.AnimeSeries.GetAllYears(); - return years.Select((s, i) => new FilterPreset - { - Name = s.ToString(), - FilterPresetID = offset - i, - ParentFilterPresetID = -3, - FilterType = GroupFilterType.Year, - Locked = true, - ApplyAtSeriesLevel = true, - Expression = new InYearExpression + => RepoFactory.AnimeSeries.GetAllYears() + .Select((s, i) => new FilterPreset { - Parameter = s - }, - SortingExpression = new NameSortingSelector() - }).ToList(); - } + Name = s.ToString(), + FilterPresetID = offset - i, + ParentFilterPresetID = -3, + FilterType = GroupFilterType.Year, + Locked = true, + ApplyAtSeriesLevel = true, + Expression = new InYearExpression + { + Parameter = s + }, + SortingExpression = new NameSortingSelector() + }) + .ToList(); public static IReadOnlyList GetAllSeasonFilters(int offset = 0) - { - var seasons = RepoFactory.AnimeSeries.GetAllSeasons(); - return seasons.Select((season, i) => new FilterPreset - { - Name = season.Season + " " + season.Year, - FilterPresetID = offset - i, - ParentFilterPresetID = -1, - Locked = true, - FilterType = GroupFilterType.Season, - ApplyAtSeriesLevel = true, - Expression = new InSeasonExpression { Season = season.Season, Year = season.Year }, - SortingExpression = new NameSortingSelector() - }).ToList(); - } + => RepoFactory.AnimeSeries.GetAllSeasons() + .Select((season, i) => new FilterPreset + { + Name = season.Season + " " + season.Year, + FilterPresetID = offset - i, + ParentFilterPresetID = -1, + Locked = true, + FilterType = GroupFilterType.Season, + ApplyAtSeriesLevel = true, + Expression = new InSeasonExpression { Season = season.Season, Year = season.Year }, + SortingExpression = new NameSortingSelector() + }) + .ToList(); public static IReadOnlyList GetAllTagFilters(int offset = 0) { @@ -335,6 +316,7 @@ public IReadOnlyList GetAllFiltersForLegacy(bool topLevel = false) var filters = GetAll().ToList(); filters.AddRange(DirectoryFilters); + var offset = -4; var result = GetAllSeasonFilters(offset); offset -= result.Count; @@ -346,8 +328,4 @@ public IReadOnlyList GetAllFiltersForLegacy(bool topLevel = false) filters.AddRange(result); return filters; } - - public FilterPresetRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/ImportFolderRepository.cs b/Shoko.Server/Repositories/Cached/ImportFolderRepository.cs index e97848135..5fcfc4489 100644 --- a/Shoko.Server/Repositories/Cached/ImportFolderRepository.cs +++ b/Shoko.Server/Repositories/Cached/ImportFolderRepository.cs @@ -5,36 +5,23 @@ using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class ImportFolderRepository : BaseCachedRepository +public class ImportFolderRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - public EventHandler ImportFolderSaved; - - public ImportFolderRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public event EventHandler? ImportFolderSaved; protected override int SelectKey(SVR_ImportFolder entity) - { - return entity.ImportFolderID; - } - - public override void PopulateIndexes() - { - } - - public override void RegenerateDb() - { - } + => entity.ImportFolderID; - public SVR_ImportFolder GetByImportLocation(string importloc) + public SVR_ImportFolder? GetByImportLocation(string importLocation) { return ReadLock(() => Cache.Values.FirstOrDefault(a => a.ImportFolderLocation?.Replace('\\', Path.DirectorySeparatorChar) .Replace('/', Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar) .Equals( - importloc?.Replace('\\', Path.DirectorySeparatorChar) + importLocation?.Replace('\\', Path.DirectorySeparatorChar) .Replace('/', Path.DirectorySeparatorChar) .TrimEnd(Path.DirectorySeparatorChar), StringComparison.InvariantCultureIgnoreCase) ?? false)); @@ -42,41 +29,27 @@ public SVR_ImportFolder GetByImportLocation(string importloc) public SVR_ImportFolder SaveImportFolder(ImportFolder folder) { - SVR_ImportFolder ns; - if (folder.ImportFolderID > 0) - { - // update - ns = GetByID(folder.ImportFolderID); - if (ns == null) - { - throw new Exception($"Could not find Import Folder ID: {folder.ImportFolderID}"); - } - } - else - { - // create - ns = new SVR_ImportFolder(); - } + var ns = (folder.ImportFolderID > 0 ? GetByID(folder.ImportFolderID) : new()) ?? + throw new Exception($"Could not find Import Folder ID: {folder.ImportFolderID}"); if (string.IsNullOrEmpty(folder.ImportFolderName)) - { throw new Exception("Must specify an Import Folder name"); - } if (string.IsNullOrEmpty(folder.ImportFolderLocation)) - { throw new Exception("Must specify an Import Folder location"); - } if (!Directory.Exists(folder.ImportFolderLocation)) - { throw new Exception("Cannot find Import Folder location"); - } - if (GetAll().ExceptBy([folder.ImportFolderID], iF => iF.ImportFolderID).Any(iF => folder.ImportFolderLocation.StartsWith(iF.ImportFolderLocation, StringComparison.OrdinalIgnoreCase) || iF.ImportFolderLocation.StartsWith(folder.ImportFolderLocation, StringComparison.OrdinalIgnoreCase))) + if (GetAll() + .ExceptBy([folder.ImportFolderID], iF => iF.ImportFolderID) + .Any(f => + folder.ImportFolderLocation.StartsWith(f.ImportFolderLocation, StringComparison.OrdinalIgnoreCase) || + f.ImportFolderLocation.StartsWith(folder.ImportFolderLocation, StringComparison.OrdinalIgnoreCase) + ) + ) throw new Exception("Unable to nest an import folder within another import folder."); - ns.ImportFolderName = folder.ImportFolderName; ns.ImportFolderLocation = folder.ImportFolderLocation; ns.IsDropDestination = folder.IsDropDestination; @@ -91,15 +64,15 @@ public SVR_ImportFolder SaveImportFolder(ImportFolder folder) return ns; } - public (SVR_ImportFolder folder, string relativePath) GetFromFullPath(string fullPath) + public (SVR_ImportFolder? folder, string relativePath) GetFromFullPath(string fullPath) { - if (string.IsNullOrEmpty(fullPath)) return default; - var shares = GetAll(); + if (string.IsNullOrEmpty(fullPath)) + return default; - // TODO make sure that import folders are not sub folders of each other - foreach (var ifolder in shares) + var folders = GetAll(); + foreach (var folder in folders) { - var importLocation = ifolder.ImportFolderLocation; + var importLocation = folder.ImportFolderLocation; var importLocationFull = importLocation.TrimEnd(Path.DirectorySeparatorChar); // add back the trailing backslashes @@ -110,7 +83,7 @@ public SVR_ImportFolder SaveImportFolder(ImportFolder folder) { var filePath = fullPath.Replace(importLocation, string.Empty); filePath = filePath.TrimStart(Path.DirectorySeparatorChar); - return (ifolder, filePath); + return (folder, filePath); } } diff --git a/Shoko.Server/Repositories/Cached/JMMUserRepository.cs b/Shoko.Server/Repositories/Cached/JMMUserRepository.cs index be337b5d7..0a4f9dc42 100644 --- a/Shoko.Server/Repositories/Cached/JMMUserRepository.cs +++ b/Shoko.Server/Repositories/Cached/JMMUserRepository.cs @@ -6,50 +6,33 @@ using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class JMMUserRepository : BaseCachedRepository +public class JMMUserRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private static Logger logger = LogManager.GetCurrentClassLogger(); - protected override int SelectKey(SVR_JMMUser entity) - { - return entity.JMMUserID; - } - - public override void PopulateIndexes() - { - } + => entity.JMMUserID; - public override void RegenerateDb() - { - } + public SVR_JMMUser? GetByUsername(string? username) + => !string.IsNullOrWhiteSpace(username) + ? ReadLock(() => Cache.Values.FirstOrDefault(user => string.Equals(user.Username, username, StringComparison.InvariantCultureIgnoreCase))) + : null; - public SVR_JMMUser GetByUsername(string username) - { - if (string.IsNullOrWhiteSpace(username)) - return null; + public IReadOnlyList GetAniDBUsers() + => ReadLock(() => Cache.Values.Where(a => a.IsAniDBUser == 1).ToList()); - return ReadLock(() => Cache.Values.FirstOrDefault(user => string.Equals(user.Username, username, StringComparison.InvariantCultureIgnoreCase))); - } + public IReadOnlyList GetTraktUsers() + => ReadLock(() => Cache.Values.Where(a => a.IsTraktUser == 1).ToList()); - public List GetAniDBUsers() - { - return ReadLock(() => Cache.Values.Where(a => a.IsAniDBUser == 1).ToList()); - } - - public List GetTraktUsers() - { - return ReadLock(() => Cache.Values.Where(a => a.IsTraktUser == 1).ToList()); - } - - public SVR_JMMUser AuthenticateUser(string userName, string password) + public SVR_JMMUser? AuthenticateUser(string userName, string? password) { password ??= string.Empty; var hashedPassword = Digest.Hash(password); return ReadLock(() => Cache.Values.FirstOrDefault(a => a.Username.Equals(userName, StringComparison.InvariantCultureIgnoreCase) && - a.Password.Equals(hashedPassword))); + a.Password.Equals(hashedPassword) + )); } public bool RemoveUser(int userID, bool skipValidation = false) @@ -57,7 +40,7 @@ public bool RemoveUser(int userID, bool skipValidation = false) var user = GetByID(userID); if (!skipValidation) { - var allAdmins = GetAll().Where(a => a.IsAdminUser()).ToList(); + var allAdmins = GetAll().Where(a => a.IsAdmin == 1).ToList(); allAdmins.Remove(user); if (allAdmins.Count < 1) { @@ -73,8 +56,4 @@ public bool RemoveUser(int userID, bool skipValidation = false) Delete(user); return true; } - - public JMMUserRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/TMDB_ImageRepository.cs b/Shoko.Server/Repositories/Cached/TMDB_ImageRepository.cs index 492899cd5..6a82068b2 100644 --- a/Shoko.Server/Repositories/Cached/TMDB_ImageRepository.cs +++ b/Shoko.Server/Repositories/Cached/TMDB_ImageRepository.cs @@ -9,19 +9,45 @@ #nullable enable namespace Shoko.Server.Repositories.Cached; -public class TMDB_ImageRepository : BaseCachedRepository +public class TMDB_ImageRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { private PocoIndex? _tmdbMovieIDs; + private PocoIndex? _tmdbEpisodeIDs; + private PocoIndex? _tmdbSeasonIDs; + private PocoIndex? _tmdbShowIDs; + private PocoIndex? _tmdbCollectionIDs; + private PocoIndex? _tmdbNetworkIDs; + private PocoIndex? _tmdbCompanyIDs; + private PocoIndex? _tmdbPersonIDs; + private PocoIndex? _tmdbTypes; + private PocoIndex? _tmdbRemoteFileNames; + protected override int SelectKey(TMDB_Image entity) + => entity.TMDB_ImageID; + + public override void PopulateIndexes() + { + _tmdbMovieIDs = new(Cache, a => a.TmdbMovieID); + _tmdbEpisodeIDs = new(Cache, a => a.TmdbEpisodeID); + _tmdbSeasonIDs = new(Cache, a => a.TmdbSeasonID); + _tmdbShowIDs = new(Cache, a => a.TmdbShowID); + _tmdbCollectionIDs = new(Cache, a => a.TmdbCollectionID); + _tmdbNetworkIDs = new(Cache, a => a.TmdbNetworkID); + _tmdbCompanyIDs = new(Cache, a => a.TmdbCompanyID); + _tmdbPersonIDs = new(Cache, a => a.TmdbPersonID); + _tmdbTypes = new(Cache, a => a.ImageType); + _tmdbRemoteFileNames = new(Cache, a => (a.RemoteFileName, a.ImageType)); + } + public IReadOnlyList GetByTmdbMovieID(int? movieId) => movieId.HasValue ? ReadLock(() => _tmdbMovieIDs!.GetMultiple(movieId)) ?? [] : []; @@ -99,44 +125,19 @@ public IReadOnlyList GetByForeignIDAndType(int? id, ForeignEntityTyp { if (string.IsNullOrEmpty(fileName)) return null; + if (fileName.EndsWith(".svg")) fileName = fileName[..^4] + ".png"; + return ReadLock(() => _tmdbRemoteFileNames!.GetOne((fileName, type))); } public ILookup GetByAnimeIDsAndType(int[] animeIds, ImageEntityType type) - { - return animeIds + => animeIds .SelectMany(animeId => RepoFactory.CrossRef_AniDB_TMDB_Movie.GetByAnidbAnimeID(animeId).SelectMany(xref => GetByTmdbMovieIDAndType(xref.TmdbMovieID, type)) .Concat(RepoFactory.CrossRef_AniDB_TMDB_Show.GetByAnidbAnimeID(animeId).SelectMany(xref => GetByTmdbShowIDAndType(xref.TmdbShowID, type))) .Select(image => (AnimeID: animeId, Image: image)) ) .ToLookup(a => a.AnimeID, a => a.Image); - } - - protected override int SelectKey(TMDB_Image entity) - => entity.TMDB_ImageID; - - public override void PopulateIndexes() - { - _tmdbMovieIDs = new(Cache, a => a.TmdbMovieID); - _tmdbEpisodeIDs = new(Cache, a => a.TmdbEpisodeID); - _tmdbSeasonIDs = new(Cache, a => a.TmdbSeasonID); - _tmdbShowIDs = new(Cache, a => a.TmdbShowID); - _tmdbCollectionIDs = new(Cache, a => a.TmdbCollectionID); - _tmdbNetworkIDs = new(Cache, a => a.TmdbNetworkID); - _tmdbCompanyIDs = new(Cache, a => a.TmdbCompanyID); - _tmdbPersonIDs = new(Cache, a => a.TmdbPersonID); - _tmdbTypes = new(Cache, a => a.ImageType); - _tmdbRemoteFileNames = new(Cache, a => (a.RemoteFileName, a.ImageType)); - } - - public override void RegenerateDb() - { - } - - public TMDB_ImageRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } } diff --git a/Shoko.Server/Repositories/Cached/VideoLocalRepository.cs b/Shoko.Server/Repositories/Cached/VideoLocalRepository.cs index 1d21e33cc..847f6e17a 100644 --- a/Shoko.Server/Repositories/Cached/VideoLocalRepository.cs +++ b/Shoko.Server/Repositories/Cached/VideoLocalRepository.cs @@ -19,16 +19,22 @@ using Shoko.Server.Services; using Shoko.Server.Utilities; +#nullable enable #pragma warning disable CS0618 +#pragma warning disable CA2012 namespace Shoko.Server.Repositories.Cached; public class VideoLocalRepository : BaseCachedRepository { - private PocoIndex _hashes; - private PocoIndex _sha1; - private PocoIndex _md5; - private PocoIndex _crc32; - private PocoIndex _ignored; + private PocoIndex? _ed2k; + + private PocoIndex? _sha1; + + private PocoIndex? _md5; + + private PocoIndex? _crc32; + + private PocoIndex? _ignored; public VideoLocalRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { @@ -40,9 +46,7 @@ public VideoLocalRepository(DatabaseFactory databaseFactory) : base(databaseFact } protected override int SelectKey(SVR_VideoLocal entity) - { - return entity.VideoLocalID; - } + => entity.VideoLocalID; public override void PopulateIndexes() { @@ -59,7 +63,7 @@ public override void PopulateIndexes() l.FileName ??= string.Empty; } - _hashes = new PocoIndex(Cache, a => a.Hash); + _ed2k = new PocoIndex(Cache, a => a.Hash); _sha1 = new PocoIndex(Cache, a => a.SHA1); _md5 = new PocoIndex(Cache, a => a.MD5); _crc32 = new PocoIndex(Cache, a => a.CRC32); @@ -73,7 +77,7 @@ public override void RegenerateDb() ); var count = 0; int max; - List list; + IReadOnlyList list; try { @@ -138,8 +142,8 @@ public override void RegenerateDb() var values = locals[hash]; values.Sort(comparer); var to = values.First(); - var froms = values.Except(to).ToList(); - foreach (var from in froms) + var fromList = values.Except(to).ToList(); + foreach (var from in fromList) { var places = from.Places; if (places == null || places.Count == 0) @@ -157,7 +161,7 @@ public override void RegenerateDb() transaction.Commit(); } - toRemove.AddRange(froms); + toRemove.AddRange(fromList); } count = 0; @@ -179,31 +183,18 @@ public override void RegenerateDb() } } - public List GetByImportFolder(int importFolderID) - { - return RepoFactory.VideoLocalPlace.GetByImportFolder(importFolderID) + public IReadOnlyList GetByImportFolder(int importFolderID) + => RepoFactory.VideoLocalPlace.GetByImportFolder(importFolderID) .Select(a => GetByID(a.VideoLocalID)) - .Where(a => a != null) + .WhereNotNull() .Distinct() .ToList(); - } - - private void UpdateMediaContracts(SVR_VideoLocal obj) - { - if (obj.MediaInfo != null && obj.MediaVersion >= SVR_VideoLocal.MEDIA_VERSION) - { - return; - } - - var place = obj.FirstResolvedPlace; - if (place != null) Utils.ServiceContainer.GetRequiredService().RefreshMediaInfo(place); - } public override void Delete(SVR_VideoLocal obj) { var list = obj.AnimeEpisodes; base.Delete(obj); - list.Where(a => a != null).ForEach(a => RepoFactory.AnimeEpisode.Save(a)); + list.WhereNotNull().ForEach(a => RepoFactory.AnimeEpisode.Save(a)); } public override void Save(SVR_VideoLocal obj) @@ -228,157 +219,192 @@ public void Save(SVR_VideoLocal obj, bool updateEpisodes) } } - public SVR_VideoLocal GetByHash(string hash) + private static void UpdateMediaContracts(SVR_VideoLocal obj) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty Hash"); - return ReadLock(() => _hashes.GetOne(hash)); + if (obj.MediaInfo != null && obj.MediaVersion >= SVR_VideoLocal.MEDIA_VERSION) + { + return; + } + + var place = obj.FirstResolvedPlace; + if (place != null) Utils.ServiceContainer.GetRequiredService().RefreshMediaInfo(place); } - public SVR_VideoLocal GetByHashAndSize(string hash, long fileSize) + public SVR_VideoLocal? GetByEd2k(string hash) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty Hash"); - if (fileSize <= 0) throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); - return ReadLock(() => _hashes.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty Hash"); + + return ReadLock(() => _ed2k!.GetOne(hash)); + } + + public SVR_VideoLocal? GetByEd2kAndSize(string hash, long fileSize) + { + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty Hash"); + + if (fileSize <= 0) + throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); + + return ReadLock(() => _ed2k!.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); } - public SVR_VideoLocal GetByMD5(string hash) + public SVR_VideoLocal? GetByMd5(string hash) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty MD5"); - return ReadLock(() => _md5.GetOne(hash)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty MD5"); + + return ReadLock(() => _md5!.GetOne(hash)); } - public SVR_VideoLocal GetByMD5AndSize(string hash, long fileSize) + + public SVR_VideoLocal? GetByMd5AndSize(string hash, long fileSize) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty MD5"); - if (fileSize <= 0) throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); - return ReadLock(() => _md5.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty MD5"); + + if (fileSize <= 0) + throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); + + return ReadLock(() => _md5!.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); } - public SVR_VideoLocal GetBySHA1(string hash) + public SVR_VideoLocal? GetBySha1(string hash) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty SHA1"); - return ReadLock(() => _sha1.GetOne(hash)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty SHA1"); + + return ReadLock(() => _sha1!.GetOne(hash)); } - public SVR_VideoLocal GetBySHA1AndSize(string hash, long fileSize) + + public SVR_VideoLocal? GetBySha1AndSize(string hash, long fileSize) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty SHA1"); - if (fileSize <= 0) throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); - return ReadLock(() => _sha1.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty SHA1"); + + if (fileSize <= 0) + throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); + + return ReadLock(() => _sha1!.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); } - public SVR_VideoLocal GetByCRC32(string hash) + public SVR_VideoLocal? GetByCrc32(string hash) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty CRC32"); - return ReadLock(() => _crc32.GetOne(hash)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty CRC32"); + + return ReadLock(() => _crc32!.GetOne(hash)); } - public SVR_VideoLocal GetByCRC32AndSize(string hash, long fileSize) + public SVR_VideoLocal? GetByCrc32AndSize(string hash, long fileSize) { - if (string.IsNullOrEmpty(hash)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty CRC32"); - if (fileSize <= 0) throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); - return ReadLock(() => _crc32.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); + if (string.IsNullOrEmpty(hash)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty CRC32"); + + if (fileSize <= 0) + throw new InvalidStateException("Trying to lookup a VideoLocal by a filesize of 0"); + + return ReadLock(() => _crc32!.GetMultiple(hash).FirstOrDefault(a => a.FileSize == fileSize)); } - public List GetByName(string fileName) + public IReadOnlyList GetByName(string fileName) { - if (string.IsNullOrEmpty(fileName)) throw new InvalidStateException("Trying to lookup a VideoLocal by an empty Filename"); - return ReadLock( - () => Cache.Values.Where( - p => p.Places.Any( - a => a.FilePath.FuzzyMatch(fileName) - ) - ) - .ToList() + if (string.IsNullOrEmpty(fileName)) + throw new InvalidStateException("Trying to lookup a VideoLocal by an empty Filename"); + + return ReadLock(() => Cache.Values + .Where(p => p.Places.Any(a => a.FilePath.FuzzyMatch(fileName))) + .ToList() ); } - public List GetMostRecentlyAdded(int maxResults, int jmmuserID) + public IReadOnlyList GetMostRecentlyAdded(int maxResults, int userID) { - var user = RepoFactory.JMMUser.GetByID(jmmuserID); + var user = RepoFactory.JMMUser.GetByID(userID); if (user == null) - { - return ReadLock(() => - maxResults == -1 - ? Cache.Values.OrderByDescending(a => a.DateTimeCreated).ToList() - : Cache.Values.OrderByDescending(a => a.DateTimeCreated).Take(maxResults).ToList()); - } - - if (maxResults == -1) - { - return ReadLock( - () => Cache.Values - .Where( - a => a.AnimeEpisodes.Select(b => b.AnimeSeries).Where(b => b != null) - .DistinctBy(b => b.AniDB_ID).All(user.AllowedSeries) - ).OrderByDescending(a => a.DateTimeCreated) - .ToList() + return ReadLock(() => maxResults < 0 + ? Cache.Values.OrderByDescending(a => a.DateTimeCreated).ToList() + : Cache.Values.OrderByDescending(a => a.DateTimeCreated).Take(maxResults).ToList()); + + if (maxResults < 0) + return ReadLock(() => Cache.Values + .Where(a => a.AnimeEpisodes + .Select(b => b.AnimeSeries) + .WhereNotNull() + .DistinctBy(b => b.AniDB_ID) + .All(user.AllowedSeries) + ).OrderByDescending(a => a.DateTimeCreated) + .ToList() ); - } - return ReadLock( - () => Cache.Values - .Where(a => a.AnimeEpisodes.Select(b => b.AnimeSeries).Where(b => b != null) - .DistinctBy(b => b.AniDB_ID).All(user.AllowedSeries)).OrderByDescending(a => a.DateTimeCreated) - .Take(maxResults).ToList() + return ReadLock(() => Cache.Values + .Where(a => a.AnimeEpisodes + .Select(b => b.AnimeSeries) + .WhereNotNull() + .DistinctBy(b => b.AniDB_ID) + .All(user.AllowedSeries) + ).OrderByDescending(a => a.DateTimeCreated) + .Take(maxResults).ToList() ); } - public List GetMostRecentlyAdded(int take, int skip, int jmmuserID) + public IReadOnlyList GetMostRecentlyAdded(int take, int skip, int userID) { if (skip < 0) - { skip = 0; - } if (take == 0) - { - return new List(); - } + return []; - var user = jmmuserID == -1 ? null : RepoFactory.JMMUser.GetByID(jmmuserID); + var user = userID == -1 ? null : RepoFactory.JMMUser.GetByID(userID); if (user == null) { - return ReadLock(() => - take == -1 - ? Cache.Values.OrderByDescending(a => a.DateTimeCreated).Skip(skip).ToList() - : Cache.Values.OrderByDescending(a => a.DateTimeCreated).Skip(skip).Take(take).ToList()); + return ReadLock(() => take < 0 + ? Cache.Values.OrderByDescending(a => a.DateTimeCreated).Skip(skip).ToList() + : Cache.Values.OrderByDescending(a => a.DateTimeCreated).Skip(skip).Take(take).ToList()); } - return ReadLock( - () => take == -1 - ? Cache.Values - .Where(a => a.AnimeEpisodes.Select(b => b.AnimeSeries).Where(b => b != null) - .DistinctBy(b => b.AniDB_ID).All(user.AllowedSeries)) - .OrderByDescending(a => a.DateTimeCreated) - .Skip(skip) - .ToList() - : Cache.Values - .Where(a => a.AnimeEpisodes.Select(b => b.AnimeSeries).Where(b => b != null) - .DistinctBy(b => b.AniDB_ID).All(user.AllowedSeries)) - .OrderByDescending(a => a.DateTimeCreated) - .Skip(skip) - .Take(take) - .ToList() + return ReadLock(() => take < 0 + ? Cache.Values + .Where(a => a.AnimeEpisodes + .Select(b => b.AnimeSeries) + .WhereNotNull() + .DistinctBy(b => b.AniDB_ID) + .All(user.AllowedSeries) + ) + .OrderByDescending(a => a.DateTimeCreated) + .Skip(skip) + .ToList() + : Cache.Values + .Where(a => a.AnimeEpisodes + .Select(b => b.AnimeSeries) + .WhereNotNull() + .DistinctBy(b => b.AniDB_ID) + .All(user.AllowedSeries) + ) + .OrderByDescending(a => a.DateTimeCreated) + .Skip(skip) + .Take(take) + .ToList() ); } - public List GetRandomFiles(int maxResults) + public IReadOnlyList GetRandomFiles(int maxResults) { - var values = ReadLock(Cache.Values.ToList).Where(a => a.EpisodeCrossRefs.Any()).ToList(); + var values = ReadLock(Cache.Values.ToList).Where(a => a.EpisodeCrossReferences.Any()).ToList(); using var en = new UniqueRandoms(0, values.Count - 1).GetEnumerator(); - var vids = new List(); + var list = new List(); if (maxResults > values.Count) - { maxResults = values.Count; - } - for (var x = 0; x < maxResults; x++) + while (en.MoveNext()) { - en.MoveNext(); - vids.Add(values.ElementAt(en.Current)); + list.Add(values.ElementAt(en.Current)); + if (list.Count >= maxResults) + break; } - return vids; + return list; } public class UniqueRandoms : IEnumerable @@ -388,8 +414,7 @@ public class UniqueRandoms : IEnumerable public UniqueRandoms(int minInclusive, int maxInclusive) { - _candidates = - Enumerable.Range(minInclusive, maxInclusive - minInclusive + 1).ToList(); + _candidates = Enumerable.Range(minInclusive, maxInclusive - minInclusive + 1).ToList(); } public IEnumerator GetEnumerator() @@ -403,9 +428,7 @@ public IEnumerator GetEnumerator() } IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + => GetEnumerator(); } @@ -415,33 +438,25 @@ IEnumerator IEnumerable.GetEnumerator() /// AniDB Episode ID /// /// - public List GetByAniDBEpisodeID(int episodeID) - { - return RepoFactory.CrossRef_File_Episode.GetByEpisodeID(episodeID) - .Select(a => GetByHash(a.Hash)) - .Where(a => a != null) + public IReadOnlyList GetByAniDBEpisodeID(int episodeID) + => RepoFactory.CrossRef_File_Episode.GetByEpisodeID(episodeID) + .Select(a => GetByEd2k(a.Hash)) + .WhereNotNull() .ToList(); - } - - public List GetMostRecentlyAddedForAnime(int maxResults, int animeID) - { - return - RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) - .Select(a => GetByHash(a.Hash)) - .Where(a => a != null) + public IReadOnlyList GetMostRecentlyAddedForAnime(int maxResults, int animeID) + => RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) + .Select(a => GetByEd2k(a.Hash)) + .WhereNotNull() .OrderByDescending(a => a.DateTimeCreated) .Take(maxResults) .ToList(); - } - public List GetByInternalVersion(int iver) - { - return RepoFactory.AniDB_File.GetByInternalVersion(iver) - .Select(a => GetByHash(a.Hash)) - .Where(a => a != null) + public IReadOnlyList GetByInternalVersion(int internalVersion) + => RepoFactory.AniDB_File.GetByInternalVersion(internalVersion) + .Select(a => GetByEd2k(a.Hash)) + .WhereNotNull() .ToList(); - } /// /// returns all the VideoLocal records associate with an AniDB_Anime Record @@ -450,128 +465,96 @@ public List GetByInternalVersion(int iver) /// Include to select only files from the selected /// cross-reference source. /// - public List GetByAniDBAnimeID(int animeID, CrossRefSource? xrefSource = null) - { - if (xrefSource.HasValue) - return - RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) - .Where(xref => xref.CrossRefSource == (int)xrefSource.Value) - .Select(xref => GetByHash(xref.Hash)) - .WhereNotNull() - .ToList(); - - return - RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) - .Select(a => GetByHash(a.Hash)) - .WhereNotNull() - .ToList(); - } + public IReadOnlyList GetByAniDBAnimeID(int animeID, CrossRefSource? xrefSource = null) + => RepoFactory.CrossRef_File_Episode.GetByAnimeID(animeID) + .Where(xref => !xrefSource.HasValue || xref.CrossRefSource != (int)xrefSource.Value) + .Select(xref => GetByEd2k(xref.Hash)) + .WhereNotNull() + .ToList(); - public List GetVideosWithoutHash() - { - return ReadLock(() => _hashes.GetMultiple("")); - } + public IReadOnlyList GetVideosWithoutHash() + => ReadLock(() => _ed2k!.GetMultiple("")); - public List GetVideosWithoutEpisode(bool includeBrokenXRefs = false) - { - return ReadLock( - () => Cache.Values - .Where(a => - { - if (a.IsIgnored) - return false; + public IReadOnlyList GetVideosWithoutEpisode(bool includeBrokenXRefs = false) + => ReadLock(() => Cache.Values + .Where(a => + { + if (a.IsIgnored) + return false; - var xrefs = RepoFactory.CrossRef_File_Episode.GetByHash(a.Hash); - if (!xrefs.Any()) - return true; + var xrefs = RepoFactory.CrossRef_File_Episode.GetByEd2k(a.Hash); + if (!xrefs.Any()) + return true; - if (includeBrokenXRefs) - return !xrefs.Any(IsImported); + if (includeBrokenXRefs) + return !xrefs.Any(IsImported); - return false; - }) - .OrderByNatural(local => - { - var place = local?.FirstValidPlace; - if (place == null) return null; - return place.FullServerPath ?? place.FilePath; - }) - .ThenBy(local => local?.VideoLocalID ?? 0) - .ToList() + return false; + }) + .OrderByNatural(local => + { + var place = local?.FirstValidPlace; + if (place == null) return null; + return place.FullServerPath ?? place.FilePath; + }) + .ThenBy(local => local?.VideoLocalID ?? 0) + .WhereNotNull() + .ToList() ); - } - public List GetVideosWithMissingCrossReferenceData() - { - return ReadLock( - () => Cache.Values - .Where(a => - { - if (a.IsIgnored) - return false; + public IReadOnlyList GetVideosWithMissingCrossReferenceData() + => ReadLock(() => Cache.Values + .Where(a => + { + if (a.IsIgnored) + return false; - var xrefs = RepoFactory.CrossRef_File_Episode.GetByHash(a.Hash); - if (!xrefs.Any()) - return false; + var xrefs = RepoFactory.CrossRef_File_Episode.GetByEd2k(a.Hash); + if (!xrefs.Any()) + return false; - return !xrefs.All(IsImported); - }) - .OrderByNatural(local => - { - var place = local?.FirstValidPlace; - if (place == null) return null; - return place.FullServerPath ?? place.FilePath; - }) - .ThenBy(local => local?.VideoLocalID ?? 0) - .ToList() + return !xrefs.All(IsImported); + }) + .OrderByNatural(local => + { + var place = local?.FirstValidPlace; + if (place == null) return null; + return place.FullServerPath ?? place.FilePath; + }) + .ThenBy(local => local?.VideoLocalID ?? 0) + .WhereNotNull() + .ToList() ); - } private static bool IsImported(SVR_CrossRef_File_Episode xref) { - if (xref.AnimeID == 0) return false; - var ep = RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(xref.EpisodeID); - if (ep?.AniDB_Episode == null) return false; + if (xref.AnimeID == 0) + return false; + + var episode = RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(xref.EpisodeID); + if (episode?.AniDB_Episode == null) + return false; + var anime = RepoFactory.AnimeSeries.GetByAnimeID(xref.AnimeID); return anime?.AniDB_Anime != null; } - public List GetVideosWithoutEpisodeUnsorted() - { - return ReadLock(() => - Cache.Values.Where(a => !a.IsIgnored && !RepoFactory.CrossRef_File_Episode.GetByHash(a.Hash).Any()) - .ToList()); - } + public IReadOnlyList GetVideosWithoutEpisodeUnsorted() + => ReadLock(() => Cache.Values + .Where(a => !a.IsIgnored && !RepoFactory.CrossRef_File_Episode.GetByEd2k(a.Hash).Any()) + .ToList() + ); - public List GetManuallyLinkedVideos() - { - return - RepoFactory.CrossRef_File_Episode.GetAll() + public IReadOnlyList GetManuallyLinkedVideos() + => RepoFactory.CrossRef_File_Episode.GetAll() .Where(a => a.CrossRefSource != 1) - .Select(a => GetByHash(a.Hash)) - .Where(a => a != null) - .ToList(); - } - - public List GetExactDuplicateVideos() - { - return - RepoFactory.VideoLocalPlace.GetAll() - .GroupBy(a => a.VideoLocalID) - .Select(a => a.ToArray()) - .Where(a => a.Length > 1) - .Select(a => GetByID(a[0].VideoLocalID)) - .Where(a => a != null) + .Select(a => GetByEd2k(a.Hash)) + .WhereNotNull() .ToList(); - } - public List GetIgnoredVideos() - { - return ReadLock(() => _ignored.GetMultiple(true)); - } + public IReadOnlyList GetIgnoredVideos() + => ReadLock(() => _ignored!.GetMultiple(true)); - public SVR_VideoLocal GetByMyListID(int myListID) - { - return ReadLock(() => Cache.Values.FirstOrDefault(a => a.MyListID == myListID)); - } + public SVR_VideoLocal? GetByMyListID(int myListID) + => ReadLock(() => Cache.Values.FirstOrDefault(a => a.MyListID == myListID)); } diff --git a/Shoko.Server/Repositories/Cached/VideoLocal_PlaceRepository.cs b/Shoko.Server/Repositories/Cached/VideoLocal_PlaceRepository.cs index 7139af2b5..3dd8547af 100644 --- a/Shoko.Server/Repositories/Cached/VideoLocal_PlaceRepository.cs +++ b/Shoko.Server/Repositories/Cached/VideoLocal_PlaceRepository.cs @@ -9,77 +9,69 @@ using Shoko.Server.Models; using Shoko.Server.Server; +#nullable enable namespace Shoko.Server.Repositories.Cached; public class VideoLocal_PlaceRepository : BaseCachedRepository { - private PocoIndex VideoLocals; - private PocoIndex ImportFolders; - private PocoIndex Paths; + private PocoIndex? _videoLocalIDs; + + private PocoIndex? _importFolderIDs; + + private PocoIndex? _paths; public VideoLocal_PlaceRepository(DatabaseFactory databaseFactory) : base(databaseFactory) { - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local BeginSaveCallback = place => { - if (place.VideoLocalID == 0) throw new InvalidStateException("Attempting to save a VideoLocal_Place with a VideoLocalID of 0"); + if (place.VideoLocalID == 0) + throw new InvalidStateException("Attempting to save a VideoLocal_Place with a VideoLocalID of 0"); + if (string.IsNullOrEmpty(place.FilePath)) + throw new InvalidStateException("Attempting to save a VideoLocal_Place with a null or empty FilePath"); + if (place.VideoLocal_Place_ID is 0 && GetByFilePathAndImportFolderID(place.FilePath, place.ImportFolderID) is { } secondPlace) + throw new InvalidStateException("Attempting to save a VideoLocal_Place with a FilePath and ImportFolderID that already exists in the database"); }; } protected override int SelectKey(SVR_VideoLocal_Place entity) - { - return entity.VideoLocal_Place_ID; - } + => entity.VideoLocal_Place_ID; public override void PopulateIndexes() { - VideoLocals = new PocoIndex(Cache, a => a.VideoLocalID); - ImportFolders = new PocoIndex(Cache, a => a.ImportFolderID); - Paths = new PocoIndex(Cache, a => a.FilePath); + _videoLocalIDs = new PocoIndex(Cache, a => a.VideoLocalID); + _importFolderIDs = new PocoIndex(Cache, a => a.ImportFolderID); + _paths = new PocoIndex(Cache, a => a.FilePath); } public override void RegenerateDb() { - ServerState.Instance.ServerStartingStatus = string.Format( - Resources.Database_Validating, nameof(VideoLocal_Place), " Removing orphaned VideoLocal_Places"); - var count = 0; - int max; - - var list = Cache.Values.Where(a => a is { VideoLocalID: 0 }).ToList(); - max = list.Count; - + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, nameof(VideoLocal_Place), " Removing orphaned VideoLocal_Places"); + var entries = Cache.Values.Where(a => a is { VideoLocalID: 0 } or { ImportFolderID: 0 } or { FilePath: null or "" }).ToList(); + var total = entries.Count; + var current = 0; using var session = _databaseFactory.SessionFactory.OpenSession(); - foreach (var batch in list.Batch(50)) + foreach (var batch in entries.Batch(50)) { using var transaction = session.BeginTransaction(); - foreach (var a in batch) + foreach (var entry in batch) { - DeleteWithOpenTransaction(session, a); - count++; - ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, nameof(VideoLocal_Place), - " Removing Orphaned VideoLocal_Places - " + count + "/" + max); + DeleteWithOpenTransaction(session, entry); + current++; + ServerState.Instance.ServerStartingStatus = string.Format(Resources.Database_Validating, nameof(VideoLocal_Place), " Removing Orphaned VideoLocal_Places - " + current + "/" + total); } transaction.Commit(); } } - public List GetByImportFolder(int importFolderID) - { - if (importFolderID == 0) throw new InvalidStateException("Trying to lookup a VideoLocal_Place by an ImportFolderID of 0"); - return ReadLock(() => ImportFolders.GetMultiple(importFolderID)); - } + public IReadOnlyList GetByImportFolder(int importFolderID) + => ReadLock(() => _importFolderIDs!.GetMultiple(importFolderID)); - public SVR_VideoLocal_Place GetByFilePathAndImportFolderID(string filePath, int importFolderID) - { - if (string.IsNullOrEmpty(filePath)) throw new InvalidStateException("Trying to lookup a VideoLocal_Place by an empty File Path"); - if (importFolderID == 0) throw new InvalidStateException("Trying to lookup a VideoLocal_Place by an ImportFolderID of 0"); - return ReadLock(() => Paths.GetMultiple(filePath).FirstOrDefault(a => a.ImportFolderID == importFolderID)); - } + public SVR_VideoLocal_Place? GetByFilePathAndImportFolderID(string filePath, int importFolderID) + => !string.IsNullOrEmpty(filePath) && importFolderID > 0 + ? ReadLock(() => _paths!.GetMultiple(filePath).FirstOrDefault(a => a.ImportFolderID == importFolderID)) + : null; - public List GetByVideoLocal(int videoLocalID) - { - if (videoLocalID == 0) throw new InvalidStateException("Trying to lookup a VideoLocal_Place by a VideoLocalID of 0"); - return ReadLock(() => VideoLocals.GetMultiple(videoLocalID)); - } + public IReadOnlyList GetByVideoLocal(int videoLocalID) + => ReadLock(() => _videoLocalIDs!.GetMultiple(videoLocalID)); } diff --git a/Shoko.Server/Repositories/Cached/VideoLocal_UserRepository.cs b/Shoko.Server/Repositories/Cached/VideoLocal_UserRepository.cs index 1a9571ec8..20e837bbf 100644 --- a/Shoko.Server/Repositories/Cached/VideoLocal_UserRepository.cs +++ b/Shoko.Server/Repositories/Cached/VideoLocal_UserRepository.cs @@ -3,47 +3,33 @@ using Shoko.Server.Databases; using Shoko.Server.Models; +#nullable enable namespace Shoko.Server.Repositories.Cached; -public class VideoLocal_UserRepository : BaseCachedRepository +public class VideoLocal_UserRepository(DatabaseFactory databaseFactory) : BaseCachedRepository(databaseFactory) { - private PocoIndex VideoLocalIDs; - private PocoIndex Users; - private PocoIndex UsersVideoLocals; + private PocoIndex? _videoLocalIDs; - protected override int SelectKey(SVR_VideoLocal_User entity) - { - return entity.VideoLocal_UserID; - } + private PocoIndex? _userIDs; - public override void PopulateIndexes() - { - VideoLocalIDs = new PocoIndex(Cache, a => a.VideoLocalID); - Users = new PocoIndex(Cache, a => a.JMMUserID); - UsersVideoLocals = - new PocoIndex(Cache, a => (a.JMMUserID, a.VideoLocalID)); - } + private PocoIndex? _userVideoLocalIDs; - public override void RegenerateDb() - { - } + protected override int SelectKey(SVR_VideoLocal_User entity) + => entity.VideoLocal_UserID; - public List GetByVideoLocalID(int vidid) + public override void PopulateIndexes() { - return ReadLock(() => VideoLocalIDs.GetMultiple(vidid)); + _videoLocalIDs = new PocoIndex(Cache, a => a.VideoLocalID); + _userIDs = new PocoIndex(Cache, a => a.JMMUserID); + _userVideoLocalIDs = new PocoIndex(Cache, a => (a.JMMUserID, a.VideoLocalID)); } - public List GetByUserID(int userid) - { - return ReadLock(() => Users.GetMultiple(userid)); - } + public IReadOnlyList GetByVideoLocalID(int videoLocalID) + => ReadLock(() => _videoLocalIDs!.GetMultiple(videoLocalID)); - public SVR_VideoLocal_User GetByUserIDAndVideoLocalID(int userid, int vidid) - { - return ReadLock(() => UsersVideoLocals.GetOne((userid, vidid))); - } + public IReadOnlyList GetByUserID(int userID) + => ReadLock(() => _userIDs!.GetMultiple(userID)); - public VideoLocal_UserRepository(DatabaseFactory databaseFactory) : base(databaseFactory) - { - } + public SVR_VideoLocal_User? GetByUserIDAndVideoLocalID(int userID, int videoLocalID) + => ReadLock(() => _userVideoLocalIDs!.GetOne((userID, videoLocalID))); } diff --git a/Shoko.Server/Repositories/Direct/AniDB_Anime_StaffRepository.cs b/Shoko.Server/Repositories/Direct/AniDB_Anime_StaffRepository.cs index 2c465b0c3..98ada4cf9 100644 --- a/Shoko.Server/Repositories/Direct/AniDB_Anime_StaffRepository.cs +++ b/Shoko.Server/Repositories/Direct/AniDB_Anime_StaffRepository.cs @@ -1,19 +1,19 @@ using System.Collections.Generic; using System.Linq; -using Shoko.Models.Server; using Shoko.Server.Databases; +using Shoko.Server.Models.AniDB; namespace Shoko.Server.Repositories.Direct; public class AniDB_Anime_StaffRepository : BaseDirectRepository { - public List GetByAnimeID(int id) + public List GetByAnimeID(int animeID) { return Lock(() => { using var session = _databaseFactory.SessionFactory.OpenStatelessSession(); return session.Query() - .Where(a => a.AnimeID == id) + .Where(a => a.AnimeID == animeID) .ToList(); }); } diff --git a/Shoko.Server/Repositories/RepoFactory.cs b/Shoko.Server/Repositories/RepoFactory.cs index 8ff4d2b94..97771128d 100644 --- a/Shoko.Server/Repositories/RepoFactory.cs +++ b/Shoko.Server/Repositories/RepoFactory.cs @@ -18,6 +18,7 @@ public class RepoFactory private readonly ICachedRepository[] _cachedRepositories; public static AniDB_Anime_CharacterRepository AniDB_Anime_Character; + public static AniDB_Anime_Character_CreatorRepository AniDB_Anime_Character_Creator; public static AniDB_Anime_PreferredImageRepository AniDB_Anime_PreferredImage; public static AniDB_Anime_RelationRepository AniDB_Anime_Relation; public static AniDB_Anime_SimilarRepository AniDB_Anime_Similar; @@ -26,7 +27,6 @@ public class RepoFactory public static AniDB_Anime_TitleRepository AniDB_Anime_Title; public static AniDB_AnimeRepository AniDB_Anime; public static AniDB_AnimeUpdateRepository AniDB_AnimeUpdate; - public static AniDB_Character_CreatorRepository AniDB_Character_Creator; public static AniDB_CharacterRepository AniDB_Character; public static AniDB_CreatorRepository AniDB_Creator; public static AniDB_Episode_PreferredImageRepository AniDB_Episode_PreferredImage; @@ -40,14 +40,12 @@ public class RepoFactory public static AniDB_ReleaseGroupRepository AniDB_ReleaseGroup; public static AniDB_TagRepository AniDB_Tag; public static AniDB_VoteRepository AniDB_Vote; - public static AnimeCharacterRepository AnimeCharacter; public static AnimeEpisode_UserRepository AnimeEpisode_User; public static AnimeEpisodeRepository AnimeEpisode; public static AnimeGroup_UserRepository AnimeGroup_User; public static AnimeGroupRepository AnimeGroup; public static AnimeSeries_UserRepository AnimeSeries_User; public static AnimeSeriesRepository AnimeSeries; - public static AnimeStaffRepository AnimeStaff; public static AuthTokensRepository AuthTokens; public static BookmarkedAnimeRepository BookmarkedAnime; public static CrossRef_AniDB_MALRepository CrossRef_AniDB_MAL; @@ -55,7 +53,6 @@ public class RepoFactory public static CrossRef_AniDB_TMDB_MovieRepository CrossRef_AniDB_TMDB_Movie; public static CrossRef_AniDB_TMDB_ShowRepository CrossRef_AniDB_TMDB_Show; public static CrossRef_AniDB_TraktV2Repository CrossRef_AniDB_TraktV2; - public static CrossRef_Anime_StaffRepository CrossRef_Anime_Staff; public static CrossRef_CustomTagRepository CrossRef_CustomTag; public static CrossRef_File_EpisodeRepository CrossRef_File_Episode; public static CrossRef_Languages_AniDB_FileRepository CrossRef_Languages_AniDB_File; @@ -104,6 +101,7 @@ public RepoFactory( ILogger logger, IEnumerable repositories, AniDB_Anime_CharacterRepository anidbAnimeCharacter, + AniDB_Anime_Character_CreatorRepository anidbAnimeCharacterCreator, AniDB_Anime_PreferredImageRepository anidbAnimePreferredImage, AniDB_Anime_RelationRepository anidbAnimeRelation, AniDB_Anime_SimilarRepository anidbAnimeSimilar, @@ -112,7 +110,6 @@ public RepoFactory( AniDB_Anime_TitleRepository anidbAnimeTitle, AniDB_AnimeRepository anidbAnime, AniDB_AnimeUpdateRepository anidbAnimeUpdate, - AniDB_Character_CreatorRepository anidbCharacterCreator, AniDB_CharacterRepository anidbCharacter, AniDB_CreatorRepository anidbCreator, AniDB_Episode_PreferredImageRepository anidbEpisodePreferredImage, @@ -126,14 +123,12 @@ public RepoFactory( AniDB_ReleaseGroupRepository anidbReleaseGroup, AniDB_TagRepository anidbTag, AniDB_VoteRepository anidbVote, - AnimeCharacterRepository animeCharacter, AnimeEpisode_UserRepository animeEpisodeUser, AnimeEpisodeRepository animeEpisode, AnimeGroup_UserRepository animeGroupUser, AnimeGroupRepository animeGroup, AnimeSeries_UserRepository animeSeriesUser, AnimeSeriesRepository animeSeries, - AnimeStaffRepository animeStaff, AuthTokensRepository authTokens, BookmarkedAnimeRepository bookmarkedAnime, CrossRef_AniDB_MALRepository crossRefAniDBMal, @@ -141,7 +136,6 @@ public RepoFactory( CrossRef_AniDB_TMDB_MovieRepository crossRefAniDBTmdbMovie, CrossRef_AniDB_TMDB_ShowRepository crossRefAniDBTmdbShow, CrossRef_AniDB_TraktV2Repository crossRefAniDBTraktV2, - CrossRef_Anime_StaffRepository crossRefAnimeStaff, CrossRef_CustomTagRepository crossRefCustomTag, CrossRef_File_EpisodeRepository crossRefFileEpisode, CrossRef_Languages_AniDB_FileRepository crossRefLanguagesAniDBFile, @@ -191,6 +185,7 @@ VideoLocalRepository videoLocal _cachedRepositories = repositories.ToArray(); AniDB_Anime = anidbAnime; AniDB_Anime_Character = anidbAnimeCharacter; + AniDB_Anime_Character_Creator = anidbAnimeCharacterCreator; AniDB_Anime_PreferredImage = anidbAnimePreferredImage; AniDB_Anime_Relation = anidbAnimeRelation; AniDB_Anime_Similar = anidbAnimeSimilar; @@ -199,7 +194,6 @@ VideoLocalRepository videoLocal AniDB_Anime_Title = anidbAnimeTitle; AniDB_AnimeUpdate = anidbAnimeUpdate; AniDB_Character = anidbCharacter; - AniDB_Character_Creator = anidbCharacterCreator; AniDB_Creator = anidbCreator; AniDB_Episode = anidbEpisode; AniDB_Episode_PreferredImage = anidbEpisodePreferredImage; @@ -212,14 +206,12 @@ VideoLocalRepository videoLocal AniDB_ReleaseGroup = anidbReleaseGroup; AniDB_Tag = anidbTag; AniDB_Vote = anidbVote; - AnimeCharacter = animeCharacter; AnimeEpisode = animeEpisode; AnimeEpisode_User = animeEpisodeUser; AnimeGroup = animeGroup; AnimeGroup_User = animeGroupUser; AnimeSeries = animeSeries; AnimeSeries_User = animeSeriesUser; - AnimeStaff = animeStaff; AuthTokens = authTokens; BookmarkedAnime = bookmarkedAnime; CrossRef_AniDB_MAL = crossRefAniDBMal; @@ -227,7 +219,6 @@ VideoLocalRepository videoLocal CrossRef_AniDB_TMDB_Movie = crossRefAniDBTmdbMovie; CrossRef_AniDB_TMDB_Show = crossRefAniDBTmdbShow; CrossRef_AniDB_TraktV2 = crossRefAniDBTraktV2; - CrossRef_Anime_Staff = crossRefAnimeStaff; CrossRef_CustomTag = crossRefCustomTag; CrossRef_File_Episode = crossRefFileEpisode; CrossRef_Languages_AniDB_File = crossRefLanguagesAniDBFile; diff --git a/Shoko.Server/Repositories/RepositoryStartup.cs b/Shoko.Server/Repositories/RepositoryStartup.cs index 9b05b9df2..030645d72 100644 --- a/Shoko.Server/Repositories/RepositoryStartup.cs +++ b/Shoko.Server/Repositories/RepositoryStartup.cs @@ -56,11 +56,11 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddCachedRepository(); services.AddCachedRepository(); + services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); - services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); @@ -69,21 +69,18 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); - services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); - services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); - services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); services.AddCachedRepository(); diff --git a/Shoko.Server/Scheduling/Jobs/AniDB/AddFileToMyListJob.cs b/Shoko.Server/Scheduling/Jobs/AniDB/AddFileToMyListJob.cs index 9b89b534d..c749106c7 100644 --- a/Shoko.Server/Scheduling/Jobs/AniDB/AddFileToMyListJob.cs +++ b/Shoko.Server/Scheduling/Jobs/AniDB/AddFileToMyListJob.cs @@ -41,7 +41,7 @@ public class AddFileToMyListJob : BaseJob public override void PostInit() { - _videoLocal = RepoFactory.VideoLocal.GetByHash(Hash); + _videoLocal = RepoFactory.VideoLocal.GetByEd2k(Hash); if (_videoLocal == null) throw new JobExecutionException($"VideoLocal not Found: {Hash}"); } @@ -186,7 +186,7 @@ await _watchedService.SetWatchedStatus(_videoLocal, false, false, null, false, j } // if we don't have xrefs, then no series or eps. - var series = _videoLocal.EpisodeCrossRefs.Select(a => a.AnimeID).Distinct().Except([0]).ToArray(); + var series = _videoLocal.EpisodeCrossReferences.Select(a => a.AnimeID).Distinct().Except([0]).ToArray(); if (series.Length <= 0) { return; diff --git a/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBCreatorJob.cs b/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBCreatorJob.cs index fcb52ab4e..7c86095e8 100644 --- a/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBCreatorJob.cs +++ b/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBCreatorJob.cs @@ -63,12 +63,10 @@ public override async Task Process() { _logger.LogError("Unable to find an AniDB Creator with the given ID: {CreatorID}", CreatorID); var anidbAnimeStaffRoles = RepoFactory.AniDB_Anime_Staff.GetByAnimeID(CreatorID); - var anidbCharacterCreators = RepoFactory.AniDB_Character_Creator.GetByCreatorID(CreatorID); + var anidbCharacterCreators = RepoFactory.AniDB_Anime_Character_Creator.GetByCreatorID(CreatorID); var anidbAnimeCharacters = anidbCharacterCreators - .SelectMany(c => RepoFactory.AniDB_Anime_Character.GetByCharID(c.CharacterID)) + .SelectMany(c => RepoFactory.AniDB_Anime_Character.GetByCharacterID(c.CharacterID)) .ToList(); - var animeStaff = RepoFactory.AnimeStaff.GetByAniDBID(CreatorID); - var animeStaffRoles = animeStaff is not null ? RepoFactory.CrossRef_Anime_Staff.GetByStaffID(animeStaff.StaffID) : []; var anidbAnime = anidbAnimeStaffRoles.Select(a => a.AnimeID) .Concat(anidbAnimeCharacters.Select(a => a.AnimeID)) .Distinct() @@ -77,13 +75,8 @@ public override async Task Process() .ToList(); RepoFactory.AniDB_Creator.Delete(CreatorID); - RepoFactory.AniDB_Character_Creator.Delete(anidbCharacterCreators); + RepoFactory.AniDB_Anime_Character_Creator.Delete(anidbCharacterCreators); RepoFactory.AniDB_Anime_Staff.Delete(anidbAnimeStaffRoles); - if (animeStaff is not null) - { - RepoFactory.AnimeStaff.Delete(animeStaff); - RepoFactory.CrossRef_Anime_Staff.Delete(animeStaffRoles); - } if (anidbAnime.Count > 0) { @@ -119,15 +112,6 @@ await scheduler.StartJob(c => creator.LastUpdatedAt = response.LastUpdateAt; RepoFactory.AniDB_Creator.Save(creator); - if (RepoFactory.AnimeStaff.GetByAniDBID(creator.CreatorID) is { } staff) - { - var creatorBasePath = ImageUtils.GetBaseAniDBCreatorImagesPath() + Path.DirectorySeparatorChar; - staff.Name = creator.Name; - staff.AlternateName = creator.OriginalName; - staff.ImagePath = creator.GetFullImagePath()?.Replace(creatorBasePath, ""); - RepoFactory.AnimeStaff.Save(staff); - } - if (!(creator.GetImageMetadata()?.IsLocalAvailable ?? true)) { _logger.LogInformation("Image not found locally, queuing image download for {Creator} (ID={CreatorID},Type={Type})", response.Name, response.ID, response.Type.ToString()); diff --git a/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBFileJob.cs b/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBFileJob.cs index ec3377407..1ac8c99f3 100644 --- a/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBFileJob.cs +++ b/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBFileJob.cs @@ -67,7 +67,7 @@ public override async Task Process() _vlocal ??= RepoFactory.VideoLocal.GetByID(VideoLocalID); if (_vlocal == null) return null; - var aniFile = RepoFactory.AniDB_File.GetByHashAndFileSize(_vlocal.Hash, _vlocal.FileSize); + var aniFile = RepoFactory.AniDB_File.GetByEd2kAndFileSize(_vlocal.Hash, _vlocal.FileSize); UDPResponse response = null; if (aniFile == null || ForceAniDB) @@ -186,7 +186,7 @@ private async Task CreateXrefs(string filename, ResponseGetFile response) { if (response.EpisodeIDs.Count <= 0) return; - var fileEps = RepoFactory.CrossRef_File_Episode.GetByHash(_vlocal.Hash); + var fileEps = RepoFactory.CrossRef_File_Episode.GetByEd2k(_vlocal.Hash).ToList(); // Use a single session A. for efficiency and B. to prevent regenerating stats @@ -253,7 +253,6 @@ await BaseRepository.Lock(fileEps, async x => } } - // There is a chance that AniDB returned a dup, however unlikely await BaseRepository.Lock(fileEps, async x => { diff --git a/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBImagesJob.cs b/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBImagesJob.cs index 746b31460..d4ff17332 100644 --- a/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBImagesJob.cs +++ b/Shoko.Server/Scheduling/Jobs/AniDB/GetAniDBImagesJob.cs @@ -80,9 +80,9 @@ await scheduler.StartJobNow(a => if (settings.AniDb.DownloadCharacters) { var characters = RepoFactory.AniDB_Anime_Character.GetByAnimeID(AnimeID) - .Select(xref => RepoFactory.AniDB_Character.GetByCharID(xref.CharID)) - .Where(a => !string.IsNullOrEmpty(a?.PicName)) - .DistinctBy(a => a.CharID) + .Select(xref => RepoFactory.AniDB_Character.GetByCharacterID(xref.CharacterID)) + .Where(a => !string.IsNullOrEmpty(a?.ImagePath)) + .DistinctBy(a => a.CharacterID) .ToList(); if (characters.Count is not 0) requests.AddRange(characters @@ -90,7 +90,7 @@ await scheduler.StartJobNow(a => .Select(c => new Action(a => { a.ParentName = _title; - a.ImageID = c.CharID; + a.ImageID = c.CharacterID; a.ImageType = ImageEntityType.Character; a.ForceDownload = ForceDownload; }))); @@ -102,8 +102,7 @@ await scheduler.StartJobNow(a => if (settings.AniDb.DownloadCreators) { // Get all voice-actors working on this anime. - var voiceActors = RepoFactory.AniDB_Anime_Character.GetByAnimeID(AnimeID) - .SelectMany(xref => RepoFactory.AniDB_Character_Creator.GetByCharacterID(xref.CharID)) + var voiceActors = RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(AnimeID) .Select(xref => RepoFactory.AniDB_Creator.GetByCreatorID(xref.CreatorID)) .Where(va => !string.IsNullOrEmpty(va?.ImagePath)); // Get all staff members working on this anime. @@ -111,8 +110,7 @@ await scheduler.StartJobNow(a => .Select(xref => RepoFactory.AniDB_Creator.GetByCreatorID(xref.CreatorID)) .Where(staff => !string.IsNullOrEmpty(staff?.ImagePath)); // Concatenate the streams into a single list. - var creators = voiceActors - .Concat(staffMembers) + var creators = voiceActors.Concat(staffMembers) .DistinctBy(creator => creator.CreatorID) .ToList(); diff --git a/Shoko.Server/Scheduling/Jobs/AniDB/SyncAniDBMyListJob.cs b/Shoko.Server/Scheduling/Jobs/AniDB/SyncAniDBMyListJob.cs index 8be02a043..13ee76318 100644 --- a/Shoko.Server/Scheduling/Jobs/AniDB/SyncAniDBMyListJob.cs +++ b/Shoko.Server/Scheduling/Jobs/AniDB/SyncAniDBMyListJob.cs @@ -99,7 +99,7 @@ public override async Task Process() var aniFile = RepoFactory.AniDB_File.GetByFileID(myItem!.FileID!.Value); // the AniDB_File should never have a null hash, but just in case - var vl = aniFile?.Hash == null ? null : RepoFactory.VideoLocal.GetByHash(aniFile.Hash); + var vl = aniFile?.Hash == null ? null : RepoFactory.VideoLocal.GetByEd2k(aniFile.Hash); if (vl != null) { @@ -179,7 +179,7 @@ private async Task CreateMyListBackup(HttpResponse> respons } } - private async Task ProcessStates(List aniDBUsers, SVR_VideoLocal vl, ResponseMyList myitem, + private async Task ProcessStates(IReadOnlyList aniDBUsers, SVR_VideoLocal vl, ResponseMyList myitem, int modifiedItems, ISet modifiedSeries) { // check watched states, read the states if needed, and update differences diff --git a/Shoko.Server/Scheduling/Jobs/AniDB/UpdateMyListFileStatusJob.cs b/Shoko.Server/Scheduling/Jobs/AniDB/UpdateMyListFileStatusJob.cs index 40bcf31fd..58878db8e 100644 --- a/Shoko.Server/Scheduling/Jobs/AniDB/UpdateMyListFileStatusJob.cs +++ b/Shoko.Server/Scheduling/Jobs/AniDB/UpdateMyListFileStatusJob.cs @@ -57,7 +57,7 @@ public override async Task Process() var settings = _settingsProvider.GetSettings(); // NOTE - we might return more than one VideoLocal record here, if there are duplicates by hash - var vid = RepoFactory.VideoLocal.GetByHash(Hash); + var vid = RepoFactory.VideoLocal.GetByEd2k(Hash); if (vid == null) return; if (vid.AniDBFile != null) @@ -79,7 +79,7 @@ public override async Task Process() else { // we have a manual link, so get the xrefs and add the episodes instead as generic files - var xrefs = vid.EpisodeCrossRefs; + var xrefs = vid.EpisodeCrossReferences; foreach (var episode in xrefs.Select(xref => xref.AniDBEpisode).Where(episode => episode != null)) { _logger.LogInformation("Updating Episode MyList Status: AnimeID: {AnimeID}, Episode Type: {Type}, Episode No: {EP}", episode.AnimeID, diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/DiscoverFileJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/DiscoverFileJob.cs index 901b18801..cd1e13070 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/DiscoverFileJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/DiscoverFileJob.cs @@ -85,7 +85,7 @@ public override async Task Process() var scheduler = await _schedulerFactory.GetScheduler(); // if !shouldHash, then we definitely have a hash - var hasXrefs = vlocal.EpisodeCrossRefs.Any(a => a.AnimeEpisode is not null && a.AnimeSeries is not null); + var hasXrefs = vlocal.EpisodeCrossReferences.Any(a => a.AnimeEpisode is not null && a.AnimeSeries is not null); if (!shouldHash && hasXrefs && !vlocal.DateTimeImported.HasValue) { vlocal.DateTimeImported = DateTime.Now; @@ -258,7 +258,7 @@ private static bool FillHashesAgainstVideoLocalRepo(SVR_VideoLocal v) var changed = false; if (!string.IsNullOrEmpty(v.Hash)) { - var n = RepoFactory.VideoLocal.GetByHash(v.Hash); + var n = RepoFactory.VideoLocal.GetByEd2k(v.Hash); if (n != null) { if (!string.IsNullOrEmpty(n.CRC32) && !n.CRC32.Equals(v.CRC32)) @@ -285,7 +285,7 @@ private static bool FillHashesAgainstVideoLocalRepo(SVR_VideoLocal v) if (!string.IsNullOrEmpty(v.SHA1)) { - var n = RepoFactory.VideoLocal.GetBySHA1(v.SHA1); + var n = RepoFactory.VideoLocal.GetBySha1(v.SHA1); if (n != null) { if (!string.IsNullOrEmpty(n.CRC32) && !n.CRC32.Equals(v.CRC32)) @@ -312,7 +312,7 @@ private static bool FillHashesAgainstVideoLocalRepo(SVR_VideoLocal v) if (!string.IsNullOrEmpty(v.MD5)) { - var n = RepoFactory.VideoLocal.GetByMD5(v.MD5); + var n = RepoFactory.VideoLocal.GetByMd5(v.MD5); if (n != null) { if (!string.IsNullOrEmpty(n.CRC32) && !n.CRC32.Equals(v.CRC32)) diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs index 708b9b207..323a200d4 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs @@ -75,7 +75,7 @@ public override async Task Process() // We should have a hash by now // before we save it, lets make sure there is not any other record with this hash (possible duplicate file) // TODO change this back to lookup by hash and filesize, but it'll need database migration and changes to other lookups - var tlocal = RepoFactory.VideoLocal.GetByHash(vlocal.Hash); + var tlocal = RepoFactory.VideoLocal.GetByEd2k(vlocal.Hash); if (tlocal != null) { @@ -338,7 +338,7 @@ private static void FillHashesAgainstVideoLocalRepo(SVR_VideoLocal v) { if (!string.IsNullOrEmpty(v.Hash)) { - var n = RepoFactory.VideoLocal.GetByHash(v.Hash); + var n = RepoFactory.VideoLocal.GetByEd2k(v.Hash); if (n != null) { if (!string.IsNullOrEmpty(n.CRC32) && !n.CRC32.Equals(v.CRC32)) v.CRC32 = n.CRC32.ToUpperInvariant(); @@ -351,7 +351,7 @@ private static void FillHashesAgainstVideoLocalRepo(SVR_VideoLocal v) if (!string.IsNullOrEmpty(v.SHA1)) { - var n = RepoFactory.VideoLocal.GetBySHA1(v.SHA1); + var n = RepoFactory.VideoLocal.GetBySha1(v.SHA1); if (n != null) { if (!string.IsNullOrEmpty(n.CRC32) && !n.CRC32.Equals(v.CRC32)) v.CRC32 = n.CRC32.ToUpperInvariant(); @@ -364,7 +364,7 @@ private static void FillHashesAgainstVideoLocalRepo(SVR_VideoLocal v) if (!string.IsNullOrEmpty(v.MD5)) { - var n = RepoFactory.VideoLocal.GetByMD5(v.MD5); + var n = RepoFactory.VideoLocal.GetByMd5(v.MD5); if (n != null) { if (!string.IsNullOrEmpty(n.CRC32) && !n.CRC32.Equals(v.CRC32)) v.CRC32 = n.CRC32.ToUpperInvariant(); diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/ManualLinkJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/ManualLinkJob.cs index 62caf1d67..48250bdf4 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/ManualLinkJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/ManualLinkJob.cs @@ -121,7 +121,7 @@ private async Task ProcessFileQualityFilter() { if (!_settings.FileQualityFilterEnabled) return; - var videoLocals = _episode.VideoLocals; + var videoLocals = _episode.VideoLocals.ToList(); if (videoLocals == null) return; videoLocals.Sort(FileQualityFilter.CompareTo); diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileJob.cs index 7b2106376..fbfe3d8a2 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileJob.cs @@ -74,7 +74,7 @@ public override async Task Process() } // Store a hash-set of the old cross-references for comparison later. - var oldXRefs = _vlocal.EpisodeCrossRefs + var oldXRefs = _vlocal.EpisodeCrossReferences .Select(xref => xref.ToString()) .Join(','); @@ -82,7 +82,7 @@ public override async Task Process() var aniFile = await ProcessFile_AniDB().ConfigureAwait(false); // Check if an AniDB file is now available and if the cross-references changed. - var newXRefs = _vlocal.EpisodeCrossRefs + var newXRefs = _vlocal.EpisodeCrossReferences .Select(xref => xref.ToString()) .Join(','); var xRefsMatch = newXRefs == oldXRefs; @@ -122,7 +122,7 @@ private async Task ProcessFile_AniDB() aniFile ??= await TryGetAniDBFileFromAniDB(animeIDs).ConfigureAwait(false); if (aniFile == null) return null; - await PopulateAnimeForFile(_vlocal, aniFile.EpisodeCrossRefs, animeIDs).ConfigureAwait(false); + await PopulateAnimeForFile(_vlocal, aniFile.EpisodeCrossReferences, animeIDs).ConfigureAwait(false); // We do this inside, as the info will not be available as needed otherwise var videoLocals = @@ -218,7 +218,7 @@ private SVR_AniDB_File GetLocalAniDBFile(SVR_VideoLocal vidLocal) SVR_AniDB_File aniFile = null; if (!ForceAniDB) { - aniFile = RepoFactory.AniDB_File.GetByHashAndFileSize(vidLocal.Hash, _vlocal.FileSize); + aniFile = RepoFactory.AniDB_File.GetByEd2kAndFileSize(vidLocal.Hash, _vlocal.FileSize); if (aniFile == null) { @@ -226,8 +226,8 @@ private SVR_AniDB_File GetLocalAniDBFile(SVR_VideoLocal vidLocal) } } - // If cross refs were wiped, but the AniDB_File was not, we unfortunately need to requery the info - var crossRefs = RepoFactory.CrossRef_File_Episode.GetByHash(vidLocal.Hash); + // If cross refs were wiped, but the AniDB_File was not, we unfortunately need to re-query the info + var crossRefs = RepoFactory.CrossRef_File_Episode.GetByEd2k(vidLocal.Hash); if (crossRefs == null || crossRefs.Count == 0) { aniFile = null; @@ -236,7 +236,7 @@ private SVR_AniDB_File GetLocalAniDBFile(SVR_VideoLocal vidLocal) return aniFile; } - private async Task PopulateAnimeForFile(SVR_VideoLocal vidLocal, List xrefs, Dictionary animeIDs) + private async Task PopulateAnimeForFile(SVR_VideoLocal vidLocal, IReadOnlyList xrefs, Dictionary animeIDs) { // check if we have the episode info // if we don't, we will need to re-download the anime info (which also has episode info) @@ -326,7 +326,7 @@ await scheduler.StartJob(job => private async Task TryGetAniDBFileFromAniDB(Dictionary animeIDs) { // check if we already have a record - var aniFile = RepoFactory.AniDB_File.GetByHashAndFileSize(_vlocal.Hash, _vlocal.FileSize); + var aniFile = RepoFactory.AniDB_File.GetByEd2kAndFileSize(_vlocal.Hash, _vlocal.FileSize); if (aniFile == null || aniFile.FileSize != _vlocal.FileSize) { @@ -366,7 +366,7 @@ await scheduler.StartJob( } // get Anime IDs from the file for processing, the episodes might not be created yet here - aniFile.EpisodeCrossRefs.Select(a => a.AnimeID).Distinct().ForEach(animeID => + aniFile.EpisodeCrossReferences.Select(a => a.AnimeID).Distinct().ForEach(animeID => { animeIDs[animeID] = false; }); diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileMovedMessageJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileMovedMessageJob.cs index c439e5ac4..ac7beb801 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileMovedMessageJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/ProcessFileMovedMessageJob.cs @@ -43,7 +43,7 @@ public override async Task Process() return; } - var vlocal = RepoFactory.VideoLocal.GetByHash(file.Hash); + var vlocal = RepoFactory.VideoLocal.GetByEd2k(file.Hash); if (vlocal == null) { _logger.LogWarning("Could not find VideoLocal for file with AniDB ID and Hash: {ID} {Hash}", fileId, file.Hash); diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/ValidateAllImagesJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/ValidateAllImagesJob.cs index 7e5b2ea6d..6a78b0181 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/ValidateAllImagesJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/ValidateAllImagesJob.cs @@ -101,7 +101,7 @@ public override async Task Process() foreach (var character in characters) { _logger.LogTrace(CorruptImageFound, character.GetFullImagePath()); - await RemoveImageAndQueueDownload(ImageEntityType.Character, character.CharID); + await RemoveImageAndQueueDownload(ImageEntityType.Character, character.CharacterID); if (++count % 10 != 0) continue; _logger.LogInformation(ReQueueingForDownload, count, characters.Count); UpdateProgress($" - AniDB Characters - {count}/{characters.Count}"); diff --git a/Shoko.Server/Scheduling/Jobs/Trakt/SearchTraktSeriesJob.cs b/Shoko.Server/Scheduling/Jobs/Trakt/SearchTraktSeriesJob.cs index 57fe2d7d1..1146307e4 100644 --- a/Shoko.Server/Scheduling/Jobs/Trakt/SearchTraktSeriesJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Trakt/SearchTraktSeriesJob.cs @@ -97,7 +97,7 @@ public override Task Process() } // finally lets try searching Trakt directly - var anime = RepoFactory.AniDB_Anime.GetByAnimeID(sessionWrapper, AnimeID); + var anime = RepoFactory.AniDB_Anime.GetByAnimeID(AnimeID); if (anime == null) return Task.CompletedTask; var searchCriteria = anime.MainTitle; diff --git a/Shoko.Server/Server/Enums.cs b/Shoko.Server/Server/Enums.cs index 0dabb2651..d23e929e1 100644 --- a/Shoko.Server/Server/Enums.cs +++ b/Shoko.Server/Server/Enums.cs @@ -90,3 +90,61 @@ public enum ForeignEntityType Person = 256, Character = 512, } + +[JsonConverter(typeof(StringEnumConverter))] +public enum CreatorRoleType +{ + /// + /// Voice actor or voice actress. + /// + Actor, + + /// + /// This can be anything involved in writing the show. + /// + Staff, + + /// + /// The studio responsible for publishing the show. + /// + Studio, + + /// + /// The main producer(s) for the show. + /// + Producer, + + /// + /// Direction. + /// + Director, + + /// + /// Series Composition. + /// + SeriesComposer, + + /// + /// Character Design. + /// + CharacterDesign, + + /// + /// Music composer. + /// + Music, + + /// + /// Responsible for the creation of the source work this show is detrived from. + /// + SourceWork, +} + +public enum CharacterAppearanceType +{ + Unknown = 0, + Main_Character, + Minor_Character, + Background_Character, + Cameo +} diff --git a/Shoko.Server/Server/ShokoEventHandler.cs b/Shoko.Server/Server/ShokoEventHandler.cs index 9bd97912a..6e503eb44 100644 --- a/Shoko.Server/Server/ShokoEventHandler.cs +++ b/Shoko.Server/Server/ShokoEventHandler.cs @@ -61,7 +61,7 @@ public void OnFileDetected(SVR_ImportFolder folder, FileInfo file) public void OnFileHashed(SVR_ImportFolder folder, SVR_VideoLocal_Place vlp, SVR_VideoLocal vl) { var relativePath = vlp.FilePath; - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() @@ -82,7 +82,7 @@ public void OnFileHashed(SVR_ImportFolder folder, SVR_VideoLocal_Place vlp, SVR_ public void OnFileDeleted(SVR_ImportFolder folder, SVR_VideoLocal_Place vlp, SVR_VideoLocal vl) { var path = vlp.FilePath; - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() @@ -103,7 +103,7 @@ public void OnFileDeleted(SVR_ImportFolder folder, SVR_VideoLocal_Place vlp, SVR public void OnFileMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl) { var path = vlp.FilePath; - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() @@ -124,7 +124,7 @@ public void OnFileMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl) public void OnFileNotMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl, int autoMatchAttempts, bool hasXRefs, bool isUDPBanned) { var path = vlp.FilePath; - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() @@ -145,7 +145,7 @@ public void OnFileNotMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl, int au public void OnFileMoved(IImportFolder oldFolder, IImportFolder newFolder, string oldPath, string newPath, SVR_VideoLocal_Place vlp) { var vl = vlp.VideoLocal!; - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() @@ -167,7 +167,7 @@ public void OnFileRenamed(IImportFolder folder, string oldName, string newName, { var path = vlp.FilePath; var vl = vlp.VideoLocal!; - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; var episodes = xrefs .Select(x => x.AnimeEpisode) .WhereNotNull() diff --git a/Shoko.Server/Services/ActionService.cs b/Shoko.Server/Services/ActionService.cs index 6740e03fc..89a566e48 100644 --- a/Shoko.Server/Services/ActionService.cs +++ b/Shoko.Server/Services/ActionService.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Quartz; using Shoko.Commons.Extensions; +using Shoko.Commons.Utils; using Shoko.Models.Enums; using Shoko.Models.Server; using Shoko.Plugin.Abstractions.Enums; @@ -124,14 +125,14 @@ await scheduler.StartJob( { // queue scan for files that are automatically linked but missing AniDB_File data var aniFile = RepoFactory.AniDB_File.GetByHash(vl.Hash); - if (aniFile == null && vl.EpisodeCrossRefs.Any(a => a.CrossRefSource == (int)CrossRefSource.AniDB)) + if (aniFile == null && vl.EpisodeCrossReferences.Any(a => a.CrossRefSource == (int)CrossRefSource.AniDB)) await scheduler.StartJob(c => c.VideoLocalID = vl.VideoLocalID); if (aniFile == null) continue; // the cross ref is created before the actually episode data is downloaded // so lets check for that - var missingEpisodes = aniFile.EpisodeCrossRefs.Any(a => RepoFactory.AniDB_Episode.GetByEpisodeID(a.EpisodeID) == null); + var missingEpisodes = aniFile.EpisodeCrossReferences.Any(a => RepoFactory.AniDB_Episode.GetByEpisodeID(a.EpisodeID) == null); // this will then download the anime etc if (missingEpisodes) await scheduler.StartJob(c => c.VideoLocalID = vl.VideoLocalID); @@ -155,7 +156,7 @@ public async Task RunImport_ScanFolder(int importFolderID, bool skipMyList = fal if (folder == null) return; // first build a list of files that we already know about, as we don't want to process them again - var filesAll = RepoFactory.VideoLocalPlace.GetByImportFolder(folder.ImportFolderID); + var filesAll = folder.Places; var dictFilesExisting = new Dictionary(); foreach (var vl in filesAll.Where(a => a.FullServerPath != null)) { @@ -507,19 +508,16 @@ private static bool ShouldUpdateAniDBCreatorImages(IServerSettings settings, SVR { if (!settings.AniDb.DownloadCreators) return false; - foreach (var seiyuu in RepoFactory.AniDB_Character.GetCharactersForAnime(anime.AnimeID) - .SelectMany(a => RepoFactory.AniDB_Character_Creator.GetByCharacterID(a.CharID)) - .Select(a => RepoFactory.AniDB_Creator.GetByCreatorID(a.CreatorID)).WhereNotNull()) + foreach (var creator in RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(anime.AnimeID).Select(a => a.Creator).WhereNotNull()) { - if (string.IsNullOrEmpty(seiyuu.ImagePath)) continue; - if (!File.Exists(seiyuu.GetFullImagePath())) return true; + if (string.IsNullOrEmpty(creator.ImagePath)) continue; + if (!Misc.IsImageValid(creator.GetFullImagePath())) return true; } - foreach (var seiyuu in RepoFactory.AniDB_Anime_Staff.GetByAnimeID(anime.AnimeID) - .Select(a => RepoFactory.AniDB_Creator.GetByCreatorID(a.CreatorID)).WhereNotNull()) + foreach (var creator in RepoFactory.AniDB_Anime_Staff.GetByAnimeID(anime.AnimeID).Select(a => RepoFactory.AniDB_Creator.GetByCreatorID(a.CreatorID)).WhereNotNull()) { - if (string.IsNullOrEmpty(seiyuu.ImagePath)) continue; - if (!File.Exists(seiyuu.GetFullImagePath())) return true; + if (string.IsNullOrEmpty(creator.ImagePath)) continue; + if (!Misc.IsImageValid(creator.GetFullImagePath())) return true; } return false; @@ -531,8 +529,8 @@ private static bool ShouldUpdateAniDBCharacterImages(IServerSettings settings, S foreach (var chr in RepoFactory.AniDB_Character.GetCharactersForAnime(anime.AnimeID)) { - if (string.IsNullOrEmpty(chr.PicName)) continue; - if (!File.Exists(chr.GetFullImagePath())) return true; + if (string.IsNullOrEmpty(chr.ImagePath)) continue; + if (!Misc.IsImageValid(chr.GetFullImagePath())) return true; } return false; @@ -694,7 +692,7 @@ public async Task RemoveRecordsWithoutPhysicalFiles(bool removeMyList = true) { if (RepoFactory.AniDB_File.GetByHash(v.Hash) == null) { - var xrefs = RepoFactory.CrossRef_File_Episode.GetByHash(v.Hash); + var xrefs = v.EpisodeCrossReferences; foreach (var xref in xrefs) { if (xref.AnimeID is 0) @@ -735,7 +733,7 @@ await scheduler.StartJob(c => // Clean up failed imports var list = RepoFactory.VideoLocal.GetAll() - .SelectMany(a => RepoFactory.CrossRef_File_Episode.GetByHash(a.Hash)) + .SelectMany(a => a.EpisodeCrossReferences) .Where(a => a.AniDBAnime == null || a.AniDBEpisode == null) .ToArray(); BaseRepository.Lock(session, s => @@ -828,7 +826,7 @@ public async Task UpdateAniDBFileData(bool missingInfo, bool outOfDate, boo var missingFiles = RepoFactory.AniDB_File.GetAll() .Where(a => a.GroupID == 0) - .Select(a => RepoFactory.VideoLocal.GetByHash(a.Hash)) + .Select(a => RepoFactory.VideoLocal.GetByEd2k(a.Hash)) .Where(f => f != null) .Select(a => a.VideoLocalID) .ToList(); @@ -1105,22 +1103,14 @@ public async Task ScheduleMissingAnidbCreators() if (!_settingsProvider.GetSettings().AniDb.DownloadCreators) return; var allCreators = RepoFactory.AniDB_Creator.GetAll(); - var allMissingCreators = RepoFactory.AnimeStaff.GetAll() - .Select(s => s.AniDBID) - .Distinct() - .Except(allCreators.Select(a => a.CreatorID)) - .ToList(); - var missingCount = allMissingCreators.Count; - allMissingCreators.AddRange( - allCreators + var allMissingCreators = allCreators .Where(creator => creator.Type is Providers.AniDB.CreatorType.Unknown) .Select(creator => creator.CreatorID) .Distinct() - ); - var partiallyMissingCount = allMissingCreators.Count - missingCount; + .ToList(); var startedAt = DateTime.Now; - _logger.LogInformation("Scheduling {Count} AniDB Creators for a refresh. (Missing={MissingCount},PartiallyMissing={PartiallyMissingCount},Total={Total})", allMissingCreators.Count, missingCount, partiallyMissingCount, allMissingCreators.Count); + _logger.LogInformation("Scheduling {Count} AniDB Creators for a refresh.", allMissingCreators.Count); var scheduler = await _schedulerFactory.GetScheduler().ConfigureAwait(false); var progressCount = 0; foreach (var creatorID in allMissingCreators) diff --git a/Shoko.Server/Services/AnimeEpisodeService.cs b/Shoko.Server/Services/AnimeEpisodeService.cs index ec40a1557..918465adb 100644 --- a/Shoko.Server/Services/AnimeEpisodeService.cs +++ b/Shoko.Server/Services/AnimeEpisodeService.cs @@ -52,7 +52,8 @@ await scheduler.StartJob(c => public List GetV1VideoDetailedContracts(SVR_AnimeEpisode ep, int userID) { // get all the cross refs - return ep?.FileCrossReferences.Select(xref => _videoLocals.GetByHash(xref.Hash)) + return ep?.FileCrossReferences + .Select(xref => xref.VideoLocal) .Where(v => v != null) .Select(v => _vlService.GetV1DetailedContract(v, userID)).ToList() ?? []; } diff --git a/Shoko.Server/Services/AnimeGroupService.cs b/Shoko.Server/Services/AnimeGroupService.cs index a43014717..97da09723 100644 --- a/Shoko.Server/Services/AnimeGroupService.cs +++ b/Shoko.Server/Services/AnimeGroupService.cs @@ -660,15 +660,16 @@ public CL_AnimeGroup_User GetContract(SVR_AnimeGroup animeGroup) private GroupVotes GetVotes(SVR_AnimeGroup animeGroup) { var groupSeries = animeGroup.AllSeries; - var votesByAnime = RepoFactory.AniDB_Vote.GetByAnimeIDs(groupSeries.Select(a => a.AniDB_ID).ToList()); - + var votesByAnime = groupSeries + .Select(a => new { AnimeID = a.AniDB_ID, Vote = RepoFactory.AniDB_Vote.GetByAnimeID(a.AniDB_ID) }) + .Where(a => a.Vote != null) + .ToDictionary(a => a.AnimeID, a => a.Vote); var allVoteTotal = 0m; var permVoteTotal = 0m; var tempVoteTotal = 0m; var allVoteCount = 0; var permVoteCount = 0; var tempVoteCount = 0; - foreach (var series in groupSeries) { if (votesByAnime.TryGetValue(series.AniDB_ID, out var vote)) diff --git a/Shoko.Server/Services/AnimeSeriesService.cs b/Shoko.Server/Services/AnimeSeriesService.cs index 66c3f0679..a5c6a947f 100644 --- a/Shoko.Server/Services/AnimeSeriesService.cs +++ b/Shoko.Server/Services/AnimeSeriesService.cs @@ -15,6 +15,7 @@ using Shoko.Plugin.Abstractions.Enums; using Shoko.Server.Extensions; using Shoko.Server.Models; +using Shoko.Server.Models.AniDB; using Shoko.Server.Providers.AniDB; using Shoko.Server.Repositories; using Shoko.Server.Repositories.Cached; @@ -261,7 +262,7 @@ public CL_AnimeSeries_User GetV1UserContract(SVR_AnimeSeries series, int userid) contract.MovieDB_Movie = tmdbMovieXrefs[0].TmdbMovie?.ToClient(); } - contract.CrossRefAniDBMAL = series.MALCrossReferences?.ToList() ?? new List(); + contract.CrossRefAniDBMAL = series.MalCrossReferences?.ToList() ?? new List(); try { @@ -404,7 +405,7 @@ private void UpdateWatchedStats(SVR_AnimeSeries series, List e { var vls = RepoFactory.CrossRef_File_Episode.GetByAnimeID(series.AniDB_ID) .Where(a => !string.IsNullOrEmpty(a?.Hash)).Select(xref => - (xref.EpisodeID, VideoLocal: RepoFactory.VideoLocal.GetByHash(xref.Hash))) + (xref.EpisodeID, xref.VideoLocal)) .Where(a => a.VideoLocal != null).ToLookup(a => a.EpisodeID, b => b.VideoLocal); var vlUsers = vls.SelectMany( xref => @@ -694,71 +695,55 @@ private void UpdateMissingEpisodeStats(SVR_AnimeSeries series, List SearchSeriesByStaff(string staffname, - bool fuzzy = false) + public Dictionary SearchSeriesByStaff(string staffName, bool fuzzy = false) { - var allseries = RepoFactory.AnimeSeries.GetAll(); - var results = new Dictionary(); + var allSeries = RepoFactory.AnimeSeries.GetAll(); + var results = new Dictionary(); var stringsToSearchFor = new List(); - if (staffname.Contains(" ")) + if (staffName.Contains(' ')) { - stringsToSearchFor.AddRange(staffname.Split(' ').GetPermutations() + stringsToSearchFor.AddRange(staffName.Split(' ').GetPermutations() .Select(permutation => string.Join(" ", permutation))); - stringsToSearchFor.Remove(staffname); - stringsToSearchFor.Insert(0, staffname); + stringsToSearchFor.Remove(staffName); + stringsToSearchFor.Insert(0, staffName); } else { - stringsToSearchFor.Add(staffname); + stringsToSearchFor.Add(staffName); } - foreach (var series in allseries) + foreach (var series in allSeries) { - List<(CrossRef_Anime_Staff, AnimeStaff)> staff = RepoFactory.CrossRef_Anime_Staff - .GetByAnimeID(series.AniDB_ID).Select(a => (a, RepoFactory.AnimeStaff.GetByID(a.StaffID))).ToList(); - - foreach (var animeStaff in staff) + foreach (var (xref, staff) in RepoFactory.AniDB_Anime_Staff.GetByAnimeID(series.AniDB_ID).Select(a => (a, a.Creator))) foreach (var search in stringsToSearchFor) { if (fuzzy) { - if (!animeStaff.Item2.Name.FuzzyMatch(search)) + if (!staff.Name.FuzzyMatch(search)) { continue; } } else { - if (!animeStaff.Item2.Name.Equals(search, StringComparison.InvariantCultureIgnoreCase)) + if (!staff.Name.Equals(search, StringComparison.InvariantCultureIgnoreCase)) { continue; } } - if (!results.TryAdd(series, animeStaff.Item1)) + if (!results.TryAdd(series, xref)) { - if (!Enum.TryParse(results[series].Role, out CharacterAppearanceType type1)) - { - continue; - } - - if (!Enum.TryParse(animeStaff.Item1.Role, out CharacterAppearanceType type2)) - { - continue; - } - - var comparison = ((int)type1).CompareTo((int)type2); + var comparison = ((int)results[series].RoleType).CompareTo((int)xref.RoleType); if (comparison == 1) - { - results[series] = animeStaff.Item1; - } + results[series] = xref; } goto label0; } -// People hate goto, but this is a legit use for it. -label0:; + // People hate goto, but this is a legit use for it. + label0:; } return results; @@ -820,14 +805,27 @@ public async Task DeleteSeries(SVR_AnimeSeries series, bool deleteFiles, bool up RepoFactory.AniDB_Anime_PreferredImage.Delete(images); var characterXrefs = RepoFactory.AniDB_Anime_Character.GetByAnimeID(series.AniDB_ID); - var characters = characterXrefs.Select(a => RepoFactory.AniDB_Character.GetByCharID(a.CharID)).ToList(); - var seiyuuXrefs = characters.SelectMany(a => RepoFactory.AniDB_Character_Creator.GetByCharacterID(a.CharID)).ToList(); - RepoFactory.AniDB_Character_Creator.Delete(seiyuuXrefs); - RepoFactory.AniDB_Character.Delete(characters); + var characters = characterXrefs + .Select(x => x.Character) + .WhereNotNull() + .Where(x => !x.GetRoles().ExceptBy(characterXrefs.Select(y => y.AniDB_Anime_CharacterID), y => y.AniDB_Anime_CharacterID).Any()) + .ToList(); RepoFactory.AniDB_Anime_Character.Delete(characterXrefs); + RepoFactory.AniDB_Character.Delete(characters); + var actorXrefs = RepoFactory.AniDB_Anime_Character_Creator.GetByAnimeID(series.AniDB_ID); var staffXrefs = RepoFactory.AniDB_Anime_Staff.GetByAnimeID(series.AniDB_ID); + var creators = actorXrefs.Select(x => x.Creator) + .Concat(staffXrefs.Select(x => x.Creator)) + .WhereNotNull() + .Where(x => + !x.Staff.ExceptBy(staffXrefs.Select(y => y.AniDB_Anime_StaffID), y => y.AniDB_Anime_StaffID).Any() && + !x.Characters.ExceptBy(actorXrefs.Select(y => y.AniDB_Anime_Character_CreatorID), y => y.AniDB_Anime_Character_CreatorID).Any() + ) + .ToList(); + RepoFactory.AniDB_Anime_Character_Creator.Delete(actorXrefs); RepoFactory.AniDB_Anime_Staff.Delete(staffXrefs); + RepoFactory.AniDB_Creator.Delete(creators); var tagXrefs = RepoFactory.AniDB_Anime_Tag.GetByAnimeID(series.AniDB_ID); RepoFactory.AniDB_Anime_Tag.Delete(tagXrefs); @@ -963,8 +961,8 @@ public class NextUpQueryOptions var (currentlyWatchingEpisode, _) = episodeList .SelectMany(tuple => tuple.shoko.VideoLocals.Select(file => (tuple.shoko, fileUR: _vlUsers.GetByUserIDAndVideoLocalID(userID, file.VideoLocalID)))) .Where(tuple => tuple.fileUR is not null) - .OrderByDescending(tuple => tuple.fileUR.LastUpdated) - .FirstOrDefault(tuple => tuple.fileUR.ResumePosition > 0); + .OrderByDescending(tuple => tuple.fileUR!.LastUpdated) + .FirstOrDefault(tuple => tuple.fileUR!.ResumePosition > 0); if (currentlyWatchingEpisode is not null) return currentlyWatchingEpisode; @@ -994,7 +992,7 @@ public class NextUpQueryOptions var (lastWatchedEpisode, _) = episodeList .SelectMany(tuple => tuple.shoko.VideoLocals.Select(file => (tuple.shoko, fileUR: _vlUsers.GetByUserIDAndVideoLocalID(userID, file.VideoLocalID)))) .Where(tuple => tuple.fileUR is { WatchedDate: not null }) - .OrderByDescending(tuple => tuple.fileUR.LastUpdated) + .OrderByDescending(tuple => tuple.fileUR!.LastUpdated) .FirstOrDefault(); if (lastWatchedEpisode is not null) diff --git a/Shoko.Server/Services/GeneratedPlaylistService.cs b/Shoko.Server/Services/GeneratedPlaylistService.cs index b800e907a..bc664ee68 100644 --- a/Shoko.Server/Services/GeneratedPlaylistService.cs +++ b/Shoko.Server/Services/GeneratedPlaylistService.cs @@ -198,7 +198,7 @@ public bool TryParsePlaylist(string[] items, out IReadOnlyList<(IReadOnlyList 0 ? _videoRepository.GetByHashAndSize(ed2kHash, fileSize) : _videoRepository.GetByHash(ed2kHash)) is not { } video0) + if ((fileSize > 0 ? _videoRepository.GetByEd2kAndSize(ed2kHash, fileSize) : _videoRepository.GetByEd2k(ed2kHash)) is not { } video0) { if (fileSize == 0) modelState?.AddModelError($"{fieldName}[{index}][{offset}]", $"Unknown hash \"{rawValue}\" at index {index} at offset {offset}"); @@ -342,7 +342,7 @@ public bool TryParsePlaylist(string[] items, out IReadOnlyList<(IReadOnlyList xref.ReleaseGroup == releaseGroupID) .ToList(); - var videos = xrefs.Select(xref => _videoRepository.GetByHashAndSize(xref.ED2K, xref.FileSize)) + var videos = xrefs.Select(xref => _videoRepository.GetByEd2kAndSize(xref.ED2K, xref.FileSize)) .WhereNotNull() .ToList(); yield return ([episode], videos); @@ -402,7 +402,7 @@ public FileStreamResult GeneratePlaylist( if (string.IsNullOrEmpty(apiKey)) { var user = _context.GetUser(); - apiKey = _authTokensRepository.CreateNewApikey(user, "playlist"); + apiKey = _authTokensRepository.CreateNewApiKey(user, "playlist"); } foreach (var (episodes, videos) in playlist) { diff --git a/Shoko.Server/Services/VideoLocalService.cs b/Shoko.Server/Services/VideoLocalService.cs index c092b9ddc..42a7a12a9 100644 --- a/Shoko.Server/Services/VideoLocalService.cs +++ b/Shoko.Server/Services/VideoLocalService.cs @@ -97,7 +97,7 @@ public CL_VideoLocal GetV1Contract(SVR_VideoLocal vl, int userID) public CL_VideoDetailed GetV1DetailedContract(SVR_VideoLocal vl, int userID) { // get the cross ref episode - var xrefs = vl.EpisodeCrossRefs; + var xrefs = vl.EpisodeCrossReferences; if (xrefs.Count == 0) return null; var userRecord = _vlUsers.GetByUserIDAndVideoLocalID(userID, vl.VideoLocalID); diff --git a/Shoko.Server/Services/VideoLocal_PlaceService.cs b/Shoko.Server/Services/VideoLocal_PlaceService.cs index 92973eb22..97fd3797b 100644 --- a/Shoko.Server/Services/VideoLocal_PlaceService.cs +++ b/Shoko.Server/Services/VideoLocal_PlaceService.cs @@ -929,7 +929,7 @@ public async Task RemoveRecord(SVR_VideoLocal_Place place, bool updateMyListStat { if (RepoFactory.AniDB_File.GetByHash(v.Hash) is null) { - var xrefs = RepoFactory.CrossRef_File_Episode.GetByHash(v.Hash); + var xrefs = v.EpisodeCrossReferences; foreach (var xref in xrefs) { if (xref.AnimeID is 0) @@ -1023,7 +1023,7 @@ public async Task RemoveRecordWithOpenTransaction(ISession session, SVR_VideoLoc var scheduler = await _schedulerFactory.GetScheduler(); if (_aniDBFile.GetByHash(v.Hash) is null) { - var xrefs = _crossRefFileEpisode.GetByHash(v.Hash); + var xrefs = _crossRefFileEpisode.GetByEd2k(v.Hash); foreach (var xref in xrefs) { if (xref.AnimeID is 0) diff --git a/Shoko.Server/Services/WatchedStatusService.cs b/Shoko.Server/Services/WatchedStatusService.cs index 7a06516a0..4d0383103 100644 --- a/Shoko.Server/Services/WatchedStatusService.cs +++ b/Shoko.Server/Services/WatchedStatusService.cs @@ -136,7 +136,7 @@ await scheduler.StartJob( SVR_AnimeSeries ser; // get all files associated with this episode - var xrefs = _fileEpisodes.GetByHash(vl.Hash); + var xrefs = vl.EpisodeCrossReferences; var toUpdateSeries = new Dictionary(); if (watched) { diff --git a/Shoko.Server/Utilities/ImageUtils.cs b/Shoko.Server/Utilities/ImageUtils.cs index c19e77305..96c87c95b 100644 --- a/Shoko.Server/Utilities/ImageUtils.cs +++ b/Shoko.Server/Utilities/ImageUtils.cs @@ -120,17 +120,11 @@ public static string GetAniDBImagePath(int animeID) { DataSourceType.AniDB => imageType switch { - ImageEntityType.Character => RepoFactory.AniDB_Character.GetByCharID(imageId)?.GetImageMetadata(), + ImageEntityType.Character => RepoFactory.AniDB_Character.GetByCharacterID(imageId)?.GetImageMetadata(), ImageEntityType.Person => RepoFactory.AniDB_Creator.GetByCreatorID(imageId)?.GetImageMetadata(), ImageEntityType.Poster => RepoFactory.AniDB_Anime.GetByAnimeID(imageId)?.GetImageMetadata(), _ => null, }, - DataSourceType.Shoko => imageType switch - { - ImageEntityType.Character => RepoFactory.AnimeCharacter.GetByID(imageId)?.GetImageMetadata(), - ImageEntityType.Person => RepoFactory.AnimeStaff.GetByID(imageId)?.GetImageMetadata(), - _ => null, - }, DataSourceType.TMDB => RepoFactory.TMDB_Image.GetByID(imageId), _ => null, }; @@ -149,28 +143,15 @@ public static string GetAniDBImagePath(int animeID) .GetRandomElement()?.GetImageMetadata(false), ImageEntityType.Character => RepoFactory.AniDB_Anime.GetAll() .Where(a => a is not null && !a.GetAllTags().Contains("18 restricted")) - .SelectMany(a => a.Characters).Select(a => a.GetCharacter()).WhereNotNull() + .SelectMany(a => a.Characters).Select(a => a.Character) + .WhereNotNull() .GetRandomElement()?.GetImageMetadata(), ImageEntityType.Person => RepoFactory.AniDB_Anime.GetAll() .Where(a => a is not null && !a.GetAllTags().Contains("18 restricted")) .SelectMany(a => a.Characters) - .SelectMany(a => RepoFactory.AniDB_Character_Creator.GetByCharacterID(a.CharID)) - .Select(a => RepoFactory.AniDB_Creator.GetByCreatorID(a.CreatorID)).WhereNotNull() - .GetRandomElement()?.GetImageMetadata(), - _ => null, - }, - DataSourceType.Shoko => imageType switch - { - ImageEntityType.Character => RepoFactory.AniDB_Anime.GetAll() - .Where(a => a is not null && !a.GetAllTags().Contains("18 restricted")) - .SelectMany(a => RepoFactory.CrossRef_Anime_Staff.GetByAnimeID(a.AnimeID)) - .Where(a => a.RoleType == (int)StaffRoleType.Seiyuu && a.RoleID.HasValue) - .Select(a => RepoFactory.AnimeCharacter.GetByID(a.RoleID!.Value)) - .GetRandomElement()?.GetImageMetadata(), - ImageEntityType.Person => RepoFactory.AniDB_Anime.GetAll() - .Where(a => a is not null && !a.GetAllTags().Contains("18 restricted")) - .SelectMany(a => RepoFactory.CrossRef_Anime_Staff.GetByAnimeID(a.AnimeID)) - .Select(a => RepoFactory.AnimeStaff.GetByID(a.StaffID)) + .SelectMany(a => RepoFactory.AniDB_Anime_Character_Creator.GetByCharacterID(a.CharacterID)) + .Select(a => RepoFactory.AniDB_Creator.GetByCreatorID(a.CreatorID)) + .WhereNotNull() .GetRandomElement()?.GetImageMetadata(), _ => null, },