Skip to content

Commit

Permalink
Merge pull request #1220 from Mik1ll/web-aom-refactor
Browse files Browse the repository at this point in the history
Move existing series relocation logic to RelocationService
  • Loading branch information
ElementalCrisis authored Feb 3, 2025
2 parents a54241d + afcd85f commit 63c0576
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 156 deletions.
48 changes: 27 additions & 21 deletions Shoko.Plugin.Abstractions/PluginUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace Shoko.Plugin.Abstractions;

Expand All @@ -7,23 +8,33 @@ namespace Shoko.Plugin.Abstractions;
/// </summary>
public static class PluginUtilities
{
/// <summary>
/// The mapping used for replacing invalid path characters with unicode alternatives
/// </summary>
public static Dictionary<string, string> InvalidPathCharacterMap = new()
{
{ "*", "\u2605" }, // ★ (BLACK STAR)
{ "|", "\u00a6" }, // ¦ (BROKEN BAR)
{ "\\", "\u29F9" }, // ⧹ (BIG REVERSE SOLIDUS)
{ "/", "\u2044" }, // ⁄ (FRACTION SLASH)
{ ":", "\u0589" }, // ։ (ARMENIAN FULL STOP)
{ "\"", "\u2033" }, // ″ (DOUBLE PRIME)
{ ">", "\u203a" }, // › (SINGLE RIGHT-POINTING ANGLE QUOTATION MARK)
{ "<", "\u2039" }, // ‹ (SINGLE LEFT-POINTING ANGLE QUOTATION MARK)
{ "?", "\uff1f" }, // ? (FULL WIDTH QUESTION MARK)
};

/// <summary>
/// Remove invalid path characters.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The sanitized path.</returns>
public static string RemoveInvalidPathCharacters(this string path)
{
var ret = path.Replace(@"*", string.Empty);
ret = ret.Replace(@"|", string.Empty);
ret = ret.Replace(@"\", string.Empty);
ret = ret.Replace(@"/", string.Empty);
ret = ret.Replace(@":", string.Empty);
ret = ret.Replace("\"", string.Empty); // double quote
ret = ret.Replace(@">", string.Empty);
ret = ret.Replace(@"<", string.Empty);
ret = ret.Replace(@"?", string.Empty);
while (ret.EndsWith("."))
var ret = path;
foreach (var key in InvalidPathCharacterMap.Keys)
ret = ret.Replace(key, string.Empty);
while (ret.EndsWith(".", StringComparison.Ordinal))
ret = ret.Substring(0, ret.Length - 1);
return ret.Trim();
}
Expand All @@ -35,17 +46,12 @@ public static string RemoveInvalidPathCharacters(this string path)
/// <returns>The sanitized path.</returns>
public static string ReplaceInvalidPathCharacters(this string path)
{
var ret = path.Replace(@"*", "\u2605"); // ★ (BLACK STAR)
ret = ret.Replace(@"|", "\u00a6"); // ¦ (BROKEN BAR)
ret = ret.Replace(@"\", "\u29F9"); // ⧹ (BIG REVERSE SOLIDUS)
ret = ret.Replace(@"/", "\u2044"); // ⁄ (FRACTION SLASH)
ret = ret.Replace(@":", "\u0589"); // ։ (ARMENIAN FULL STOP)
ret = ret.Replace("\"", "\u2033"); // ″ (DOUBLE PRIME)
ret = ret.Replace(@">", "\u203a"); // › (SINGLE RIGHT-POINTING ANGLE QUOTATION MARK)
ret = ret.Replace(@"<", "\u2039"); // ‹ (SINGLE LEFT-POINTING ANGLE QUOTATION MARK)
ret = ret.Replace(@"?", "\uff1f"); // ? (FULL WIDTH QUESTION MARK)
ret = ret.Replace(@"...", "\u2026"); // … (HORIZONTAL ELLIPSIS)
if (ret.StartsWith(".", StringComparison.Ordinal)) ret = "․" + ret.Substring(1, ret.Length - 1);
var ret = path;
foreach (var kvp in InvalidPathCharacterMap)
ret = ret.Replace(kvp.Key, kvp.Value);
ret = ret.Replace("...", "\u2026"); // … (HORIZONTAL ELLIPSIS))
if (ret.StartsWith(".", StringComparison.Ordinal)) // U+002E
ret = "․" + ret.Substring(1, ret.Length - 1); // U+2024
if (ret.EndsWith(".", StringComparison.Ordinal)) // U+002E
ret = ret.Substring(0, ret.Length - 1) + "․"; // U+2024
return ret.Trim();
Expand Down
16 changes: 13 additions & 3 deletions Shoko.Plugin.Abstractions/Services/IRelocationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,27 @@ namespace Shoko.Plugin.Abstractions.Services;
public interface IRelocationService
{
/// <summary>
/// Get the first destination with enough space for the given file, if any
/// Get the first destination with enough space for the given file, if any.
/// </summary>
/// <param name="args">The relocation event args</param>
/// <param name="args">The relocation event args.</param>
/// <returns></returns>
IImportFolder? GetFirstDestinationWithSpace(RelocationEventArgs args);

/// <summary>
/// Check if the given import folder has enough space for the given file.
/// </summary>
/// <param name="folder">The import folder</param>
/// <param name="folder">The import folder.</param>
/// <param name="file">The file</param>
/// <returns></returns>
bool ImportFolderHasSpace(IImportFolder folder, IVideoFile file);

/// <summary>
/// Get the location of the folder that contains a file for the latest (airdate) episode in the current collection.
/// </summary>
/// <remarks>
/// Will only look for files in import folders of type <see cref="DropFolderType.Excluded"/> or <see cref="DropFolderType.Destination"/>.
/// </remarks>
/// <param name="args">The relocation event args.</param>
/// <returns></returns>
public (IImportFolder ImportFolder, string RelativePath)? GetExistingSeriesLocationWithSpace(RelocationEventArgs args);
}
51 changes: 51 additions & 0 deletions Shoko.Server/Renamer/RelocationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,55 @@ public bool ImportFolderHasSpace(IImportFolder folder, IVideoFile file)
{
return folder.ID == file.ImportFolderID || folder.AvailableFreeSpace >= file.Size;
}

public (IImportFolder ImportFolder, string RelativePath)? GetExistingSeriesLocationWithSpace(RelocationEventArgs args)
{
var series = args.Series.Select(s => s.AnidbAnime).FirstOrDefault();
if (series is null)
return null;

// sort the episodes by air date, so that we will move the file to the location of the latest episode
var allEps = series.Episodes
.OrderByDescending(a => a.AirDate ?? DateTime.MinValue)

Check failure on line 45 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build Tray Service — Installer (Daily) (8.x)

The name 'DateTime' does not exist in the current context

Check failure on line 45 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build CLI — Standalone linux-x64 (Daily)

The name 'DateTime' does not exist in the current context

Check failure on line 45 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build Tray Service — Framework (Daily)

The name 'DateTime' does not exist in the current context

Check failure on line 45 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build CLI — Standalone linux-arm64 (Daily)

The name 'DateTime' does not exist in the current context

Check failure on line 45 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build CLI — Framework linux-x64 (Daily)

The name 'DateTime' does not exist in the current context
.ToList();

var skipDiskSpaceChecks = _settingsProvider.GetSettings().Import.SkipDiskSpaceChecks;
foreach (var ep in allEps)
{
var videoList = ep.VideoList;
// check if this episode belongs to more than one anime
// if it does, we will ignore it
if (videoList.SelectMany(v => v.Series).DistinctBy(s => s.AnidbAnimeID).Count() > 1)
continue;

foreach (var vid in videoList)
{
if (vid.Hashes.ED2K == args.File.Video.Hashes.ED2K) continue;

var place = vid.Locations.FirstOrDefault(b =>
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
b.ImportFolder is not null &&
!b.ImportFolder.DropFolderType.HasFlag(DropFolderType.Source) &&
!string.IsNullOrWhiteSpace(b.RelativePath));
if (place is null) continue;

var placeFld = place.ImportFolder;

// check space
if (!skipDiskSpaceChecks && !ImportFolderHasSpace(placeFld, args.File))
continue;

var placeDir = Path.GetDirectoryName(place.Path);
if (placeDir is null)
continue;
// ensure we aren't moving to the current directory
if (placeDir.Equals(Path.GetDirectoryName(args.File.Path), StringComparison.InvariantCultureIgnoreCase))

Check failure on line 78 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build Tray Service — Installer (Daily) (8.x)

The name 'StringComparison' does not exist in the current context

Check failure on line 78 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build CLI — Standalone linux-x64 (Daily)

The name 'StringComparison' does not exist in the current context

Check failure on line 78 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build Tray Service — Framework (Daily)

The name 'StringComparison' does not exist in the current context

Check failure on line 78 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build CLI — Standalone linux-arm64 (Daily)

The name 'StringComparison' does not exist in the current context

Check failure on line 78 in Shoko.Server/Renamer/RelocationService.cs

View workflow job for this annotation

GitHub Actions / Build CLI — Framework linux-x64 (Daily)

The name 'StringComparison' does not exist in the current context
continue;

return (placeFld, Path.GetRelativePath(placeFld.Path, placeDir));
}
}

return null;
}
}
131 changes: 24 additions & 107 deletions Shoko.Server/Renamer/WebAOMRenamer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
using Shoko.Server.Models;
using Shoko.Server.Repositories;
using Shoko.Server.Server;
using Shoko.Server.Utilities;
using EpisodeType = Shoko.Models.Enums.EpisodeType;
using ISettingsProvider = Shoko.Server.Settings.ISettingsProvider;

namespace Shoko.Server.Renamer;

Expand All @@ -23,13 +21,11 @@ public class WebAOMRenamer : IRenamer<WebAOMSettings>
{
private const string RENAMER_ID = "WebAOM";
private readonly ILogger<WebAOMRenamer> _logger;
private readonly ISettingsProvider _settingsProvider;
private readonly IRelocationService _relocationService;

public WebAOMRenamer(ILogger<WebAOMRenamer> logger, ISettingsProvider settingsProviderProvider, IRelocationService relocationService)
public WebAOMRenamer(ILogger<WebAOMRenamer> logger, IRelocationService relocationService)
{
_logger = logger;
_settingsProvider = settingsProviderProvider;
_relocationService = relocationService;
}

Expand Down Expand Up @@ -490,7 +486,7 @@ private bool EvaluateTestW(string test, SVR_VideoLocal vid)
return false;
}

var width = GetVideoWidth(vid.VideoResolution);
var width = SplitVideoResolution(vid.VideoResolution).Width;

var hasFileVersionOperator = greaterThan | greaterThanEqual | lessThan | lessThanEqual;

Expand Down Expand Up @@ -528,21 +524,6 @@ private bool EvaluateTestW(string test, SVR_VideoLocal vid)
}
}

private static int GetVideoWidth(string videoResolution)
{
var videoWidth = 0;
if (videoResolution.Trim().Length > 0)
{
var dimensions = videoResolution.Split('x');
if (dimensions.Length > 0)
{
int.TryParse(dimensions[0], out videoWidth);
}
}

return videoWidth;
}

private bool EvaluateTestU(string test, SVR_VideoLocal vid)
{
try
Expand All @@ -560,7 +541,7 @@ private bool EvaluateTestU(string test, SVR_VideoLocal vid)
return false;
}

var height = GetVideoHeight(vid.VideoResolution);
var height = SplitVideoResolution(vid.VideoResolution).Height;

var hasFileVersionOperator = greaterThan | greaterThanEqual | lessThan | lessThanEqual;

Expand Down Expand Up @@ -598,20 +579,6 @@ private bool EvaluateTestU(string test, SVR_VideoLocal vid)
}
}

