mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-19 06:49:30 -06:00
movie lookup
This commit is contained in:
@@ -19,6 +19,8 @@ public class TVShowHelper(NodeParameters args)
|
||||
{
|
||||
lookupName = FileHelper.GetShortFileNameWithoutExtension(filename);
|
||||
}
|
||||
|
||||
// lookupName = lookupName.Replace("!", "");
|
||||
|
||||
var result = GetTVShowInfo(lookupName);
|
||||
return (result.ShowName, result.Year);
|
||||
|
||||
@@ -194,6 +194,7 @@ public class MovieLookupTests : TestBase
|
||||
});
|
||||
File.WriteAllText(@"D:\videos\metadata.json", json);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MovieLookupTests_WonderWoman_Nfo()
|
||||
{
|
||||
|
||||
@@ -49,6 +49,28 @@ public class TVShowLookupTests : TestBase
|
||||
Assert.AreEqual(2004, args.Variables["tvshow.Year"]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TeenTitans()
|
||||
{
|
||||
var args = GetNodeParameters("/Internal/Downloads/TV/Teen Titans Go! S09E051.mkv");
|
||||
|
||||
var element = new TVShowLookup();
|
||||
element.UseFolderName = false;
|
||||
|
||||
var result = element.Execute(args);
|
||||
Assert.AreEqual(1, result);
|
||||
Assert.IsTrue(args.Parameters.ContainsKey(Globals.TV_SHOW_INFO));
|
||||
|
||||
var info = args.Parameters[Globals.TV_SHOW_INFO] as TVShowInfo;
|
||||
Assert.IsNotNull(info);
|
||||
|
||||
Assert.AreEqual("Teen Titans Go!", info.Name);
|
||||
Assert.AreEqual(2013, info.FirstAirDate.Year);
|
||||
Assert.AreEqual("en", info.OriginalLanguage);
|
||||
Assert.AreEqual("Teen Titans Go!", args.Variables["tvshow.Title"]);
|
||||
Assert.AreEqual(2013, args.Variables["tvshow.Year"]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void YearInFilename()
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using DM.MovieApi;
|
||||
using DM.MovieApi.MovieDb.Movies;
|
||||
using FileFlows.Plugin;
|
||||
using FileFlows.Plugin.Attributes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting.Logging;
|
||||
using FileHelper = FileFlows.Plugin.Helpers.FileHelper;
|
||||
|
||||
namespace MetaNodes.TheMovieDb;
|
||||
@@ -59,182 +60,233 @@ public class MovieLookup : Node
|
||||
[Boolean(1)]
|
||||
public bool UseFolderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the flow element
|
||||
/// </summary>
|
||||
/// <param name="args">the node parameters</param>
|
||||
/// <returns>the output to call next</returns>
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
string lookupName = PrepareLookupName(args, out int year);
|
||||
args.Logger?.ILog("Lookup name: " + lookupName);
|
||||
|
||||
MovieDbFactory.RegisterSettings(Globals.MovieDbBearerToken);
|
||||
var movieApi = MovieDbFactory.Create<IApiMovieRequest>().Value;
|
||||
|
||||
var result = SearchMovie(args, movieApi, lookupName, year);
|
||||
if (result == null)
|
||||
return 2; // No match found
|
||||
|
||||
args.SetParameter(Globals.MOVIE_INFO, result);
|
||||
PopulateMovieVariables(args, result);
|
||||
args.SetDisplayName($"{result.Title} ({result.ReleaseDate.Year})");
|
||||
|
||||
return RetrieveAdditionalMetadata(args, movieApi, result.Id) ? 1 : 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the lookup name from the input file name or folder name.
|
||||
/// Extracts the year from the name.
|
||||
/// </summary>
|
||||
/// <param name="args">The node parameters.</param>
|
||||
/// <param name="year">The extracted year from the name.</param>
|
||||
/// <returns>The cleaned-up lookup name.</returns>
|
||||
private string PrepareLookupName(NodeParameters args, out int year)
|
||||
{
|
||||
var originalName = UseFolderName
|
||||
? FileHelper.GetDirectoryName(args.LibraryFileName)
|
||||
: FileHelper.GetShortFileNameWithoutExtension(args.LibraryFileName);
|
||||
string lookupName = originalName;
|
||||
lookupName = lookupName.Replace(".", " ").Replace("_", " ");
|
||||
|
||||
// look for year
|
||||
int year = 0;
|
||||
string lookupName = originalName.Replace(".", " ").Replace("_", " ");
|
||||
lookupName = RemoveYearFromName(lookupName, out year);
|
||||
lookupName = lookupName.TrimEnd('(', '-');
|
||||
|
||||
args.Logger?.ILog($"Prepared lookup name: {lookupName}, Detected Year: {year}");
|
||||
return lookupName;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes the year from the lookup name if present.
|
||||
/// </summary>
|
||||
private static string RemoveYearFromName(string lookupName, out int year)
|
||||
{
|
||||
year = 0;
|
||||
var match = Regex.Matches(lookupName, @"((19[2-9][0-9])|(20[0-9]{2}))(?=([\.\s_\-\)\]]|$))").LastOrDefault();
|
||||
if (match != null)
|
||||
{
|
||||
int.TryParse(match.Value, out year);
|
||||
lookupName = lookupName[..lookupName.IndexOf(match.Value, StringComparison.Ordinal)].TrimEnd('(');
|
||||
}
|
||||
|
||||
// remove double spaces in case they were added when removing the year
|
||||
while (lookupName.IndexOf(" ", StringComparison.Ordinal) > 0)
|
||||
lookupName = lookupName.Replace(" ", " ");
|
||||
|
||||
lookupName = lookupName.TrimEnd('(', '-');
|
||||
|
||||
args.Logger?.ILog("Lookup name: " + lookupName);
|
||||
|
||||
// RegisterSettings only needs to be called one time when your application starts-up.
|
||||
MovieDbFactory.RegisterSettings(Globals.MovieDbBearerToken);
|
||||
|
||||
var movieApi = MovieDbFactory.Create<IApiMovieRequest>().Value;
|
||||
|
||||
var response = movieApi.SearchByTitleAsync(lookupName).Result;
|
||||
|
||||
if(response.Results.Count == 0 && originalName.Contains("german", StringComparison.CurrentCultureIgnoreCase) && lookupName.Contains("Ae", StringComparison.InvariantCultureIgnoreCase))
|
||||
return lookupName.Replace(" ", " ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a movie using the API.
|
||||
/// </summary>
|
||||
/// <param name="args">The node parameters for logging.</param>
|
||||
/// <param name="movieApi">The API client for movie lookup.</param>
|
||||
/// <param name="lookupName">The name of the movie to search for.</param>
|
||||
/// <param name="year">The extracted year from the title.</param>
|
||||
/// <returns>The movie information if found; otherwise, null.</returns>
|
||||
private MovieInfo SearchMovie(NodeParameters args, IApiMovieRequest movieApi, string lookupName, int year)
|
||||
{
|
||||
try
|
||||
{
|
||||
lookupName = lookupName.Replace("Ae", "Ä", StringComparison.InvariantCultureIgnoreCase);
|
||||
response = movieApi.SearchByTitleAsync(lookupName).Result;
|
||||
args.Logger?.ILog($"Searching for movie: {lookupName}");
|
||||
|
||||
var response = movieApi.SearchByTitleAsync(lookupName).Result;
|
||||
|
||||
if (response.Results.Count == 0 && lookupName.Contains("Ae", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
lookupName = lookupName.Replace("Ae", "Ä", StringComparison.InvariantCultureIgnoreCase);
|
||||
args.Logger?.ILog($"Retrying search with modified title: {lookupName}");
|
||||
response = movieApi.SearchByTitleAsync(lookupName).Result;
|
||||
}
|
||||
|
||||
if (response.Results.Count == 0)
|
||||
{
|
||||
args.Logger?.WLog($"No results found for '{lookupName}'");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store the year in a local variable to use inside the lambda
|
||||
int searchYear = year;
|
||||
|
||||
var movies = response.Results
|
||||
.OrderBy(x =>
|
||||
{
|
||||
if (searchYear > 0)
|
||||
return Math.Abs(searchYear - x.ReleaseDate.Year) < 2 ? 0 : 1;
|
||||
return 0;
|
||||
})
|
||||
.ThenBy(x => x.VoteCount > 250 ? 1 : 2) // behind the scenes etc, try treduce those
|
||||
.ThenBy(x => NormalizeTitle(x.Title) == NormalizeTitle(lookupName) ? 0 : 1)
|
||||
.ToList();
|
||||
|
||||
var movie = movies.FirstOrDefault();
|
||||
|
||||
args.Logger?.ILog($"Found movie: {movie?.Title} ({movie?.ReleaseDate.Year})");
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
|
||||
// try find an exact match
|
||||
var results = response.Results.OrderBy(x =>
|
||||
{
|
||||
if (year > 0)
|
||||
{
|
||||
// sometimes a year can be off by 1, if a movie was released late in the year but recorded in the next year
|
||||
return Math.Abs(year - x.ReleaseDate.Year) < 2 ? 0 : 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.ThenBy(x => x.Title.ToLower().Trim().Replace(" ", "") == lookupName.ToLower().Trim().Replace(" ", "") ? 0 : 1)
|
||||
.ThenBy(x =>
|
||||
{
|
||||
// do some fuzzy logic with roman numerals
|
||||
var numMatch = Regex.Match(lookupName, @"[\s]([\d]+)$");
|
||||
if (numMatch.Success == false)
|
||||
return 0;
|
||||
int number = int.Parse(numMatch.Groups[1].Value);
|
||||
string roman = number switch
|
||||
{
|
||||
1 => "i",
|
||||
2 => "ii",
|
||||
3 => "iii",
|
||||
4 => "iv",
|
||||
5 => "v",
|
||||
6 => "vi,",
|
||||
7 => "vii",
|
||||
8 => "viii",
|
||||
9 => "ix",
|
||||
10 => "x",
|
||||
11 => "xi",
|
||||
12 => "xii",
|
||||
13 => "xiii",
|
||||
_ => string.Empty
|
||||
};
|
||||
string ln = lookupName[..lookupName.LastIndexOf(number.ToString(), StringComparison.Ordinal)].ToLower().Trim().Replace(" ", "");
|
||||
string softTitle = x.Title.ToLower().Replace(" ", "").Trim();
|
||||
if (softTitle == ln + roman)
|
||||
return 0;
|
||||
if (softTitle.StartsWith(ln) && softTitle.EndsWith(roman))
|
||||
return 0;
|
||||
return 1;
|
||||
})
|
||||
.ThenBy(x => lookupName.ToLower().Trim().Replace(" ", "").StartsWith(x.Title.ToLower().Trim().Replace(" ", "")) ? 0 : 1)
|
||||
// .ThenBy(x => x.Title)
|
||||
.ToList();
|
||||
var result = results.FirstOrDefault();
|
||||
|
||||
if (result == null)
|
||||
return 2; // no match
|
||||
|
||||
args.SetParameter(Globals.MOVIE_INFO, result);
|
||||
|
||||
args.Variables["movie.Title"] = result.Title;
|
||||
args.Logger?.ILog("Detected Movie Title: " + result.Title);
|
||||
args.Variables["movie.Year"] = result.ReleaseDate.Year;
|
||||
|
||||
args.SetDisplayName($"{result.Title} ({result.ReleaseDate.Year})");
|
||||
|
||||
args.Logger?.ILog("Detected Movie Year: " + result.ReleaseDate.Year);
|
||||
var meta = GetVideoMetadata(args, movieApi, result.Id, args.TempPath);
|
||||
args.Variables["VideoMetadata"] = meta;
|
||||
if (string.IsNullOrWhiteSpace(meta.OriginalLanguage) == false)
|
||||
catch (Exception ex)
|
||||
{
|
||||
args.Logger?.ILog("Detected Original Language: " + meta.OriginalLanguage);
|
||||
args.Variables["OriginalLanguage"] = meta.OriginalLanguage;
|
||||
args.Logger?.ELog($"Error searching for movie '{lookupName}': {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
args.Variables[Globals.MOVIE_INFO] = result;
|
||||
var movie = movieApi.FindByIdAsync(result.Id).Result.Item;
|
||||
if(movie != null)
|
||||
args.Variables[Globals.MOVIE] = movie;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the VideoMetadata
|
||||
/// Normalizes a movie title by removing spaces and converting to lowercase.
|
||||
/// </summary>
|
||||
private static string NormalizeTitle(string title)
|
||||
=> title.ToLower().Trim().Replace(" ", "");
|
||||
|
||||
/// <summary>
|
||||
/// Populates movie-related variables into the execution context.
|
||||
/// </summary>
|
||||
private void PopulateMovieVariables(NodeParameters args, MovieInfo result)
|
||||
{
|
||||
args.Variables["movie.Title"] = result.Title;
|
||||
args.Logger?.ILog("Detected Movie Title: " + result.Title);
|
||||
args.Variables["movie.Year"] = result.ReleaseDate.Year;
|
||||
args.Logger?.ILog("Detected Movie Year: " + result.ReleaseDate.Year);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves additional metadata such as cast and crew information.
|
||||
/// </summary>
|
||||
private bool RetrieveAdditionalMetadata(NodeParameters args, IApiMovieRequest movieApi, int movieId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var meta = GetVideoMetadata(args, movieApi, movieId, args.TempPath);
|
||||
if (meta == null)
|
||||
return false;
|
||||
|
||||
args.Variables["VideoMetadata"] = meta;
|
||||
if (!string.IsNullOrWhiteSpace(meta.OriginalLanguage))
|
||||
{
|
||||
args.Logger?.ILog("Detected Original Language: " + meta.OriginalLanguage);
|
||||
args.Variables["OriginalLanguage"] = meta.OriginalLanguage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
args.Logger?.WLog($"Failed looking up movie: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves video metadata including movie details and credits.
|
||||
/// </summary>
|
||||
/// <param name="movieApi">the movie API</param>
|
||||
/// <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(NodeParameters args, IApiMovieRequest movieApi, int id, string tempPath)
|
||||
{
|
||||
var movie = movieApi.FindByIdAsync(id).Result?.Item;
|
||||
if (movie == null)
|
||||
return null;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(movie.ImdbId) == false)
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(movie.ImdbId))
|
||||
args.Variables["movie.ImdbId"] = movie.ImdbId;
|
||||
if(movie.Genres?.Any() != false)
|
||||
if (movie.Genres?.Any() == true)
|
||||
args.Variables["movie.Genre"] = movie.Genres.First().Name;
|
||||
|
||||
args.SetParameter(Globals.MOVIE, movie);
|
||||
|
||||
var meta = new VideoMetadata
|
||||
{
|
||||
Title = movie.Title,
|
||||
Genres = movie.Genres?.Select(x => x.Name).ToList(),
|
||||
Description = movie.Overview,
|
||||
Year = movie.ReleaseDate.Year,
|
||||
Subtitle = movie.Tagline,
|
||||
ReleaseDate = movie.ReleaseDate,
|
||||
OriginalLanguage = movie.OriginalLanguage
|
||||
};
|
||||
|
||||
DownloadPosterImage(args, movie, tempPath, meta);
|
||||
PopulateCredits(args, movieApi, id, meta);
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads and assigns the movie poster image.
|
||||
/// </summary>
|
||||
private static void DownloadPosterImage(NodeParameters args, Movie movie, string tempPath, VideoMetadata meta)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(movie.PosterPath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var stream = httpClient.GetStreamAsync("https://image.tmdb.org/t/p/w500" + movie.PosterPath).Result;
|
||||
string file = Path.Combine(tempPath, Guid.NewGuid() + ".jpg");
|
||||
using var fileStream = new FileStream(file, FileMode.CreateNew);
|
||||
stream.CopyTo(fileStream);
|
||||
meta.ArtJpeg = file;
|
||||
args.SetThumbnail(file);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the cast and crew information.
|
||||
/// </summary>
|
||||
private static void PopulateCredits(NodeParameters args, IApiMovieRequest movieApi, int id, VideoMetadata meta)
|
||||
{
|
||||
var credits = movieApi.GetCreditsAsync(id).Result?.Item;
|
||||
if (credits == null) return;
|
||||
|
||||
VideoMetadata md = new();
|
||||
md.Title = movie.Title;
|
||||
md.Genres = movie.Genres?.Select(x => x.Name).ToList();
|
||||
md.Description = movie.Overview;
|
||||
md.Year = movie.ReleaseDate.Year;
|
||||
md.Subtitle = movie.Tagline;
|
||||
md.ReleaseDate = movie.ReleaseDate;
|
||||
md.OriginalLanguage = movie.OriginalLanguage;
|
||||
if (string.IsNullOrWhiteSpace(movie.PosterPath) == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var stream = httpClient.GetStreamAsync("https://image.tmdb.org/t/p/w500" + movie.PosterPath).Result;
|
||||
string file = Path.Combine(tempPath, Guid.NewGuid() + ".jpg");
|
||||
using var fileStream = new FileStream(file, FileMode.CreateNew);
|
||||
stream.CopyTo(fileStream);
|
||||
md.ArtJpeg = file;
|
||||
args.SetThumbnail(file);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
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.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();
|
||||
}
|
||||
|
||||
return md;
|
||||
args.Variables[Globals.MOVIE_CREDITS] = credits;
|
||||
meta.Actors = credits.CastMembers?.Select(x => x.Name)?.ToList();
|
||||
meta.Directors = credits.CrewMembers?.Where(x => x.Job == "Director")?.Select(x => x.Name)?.ToList();
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,13 @@ public class NfoFileCreator : Node
|
||||
args.Logger?.ILog("MovieInformation found");
|
||||
nfoXml = CreateMovieNfo(args, movie);
|
||||
}
|
||||
else if (args.Parameters.TryGetValue(Globals.MOVIE, out object oMovieInfo2) &&
|
||||
oMovieInfo2 is Movie movie2)
|
||||
{
|
||||
args.Logger?.ILog("MovieInformation found");
|
||||
nfoXml = CreateMovieNfo(args, movie2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nfoXml))
|
||||
|
||||
@@ -79,7 +79,7 @@ public class TVShowLookup : Node
|
||||
args.Logger?.ILog("Lookup TV Show: " + lookupName);
|
||||
|
||||
string tvShowInfoCacheKey = $"TVShowInfo: {lookupName} ({year})";
|
||||
TVShowInfo result =args.Cache.GetObject<TVShowInfo>(tvShowInfoCacheKey);
|
||||
TVShowInfo result = args.Cache?.GetObject<TVShowInfo>(tvShowInfoCacheKey);
|
||||
if (result != null)
|
||||
{
|
||||
args.Logger?.ILog("Got TV show info from cache: " + result.Name);
|
||||
@@ -93,17 +93,17 @@ public class TVShowLookup : Node
|
||||
args.Logger?.ILog("No result found for: " + lookupName);
|
||||
return 2; // no match
|
||||
}
|
||||
args.Cache.SetObject(tvShowInfoCacheKey, result);
|
||||
args.Cache?.SetObject(tvShowInfoCacheKey, result);
|
||||
}
|
||||
|
||||
string tvShowCacheKey = $"TVShow: {result.Id}";
|
||||
TVShow? tv = args.Cache.GetObject<TVShow>(tvShowCacheKey);
|
||||
TVShow? tv = args.Cache?.GetObject<TVShow>(tvShowCacheKey);
|
||||
if (tv == null)
|
||||
{
|
||||
var tvApi = MovieDbFactory.Create<IApiTVShowRequest>().Value;
|
||||
tv = tvApi.FindByIdAsync(result.Id).Result?.Item;
|
||||
if (tv != null)
|
||||
args.Cache.SetObject(tvShowCacheKey, tv);
|
||||
args.Cache?.SetObject(tvShowCacheKey, tv);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -107,7 +107,11 @@ public abstract class TestBase
|
||||
var args = new NodeParameters(filename, Logger, isDirectory, libPath, new LocalFileService())
|
||||
{
|
||||
LibraryFileName = filename,
|
||||
TempPath = tempPath
|
||||
TempPath = tempPath,
|
||||
SetDisplayNameActual = _ =>
|
||||
{
|
||||
|
||||
}
|
||||
};
|
||||
return args;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user