FF-1256 - added nfo file creator

This commit is contained in:
John Andrews
2024-02-13 16:21:04 +13:00
parent daf857ed7c
commit 8be2643c90
7 changed files with 445 additions and 5 deletions

View File

@@ -3,6 +3,9 @@
internal class Globals
{
public static string MOVIE_INFO = "MovieInfo";
public static string MOVIE = "Movie";
public static string MOVIE_CREDITS = "MovieCredits";
public static string TV_SHOW_INFO = "TVShowInfo";
public static string TV_EPISODE_INFO = "TVEpisodeInfo";
}
}

View File

@@ -26,6 +26,19 @@
"MusicMeta": {
"Description": "Loads the metadata of a music file into the flow variables."
},
"NfoFileCreator": {
"Description": "Creates a Kodi NFO file from previously loaded metadata.",
"Fields": {
"DestinationPath": "Destination Folder",
"DestinationPath-Help": "The folder where the NFO file will be created in.\nIf empty will be created in the same directory as the original file.",
"DestinationFile": "Destination File",
"DestinationFile-Help": "The filename of the new NFO file. If empty, the original filename will be used with the extension changed to `.nfo`"
},
"Outputs": {
"1": "NFO File created",
"2": "NFO failed to be created"
}
},
"TVShowLookup": {
"Description": "Performs a search on TheMovieDB.org for a TV Show.\nStores the Metadata inside the variable 'TVShowInfo'.",
"Outputs": {

View File

@@ -2,6 +2,7 @@
using DM.MovieApi;
using DM.MovieApi.MovieDb.Movies;
using FileFlows.Plugin;
using MetaNodes.TheMovieDb;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -181,7 +182,9 @@ public class MovieLookupTests
{
MovieDbFactory.RegisterSettings(MovieLookup.MovieDbBearerToken);
var movieApi = MovieDbFactory.Create<IApiMovieRequest>().Value;
var md = MovieLookup.GetVideoMetadata(movieApi, 414906, @"D:\videos\temp");
var args = new FileFlows.Plugin.NodeParameters(@"/test/Ghostbusters 1984.mkv", new TestLogger(), false, string.Empty, null);;
var md = MovieLookup.GetVideoMetadata(args, movieApi, 414906, @"D:\videos\temp");
Assert.IsNotNull(md);
string json = System.Text.Json.JsonSerializer.Serialize(md, new System.Text.Json.JsonSerializerOptions
{
@@ -189,6 +192,31 @@ public class MovieLookupTests
});
File.WriteAllText(@"D:\videos\metadata.json", json);
}
[TestMethod]
public void MovieLookupTests_WonderWoman_Nfo()
{
var logger = new TestLogger();
var args = new NodeParameters(@"/test/Wonder.Woman.1984.2020.German.DL.AC3.1080p.BluRay.x265-Fun{{fdg$ERGESDG32fesdfgds}}/Wonder.Woman.1984.2020.German.DL.AC3.1080p.BluRay.x265-Fun{{fdg$ERGESDG32fesdfgds}}.mkv",
logger, false, string.Empty, null);;
MovieLookup ml = new MovieLookup();
ml.UseFolderName = false;
var result = ml.Execute(args);
Assert.AreEqual(1, result);
Assert.IsTrue(args.Parameters.ContainsKey(Globals.MOVIE_INFO));
var mi = args.Parameters[Globals.MOVIE_INFO] as MovieInfo;
Assert.IsNotNull(mi);
Assert.AreEqual("Wonder Woman 1984", mi.Title);
Assert.AreEqual(2020, mi.ReleaseDate.Year);
var eleNfo = new NfoFileCreator();
result = eleNfo.Execute(args);
Assert.AreEqual(1, result);
string nfo = (string)args.Variables["NFO"];
Assert.IsFalse(string.IsNullOrWhiteSpace(nfo));
}
}
#endif

View File

@@ -1,5 +1,6 @@
#if(DEBUG)
using System.Diagnostics.CodeAnalysis;
using DM.MovieApi.MovieDb.Movies;
using DM.MovieApi.MovieDb.TV;
using MetaNodes.TheMovieDb;
@@ -102,6 +103,47 @@ public class TVEpisodeLookupTests
Assert.AreEqual("The Batman/Superman Story (1)", args.Variables["tvepisode.Subtitle"]);
Assert.IsFalse(string.IsNullOrWhiteSpace(args.Variables["tvepisode.Overview"] as string));
}
[TestMethod]
public void TheBatman_2x03_nfo()
{
var logger = new TestLogger();
var args = new FileFlows.Plugin.NodeParameters("/test/tv/The Batman/Season 2/The Batman - 2x03.mkv", logger, false, string.Empty, null);
var element = new TVEpisodeLookup();
var result = element.Execute(args);
Assert.AreEqual(1, result);
var eleNfo = new NfoFileCreator();
result = eleNfo.Execute(args);
Assert.AreEqual(1, result);
TVShowInfo tvShowInfo = (TVShowInfo)args.Variables[Globals.TV_SHOW_INFO];
Episode epInfo = (Episode)args.Variables[Globals.TV_EPISODE_INFO];
string nfo = eleNfo.CreateTvShowNfo(args, tvShowInfo, epInfo);
Assert.IsNotNull(nfo);
}
[TestMethod]
public void TheBatman_s5e1_2_3_Nfo()
{
var logger = new TestLogger();
var args = new FileFlows.Plugin.NodeParameters("/test/tv/The Batman/Season 5/The Batman - s5e1-3.mkv", logger, false, string.Empty, null);
var element = new TVEpisodeLookup();
var result = element.Execute(args);
Assert.AreEqual(1, result);
var eleNfo = new NfoFileCreator();
result = eleNfo.Execute(args);
Assert.AreEqual(1, result);
string nfo = (string)args.Variables["NFO"];
}
}