private static int GetVideoHeight(string videoResolution)
{
var videoHeight = 0;
if (videoResolution.Trim().Length > 0)
{
var dimensions = videoResolution.Split('x');
if (dimensions.Length > 1)
{
int.TryParse(dimensions[1], out videoHeight);
}
}

return videoHeight;
}

private bool EvaluateTestR(string test, SVR_AniDB_File aniFile)
{
Expand Down Expand Up @@ -1498,7 +1465,7 @@ private bool EvaluateTestI(string test, SVR_VideoLocal vid, SVR_AniDB_File aniFi
if (string.IsNullOrEmpty(ext)) return (false, "Unable to get the file's extension"); // fail if we get a blank extension, something went wrong.

// finally add back the extension
return (true, Utils.ReplaceInvalidFolderNameCharacters($"{newFileName.Replace("`", "'")}{ext}"));
return (true, $"{newFileName.Replace("`", "'")}{ext}".ReplaceInvalidPathCharacters());
}

private (bool, string) PerformActionOnFileName(string newFileName, string action, WebAOMSettings settings, SVR_VideoLocal vid, SVR_AniDB_File aniFile, List<SVR_AniDB_Episode> episodes, SVR_AniDB_Anime anime)
Expand Down Expand Up @@ -2183,7 +2150,7 @@ private bool EvaluateTest(char testChar, string testCondition, SVR_VideoLocal vi
}
else
{
var groupName = Utils.ReplaceInvalidFolderNameCharacters(group.PreferredTitle);
var groupName = group.PreferredTitle.ReplaceInvalidPathCharacters();
path = Path.Combine(groupName, name);
}

Expand All @@ -2207,85 +2174,35 @@ private static bool ValidDestinationFolder(IImportFolder dest)

private (IImportFolder dest, string folder) GetFlatFolderDestination(RelocationEventArgs args)
{
// TODO make this only dependent on PluginAbstractions
var destFolder = _relocationService.GetFirstDestinationWithSpace(args);

var xrefs = args.Episodes;
if (xrefs.Count == 0)
{
return (null, "No xrefs");
}

var xref = xrefs.FirstOrDefault();
if (xref == null)
{
return (null, "No xrefs");
}
if (_relocationService.GetExistingSeriesLocationWithSpace(args) is { } existingSeriesLocation)
return existingSeriesLocation;

// find the series associated with this episode
if (xref.Series is not SVR_AnimeSeries series)
if (_relocationService.GetFirstDestinationWithSpace(args) is { } firstDestinationWithSpace)
{
return (null, "Series not Found");
var series = args.Series.Select(s => s.AnidbAnime).FirstOrDefault();
if (series is null)
return (null, "Series not found");
return (firstDestinationWithSpace, series.PreferredTitle.ReplaceInvalidPathCharacters());
}

// TODO move this into the RelocationService
// sort the episodes by air date, so that we will move the file to the location of the latest episode
var allEps = series.AllAnimeEpisodes
.OrderByDescending(a => a.AniDB_Episode?.AirDate ?? 0)
.ToList();
return (null, "Unable to resolve a destination");
}

foreach (var ep in allEps)
private static (int Width, int Height) SplitVideoResolution(string resolution)
{
var videoWidth = 0;
var videoHeight = 0;
if (resolution.Trim().Length > 0)
{
// check if this episode belongs to more than one anime
// if it does, we will ignore it
var fileEpXrefs =
RepoFactory.CrossRef_File_Episode.GetByEpisodeID(ep.AniDB_EpisodeID);
int? animeID = null;
var crossOver = false;
foreach (var fileEpXref in fileEpXrefs)
{
if (!animeID.HasValue) animeID = fileEpXref.AnimeID;
else if (animeID.Value != fileEpXref.AnimeID) crossOver = true;
}

if (crossOver) continue;

var settings = _settingsProvider.GetSettings();
foreach (var vid in ep.VideoLocals.Where(a => a.Places.Any(b => b.ImportFolder.IsDropSource == 0)).ToList())
var dimensions = resolution.Split('x');
if (dimensions.Length > 1)
{
if (vid.Hash == args.File.Video.Hashes.ED2K) continue;

var place = vid.Places.FirstOrDefault();
var thisFileName = place?.FilePath;
if (thisFileName == null) continue;

var folderName = Path.GetDirectoryName(thisFileName);

var dstImportFolder = place.ImportFolder;
if (dstImportFolder == null) continue;

// check space
if (!settings.Import.SkipDiskSpaceChecks && !_relocationService.ImportFolderHasSpace(dstImportFolder, args.File))
continue;

if (!Directory.Exists(Path.Combine(place.ImportFolder.ImportFolderLocation, folderName!))) continue;

// ensure we aren't moving to the current directory
if (Path.Combine(place.ImportFolder.ImportFolderLocation, folderName).Equals(Path.GetDirectoryName(args.File.Path), StringComparison.InvariantCultureIgnoreCase))
continue;

destFolder = place.ImportFolder;

return (destFolder, folderName);
int.TryParse(dimensions[0], out videoWidth);
int.TryParse(dimensions[1], out videoHeight);
}
}

if (destFolder == null)
{
return (null, "Unable to resolve a destination");
}

return (destFolder, Utils.ReplaceInvalidFolderNameCharacters(series.PreferredTitle));
return (videoWidth, videoHeight);
}

public WebAOMSettings DefaultSettings => new()
Expand Down
Loading

0 comments on commit 63c0576

Please sign in to comment.