From 122976f906277b9d824a277ed1747fff7f364243 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Tue, 1 Apr 2025 13:19:47 +1300 Subject: [PATCH] movie lookup --- MetaNodes/Helpers/TVShowHelper.cs | 2 + .../Tests/TheMovieDb/MovieLookupTests.cs | 1 + .../Tests/TheMovieDb/TVShowLookupTests.cs | 22 ++ MetaNodes/TheMovieDb/MovieLookup.cs | 344 ++++++++++-------- MetaNodes/TheMovieDb/NfoFileCreator.cs | 7 + MetaNodes/TheMovieDb/TVShowLookup.cs | 8 +- PluginTestLibrary/_TestBase.cs | 6 +- 7 files changed, 239 insertions(+), 151 deletions(-) diff --git a/MetaNodes/Helpers/TVShowHelper.cs b/MetaNodes/Helpers/TVShowHelper.cs index 08da3066..52c2c53c 100644 --- a/MetaNodes/Helpers/TVShowHelper.cs +++ b/MetaNodes/Helpers/TVShowHelper.cs @@ -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); diff --git a/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs b/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs index 7fe9e318..3de04705 100644 --- a/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs +++ b/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs @@ -194,6 +194,7 @@ public class MovieLookupTests : TestBase }); File.WriteAllText(@"D:\videos\metadata.json", json); } + [TestMethod] public void MovieLookupTests_WonderWoman_Nfo() { diff --git a/MetaNodes/Tests/TheMovieDb/TVShowLookupTests.cs b/MetaNodes/Tests/TheMovieDb/TVShowLookupTests.cs index a7d701b7..073f57c0 100644 --- a/MetaNodes/Tests/TheMovieDb/TVShowLookupTests.cs +++ b/MetaNodes/Tests/TheMovieDb/TVShowLookupTests.cs @@ -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() { diff --git a/MetaNodes/TheMovieDb/MovieLookup.cs b/MetaNodes/TheMovieDb/MovieLookup.cs index dfeb8fa9..f8aad9ae 100644 --- a/MetaNodes/TheMovieDb/MovieLookup.cs +++ b/MetaNodes/TheMovieDb/MovieLookup.cs @@ -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; } - /// - /// Executes the flow element - /// - /// the node parameters - /// the output to call next + + /// 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().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; + } + + /// + /// Prepares the lookup name from the input file name or folder name. + /// Extracts the year from the name. + /// + /// The node parameters. + /// The extracted year from the name. + /// The cleaned-up lookup name. + 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; + } + + + /// + /// Removes the year from the lookup name if present. + /// + 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().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(" ", " "); + } + + /// + /// Searches for a movie using the API. + /// + /// The node parameters for logging. + /// The API client for movie lookup. + /// The name of the movie to search for. + /// The extracted year from the title. + /// The movie information if found; otherwise, null. + 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; } + /// - /// Gets the VideoMetadata + /// Normalizes a movie title by removing spaces and converting to lowercase. + /// + private static string NormalizeTitle(string title) + => title.ToLower().Trim().Replace(" ", ""); + + /// + /// Populates movie-related variables into the execution context. + /// + 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); + } + + /// + /// Retrieves additional metadata such as cast and crew information. + /// + 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; + } + } + + /// + /// Retrieves video metadata including movie details and credits. /// - /// the movie API - /// the ID of the movie - /// the temp path to save any images to - /// the VideoMetadata 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; + } + + /// + /// Downloads and assigns the movie poster image. + /// + 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 + } + } + + /// + /// Populates the cast and crew information. + /// + 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(); } } \ No newline at end of file diff --git a/MetaNodes/TheMovieDb/NfoFileCreator.cs b/MetaNodes/TheMovieDb/NfoFileCreator.cs index 825bf155..8b34f3c4 100644 --- a/MetaNodes/TheMovieDb/NfoFileCreator.cs +++ b/MetaNodes/TheMovieDb/NfoFileCreator.cs @@ -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)) diff --git a/MetaNodes/TheMovieDb/TVShowLookup.cs b/MetaNodes/TheMovieDb/TVShowLookup.cs index 91de16fd..b724e16e 100644 --- a/MetaNodes/TheMovieDb/TVShowLookup.cs +++ b/MetaNodes/TheMovieDb/TVShowLookup.cs @@ -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(tvShowInfoCacheKey); + TVShowInfo result = args.Cache?.GetObject(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(tvShowCacheKey); + TVShow? tv = args.Cache?.GetObject(tvShowCacheKey); if (tv == null) { var tvApi = MovieDbFactory.Create().Value; tv = tvApi.FindByIdAsync(result.Id).Result?.Item; if (tv != null) - args.Cache.SetObject(tvShowCacheKey, tv); + args.Cache?.SetObject(tvShowCacheKey, tv); } else { diff --git a/PluginTestLibrary/_TestBase.cs b/PluginTestLibrary/_TestBase.cs index 2f46edec..191f7224 100644 --- a/PluginTestLibrary/_TestBase.cs +++ b/PluginTestLibrary/_TestBase.cs @@ -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; }