View File

@@ -81,7 +81,7 @@ public class MovieLookup : Node
}
// remove double spaces in case they were added when removing the year
while (lookupName.IndexOf(" ") > 0)
while (lookupName.IndexOf(" ", StringComparison.Ordinal) > 0)
lookupName = lookupName.Replace(" ", " ");
args.Logger?.ILog("Lookup name: " + lookupName);
@@ -145,14 +145,18 @@ public class MovieLookup : Node
args.SetParameter(Globals.MOVIE_INFO, result);
Variables["movie.Title"] = result.Title;
Variables["movie.Year"] = result.ReleaseDate.Year;
var meta = GetVideoMetadata(movieApi, result.Id, args.TempPath);
var meta = GetVideoMetadata(args, movieApi, result.Id, args.TempPath);
Variables["VideoMetadata"] = meta;
if (string.IsNullOrWhiteSpace(meta.OriginalLanguage) == false)
Variables["OriginalLanguage"] = meta.OriginalLanguage;
Variables[Globals.MOVIE_INFO] = result;
var movie = movieApi.FindByIdAsync(result.Id).Result.Item;
if(movie != null)
Variables[Globals.MOVIE] = movie;
args.UpdateVariables(Variables);
return 1;
@@ -167,7 +171,7 @@ public class MovieLookup : Node
/// <param name="id">the ID of the movie</param>
/// <param name="tempPath">the temp path to save any images to</param>
/// <returns>the VideoMetadata</returns>
internal static VideoMetadata GetVideoMetadata(IApiMovieRequest movieApi, int id, string tempPath)
internal static VideoMetadata GetVideoMetadata(NodeParameters args, IApiMovieRequest movieApi, int id, string tempPath)
{
var movie = movieApi.FindByIdAsync(id).Result?.Item;
if (movie == null)
@@ -202,8 +206,9 @@ public class MovieLookup : Node
if(credits != null)
{
args.Variables[Globals.MOVIE_CREDITS] = credits;
md.Actors = credits.CastMembers?.Select(x => x.Name)?.ToList();
md.Writers = credits.CrewMembers?.Where(x => x.Job == "Writer" || x.Job == "Screenplay") ?.Select(x => x.Name)?.ToList();
md.Writers = credits.CrewMembers?.Where(x => x.Department == "Writing" || x.Job == "Writer" || x.Job == "Screenplay") ?.Select(x => x.Name)?.ToList();
md.Directors = credits.CrewMembers?.Where(x => x.Job == "Director")?.Select(x => x.Name)?.ToList();
md.Producers = credits.CrewMembers?.Where(x => x.Job == "Producer")?.Select(x => x.Name)?.ToList();
}

View File

@@ -0,0 +1,335 @@
using System.Text;
using System.Xml;
using DM.MovieApi.MovieDb.Movies;
using DM.MovieApi.MovieDb.TV;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using FileFlows.Plugin.Helpers;
namespace MetaNodes.TheMovieDb;
/// <summary>
/// A flow element that will create a NFO file for a video once the Movie or TV Show info has been read
/// </summary>
public class NfoFileCreator : Node
{
/// <summary>
/// Gets the number of inputs
/// </summary>
public override int Inputs => 1;
/// <summary>
/// Gets the number of outputs
/// </summary>
public override int Outputs => 2;
/// <summary>
/// Gets the flow element type
/// </summary>
public override FlowElementType Type => FlowElementType.Process;
/// <summary>
/// Gets the help URL
/// </summary>
public override string HelpUrl => "https://fileflows.com/docs/plugins/meta-nodes/nfo-file-creator";
/// <summary>
/// Gets the icon
/// </summary>
public override string Icon => "fas fa-file-code";
private const string TAB = " ";
private string _DestinationPath = string.Empty;
private string _DestinationFile = string.Empty;
/// <summary>
/// Gets or sets the destination path for zipping.
/// </summary>
[Folder(1)]
public string DestinationPath
{
get => _DestinationPath;
set { _DestinationPath = value ?? ""; }
}
/// <summary>
/// Gets or sets the destination file name for zipping.
/// </summary>
[TextVariable(2)]
public string DestinationFile
{
get => _DestinationFile;
set { _DestinationFile = value ?? ""; }
}
/// <summary>
/// Executes the flow element
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public override int Execute(NodeParameters args)
{
string nfoXml = null;
if (args.Variables.TryGetValue(Globals.TV_SHOW_INFO, out object oTvShowInfo) &&
oTvShowInfo is TVShowInfo tvShowInfo)
{
if (args.Variables.TryGetValue(Globals.TV_EPISODE_INFO, out object oTvEpisodeInfo) &&
oTvEpisodeInfo is Episode epInfo)
{
args.Logger?.ILog("TVEpisodeInfo found");
nfoXml = CreateTvShowNfo(args, tvShowInfo, epInfo);
// look for more episodes
for (int i = 1; 1 <= 100; i++)
{
if (args.Variables.TryGetValue(Globals.TV_EPISODE_INFO + "_" + i, out object oOther) == false)
break;
if (oOther is Episode epOther == false)
break;
nfoXml += CreateTvShowNfo(args, tvShowInfo, epOther);
}
}
}
else if (args.Variables.TryGetValue(Globals.MOVIE, out object oMovieInfo) &&
oMovieInfo is Movie movie)
{
args.Logger?.ILog("MovieInformation found");
nfoXml = CreateMovieNfo(args, movie);
}
if (string.IsNullOrWhiteSpace(nfoXml))
{
args.Logger?.ILog("No TV or Movie information found in flow");
return 2;
}
// remove any white space
nfoXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>" + Environment.NewLine + nfoXml.Trim();
args.Variables["NFO"] = nfoXml;
string path = args.ReplaceVariables(DestinationPath, stripMissing: true)?.EmptyAsNull() ?? FileHelper.GetDirectory(args.LibraryFileName ?? string.Empty);
string filename = args.ReplaceVariables(DestinationFile, stripMissing: true)?.EmptyAsNull() ??
FileHelper.ChangeExtension(FileHelper.GetShortFileName(args.LibraryFileName ?? string.Empty), "nfo");
if ((path + filename) == ".nfo")
{
// this is extremely likely to be a unit test, the library file would be set otherwise
args.Logger?.ILog("No file set to output to.");
return 1;
}
string output = FileHelper.Combine(path, filename);
string tempFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + ".nfo");
File.WriteAllText(tempFile, nfoXml);
if (args.FileService.FileMove(tempFile, output).Failed(out string error))
{
args.FailureReason = error;
return -1;
}
args.Logger.ILog("NFO File Created at: " + output);
return 1;
}
private string? CreateMovieNfo(NodeParameters args, Movie movie)
{
StringBuilder output = new();
output.AppendLine("<movie>");
WriteXmlEscapedElement(output, "title", movie.Title);
WriteXmlEscapedElement(output, "originaltitle", movie.OriginalTitle);
WriteXmlEscapedElement(output, "plot", movie.Overview);
WriteXmlEscapedElement(output, "tagline", movie.Tagline);
if(movie.Runtime > 0)
WriteXmlEscapedElement(output, "runtime", movie.Runtime.ToString());
WriteXmlEscapedElement(output, "premiered", movie.ReleaseDate.ToString("yyyy-MM-dd"));
output.AppendLine($"{TAB}<uniqueid type=\"tmdb\" default=\"true\">{movie.Id}</uniqueid>");
if(string.IsNullOrWhiteSpace(movie.ImdbId) == false)
output.AppendLine($"{TAB}<uniqueid type=\"imdb\">{movie.ImdbId}</uniqueid>");
// Genres
if (movie.Genres?.Any() == true)
{
string genres = string.Join(",", movie.Genres.Select(g => g.Name));
WriteXmlEscapedElement(output, "genres", genres);
}
if(movie.ProductionCompanies?.Any() == true)
WriteXmlEscapedElement(output, "studio", movie.ProductionCompanies.First().Name);
WriteThumb(output, "poster", movie.PosterPath);
WriteThumb(output, "landscape", movie.BackdropPath);
object oCredits = null;
if (args.Variables.TryGetValue(Globals.MOVIE_CREDITS, out oCredits) && oCredits is MovieCredit credit)
{
var writers = credit.CrewMembers
.Where(x => x.Department == "Writing" || x.Job == "Writer" || x.Job == "Screenplay")
.Select(crewMember => crewMember.Name).Distinct();
var directors = credit.CrewMembers.Where(crewMember => crewMember.Job == "Director")
.Select(crewMember => crewMember.Name).Distinct();
foreach (var epWriter in writers)
{
WriteXmlEscapedElement(output, "credits", epWriter);
}
foreach (var director in directors)
{
WriteXmlEscapedElement(output, "director", director);
}
}
if (movie.VoteCount > 0)
{
output.AppendLine(TAB + "<ratings>");
output.AppendLine(TAB + TAB + "<rating name=\"themoviedb\" max=\"10\">");
WriteXmlEscapedElement(output, "value", movie.VoteAverage.ToString(), tabs: 3);
WriteXmlEscapedElement(output, "votes", movie.VoteCount.ToString(), tabs: 3);
output.AppendLine(TAB + TAB + "</rating>");
output.AppendLine(TAB + "</ratings>");
}
if (string.IsNullOrWhiteSpace(movie.MovieCollectionInfo?.Name) == false)
{
output.AppendLine(TAB + "<set>");
WriteXmlEscapedElement(output, "name", movie.MovieCollectionInfo.Name, tabs: 2);
output.AppendLine(TAB + "</set>");
}
if (oCredits is MovieCredit credit2)
{
var cast = credit2.CastMembers?.ToList();
if(cast?.Any() == true)
{
for (int i = 0; i < cast.Count; i++)
{
var castMember = cast[i];
WriteActorElement(output, castMember.Name, castMember.Character, i, castMember.ProfilePath);
}
}
}
output.AppendLine("</movie>");
return output.ToString();
}
internal string CreateTvShowNfo(NodeParameters args, TVShowInfo tvShowInfo, Episode episode)
{
StringBuilder output = new();
output.AppendLine("<episodedetails>");
WriteXmlEscapedElement(output, "title", episode.Name);
WriteXmlEscapedElement(output, "originaltitle", tvShowInfo.OriginalName);
WriteXmlEscapedElement(output, "showtitle", tvShowInfo.Name);
WriteXmlEscapedElement(output, "season", episode.SeasonNumber.ToString());
WriteXmlEscapedElement(output, "episode", episode.EpisodeNumber.ToString());
WriteXmlEscapedElement(output, "plot", episode.Overview);
WriteXmlEscapedElement(output, "premiered", episode.AirDate.ToString("yyyy-MM-dd"));
output.AppendLine($"{TAB}<uniqueid type=\"tmdb\" default=\"true\">{episode.Id}</uniqueid>");
// Additional episode information from TVShowInfo
//WriteXmlEscapedElement(writer, "tvshow_id", tvShowInfo.Id.ToString());
//WriteXmlEscapedElement(writer, "tvshow_name", tvShowInfo.Name);
//WriteXmlEscapedElement(writer, "original_name", tvShowInfo.OriginalName);
// WriteXmlEscapedElement(writer, "poster_path", tvShowInfo.PosterPath);
// WriteXmlEscapedElement(writer, "backdrop_path", tvShowInfo.BackdropPath);
WriteXmlEscapedElement(output, "overview", tvShowInfo.Overview);
WriteXmlEscapedElement(output, "first_air_date", tvShowInfo.FirstAirDate.ToString("yyyy-MM-dd"));
WriteXmlEscapedElement(output, "origin_country", string.Join(",", tvShowInfo.OriginCountry));
WriteXmlEscapedElement(output, "original_language", tvShowInfo.OriginalLanguage);
// Genres
if (tvShowInfo.Genres != null && tvShowInfo.Genres.Any())
{
string genres = string.Join(",", tvShowInfo.Genres.Select(g => g.Name));
WriteXmlEscapedElement(output, "genres", genres);
}
// Additional episode information
WriteXmlEscapedElement(output, "episode_id", episode.Id.ToString());
WriteXmlEscapedElement(output, "still_path", episode.StillPath);
if (episode.VoteCount > 0)
{
output.AppendLine(TAB + "<ratings>");
output.AppendLine(TAB + TAB + "<rating namee=\"themoviedb\" max=\"10\">");
WriteXmlEscapedElement(output, "value", episode.VoteAverage.ToString(), tabs: 3);
WriteXmlEscapedElement(output, "votes", episode.VoteCount.ToString(), tabs: 3);
output.AppendLine(TAB + TAB + "</rating>");
output.AppendLine(TAB + "</ratings>");
}
if (episode.Crew?.Any() == true)
{
var writers = episode.Crew.Where(crewMember => crewMember.Department == "Writing").Select(crewMember => crewMember.Name).Distinct();
var directors = episode.Crew.Where(crewMember => crewMember.Job == "Director").Select(crewMember => crewMember.Name).Distinct();
foreach (var epWriter in writers)
{
WriteXmlEscapedElement(output, "credits", epWriter);
}
foreach (var director in directors)
{
WriteXmlEscapedElement(output, "director", director);
}
}
if (episode.GuestStars?.Any() == true)
{
for (int i = 0; i < episode.GuestStars.Count; i++)
{
var guestStar = episode.GuestStars[i];
WriteActorElement(output, guestStar.Name, guestStar.Character, i, guestStar.ProfilePath);
}
}
output.AppendLine("</episodedetails>");
return output.ToString();
}
private void WriteXmlEscapedElement(StringBuilder output, string elementName, string elementValue, int tabs = 1)
{
if (string.IsNullOrEmpty(elementValue))
return; // Skip writing if the value is null or empty
output.Append($"{string.Join("", Enumerable.Range(1, tabs).Select(x => TAB))}<{elementName}>");
output.Append(XmlEscape(elementValue));
output.AppendLine($"</{elementName}>");
}
private string XmlEscape(string unescaped)
{
XmlDocument doc = new XmlDocument();
var node = doc.CreateElement("root");
node.InnerText = unescaped;
return node.InnerXml;
}
private void WriteActorElement(StringBuilder output, string name, string role, int order, string thumb)
{
output.AppendLine(TAB +"<actor>");
WriteXmlEscapedElement(output, "name", name, tabs: 2);
WriteXmlEscapedElement(output, "role", role, tabs: 2);
WriteXmlEscapedElement(output, "order", order.ToString(), tabs: 2);
if(string.IsNullOrWhiteSpace(thumb) == false)
WriteXmlEscapedElement(output, "thumb", "https://image.tmdb.org/t/p/original" + thumb, tabs: 2);
output.AppendLine(TAB + "</actor>");
}
private void WriteThumb(StringBuilder output, string aspect, string url)
{
if (string.IsNullOrWhiteSpace(url))
return;
string preview = "https://image.tmdb.org/t/p/original" + url;
output.AppendLine(TAB +$"<thumb aspect=\"{aspect}\" preview=\"{preview}\" />");
}
}

