mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-30 19:00:20 -06:00
552 lines
20 KiB
C#
552 lines
20 KiB
C#
using System.ComponentModel;
|
|
using System.Xml;
|
|
using FileFlows.ComicNodes.Helpers;
|
|
using FileHelper = FileFlows.Plugin.Helpers.FileHelper;
|
|
|
|
namespace FileFlows.ComicNodes.Comics;
|
|
|
|
/// <summary>
|
|
/// Creates comic info xml for a file
|
|
/// </summary>
|
|
public class CreateComicInfo : Node
|
|
{
|
|
/// <inheritdoc />
|
|
public override int Inputs => 1;
|
|
|
|
/// <inheritdoc />
|
|
public override int Outputs => 2;
|
|
|
|
/// <inheritdoc />
|
|
public override FlowElementType Type => FlowElementType.Process;
|
|
|
|
/// <inheritdoc />
|
|
public override string Icon => "fas fa-user-secret";
|
|
|
|
/// <inheritdoc />
|
|
public override string HelpUrl => "https://fileflows.com/docs/plugins/comic-nodes/create-comic-info";
|
|
|
|
/// <summary>
|
|
/// Gets or sets if comics are put into publisher folders
|
|
/// </summary>
|
|
[Boolean(1)]
|
|
public bool Publisher { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets if the file should be renamed
|
|
/// </summary>
|
|
[Boolean(2)]
|
|
public bool RenameFile { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets how many digits the issues should use when renaming
|
|
/// </summary>
|
|
[NumberInt(3)]
|
|
[DefaultValue(3)]
|
|
[ConditionEquals(nameof(RenameFile), true)]
|
|
public int IssueDigits { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public override int Execute(NodeParameters args)
|
|
{
|
|
var localFileResult = args.FileService.GetLocalPath(args.WorkingFile);
|
|
if (localFileResult.Failed(out string error))
|
|
{
|
|
args.FailureReason = error;
|
|
args.Logger?.ELog(error);
|
|
return -1;
|
|
}
|
|
|
|
var localFile = localFileResult.Value;
|
|
|
|
var infoResult = GetInfo(args.Logger, args.LibraryFileName, args.LibraryPath, Publisher);
|
|
if (infoResult.Failed(out error))
|
|
{
|
|
args.FailureReason = error;
|
|
args.Logger?.ELog(error);
|
|
return -1;
|
|
}
|
|
|
|
var info = infoResult.Value;
|
|
args.Logger?.ILog("Got ComicInfo from filename");
|
|
args.Logger?.ILog("Series: " + info.Series);
|
|
if(info.Number != null)
|
|
args.Logger?.ILog("Issue: " + info.Number);
|
|
if(info.Volume != null)
|
|
args.Logger?.ILog("Volume: " + info.Volume);
|
|
if(string.IsNullOrWhiteSpace(info.Title) == false)
|
|
args.Logger?.ILog("Title: " + info.Title);
|
|
|
|
var newMetadata = new Dictionary<string, object?>
|
|
{
|
|
{ nameof(info.Title), info.Title },
|
|
{ nameof(info.Series), info.Series },
|
|
{ nameof(info.Publisher), info.Publisher },
|
|
{ nameof(info.Number), info.Number },
|
|
{ nameof(info.Count), info.Count },
|
|
{ nameof(info.Volume), info.Volume },
|
|
{ nameof(info.AlternateSeries), info.AlternateSeries },
|
|
{ nameof(info.AlternateNumber), info.AlternateNumber },
|
|
{ nameof(info.AlternateCount), info.AlternateCount },
|
|
{ nameof(info.Summary), info.Summary },
|
|
{ nameof(info.Notes), info.Notes },
|
|
{ nameof(info.ReleaseDate), info.ReleaseDate },
|
|
{ nameof(info.Tags), info.Tags?.Any() == true ? string.Join(", ", info.Tags) : null }
|
|
}.Where(x => x.Value != null)
|
|
.ToDictionary(x => x.Key, x => x.Value);
|
|
|
|
if (args.Metadata != null)
|
|
{
|
|
foreach (var key in args.Metadata.Keys)
|
|
{
|
|
if (newMetadata.ContainsKey(key))
|
|
continue;
|
|
newMetadata[key] = args.Metadata[key];
|
|
}
|
|
}
|
|
|
|
var result = args.ArchiveHelper.FileExists(localFile, "comicinfo.xml");
|
|
if (result.Failed(out error))
|
|
{
|
|
args.FailureReason = error;
|
|
args.Logger?.ELog(error);
|
|
return -1;
|
|
}
|
|
|
|
if (result.Value && RenameFile == false)
|
|
{
|
|
args.Logger?.ILog("comicinfo.xml already present");
|
|
return 2;
|
|
}
|
|
|
|
if (result.Value == false)
|
|
{
|
|
string xml = SerializeToXml(info);
|
|
if (string.IsNullOrWhiteSpace(xml))
|
|
{
|
|
args.FailureReason = "Failed to serialize to XML";
|
|
args.Logger?.ELog(args.FailureReason);
|
|
return -1;
|
|
}
|
|
|
|
args.Logger?.ILog("Created XML of ComicInfo");
|
|
|
|
string comicInfoFile = FileHelper.Combine(args.TempPath, "comicinfo.xml");
|
|
args.Logger?.ILog("comicinfo.xml created: " + comicInfoFile);
|
|
File.WriteAllText(comicInfoFile, xml);
|
|
|
|
if (args.ArchiveHelper.AddToArchive(localFile, comicInfoFile).Failed(out error))
|
|
{
|
|
args.FailureReason = error;
|
|
args.Logger?.ELog(error);
|
|
return -1;
|
|
}
|
|
|
|
args.Logger?.ILog("Added comicinfo.xml to archive");
|
|
if (localFile != args.WorkingFile &&
|
|
args.FileService.FileMove(localFile, args.WorkingFile).Failed(out error))
|
|
{
|
|
args.Logger?.ELog("Failed to move saved file: " + error);
|
|
}
|
|
}
|
|
|
|
if (RenameFile)
|
|
{
|
|
var newNameResult = GetNewName(info, FileHelper.GetExtension(args.WorkingFile), IssueDigits);
|
|
if (newNameResult.Failed(out error))
|
|
{
|
|
args.Logger?.WLog(error);
|
|
return 1;
|
|
}
|
|
string path = FileHelper.GetDirectory(args.WorkingFile);
|
|
string newFile = FileHelper.Combine(path, newNameResult.Value);
|
|
args.Logger?.ILog("New file name: " + newFile);
|
|
if (args.FileService.FileMove(args.WorkingFile, newFile).Failed(out error))
|
|
{
|
|
args.Logger?.WLog("Failed ot rename file: " + error);
|
|
}
|
|
|
|
args.SetWorkingFile(newFile);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the name of the book
|
|
/// </summary>
|
|
/// <param name="info">the comic info</param>
|
|
/// <param name="extension">the extension to use</param>
|
|
/// <param name="issueDigits">the number of digits to pad by</param>
|
|
/// <returns>the new name</returns>
|
|
internal static Result<string> GetNewName(ComicInfo info, string extension, int issueDigits)
|
|
{
|
|
if (info.Number == null && info.Volume?.StartsWith("Volume") == false)
|
|
return Result<string>.Fail("No issue number found, cannot rename");
|
|
if (string.IsNullOrWhiteSpace(info.Series))
|
|
return Result<string>.Fail("No series found, cannot rename");
|
|
|
|
string name = info.Series;
|
|
if (info.Number != null)
|
|
{
|
|
if (issueDigits > 0)
|
|
{
|
|
if (info.Volume == "Annual")
|
|
{
|
|
name += " - Annual " + info.Number;
|
|
}
|
|
else if (info.Number > 1960)
|
|
{
|
|
name += " - " + info.Number;
|
|
}
|
|
else
|
|
{
|
|
// Pad the number with leading zeros based on the specified number of digits
|
|
string paddedNumber = info.Number < 0 ?
|
|
("-" + info.Number.Value.ToString()[1..].PadLeft(issueDigits -1, '0')) :
|
|
info.Number.Value.ToString().PadLeft(issueDigits, '0');
|
|
|
|
// Add the padded number to the name
|
|
name += $" - #{paddedNumber}";
|
|
|
|
// Optionally add information about the count
|
|
if (info.Count > 0)
|
|
{
|
|
name += $" (of {info.Count})";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
name += $" - #{info.Number + (info.Count > 0 ? $" (of {info.Count})" : "")}";
|
|
|
|
}
|
|
else if(string.IsNullOrWhiteSpace(info.Volume) == false)
|
|
name += " - " + info.Volume;
|
|
|
|
if (string.IsNullOrWhiteSpace(info.Title) == false)
|
|
name += " - " + info.Title;
|
|
|
|
if(info.Tags?.Any() == true)
|
|
name += " " + string.Join(" ", info.Tags.Select(x => "(" + x + ")"));
|
|
|
|
return name += "." + extension.TrimStart('.');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the comic book info from its file name
|
|
/// </summary>
|
|
/// <param name="logger">the logger to log with</param>
|
|
/// <param name="libraryFile">the library filename to use</param>
|
|
/// <param name="libraryPath">the library path</param>
|
|
/// <param name="publisher">if there is a publisher folder</param>
|
|
/// <returns>the comic info</returns>
|
|
public static Result<ComicInfo> GetInfo(ILogger? logger, string libraryFile, string libraryPath, bool publisher)
|
|
{
|
|
// Publisher / Series (year?) / Title - #number (of #)- Issue Title.extension
|
|
ComicInfo info = new();
|
|
info.Series = FileHelper.GetDirectoryName(libraryFile);
|
|
if (info.Series.ToLowerInvariant() is "annuals" or "specials")
|
|
{
|
|
// go up one more directory
|
|
info.Series = FileHelper.GetDirectoryName(FileHelper.GetDirectory(libraryFile));
|
|
}
|
|
|
|
var yearMatch = Regex.Match(info.Series, @"\((?<year>(19|20)\d{2})\)");
|
|
int? year = null;
|
|
bool yearInFolder = yearMatch.Success;
|
|
|
|
if (yearInFolder)
|
|
{
|
|
year = int.Parse(yearMatch.Groups["year"].Value);
|
|
info.Volume = year.ToString();
|
|
// info.Series = Regex.Replace(info.Series, @"\((19|20)\d{2}\)", "").Trim();
|
|
}
|
|
|
|
string relative = libraryFile[(libraryPath.Length + 1)..];
|
|
|
|
info.Publisher = publisher ? relative.Replace("\\", "/").Split('/').First() : null;
|
|
string shortname = FileHelper.GetShortFileName(libraryFile);
|
|
info.Tags = GetTags(ref shortname);
|
|
shortname = shortname[..shortname.LastIndexOf('.')];
|
|
//(shortname, int? year2) = ExtractYear(shortname);
|
|
// year ??= year2;
|
|
// Title - #number (of #) - Issue Title
|
|
var ofMatch = Regex.Match(shortname, @"\(of\s+#?(\d+)\)");
|
|
if (ofMatch.Success)
|
|
{
|
|
// Extract the issue number
|
|
var ofNumber = int.Parse(ofMatch.Groups[1].Value);
|
|
// Remove the issue number from the string
|
|
shortname = ofMatch.Groups[0].Success
|
|
? shortname.Replace(ofMatch.Value, "").Trim()
|
|
: shortname;
|
|
|
|
// Use the extracted issue number as needed
|
|
info.Count = ofNumber;
|
|
}
|
|
|
|
var volMatch = Regex.Match(shortname, @"\b[Vv](?:olume|ol)?\s*(\d+)\b", RegexOptions.IgnoreCase);
|
|
if (volMatch.Success)
|
|
{
|
|
info.Volume = "Volume " + int.Parse(volMatch.Groups[1].Value);
|
|
// Remove the issue number from the string
|
|
shortname = volMatch.Groups[0].Success
|
|
? shortname.Replace(volMatch.Value, "").Trim()
|
|
: shortname;
|
|
|
|
}
|
|
//var issueNumberMatch2 = Regex.Match(shortname, @"(?<!['])[#]?\b(\-?\d{1,3})\b(?!\w)");
|
|
ExtractIssueNumber(info, ref shortname);
|
|
|
|
if (info.Number != null)
|
|
ExtractYear(ref shortname);
|
|
else
|
|
ExtractAnnualInfo(info, ref shortname);
|
|
|
|
|
|
// remove any junk
|
|
shortname = Regex.Replace(shortname, @"\(([\-]?\d+)\)", "$1").Trim();
|
|
shortname = Regex.Replace(shortname, @"\s*\([^)]*\)\s*", " ").Trim();
|
|
var parts = shortname.Split(" - ");
|
|
if (Regex.IsMatch(info.Series, "^(other|misc|miscellaneous|assorted)$",
|
|
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
|
|
info.Series = parts[0].Trim();
|
|
if (parts.Length > 1)
|
|
info.Title = parts.Last();
|
|
|
|
|
|
return info;
|
|
}
|
|
|
|
private static void ExtractIssueNumber(ComicInfo info, ref string shortname)
|
|
{
|
|
// Define the regex pattern to match numbers up to 3 characters long
|
|
string pattern = @"\b(\d{1,3})\b";
|
|
|
|
// Match against the shortname using the regex pattern
|
|
MatchCollection matches = Regex.Matches(shortname, pattern);
|
|
|
|
// Iterate through the matches
|
|
foreach (Match match in matches)
|
|
{
|
|
int issueNumber;
|
|
if (int.TryParse(match.Groups[1].Value, out issueNumber))
|
|
{
|
|
// Check if the match is preceded by a hyphen
|
|
int matchIndex = match.Index;
|
|
if (matchIndex > 0 && shortname[matchIndex - 1] == '-')
|
|
{
|
|
// Check if the hyphen is not part of a word and is preceded by specific characters
|
|
if (matchIndex == 1 || shortname[matchIndex - 2] is '#' or ' ' or '(' or '[' or '.' or '_' or '-')
|
|
{
|
|
// Remove the issue number from the string
|
|
int length = match.Length;
|
|
if (shortname[matchIndex - 1] == '#')
|
|
{
|
|
matchIndex--;
|
|
length++;
|
|
}
|
|
shortname = shortname.Remove(matchIndex - 1, length).Trim();
|
|
|
|
// Use the extracted issue number as needed
|
|
info.Number = -issueNumber;
|
|
return; // Exit the loop after finding the issue number
|
|
}
|
|
}
|
|
else if (matchIndex == 0 || shortname[matchIndex - 1] is '#' or ' ' or '(' or '[' or '.' or '_' or '-')
|
|
{
|
|
// Remove the issue number from the string
|
|
int length = match.Length;
|
|
if (shortname[matchIndex - 1] == '#')
|
|
{
|
|
matchIndex--;
|
|
length++;
|
|
}
|
|
|
|
shortname = shortname.Remove(matchIndex, length).Trim();
|
|
|
|
// Use the extracted issue number as needed
|
|
info.Number = issueNumber;
|
|
return; // Exit the loop after finding the issue number
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private static void ExtractYear(ref string shortname)
|
|
{
|
|
var yearMatches = Regex.Matches(shortname, @"\b(?:\((19|20)\d{2}\)|\b(19|20)\d{2}\b)\b");
|
|
// Iterate over the matches in reverse order
|
|
for (int i = yearMatches.Count - 1; i >= 0; i--)
|
|
{
|
|
var match = yearMatches[i];
|
|
// Remove the year from the shortname string
|
|
shortname = shortname.Remove(match.Index, match.Length).Trim();
|
|
}
|
|
}
|
|
|
|
private static void ExtractAnnualInfo(ComicInfo info, ref string shortname)
|
|
{
|
|
int year;
|
|
// Match the "Annual" pattern followed by a year in various formats
|
|
var annualMatch = Regex.Match(shortname, @"\bAnnual\s*(?:['#]?\s*-?\s*)?(\d{2}|\d{4})\b");
|
|
if (annualMatch.Success == false)
|
|
{
|
|
var yearMatches = Regex.Matches(shortname, @"\b(?:\((19|20)\d{2}\)|\b(19|20)\d{2}\b)\b");
|
|
if (yearMatches.Count > 0)
|
|
{
|
|
// Extract the matched year
|
|
var lastMatch = yearMatches.Cast<Match>().Last();
|
|
year = int.Parse(lastMatch.Value.Trim('(', ')'));
|
|
if (info.Series?.Contains(year.ToString()) == true)
|
|
return;
|
|
|
|
info.Number = year;
|
|
info.Volume = "Annual";
|
|
// Remove the year from the shortname string
|
|
shortname = shortname.Remove(lastMatch.Index, lastMatch.Length).Trim().Replace("()", string.Empty);
|
|
}
|
|
return;
|
|
}
|
|
|
|
string yearString = annualMatch.Groups[1].Value;
|
|
|
|
if (yearString.Length == 2) // Two-digit year
|
|
{
|
|
year = int.Parse(yearString);
|
|
year += year >= 40 ? 1900 : 2000;
|
|
info.Number = year;
|
|
}
|
|
else if (yearString.Length == 4) // Four-digit year
|
|
{
|
|
year = int.Parse(yearString);
|
|
info.Number = year;
|
|
}
|
|
|
|
info.Volume = "Annual";
|
|
|
|
// Remove "Annual + year" from the shortname string
|
|
shortname = shortname.Replace(annualMatch.Value, "").Trim();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the year from a file
|
|
/// </summary>
|
|
/// <param name="shortname">the name of the file</param>
|
|
/// <returns>the new name and the year if found</returns>
|
|
private static (string Name, int? Year) ExtractYear(string shortname)
|
|
{
|
|
int? year = null;
|
|
|
|
// Regular expression to match the year in the specified formats
|
|
Match match = Regex.Match(shortname, @"(?:(?:19|20)\d{2})|\((?:19|20)\d{2}\)|-(?:\s)?(?:19|20)\d{2}(?:\s)?-");
|
|
|
|
if (match.Success)
|
|
{
|
|
// Extract the matched year
|
|
year = int.Parse(match.Value.Trim('(', ')', '-', ' '));
|
|
|
|
// Remove the matched year from the input string
|
|
shortname = shortname.Remove(match.Index, match.Length).Trim();
|
|
}
|
|
else
|
|
{
|
|
match = Regex.Match(shortname, @"'(\d{2})\b");
|
|
|
|
if (match.Success)
|
|
{
|
|
// Extract the matched year
|
|
int number = int.Parse(match.Groups[1].Value);
|
|
// Determine the year
|
|
year = number > 50 ? 1900 + number : 2000 + number;
|
|
// Remove the matched year from the input string
|
|
shortname = shortname.Remove(match.Index, match.Length).Trim();
|
|
}
|
|
}
|
|
|
|
return (shortname, year);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets the tag from the filename
|
|
/// </summary>
|
|
/// <param name="input">the input filename</param>
|
|
/// <returns>the tags</returns>
|
|
static string[] GetTags(ref string input)
|
|
{
|
|
List<string> extracted = new List<string>();
|
|
string pattern = @"\[(.*?)\]";
|
|
|
|
MatchCollection matches = Regex.Matches(input, pattern);
|
|
foreach (Match match in matches)
|
|
{
|
|
string extractedString = match.Groups[1].Value;
|
|
extracted.Add(extractedString);
|
|
}
|
|
|
|
// Remove the substrings inside square brackets
|
|
input = Regex.Replace(input, pattern, "").Trim();
|
|
|
|
return extracted.ToArray();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Serializes a ComicInfo object to XML according to a specific schema.
|
|
/// </summary>
|
|
/// <param name="comicInfo">The ComicInfo object to serialize.</param>
|
|
public static string SerializeToXml(ComicInfo comicInfo)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
using var writer = XmlWriter.Create(stream, new()
|
|
{
|
|
Indent = true,
|
|
Encoding = Encoding.UTF8
|
|
});
|
|
writer.WriteStartDocument();
|
|
writer.WriteStartElement("ComicInfo", "http://www.w3.org/2001/XMLSchema");
|
|
|
|
WriteXmlElement(writer, "Title", comicInfo.Title);
|
|
WriteXmlElement(writer, "Series", comicInfo.Series);
|
|
WriteXmlElement(writer, "Number", comicInfo.Number);
|
|
WriteXmlElement(writer, "Count", comicInfo.Count);
|
|
WriteXmlElement(writer, "Volume", comicInfo.Volume);
|
|
WriteXmlElement(writer, "AlternateSeries", comicInfo.AlternateSeries);
|
|
WriteXmlElement(writer, "AlternateNumber", comicInfo.AlternateNumber);
|
|
WriteXmlElement(writer, "AlternateCount", comicInfo.AlternateCount);
|
|
WriteXmlElement(writer, "Summary", comicInfo.Summary);
|
|
WriteXmlElement(writer, "Notes", comicInfo.Notes);
|
|
if (comicInfo.Tags?.Any() == true)
|
|
WriteXmlElement(writer, "Tags", string.Join(",", comicInfo.Tags));
|
|
|
|
if (comicInfo.ReleaseDate != null)
|
|
{
|
|
WriteXmlElement(writer, "Year", comicInfo.ReleaseDate.Value.Year.ToString());
|
|
WriteXmlElement(writer, "Month", comicInfo.ReleaseDate.Value.Month.ToString());
|
|
WriteXmlElement(writer, "Day", comicInfo.ReleaseDate.Value.Day.ToString());
|
|
}
|
|
|
|
writer.WriteEndElement(); // Close ComicInfo
|
|
writer.WriteEndDocument();
|
|
writer.Flush();
|
|
|
|
return Encoding.UTF8.GetString(stream.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an XML element to the XML writer with proper encoding.
|
|
/// </summary>
|
|
/// <param name="writer">The XML writer.</param>
|
|
/// <param name="name">The name of the XML element.</param>
|
|
/// <param name="value">The value of the XML element.</param>
|
|
static void WriteXmlElement(XmlWriter writer, string name, object? value)
|
|
{
|
|
if (value == null)
|
|
return;
|
|
|
|
writer.WriteStartElement(name);
|
|
writer.WriteValue(value);
|
|
writer.WriteEndElement();
|
|
}
|
|
} |