From 930c53bdaeab6888baa2d8d6a88e98cbcdbafc9e Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Sun, 6 Oct 2024 20:39:16 +0200 Subject: [PATCH] refactor: add `.GetStream()` to abstraction and enable nullable - Added `Stream? IVideo.GetStream()` and `Stream? IVideoFile.GetStream()` to the plugin abstraction, so plugins can more easily get a file stream to use through the abstraction instead of needing to rely on the `System.IO` in their code. We'll handle the lifting for you. - Enabled nullable on the `SVR_VideoLocal_Place` file, marking any fields that should be nullable as nullable and fixing the nullable warnings that popped up as a result of this change. Also removed some left-over fields from older code that is no longer used (0 references). --- .../DataModels/IVideo.cs | 6 +++ .../DataModels/IVideoFile.cs | 7 ++++ .../Shoko.Plugin.Abstractions.csproj | 2 +- Shoko.Server/API/v3/Models/Shoko/WebUI.cs | 2 +- Shoko.Server/Models/SVR_VideoLocal.cs | 23 +++++++---- Shoko.Server/Models/SVR_VideoLocal_Place.cs | 40 ++++++++++++------- Shoko.Server/Plex/TVShow/SVR_Episode.cs | 2 +- Shoko.Server/Renamer/RenameFileService.cs | 2 +- .../Cached/AnimeEpisodeRepository.cs | 1 + .../Scheduling/Jobs/Shoko/HashFileJob.cs | 4 +- Shoko.Server/Server/ShokoEventHandler.cs | 8 ++-- .../Services/VideoLocal_PlaceService.cs | 28 ++++++++----- 12 files changed, 85 insertions(+), 40 deletions(-) diff --git a/Shoko.Plugin.Abstractions/DataModels/IVideo.cs b/Shoko.Plugin.Abstractions/DataModels/IVideo.cs index f0f80bdd2..ac5434a51 100644 --- a/Shoko.Plugin.Abstractions/DataModels/IVideo.cs +++ b/Shoko.Plugin.Abstractions/DataModels/IVideo.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; +using System.IO; using Shoko.Plugin.Abstractions.DataModels.Shoko; namespace Shoko.Plugin.Abstractions.DataModels; @@ -58,4 +59,9 @@ public interface IVideo : IMetadata /// Information about the group /// IReadOnlyList Groups { get; } + + /// + /// Get the stream for the video, if any files are still available. + /// + Stream? GetStream(); } diff --git a/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs b/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs index 5638340c4..ca07ea716 100644 --- a/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs +++ b/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs @@ -1,3 +1,5 @@ +using System.IO; + namespace Shoko.Plugin.Abstractions.DataModels; /// @@ -50,4 +52,9 @@ public interface IVideoFile /// The import folder tied to the video file location. /// IImportFolder ImportFolder { get; } + + /// + /// Get the stream for the video file, if the file is still available. + /// + Stream? GetStream(); } diff --git a/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj b/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj index 02c5ea81a..9d08a78ba 100644 --- a/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj +++ b/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj @@ -13,7 +13,7 @@ https://github.com/ShokoAnime/ShokoServer plugins, shoko, anime, metadata, tagging Renamer Rewrite - 4.0.0-beta8 + 4.0.0-beta9 Debug;Release;Benchmarks AnyCPU;x64 false diff --git a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs index becc1e0ac..327cc8491 100644 --- a/Shoko.Server/API/v3/Models/Shoko/WebUI.cs +++ b/Shoko.Server/API/v3/Models/Shoko/WebUI.cs @@ -239,7 +239,7 @@ public WebUISeriesFileSummary( if (groupByCriteria.Contains(FileSummaryGroupByCriteria.FileIsDeprecated)) groupByDetails.FileIsDeprecated = anidbFile?.IsDeprecated ?? false; if (groupByCriteria.Contains(FileSummaryGroupByCriteria.ImportFolder)) - groupByDetails.ImportFolder = $"{location.ImportFolder.ImportFolderName} (ID: {location.ImportFolderID})"; + groupByDetails.ImportFolder = $"{location.ImportFolder?.ImportFolderName ?? "N/A"} (ID: {location.ImportFolderID})"; // Video criteria if (groupByCriteria.Contains(FileSummaryGroupByCriteria.VideoCodecs)) diff --git a/Shoko.Server/Models/SVR_VideoLocal.cs b/Shoko.Server/Models/SVR_VideoLocal.cs index c703683ee..f99bc90b1 100644 --- a/Shoko.Server/Models/SVR_VideoLocal.cs +++ b/Shoko.Server/Models/SVR_VideoLocal.cs @@ -18,7 +18,7 @@ #nullable enable namespace Shoko.Server.Models; -public class SVR_VideoLocal : VideoLocal, IHash, IHashes, IVideo +public class SVR_VideoLocal : VideoLocal, IHashes, IVideo { #region DB columns @@ -194,6 +194,21 @@ public bool HasAnyEmptyHashes() DataSourceEnum IMetadata.Source => DataSourceEnum.Shoko; + Stream? IVideo.GetStream() + { + if (FirstResolvedPlace is not { } fileLocation) + return null; + + var filePath = fileLocation.FullServerPath; + if (string.IsNullOrEmpty(filePath)) + return null; + + if (!File.Exists(filePath)) + return null; + + return File.OpenRead(filePath); + } + #endregion #region IHashes Implementation @@ -207,12 +222,6 @@ public bool HasAnyEmptyHashes() string IHashes.SHA1 => SHA1; #endregion - - string IHash.ED2KHash - { - get => Hash; - set => Hash = value; - } } // This is a comparer used to sort the completeness of a video local, more complete first. diff --git a/Shoko.Server/Models/SVR_VideoLocal_Place.cs b/Shoko.Server/Models/SVR_VideoLocal_Place.cs index 13293e5bb..3057f41c1 100644 --- a/Shoko.Server/Models/SVR_VideoLocal_Place.cs +++ b/Shoko.Server/Models/SVR_VideoLocal_Place.cs @@ -1,32 +1,33 @@ -using System.IO; +using System; +using System.IO; using Shoko.Models.Server; using Shoko.Plugin.Abstractions.DataModels; using Shoko.Server.Repositories; +#nullable enable namespace Shoko.Server.Models; public class SVR_VideoLocal_Place : VideoLocal_Place, IVideoFile { - internal SVR_ImportFolder ImportFolder => RepoFactory.ImportFolder.GetByID(ImportFolderID); + internal SVR_ImportFolder? ImportFolder => RepoFactory.ImportFolder.GetByID(ImportFolderID); - public string FullServerPath + public string? FullServerPath { get { - if (string.IsNullOrEmpty(ImportFolder?.ImportFolderLocation) || string.IsNullOrEmpty(FilePath)) - { + var importFolderLocation = ImportFolder?.ImportFolderLocation; + if (string.IsNullOrEmpty(importFolderLocation) || string.IsNullOrEmpty(FilePath)) return null; - } - return Path.Combine(ImportFolder.ImportFolderLocation, FilePath); + return Path.Combine(importFolderLocation, FilePath); } } public string FileName => Path.GetFileName(FilePath); - public SVR_VideoLocal VideoLocal => VideoLocalID == 0 ? null : RepoFactory.VideoLocal.GetByID(VideoLocalID); + public SVR_VideoLocal? VideoLocal => VideoLocalID is 0 ? null : RepoFactory.VideoLocal.GetByID(VideoLocalID); - public FileInfo GetFile() + public FileInfo? GetFile() { if (!File.Exists(FullServerPath)) { @@ -42,9 +43,11 @@ public FileInfo GetFile() int IVideoFile.VideoID => VideoLocalID; - IVideo IVideoFile.Video => VideoLocal; + IVideo IVideoFile.Video => VideoLocal + ?? throw new NullReferenceException("Unable to get the associated IVideo for the IVideoFile with ID " + VideoLocal_Place_ID); - string IVideoFile.Path => FullServerPath; + string IVideoFile.Path => FullServerPath + ?? throw new NullReferenceException("Unable to get the absolute path for the IVideoFile with ID " + VideoLocal_Place_ID); string IVideoFile.RelativePath { @@ -60,11 +63,20 @@ string IVideoFile.RelativePath long IVideoFile.Size => VideoLocal?.FileSize ?? 0; - IImportFolder IVideoFile.ImportFolder => ImportFolder; + IImportFolder IVideoFile.ImportFolder => ImportFolder + ?? throw new NullReferenceException("Unable to get the associated IImportFolder for the IVideoFile with ID " + VideoLocal_Place_ID); - public IHashes Hashes => VideoLocal; + Stream? IVideoFile.GetStream() + { + var filePath = FullServerPath; + if (string.IsNullOrEmpty(filePath)) + return null; - public IMediaInfo MediaInfo => VideoLocal?.MediaInfo; + if (!File.Exists(filePath)) + return null; + + return File.OpenRead(filePath); + } #endregion } diff --git a/Shoko.Server/Plex/TVShow/SVR_Episode.cs b/Shoko.Server/Plex/TVShow/SVR_Episode.cs index e24e7c3ee..ac2b1282f 100644 --- a/Shoko.Server/Plex/TVShow/SVR_Episode.cs +++ b/Shoko.Server/Plex/TVShow/SVR_Episode.cs @@ -38,7 +38,7 @@ public SVR_AnimeEpisode AnimeEpisode .GetAll() .FirstOrDefault(location => location.FullServerPath?.EndsWith(filenameWithParent, StringComparison.OrdinalIgnoreCase) ?? false); - return file is null ? null : RepoFactory.AnimeEpisode.GetByHash(file.Hashes.ED2K).FirstOrDefault(); + return file is null ? null : RepoFactory.AnimeEpisode.GetByHash(file.VideoLocal?.Hash).FirstOrDefault(); } } diff --git a/Shoko.Server/Renamer/RenameFileService.cs b/Shoko.Server/Renamer/RenameFileService.cs index 15b9c14c2..c90012852 100644 --- a/Shoko.Server/Renamer/RenameFileService.cs +++ b/Shoko.Server/Renamer/RenameFileService.cs @@ -225,7 +225,7 @@ private static RelocationResult UnAbstractResult(SVR_VideoLocal_Place place, Abs Exception = result.Error.Exception, }; - var newImportFolder = shouldMove && !result.SkipMove ? result.DestinationImportFolder! : place.ImportFolder; + var newImportFolder = shouldMove && !result.SkipMove ? result.DestinationImportFolder! : place.ImportFolder!; var newFileName = shouldRename && !result.SkipRename ? result.FileName! : place.FileName; var newRelativeDirectory = shouldMove && !result.SkipMove ? result.Path! : Path.GetDirectoryName(place.FilePath)!; var newRelativePath = newRelativeDirectory.Length > 0 ? Path.Combine(newRelativeDirectory, newFileName) : newFileName; diff --git a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs index b55be513b..dee692e3c 100644 --- a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs @@ -84,6 +84,7 @@ public SVR_AnimeEpisode GetByFilename(string name) /// public List GetByHash(string hash) { + if (string.IsNullOrEmpty(hash)) return []; return RepoFactory.CrossRef_File_Episode.GetByHash(hash) .Select(a => GetByAniDBEpisodeID(a.EpisodeID)) .Where(a => a != null) diff --git a/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs b/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs index 06192912b..708b9b207 100644 --- a/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs +++ b/Shoko.Server/Scheduling/Jobs/Shoko/HashFileJob.cs @@ -391,13 +391,13 @@ private async Task ProcessDuplicates(SVR_VideoLocal vlocal, SVR_VideoLocal // remove missing files var preps = vlocal.Places.Where(a => { - if (vlocalplace.FullServerPath.Equals(a.FullServerPath)) return false; + if (string.Equals(a.FullServerPath, vlocalplace.FullServerPath)) return false; if (a.FullServerPath == null) return true; return !File.Exists(a.FullServerPath); }).ToList(); RepoFactory.VideoLocalPlace.Delete(preps); - var dupPlace = vlocal.Places.FirstOrDefault(a => !vlocalplace.FullServerPath.Equals(a.FullServerPath)); + var dupPlace = vlocal.Places.FirstOrDefault(a => !string.Equals(a.FullServerPath, vlocalplace.FullServerPath)); if (dupPlace == null) return false; _logger.LogWarning("Found Duplicate File"); diff --git a/Shoko.Server/Server/ShokoEventHandler.cs b/Shoko.Server/Server/ShokoEventHandler.cs index 84de54e05..9bd97912a 100644 --- a/Shoko.Server/Server/ShokoEventHandler.cs +++ b/Shoko.Server/Server/ShokoEventHandler.cs @@ -118,7 +118,7 @@ public void OnFileMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl) .Select(a => a.AnimeGroup) .WhereNotNull() .ToList(); - FileMatched?.Invoke(null, new(path, vlp.ImportFolder, vlp, vl, episodes, series, groups)); + FileMatched?.Invoke(null, new(path, vlp.ImportFolder!, vlp, vl, episodes, series, groups)); } public void OnFileNotMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl, int autoMatchAttempts, bool hasXRefs, bool isUDPBanned) @@ -139,12 +139,12 @@ public void OnFileNotMatched(SVR_VideoLocal_Place vlp, SVR_VideoLocal vl, int au .Select(a => a.AnimeGroup) .WhereNotNull() .ToList(); - FileNotMatched?.Invoke(null, new(path, vlp.ImportFolder, vlp, vl, episodes, series, groups, autoMatchAttempts, hasXRefs, isUDPBanned)); + FileNotMatched?.Invoke(null, new(path, vlp.ImportFolder!, vlp, vl, episodes, series, groups, autoMatchAttempts, hasXRefs, isUDPBanned)); } public void OnFileMoved(IImportFolder oldFolder, IImportFolder newFolder, string oldPath, string newPath, SVR_VideoLocal_Place vlp) { - var vl = vlp.VideoLocal; + var vl = vlp.VideoLocal!; var xrefs = vl.EpisodeCrossRefs; var episodes = xrefs .Select(x => x.AnimeEpisode) @@ -166,7 +166,7 @@ public void OnFileMoved(IImportFolder oldFolder, IImportFolder newFolder, string public void OnFileRenamed(IImportFolder folder, string oldName, string newName, SVR_VideoLocal_Place vlp) { var path = vlp.FilePath; - var vl = vlp.VideoLocal; + var vl = vlp.VideoLocal!; var xrefs = vl.EpisodeCrossRefs; var episodes = xrefs .Select(x => x.AnimeEpisode) diff --git a/Shoko.Server/Services/VideoLocal_PlaceService.cs b/Shoko.Server/Services/VideoLocal_PlaceService.cs index d3855cdaa..bfb65cc27 100644 --- a/Shoko.Server/Services/VideoLocal_PlaceService.cs +++ b/Shoko.Server/Services/VideoLocal_PlaceService.cs @@ -93,6 +93,17 @@ public async Task DirectlyRelocateFile(SVR_VideoLocal_Place pl ErrorMessage = "Invalid request object, import folder, or relative path.", }; + if (place.VideoLocal is not { } video) + { + _logger.LogWarning("Could not find the associated video for the file location: {LocationID}", place.VideoLocal_Place_ID); + return new() + { + Success = false, + ShouldRetry = false, + ErrorMessage = $"Could not find the associated video for the file location: {place.VideoLocal_Place_ID}", + }; + } + // Sanitize relative path and reject paths leading to outside the import folder. var fullPath = Path.GetFullPath(Path.Combine(request.ImportFolder.Path, request.RelativePath)); if (!fullPath.StartsWith(request.ImportFolder.Path, StringComparison.OrdinalIgnoreCase)) @@ -217,7 +228,7 @@ public async Task DirectlyRelocateFile(SVR_VideoLocal_Place pl }; } - if (destVideoLocal.Hash == place.VideoLocal.Hash) + if (destVideoLocal.Hash == video.Hash) { _logger.LogDebug("Not moving file as it already exists at the new location, deleting source file instead: {PreviousPath} to {NextPath}", oldFullPath, newFullPath); @@ -257,7 +268,7 @@ public async Task DirectlyRelocateFile(SVR_VideoLocal_Place pl }; } - var aniDBFile = place.VideoLocal.AniDBFile; + var aniDBFile = video.AniDBFile; if (aniDBFile is null) { _logger.LogWarning("The file does not have AniDB info. Not moving."); @@ -361,7 +372,6 @@ public async Task DirectlyRelocateFile(SVR_VideoLocal_Place pl if (renamed) { // Add a new or update an existing lookup entry. - var video = place.VideoLocal; var existingEntries = RepoFactory.FileNameHash.GetByHash(video.Hash); if (!existingEntries.Any(a => a.FileName.Equals(newFileName))) { @@ -791,7 +801,7 @@ public async Task RemoveRecordAndDeletePhysicalFile(SVR_VideoLocal_Place place, } if (deleteFolder) - RecursiveDeleteEmptyDirectories(Path.GetDirectoryName(place.FullServerPath), place.ImportFolder.ImportFolderLocation); + RecursiveDeleteEmptyDirectories(Path.GetDirectoryName(place.FullServerPath), place.ImportFolder!.ImportFolderLocation); await RemoveRecord(place); } @@ -824,7 +834,7 @@ public async Task RemoveAndDeleteFileWithOpenTransaction(ISession session, SVR_V return; } - if (deleteFolders) RecursiveDeleteEmptyDirectories(Path.GetDirectoryName(place.FullServerPath), place.ImportFolder.ImportFolderLocation); + if (deleteFolders) RecursiveDeleteEmptyDirectories(Path.GetDirectoryName(place.FullServerPath), place.ImportFolder!.ImportFolderLocation); await RemoveRecordWithOpenTransaction(session, place, seriesToUpdate, updateMyList); // For deletion of files from Trakt, we will rely on the Daily sync } @@ -910,7 +920,7 @@ await scheduler.StartJob(c => try { - ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder, place, v); + ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder!, place, v); } catch { @@ -939,7 +949,7 @@ await scheduler.StartJob(c => { try { - ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder, place, v); + ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder!, place, v); } catch { @@ -1005,7 +1015,7 @@ await scheduler.StartJob(c => try { - ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder, place, v); + ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder!, place, v); } catch { @@ -1026,7 +1036,7 @@ await scheduler.StartJob(c => { try { - ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder, place, v); + ShokoEventHandler.Instance.OnFileDeleted(place.ImportFolder!, place, v); } catch {