View File

@@ -145,6 +145,19 @@ public class TVEpisodeLookup : Node
args.SetParameter(Globals.TV_SHOW_INFO, result);
if (lastEpisode > episode)
{
for (int i = episode.Value + 1; i <= lastEpisode; i++)
{
var epInfoExtra = show.Item.Episodes.FirstOrDefault(x => x.EpisodeNumber == i);
if (epInfoExtra == null)
continue;
int diff = i - episode.Value;
args.Variables[Globals.TV_EPISODE_INFO + "_" + diff] = epInfoExtra;
}
}
Variables["tvepisode.Title"] = result.Name;
Variables["tvepisode.Subtitle"] = epInfo.Name;
Variables["tvepisode.Year"] = epInfo.AirDate.Year;
@@ -156,6 +169,7 @@ public class TVEpisodeLookup : Node
Variables["tvepisode.Overview"] = epInfo.Overview;
//Variables["VideoMetadata"] = GetVideoMetadata(movieApi, result.Id, args.TempPath);
Variables[Globals.TV_SHOW_INFO] = result;
Variables[Globals.TV_EPISODE_INFO] = epInfo;
if (string.IsNullOrWhiteSpace(result.OriginalLanguage) == false)
Variables["OriginalLanguage"] = result.OriginalLanguage;