diff --git a/.gitignore b/.gitignore index 1937c802..62376d51 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ Plugin.dll */.vs *.suo TestStore/ +.vs/FileFlowsPlugins/v17/.futdcache.v1 +.vs/FileFlowsPlugins/DesignTimeBuild/.dtbcache.v2 +.vs/FileFlowsPlugins/project-colors.json diff --git a/BasicNodes/BasicNodes.csproj b/BasicNodes/BasicNodes.csproj index b2b130ce..234feee3 100644 Binary files a/BasicNodes/BasicNodes.csproj and b/BasicNodes/BasicNodes.csproj differ diff --git a/BasicNodes/File/MoveFile.cs b/BasicNodes/File/MoveFile.cs index e0d8b4c5..8602b9e1 100644 --- a/BasicNodes/File/MoveFile.cs +++ b/BasicNodes/File/MoveFile.cs @@ -26,7 +26,7 @@ namespace FileFlows.BasicNodes.File string dest = DestinationPath; if (string.IsNullOrEmpty(dest)) { - args.Logger.ELog("No destination specified"); + args.Logger?.ELog("No destination specified"); args.Result = NodeResult.Failure; return -1; } @@ -47,52 +47,24 @@ namespace FileFlows.BasicNodes.File } var destDir = fiDest.DirectoryName; - if (Directory.Exists(destDir) == false) + if (string.IsNullOrEmpty(destDir) == false && Directory.Exists(destDir) == false) Directory.CreateDirectory(destDir); - long fileSize = new FileInfo(args.WorkingFile).Length; + string original = args.WorkingFile; + if (args.MoveFile(dest) == false) + return -1; - bool moved = false; - Task task = Task.Run(() => + if (DeleteOriginal && original != args.FileName) { try { - if (System.IO.File.Exists(dest)) - System.IO.File.Delete(dest); - args.Logger.ILog($"Moving file: \"{args.WorkingFile}\" to \"{dest}\""); - System.IO.File.Move(args.WorkingFile, dest, true); - - if (DeleteOriginal && args.WorkingFile != args.FileName) - { - System.IO.File.Delete(args.FileName); - } - args.SetWorkingFile(dest); - - moved = true; - } - catch (Exception ex) + System.IO.File.Delete(original); + }catch(Exception ex) { - args.Logger.ELog("Failed to move file: " + ex.Message); + args.Logger?.WLog("Failed to delete original file: " + ex.Message); } - }); - - while (task.IsCompleted == false) - { - long currentSize = 0; - var destFileInfo = new FileInfo(dest); - if (destFileInfo.Exists) - currentSize = destFileInfo.Length; - - args.PartPercentageUpdate(currentSize / fileSize * 100); - System.Threading.Thread.Sleep(50); } - - if (moved == false) - return -1; - - args.PartPercentageUpdate(100); - - return base.Execute(args); + return 1; } } } \ No newline at end of file diff --git a/Builds/BasicNodes.zip b/Builds/BasicNodes.zip index bd2f7afd..ab046b5e 100644 Binary files a/Builds/BasicNodes.zip and b/Builds/BasicNodes.zip differ diff --git a/Builds/MetaNodes.zip b/Builds/MetaNodes.zip new file mode 100644 index 00000000..b941a156 Binary files /dev/null and b/Builds/MetaNodes.zip differ diff --git a/Builds/VideoNodes.zip b/Builds/VideoNodes.zip index 23ae3484..45b12419 100644 Binary files a/Builds/VideoNodes.zip and b/Builds/VideoNodes.zip differ diff --git a/FileFlowsPlugins.sln b/FileFlowsPlugins.sln index 50674bef..ab0e658e 100644 --- a/FileFlowsPlugins.sln +++ b/FileFlowsPlugins.sln @@ -1,20 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicNodes", "BasicNodes\BasicNodes.csproj", "{7AE24315-9FE7-429F-83D9-C989CFF5420D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicNodes", "BasicNodes\BasicNodes.csproj", "{7AE24315-9FE7-429F-83D9-C989CFF5420D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoNodes", "VideoNodes\VideoNodes.csproj", "{CF96D3D1-1D8B-47F7-BEA7-BB238F7A566A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoNodes", "VideoNodes\VideoNodes.csproj", "{CF96D3D1-1D8B-47F7-BEA7-BB238F7A566A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaNodes", "MetaNodes\MetaNodes.csproj", "{E6F8E9F6-31D7-4A89-966D-2690CA7C0528}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7AE24315-9FE7-429F-83D9-C989CFF5420D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7AE24315-9FE7-429F-83D9-C989CFF5420D}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -24,5 +23,15 @@ Global {CF96D3D1-1D8B-47F7-BEA7-BB238F7A566A}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF96D3D1-1D8B-47F7-BEA7-BB238F7A566A}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF96D3D1-1D8B-47F7-BEA7-BB238F7A566A}.Release|Any CPU.Build.0 = Release|Any CPU + {E6F8E9F6-31D7-4A89-966D-2690CA7C0528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6F8E9F6-31D7-4A89-966D-2690CA7C0528}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6F8E9F6-31D7-4A89-966D-2690CA7C0528}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6F8E9F6-31D7-4A89-966D-2690CA7C0528}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B689155F-BBFF-477B-A041-35C1CAD6D24D} EndGlobalSection EndGlobal diff --git a/MetaNodes/Globals.cs b/MetaNodes/Globals.cs new file mode 100644 index 00000000..03111bdd --- /dev/null +++ b/MetaNodes/Globals.cs @@ -0,0 +1,7 @@ +namespace MetaNodes +{ + internal class Globals + { + public static string MOVIE_INFO = "MovieInfo"; + } +} diff --git a/MetaNodes/MetaNodes.csproj b/MetaNodes/MetaNodes.csproj new file mode 100644 index 00000000..c97fa416 Binary files /dev/null and b/MetaNodes/MetaNodes.csproj differ diff --git a/MetaNodes/MetaNodes.en.json b/MetaNodes/MetaNodes.en.json new file mode 100644 index 00000000..978e3608 --- /dev/null +++ b/MetaNodes/MetaNodes.en.json @@ -0,0 +1,24 @@ +{ + "Flow":{ + "Parts": { + "MovieLookup": { + "Description": "Looks performs a search on TheMovieDB.org.\nStores the Metadata inside the parameter 'MovieInfo'.\n\nOutputs 1: Movie found\nOutputs 2: Movie not found", + "Fields": { + "UseFolderName": "Use Folder Name", + "UseFolderName-Help": "If the folder name should be used instead of the filename." + } + }, + "MovieRenamer": { + "Description": "Renames the working file using the metadata stored in 'MovieInfo'.\nNote: MovieLookup should be executed in the flow before this node to work.\n\nOutput 1: File was renamed\nOutput 2: File failed to be renamed", + "Fields": { + "Pattern": "Pattern", + "Pattern-Help": "The pattern to use to rename the folder. '{Title}', '{Year}', '{Extension}'.", + "DestinationPath": "Destination Path", + "DestinationPath-Help": "If the file should be moved to a different directory.", + "LogOnly": "Log Only", + "LogOnly-Help": "Turn on if you just want to test this node without it actually renaming the file" + } + } + } + } +} \ No newline at end of file diff --git a/MetaNodes/Plugin.cs b/MetaNodes/Plugin.cs new file mode 100644 index 00000000..fae22c07 --- /dev/null +++ b/MetaNodes/Plugin.cs @@ -0,0 +1,11 @@ +namespace MetaNodes +{ + using System.ComponentModel.DataAnnotations; + + public class Plugin : FileFlows.Plugin.IPlugin + { + public string Name => "Meta Nodes"; + + public void Init() { } + } +} \ No newline at end of file diff --git a/MetaNodes/Tests/TestLogger.cs b/MetaNodes/Tests/TestLogger.cs new file mode 100644 index 00000000..c3dbeb02 --- /dev/null +++ b/MetaNodes/Tests/TestLogger.cs @@ -0,0 +1,43 @@ +namespace BasicNodes.Tests +{ + using FileFlows.Plugin; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + internal class TestLogger : ILogger + { + private List Messages = new List(); + + public void DLog(params object[] args) => Log("DBUG", args); + + public void ELog(params object[] args) => Log("ERRR", args); + + public void ILog(params object[] args) => Log("INFO", args); + + public void WLog(params object[] args) => Log("WARN", args); + + private void Log(string type, object[] args) + { + if (args == null || args.Length == 0) + return; + string message = type + " -> " + + string.Join(", ", args.Select(x => + x == null ? "null" : + x.GetType().IsPrimitive || x is string ? x.ToString() : + System.Text.Json.JsonSerializer.Serialize(x))); + Messages.Add(message); + } + + public bool Contains(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + string log = string.Join(Environment.NewLine, Messages); + return log.Contains(message); + } + } +} diff --git a/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs b/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs new file mode 100644 index 00000000..20c8d81b --- /dev/null +++ b/MetaNodes/Tests/TheMovieDb/MovieLookupTests.cs @@ -0,0 +1,115 @@ +#if(DEBUG) + +namespace MetaNodes.Tests.TheMovieDb +{ + using BasicNodes.Tests; + using DM.MovieApi.MovieDb.Movies; + using MetaNodes.TheMovieDb; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MovieLookupTests + { + [TestMethod] + public void MovieLookupTests_File_Ghostbusters() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Ghostbusters.mkv"); + args.Logger = new TestLogger(); + + 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("Ghostbusters", mi.Title); + Assert.AreEqual(1984, mi.ReleaseDate.Year); + } + + [TestMethod] + public void MovieLookupTests_File_Ghostbusters2() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Ghostbusters 2.mkv"); + args.Logger = new TestLogger(); + + 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("Ghostbusters II", mi.Title); + Assert.AreEqual(1989, mi.ReleaseDate.Year); + } + + [TestMethod] + public void MovieLookupTests_File_WithDots() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Back.To.The.Future.2.mkv"); + args.Logger = new TestLogger(); + + 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("Back to the Future Part II", mi.Title); + Assert.AreEqual(1989, mi.ReleaseDate.Year); + } + + [TestMethod] + public void MovieLookupTests_File_WithYear() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Back.To.The.Future.1989.mkv"); + args.Logger = new TestLogger(); + + 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("Back to the Future Part II", mi.Title); + Assert.AreEqual(1989, mi.ReleaseDate.Year); + } + + [TestMethod] + public void MovieLookupTests_Folder_WithYear() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Back To The Future (1989)\Jaws.mkv"); + args.Logger = new TestLogger(); + + MovieLookup ml = new MovieLookup(); + ml.UseFolderName = true; + + 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("Back to the Future Part II", mi.Title); + Assert.AreEqual(1989, mi.ReleaseDate.Year); + } + } +} + +#endif \ No newline at end of file diff --git a/MetaNodes/Tests/TheMovieDb/MovieRenamerTests.cs b/MetaNodes/Tests/TheMovieDb/MovieRenamerTests.cs new file mode 100644 index 00000000..cf7b3433 --- /dev/null +++ b/MetaNodes/Tests/TheMovieDb/MovieRenamerTests.cs @@ -0,0 +1,134 @@ +#if(DEBUG) + +namespace MetaNodes.Tests.TheMovieDb +{ + using BasicNodes.Tests; + using DM.MovieApi.MovieDb.Movies; + using MetaNodes.TheMovieDb; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class RenamerTests + { + [TestMethod] + public void RenamerTests_File_TitleYearExt() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Ghostbusters.mkv"); + var logger = new TestLogger(); + args.Logger = logger; + args.SetParameter(Globals.MOVIE_INFO, new MovieInfo + { + Title = "Back to the Future Part II", + ReleaseDate = new DateTime(1989, 5, 5) + }); + + Renamer node = new Renamer(); + node.Pattern = "{Title} ({Year}).{ext}"; + node.LogOnly = true; + + var result = node.Execute(args); + Assert.AreEqual(1, result); + + Assert.IsTrue(logger.Contains("Renaming file to: Back to the Future Part II (1989).mkv")); + } + + [TestMethod] + public void RenamerTests_File_TitleExt() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Ghostbusters.mkv"); + var logger = new TestLogger(); + args.Logger = logger; + args.SetParameter(Globals.MOVIE_INFO, new MovieInfo + { + Title = "Back to the Future Part II", + ReleaseDate = new DateTime(1989, 5, 5) + }); + + Renamer node = new Renamer(); + node.Pattern = "{Title}.{ext}"; + node.LogOnly = true; + + var result = node.Execute(args); + Assert.AreEqual(1, result); + + Assert.IsTrue(logger.Contains("Renaming file to: Back to the Future Part II.mkv")); + } + + [TestMethod] + public void RenamerTests_Folder_TitleYear_Windows() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Ghostbusters.mkv"); + var logger = new TestLogger(); + args.Logger = logger; + args.SetParameter(Globals.MOVIE_INFO, new MovieInfo + { + Title = "Back to the Future Part II", + ReleaseDate = new DateTime(1989, 5, 5) + }); + + Renamer node = new Renamer(); + node.Pattern = @"{Title} ({Year})\{Title}.{ext}"; + node.LogOnly = true; + + var result = node.Execute(args); + Assert.AreEqual(1, result); + + Assert.IsTrue(logger.Contains($"Renaming file to: Back to the Future Part II (1989){Path.DirectorySeparatorChar}Back to the Future Part II.mkv")); + } + + [TestMethod] + public void RenamerTests_Folder_TitleYear_Linux() + { + var args = new FileFlows.Plugin.NodeParameters(@"c:\test\Ghostbusters.mkv"); + var logger = new TestLogger(); + args.Logger = logger; + args.SetParameter(Globals.MOVIE_INFO, new MovieInfo + { + Title = "Back to the Future Part II", + ReleaseDate = new DateTime(1989, 5, 5) + }); + + Renamer node = new Renamer(); + node.Pattern = @"{Title} ({Year})/{Title}.{ext}"; + node.LogOnly = true; + + var result = node.Execute(args); + Assert.AreEqual(1, result); + + Assert.IsTrue(logger.Contains($"Renaming file to: Back to the Future Part II (1989){Path.DirectorySeparatorChar}Back to the Future Part II.mkv")); + } + + [TestMethod] + public void RenamerTests_Folder_TitleYear_MoveActual() + { + string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".mkv"); + string path = new FileInfo(tempFile).DirectoryName; + File.WriteAllText(tempFile, "test"); + + var args = new FileFlows.Plugin.NodeParameters(tempFile); + var logger = new TestLogger(); + args.Logger = logger; + args.SetParameter(Globals.MOVIE_INFO, new MovieInfo + { + Title = "Back to the Future Part II", + ReleaseDate = new DateTime(1989, 5, 5) + }); + + Renamer node = new Renamer(); + node.Pattern = @"{Title} ({Year})/{Title}.{ext}"; + + var result = node.Execute(args); + Assert.AreEqual(1, result); + + string expectedShort = $"Back to the Future Part II (1989){Path.DirectorySeparatorChar}Back to the Future Part II.mkv"; + Assert.IsTrue(logger.Contains($"Renaming file to: " + expectedShort)); + + string expected = Path.Combine(path, expectedShort); + Assert.IsTrue(File.Exists(expected)); + + Directory.Delete(new FileInfo(expected).DirectoryName, true); + } + } +} + +#endif \ No newline at end of file diff --git a/MetaNodes/TheMovieDb/MovieLookup.cs b/MetaNodes/TheMovieDb/MovieLookup.cs new file mode 100644 index 00000000..8647733c --- /dev/null +++ b/MetaNodes/TheMovieDb/MovieLookup.cs @@ -0,0 +1,105 @@ +namespace MetaNodes.TheMovieDb +{ + using System.Text.RegularExpressions; + using DM.MovieApi; + using DM.MovieApi.ApiResponse; + using DM.MovieApi.MovieDb.Movies; + using FileFlows.Plugin; + using FileFlows.Plugin.Attributes; + + public class MovieLookup : Node + { + public override int Inputs => 1; + public override int Outputs => 2; + public override string Icon => "fas fa-film"; + + + [Boolean(1)] + public bool UseFolderName { get; set; } + + public override int Execute(NodeParameters args) + { + var fileInfo = new FileInfo(args.FileName); + string lookupName = UseFolderName ? fileInfo.Directory.Name : fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf(fileInfo.Extension)); + lookupName = lookupName.Replace(".", " ").Replace("_", " "); + + // look for year + string year = string.Empty; + var match = Regex.Match(lookupName, @"((19[2-9][0-9])|(20[0-9]{2}))(?=([\.\s_\-\)\]]|$))"); + if (match.Success) + { + year = match.Groups[1].Value; + lookupName = lookupName.Replace(year, ""); + } + + // remove double spaces incase they were added when removing the year + while (lookupName.IndexOf(" ") > 0) + lookupName = lookupName.Replace(" ", " "); + + string bearerToken = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxZjVlNTAyNmJkMDM4YmZjZmU2MjI2MWU2ZGEwNjM0ZiIsInN1YiI6IjRiYzg4OTJjMDE3YTNjMGY5MjAwMDIyZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.yMwyT8DEK1rF1gQMKJ-ZSy-dUGxFs5T345XwBLrvrWE"; + + // RegisterSettings only needs to be called one time when your application starts-up. + MovieDbFactory.RegisterSettings(bearerToken); + + var movieApi = MovieDbFactory.Create().Value; + + + ApiSearchResponse response = movieApi.SearchByTitleAsync(lookupName).Result; + + // try find an exact match + var result = response.Results.OrderBy(x => + { + if (string.IsNullOrEmpty(year) == false) + { + return year == x.ReleaseDate.Year.ToString() ? 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.Substring(0, lookupName.LastIndexOf(number.ToString())).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) + .FirstOrDefault(); + + if (result == null) + return 2; // no match + + args.SetParameter(Globals.MOVIE_INFO, result); + + return 1; + + } + + } +} \ No newline at end of file diff --git a/MetaNodes/TheMovieDb/MovieRenamer.cs b/MetaNodes/TheMovieDb/MovieRenamer.cs new file mode 100644 index 00000000..2efbbed8 --- /dev/null +++ b/MetaNodes/TheMovieDb/MovieRenamer.cs @@ -0,0 +1,80 @@ +namespace MetaNodes.TheMovieDb +{ + using System.Text.RegularExpressions; + using DM.MovieApi; + using DM.MovieApi.ApiResponse; + using DM.MovieApi.MovieDb.Movies; + using FileFlows.Plugin; + using FileFlows.Plugin.Attributes; + + public class MovieRenamer : Node + { + public override int Inputs => 1; + public override int Outputs => 1; + public override string Icon => "fas fa-font"; + + public string _Pattern = string.Empty; + + [Text(1)] + public string? Pattern + { + get => _Pattern; + set { _Pattern = value ?? ""; } + } + + private string _DestinationPath = string.Empty; + + [Folder(2)] + public string DestinationPath + { + get => _DestinationPath; + set { _DestinationPath = value ?? ""; } + } + + [Boolean(3)] + public bool LogOnly { get; set; } + + public override int Execute(NodeParameters args) + { + if(string.IsNullOrEmpty(Pattern)) + { + args.Logger?.ELog("No pattern specified"); + return -1; + } + var movieInfo = args.GetParameter(Globals.MOVIE_INFO); + if (movieInfo == null) { + args.Logger?.ELog("MovieInfo not found, you must execute the Movie Lookup node first"); + return -1; + } + + string newFile = Pattern; + // incase they set a linux path on windows or vice versa + newFile = newFile.Replace('\\', Path.DirectorySeparatorChar); + newFile = newFile.Replace('/', Path.DirectorySeparatorChar); + + newFile = ReplaceVariable(newFile, "Year", movieInfo.ReleaseDate.Year.ToString()); + newFile = ReplaceVariable(newFile, "Title", movieInfo.Title); + newFile = ReplaceVariable(newFile, "Extension", args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".")+1)); + newFile = ReplaceVariable(newFile, "Ext", args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".") + 1)); + + string destFolder = DestinationPath; + if (string.IsNullOrEmpty(destFolder)) + destFolder = new FileInfo(args.WorkingFile).Directory?.FullName ?? ""; + + var dest = new FileInfo(Path.Combine(destFolder, newFile)); + + args.Logger?.ILog("Renaming file to: " + (string.IsNullOrEmpty(DestinationPath) ? "" : DestinationPath + Path.DirectorySeparatorChar) + newFile); + + + if (LogOnly) + return 1; + + return args.MoveFile(dest.FullName) ? 1 : -1; + } + + private string ReplaceVariable(string input, string variable, string value) + { + return Regex.Replace(input, @"{" + Regex.Escape(variable) + @"}", value, RegexOptions.IgnoreCase); + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/ApiRequestBase.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/ApiRequestBase.cs new file mode 100644 index 00000000..b414d768 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/ApiRequestBase.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using DM.MovieApi.ApiResponse; +using Newtonsoft.Json; + +namespace DM.MovieApi.ApiRequest +{ + internal abstract class ApiRequestBase + { + private readonly IApiSettings _settings; + + protected ApiRequestBase( IApiSettings settings ) + { + _settings = settings; + } + + public async Task> QueryAsync( string command ) + => await QueryAsync( command, new Dictionary() ); + + public async Task> QueryAsync( string command, IDictionary parameters ) + { + var settings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + }; + settings.Converters.Add( new IsoDateTimeConverterEx() ); + + Func deserializer = json => JsonConvert.DeserializeObject( json, settings ); + + return await QueryAsync( command, parameters, deserializer ); + } + + public async Task> QueryAsync( string command, Func deserializer ) + => await QueryAsync( command, new Dictionary(), deserializer ); + + public async Task> QueryAsync( string command, IDictionary parameters, Func deserializer ) + { + using( HttpClient client = CreateClient() ) + { + string cmd = CreateCommand( command, parameters ); + + HttpResponseMessage response = await client.GetAsync( cmd ).ConfigureAwait( false ); + + string json = await response.Content.ReadAsStringAsync().ConfigureAwait( false ); + + if( !response.IsSuccessStatusCode ) + { + // rate limit will not exist if there is an error. + var error = new ApiQueryResponse + { + Error = JsonConvert.DeserializeObject( json ), + CommandText = response.RequestMessage.RequestUri.ToString(), + Json = json, + }; + + return error; + } + + var result = new ApiQueryResponse + { + CommandText = response.RequestMessage.RequestUri.ToString(), + Json = json, + }; + + T item = deserializer( json ); + result.Item = item; + return result; + } + } + + public async Task> SearchAsync( string command ) + => await SearchAsync( command, 1 ); + + public async Task> SearchAsync( string command, int pageNumber ) + => await SearchAsync( command, pageNumber, new Dictionary() ); + + public async Task> SearchAsync( string command, IDictionary parameters ) + => await SearchAsync( command, 1, parameters ); + + public async Task> SearchAsync( string command, int pageNumber, IDictionary parameters ) + { + pageNumber = pageNumber < 1 ? 1 : pageNumber; + pageNumber = pageNumber > 1000 ? 1000 : pageNumber; + + if( !parameters.Keys.Contains( "page", StringComparer.OrdinalIgnoreCase ) ) + { + parameters.Add( "page", pageNumber.ToString() ); + } + + using( HttpClient client = CreateClient() ) + { + string cmd = CreateCommand( command, parameters ); + + HttpResponseMessage response = await client.GetAsync( cmd ).ConfigureAwait( false ); + + string json = await response.Content.ReadAsStringAsync().ConfigureAwait( false ); + + // rate limit will not exist if there is an error. + if( !response.IsSuccessStatusCode ) + { + var error = new ApiSearchResponse + { + // This will throw up if the error is page number = 0; the resultant json will be: {"errors":["page must be greater than 0"]} + // in other words, the json will not include a status_code. Asked the api devs and this is a known issue they are working on. + // What to do? Nothing really, the page guard at the top of the method will keep the page number > 0. + Error = JsonConvert.DeserializeObject( json ), + CommandText = response.RequestMessage.RequestUri.ToString(), + Json = json, + }; + + return error; + } + + var result = JsonConvert.DeserializeObject>( json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + + result.CommandText = response.RequestMessage.RequestUri.ToString(); + result.Json = json; + + return result; + } + } + + protected HttpClient CreateClient() + { + var handler = new HttpClientHandler + { + AllowAutoRedirect = false, + UseCookies = false, + UseDefaultCredentials = true, + AutomaticDecompression = DecompressionMethods.GZip, + }; + + var client = new HttpClient( handler ); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue( "application/json" ) ); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", _settings.BearerToken ); + client.BaseAddress = new Uri( _settings.ApiUrl ); + + return client; + } + + protected string CreateCommand( string rootCommand ) + => CreateCommand( rootCommand, new Dictionary() ); + + protected string CreateCommand( string rootCommand, IDictionary parameters ) + { + string tokens = parameters.Any() + ? string.Join( "&", parameters.Select( x => x.Key + "=" + x.Value ) ) + : string.Empty; + + if( string.IsNullOrWhiteSpace( tokens ) == false ) + { + rootCommand += $"?{tokens}"; + } + + return rootCommand; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/IApiRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/IApiRequest.cs new file mode 100644 index 00000000..809449c6 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/IApiRequest.cs @@ -0,0 +1,8 @@ +namespace DM.MovieApi.ApiRequest +{ + /// + /// Interface to provide a constraint for all MovieDb Api Request interfaces/classes. + /// + public interface IApiRequest + { } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/IsoDateTimeConverterEx.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/IsoDateTimeConverterEx.cs new file mode 100644 index 00000000..a56ea147 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiRequest/IsoDateTimeConverterEx.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace DM.MovieApi.ApiRequest +{ + /// + /// Extends the native Newtonsoft IsoDateTimeConverter to allow deserializing partial dates. + /// + public class IsoDateTimeConverterEx : IsoDateTimeConverter + { + /// + /// Reads the JSON representation of the object. + /// + /// The Newtonsoft.Json.JsonReader to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) + { + ConditionalTraceReaderValue( reader ); + + try + { + return base.ReadJson( reader, objectType, existingValue, serializer ); + } + catch( Exception ex ) when( ex is FormatException + || ex is JsonSerializationException jse + && jse.Message.Contains( "System.DateTime" ) ) + { + string val = reader.Value?.ToString(); + + if( val?.Length == 4 && int.TryParse( val, out int year ) ) + { + return new DateTime( year, 1, 1 ); + } + + return default( DateTime ); + } + } + + [Conditional( "DEBUG" )] + private void ConditionalTraceReaderValue( JsonReader reader ) + { + string val = reader.Value?.ToString(); + if( string.IsNullOrWhiteSpace( val ) ) + { + val = ""; + } + + Debug.WriteLine( $"IsoDateTimeConverterEx.JsonReader.Value: {val}" ); + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiError.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiError.cs new file mode 100644 index 00000000..696d79fe --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiError.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.Serialization; + +namespace DM.MovieApi.ApiResponse +{ + [DataContract] + public class ApiError + { + private int _statusCode; + + [DataMember( Name = "status_code" )] + public int StatusCode + { + get => _statusCode; + private set + { + _statusCode = value; + + TmdbStatusCode = Enum.IsDefined( typeof( TmdbStatusCode ), _statusCode ) + ? ( TmdbStatusCode )_statusCode + : TmdbStatusCode.Unknown; + } + } + + [DataMember( Name = "status_message" )] + public string Message { get; private set; } + + public TmdbStatusCode TmdbStatusCode { get; private set; } + + public override string ToString() + => $"Status: {StatusCode}: {Message}"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiQueryResponse.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiQueryResponse.cs new file mode 100644 index 00000000..0551d511 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiQueryResponse.cs @@ -0,0 +1,17 @@ +namespace DM.MovieApi.ApiResponse +{ + /// + /// Standard response from an API call returning a single specific result. + /// Multiple item based based results (i.e., searches) are returned with an . + /// + public class ApiQueryResponse : ApiResponseBase + { + /// + /// The item returned from the API call. + /// + public T Item { get; internal set; } + + public override string ToString() + => Item.ToString(); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiResponseBase.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiResponseBase.cs new file mode 100644 index 00000000..e151fd2a --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiResponseBase.cs @@ -0,0 +1,26 @@ +namespace DM.MovieApi.ApiResponse +{ + /// + /// Base class for all API responses from themoviedb.org. + /// + public abstract class ApiResponseBase + { + /// + /// Contains specific error information if an error was encountered during the API call to themoviedb.org. + /// + public ApiError Error { get; internal set; } + + /// + /// The API command text used for the API call to themoviedb.org. + /// + public string CommandText { get; internal set; } + + /// + /// The JSON returned from themoviedb.org based on the query. + /// + public string Json { get; internal set; } + + public override string ToString() + => CommandText; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiSearchResponse.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiSearchResponse.cs new file mode 100644 index 00000000..80efba23 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/ApiSearchResponse.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace DM.MovieApi.ApiResponse +{ + /// + /// Standard response from an API call returning a more than one result, i.e., a Search Result. + /// Single item based results are returned with an + /// . + /// + [DataContract] + public class ApiSearchResponse : ApiResponseBase + { + /// + /// The list of results from the search. + /// + [DataMember( Name = "results" )] + public IReadOnlyList Results { get; private set; } + + /// + /// The current page number of the search result. + /// + [DataMember( Name = "page" )] + public int PageNumber { get; private set; } + + /// + /// The total number of pages found from the search result. + /// + [DataMember( Name = "total_pages" )] + public int TotalPages { get; private set; } + + /// + /// The total number of results from the search. + /// + [DataMember( Name = "total_results" )] + public int TotalResults { get; private set; } + + public override string ToString() + => $"Page {PageNumber} of {TotalPages} ({TotalResults} total results)"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/TmdbStatusCode.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/TmdbStatusCode.cs new file mode 100644 index 00000000..235d9049 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/ApiResponse/TmdbStatusCode.cs @@ -0,0 +1,217 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DM.MovieApi.ApiResponse +{ + /// + /// themoviedb.org Status Codes as defined by: https://www.themoviedb.org/documentation/api/status-codes + /// + [SuppressMessage( "ReSharper", "UnusedMember.Global" )] + public enum TmdbStatusCode + { + Unknown = 0, + + /// + /// 200: Success. + /// + //[Description( "200: Success." )] + Success = 1, + + /// + /// 501: Invalid service: this service does not exist. + /// + //[Description( "501: Invalid service: this service does not exist." )] + InvalidService = 2, + + /// + /// 401: Authentication failed: You do not have permissions to access the service. + /// + //[Description( "401: Authentication failed: You do not have permissions to access the service." )] + InsufficientPermissions = 3, + + /// + /// 405: Invalid format: This service doesn't exist in that format. + /// + //[Description( "405: Invalid format: This service doesn't exist in that format." )] + InvalidFormat = 4, + + /// + /// 422: Invalid parameters: Your request parameters are incorrect. + /// + //[Description( "422: Invalid parameters: Your request parameters are incorrect." )] + InvalidParameters = 5, + + /// + /// 404: Invalid id: The pre-requisite id is invalid or not found. + /// + //[Description( "404: Invalid id: The pre-requisite id is invalid or not found." )] + InvalidId = 6, + + /// + /// 401: Invalid API key: You must be granted a valid key. + /// + //[Description( "401: Invalid API key: You must be granted a valid key." )] + InvalidApiKey = 7, + + /// + /// 403: Duplicate entry: The data you tried to submit already exists. + /// + //[Description( "403: Duplicate entry: The data you tried to submit already exists." )] + DuplicateEntry = 8, + + /// + /// 503: Service offline: This service is temporarily offline, try again later. + /// + //[Description( "503: Service offline: This service is temporarily offline, try again later." )] + ServiceOffline = 9, + + /// + /// 503: Service offline: This service is temporarily offline, try again later. + /// + //[Description( "401: Suspended API key: Access to your account has been suspended, contact TMDb." )] + SuspendedApiKey = 10, + + /// + /// 503: Service offline: This service is temporarily offline, try again later. + /// + //[Description( "500: Internal error: Something went wrong, contact TMDb." )] + InternalError = 11, + + /// + /// 201: The item/record was updated successfully. + /// + //[Description( "201: The item/record was updated successfully." )] + SuccessfulUpdate = 12, + + /// + /// 200: The item/record was deleted successfully. + /// + //[Description( "200: The item/record was deleted successfully." )] + SuccessfulDelete = 13, + + /// + /// 401: Authentication failed. + /// + //[Description( "401: Authentication failed." )] + AuthenticationFailed = 14, + + /// + /// 500: Failed. + /// + //[Description( "500: Failed." )] + Failed = 15, + + /// + /// 401: Device denied. + /// + //[Description( "401: Device denied." )] + DeviceDenied = 16, + + /// + /// 401: Session denied. + /// + //[Description( "401: Session denied." )] + SessionDenied = 17, + + /// + /// 400: Validation failed. + /// + //[Description( "400: Validation failed." )] + ValidationFailed = 18, + + /// + /// 406: Invalid accept header. + /// + //[Description( "406: Invalid accept header." )] + InvalidAcceptHeader = 19, + + /// + /// 422: Invalid date range: Should be a range no longer than 14 days. + /// + //[Description( "422: Invalid date range: Should be a range no longer than 14 days." )] + InvalidDateRange = 20, + + /// + /// 200: Entry not found: The item you are trying to edit cannot be found. + /// + //[Description( "200: Entry not found: The item you are trying to edit cannot be found." )] + EntryNotFound = 21, + + /// + /// 400: Invalid page: Pages start at 1 and max at 1000. They are expected to be an integer. + /// + //[Description( "400: Invalid page: Pages start at 1 and max at 1000. They are expected to be an integer." )] + InvalidPage = 22, + + /// + /// 400: Invalid date: Format needs to be YYYY-MM-DD. + /// + //[Description( "400: Invalid date: Format needs to be YYYY-MM-DD." )] + InvalidDate = 23, + + /// + /// 400: Invalid date: Format needs to be YYYY-MM-DD. + /// + //[Description( "504: Your request to the backend server timed out. Try again." )] + ServerTimeout = 24, + + /// + /// 400: Invalid date: Format needs to be YYYY-MM-DD. + /// + //[Description( "429: Your request count (#) is over the allowed limit of (40)." )] + RequestOverLimit = 25, + + /// + /// "400: You must provide a username and password. + /// + //[Description( "400: You must provide a username and password." )] + AuthenticationRequired = 26, + + /// + /// 400: Too many append to response objects: The maximum number of remote calls is 20. + /// + //[Description( "400: Too many append to response objects: The maximum number of remote calls is 20." )] + ResponseObjectOverflow = 27, + + /// + /// 400: Invalid timezone: Please consult the documentation for a valid timezone. + /// + //[Description( "400: Invalid timezone: Please consult the documentation for a valid timezone." )] + InvalidTimezone = 28, + + /// + /// 400: Invalid timezone: Please consult the documentation for a valid timezone. + /// + //[Description( "400: You must confirm this action: Please provide a confirm=true parameter." )] + ActionMustBeConfirmed = 29, + + /// + /// 401: Invalid username and/or password: You did not provide a valid login. + /// + //[Description( "401: Invalid username and/or password: You did not provide a valid login." )] + InvalidAuthentication = 30, + + /// + /// 401: Account disabled: Your account is no longer active. Contact TMDb if this is an error. + /// + //[Description( "401: Account disabled: Your account is no longer active. Contact TMDb if this is an error." )] + AccountDisabled = 31, + + /// + /// 401: Email not verified: Your email address has not been verified. + /// + //[Description( "401: Email not verified: Your email address has not been verified." )] + EmailNotVerified = 32, + + /// + /// 401: Invalid request token: The request token is either expired or invalid. + /// + //[Description( "401: Invalid request token: The request token is either expired or invalid." )] + InvalidRequestToken = 33, + + /// + /// 401: The resource you requested could not be found. + /// + //[Description( "401: The resource you requested could not be found." )] + ResourceNotFound = 34, + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/IApiSettings.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/IApiSettings.cs new file mode 100644 index 00000000..d90d93bd --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/IApiSettings.cs @@ -0,0 +1,9 @@ +namespace DM.MovieApi +{ + internal interface IApiSettings + { + string ApiUrl { get; } + + string BearerToken { get; } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/IMovieDbApi.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/IMovieDbApi.cs new file mode 100644 index 00000000..bf525b45 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/IMovieDbApi.cs @@ -0,0 +1,58 @@ +using DM.MovieApi.MovieDb.Certifications; +using DM.MovieApi.MovieDb.Companies; +using DM.MovieApi.MovieDb.Configuration; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.MovieDb.IndustryProfessions; +using DM.MovieApi.MovieDb.Movies; +using DM.MovieApi.MovieDb.People; +using DM.MovieApi.MovieDb.TV; + +namespace DM.MovieApi +{ + /// + /// Global interface exposing all API interfaces against themoviedb.org that are + /// currently available in this release. + /// + public interface IMovieDbApi + { + /// + /// Provides access for retrieving production company information. + /// + IApiCompanyRequest Companies { get; } + + /// + /// Provides access for retrieving themoviedb.org configuration information. + /// + IApiConfigurationRequest Configuration { get; } + + /// + /// Provides access for retrieving Movie and TV genres. + /// + IApiGenreRequest Genres { get; } + + /// + /// Provides access for retrieving information about Movie/TV industry specific professions. + /// + IApiProfessionRequest IndustryProfessions { get; } + + /// + /// Provides access for retrieving information about Movies. + /// + IApiMovieRequest Movies { get; } + + /// + /// Provides access for retrieving movie rating information. + /// + IApiMovieRatingRequest MovieRatings { get; } + + /// + /// Provides access for retrieving information about Television shows. + /// + IApiTVShowRequest Television { get; } + + /// + /// Provides access for retrieving information about People. + /// + IApiPeopleRequest People { get; } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/LICENSE b/MetaNodes/ThirdParty/TheMovieDbWrapper/LICENSE new file mode 100644 index 00000000..d2bb5f40 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 kindler chase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/ApiMovieRatingRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/ApiMovieRatingRequest.cs new file mode 100644 index 00000000..922cb3e6 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/ApiMovieRatingRequest.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.Shims; +using Newtonsoft.Json.Linq; + +namespace DM.MovieApi.MovieDb.Certifications +{ + internal class ApiMovieRatingRequest : ApiRequestBase, IApiMovieRatingRequest + { + [ImportingConstructor] + public ApiMovieRatingRequest( IApiSettings settings ) + : base( settings ) + { } + + public async Task> GetMovieRatingsAsync() + { + const string command = "certification/movie/list"; + + ApiQueryResponse response = await base.QueryAsync( command, RatingsDeserializer ); + + return response; + } + + private MovieRatings RatingsDeserializer( string json ) + { + var obj = JObject.Parse( json ); + + JToken certs = obj["certifications"]; + + // ReSharper disable once PossibleNullReferenceException + var ratings = certs.ToObject(); + + Func, IReadOnlyList> reorder = + list => list.OrderBy( x => x.Order ).ThenBy( x => x.Rating ).ToList().AsReadOnly(); + + // ReSharper disable once PossibleNullReferenceException + ratings.Australia = reorder( ratings.Australia ); + ratings.Canada = reorder( ratings.Canada ); + ratings.France = reorder( ratings.France ); + ratings.Germany = reorder( ratings.Germany ); + ratings.India = reorder( ratings.India ); + ratings.NewZealand = reorder( ratings.NewZealand ); + ratings.UnitedKingdom = reorder( ratings.UnitedKingdom ); + ratings.UnitedStates = reorder( ratings.UnitedStates ); + + return ratings; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/IApiMovieRatingRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/IApiMovieRatingRequest.cs new file mode 100644 index 00000000..337a7490 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/IApiMovieRatingRequest.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; + +namespace DM.MovieApi.MovieDb.Certifications +{ + /// + /// Interface for retrieving movie rating information. + /// + public interface IApiMovieRatingRequest : IApiRequest + { + /// + /// Gets the list of supported certifications (movie ratings) for movies. + /// + Task> GetMovieRatingsAsync(); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/MovieRatings.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/MovieRatings.cs new file mode 100644 index 00000000..afd99e16 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Certifications/MovieRatings.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Certifications +{ + [DataContract] + public class Certification + { + [DataMember( Name = "certification" )] + public string Rating { get; set; } + + [DataMember( Name = "meaning" )] + public string Meaning { get; set; } + + [DataMember( Name = "order" )] + public int Order { get; set; } + + public override string ToString() + => $"{Rating}: {Meaning.Substring( 75 )}"; + } + + [DataContract] + public class MovieRatings + { + [DataMember( Name = "AU" )] + public IReadOnlyList Australia { get; set; } + + [DataMember( Name = "CA" )] + public IReadOnlyList Canada { get; set; } + + [DataMember( Name = "FR" )] + public IReadOnlyList France { get; set; } + + [DataMember( Name = "DE" )] + public IReadOnlyList Germany { get; set; } + + [DataMember( Name = "IN" )] + public IReadOnlyList India { get; set; } + + [DataMember( Name = "NZ" )] + public IReadOnlyList NewZealand { get; set; } + + [DataMember( Name = "US" )] + public IReadOnlyList UnitedStates { get; set; } + + [DataMember( Name = "GB" )] + public IReadOnlyList UnitedKingdom { get; set; } + + public MovieRatings() + { + UnitedStates = Array.Empty(); + Canada = Array.Empty(); + Australia = Array.Empty(); + Germany = Array.Empty(); + France = Array.Empty(); + NewZealand = Array.Empty(); + India = Array.Empty(); + UnitedKingdom = Array.Empty(); + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Collections/CollectionInfo.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Collections/CollectionInfo.cs new file mode 100644 index 00000000..e1e7d2c9 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Collections/CollectionInfo.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Collections +{ + [DataContract] + public class CollectionInfo + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "backdrop_path" )] + public string BackdropPath { get; set; } + + public override string ToString() + { + if( string.IsNullOrWhiteSpace( Name ) ) + { + return "n/a"; + } + + return $"{Name} ({Id})"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ApiCompanyRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ApiCompanyRequest.cs new file mode 100644 index 00000000..bdc7dc4f --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ApiCompanyRequest.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.MovieDb.Movies; +using DM.MovieApi.Shims; + +namespace DM.MovieApi.MovieDb.Companies +{ + internal class ApiCompanyRequest : ApiRequestBase, IApiCompanyRequest + { + private readonly IApiGenreRequest _genreApi; + + [ImportingConstructor] + public ApiCompanyRequest( IApiSettings settings, IApiGenreRequest genreApi ) + : base( settings ) + { + _genreApi = genreApi; + } + + public async Task> FindByIdAsync( int companyId ) + { + string command = $"company/{companyId}"; + + ApiQueryResponse response = await base.QueryAsync( command ); + + return response; + } + + public async Task> GetMoviesAsync( int companyId, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + string command = $"company/{companyId}/movies"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/IApiCompanyRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/IApiCompanyRequest.cs new file mode 100644 index 00000000..5a7cbef5 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/IApiCompanyRequest.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Movies; + +namespace DM.MovieApi.MovieDb.Companies +{ + /// + /// Interface for retrieving information about a production company. + /// + public interface IApiCompanyRequest : IApiRequest + { + /// + /// Gets all the basic information about a specific company. + /// + /// The company Id is typically found from a Movie or TV query. + Task> FindByIdAsync( int companyId ); + + /// + /// Get the list of movies associated with a particular company. + /// + /// The company Id is typically found from a Movie or TV query. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + /// + Task> GetMoviesAsync( int companyId, int pageNumber = 1, string language = "en" ); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ParentCompany.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ParentCompany.cs new file mode 100644 index 00000000..4600a586 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ParentCompany.cs @@ -0,0 +1,27 @@ +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Companies +{ + [DataContract] + public class ParentCompany + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "logo_path" )] + public string LogoPath { get; set; } + + public override string ToString() + { + if( string.IsNullOrWhiteSpace( Name ) ) + { + return "n/a"; + } + + return $"{Name} ({Id})"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ProductionCompany.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ProductionCompany.cs new file mode 100644 index 00000000..2daa3c10 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ProductionCompany.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Companies +{ + [DataContract] + public class ProductionCompany + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "description" )] + public string Description { get; set; } + + [DataMember( Name = "headquarters" )] + public string Headquarters { get; set; } + + [DataMember( Name = "homepage" )] + public string Homepage { get; set; } + + [DataMember( Name = "logo_path" )] + public string LogoPath { get; set; } + + [DataMember( Name = "parent_company" )] + public ParentCompany ParentCompany { get; set; } + + public override string ToString() + => $"{Name} ({Id})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ProductionCompanyInfo.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ProductionCompanyInfo.cs new file mode 100644 index 00000000..54266b31 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Companies/ProductionCompanyInfo.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Companies +{ + [DataContract] + public class ProductionCompanyInfo : IEqualityComparer + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + public ProductionCompanyInfo( int id, string name ) + { + Id = id; + Name = name; + } + + public override bool Equals( object obj ) + { + if( obj is not ProductionCompanyInfo info ) + { + return false; + } + + return Equals( this, info ); + } + + public bool Equals( ProductionCompanyInfo x, ProductionCompanyInfo y ) + => x != null && y != null && x.Id == y.Id && x.Name == y.Name; + + public override int GetHashCode() + => GetHashCode( this ); + + public int GetHashCode( ProductionCompanyInfo obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Id.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override string ToString() + { + if( string.IsNullOrWhiteSpace( Name ) ) + { + return "n/a"; + } + + return $"{Name} ({Id})"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ApiConfiguration.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ApiConfiguration.cs new file mode 100644 index 00000000..fa3fb4ed --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ApiConfiguration.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Configuration +{ + [DataContract] + public class ApiConfiguration + { + [DataMember( Name = "images" )] + public ImageConfiguration Images { get; private set; } + + [DataMember( Name = "change_keys" )] + public IReadOnlyList ChangeKeys { get; private set; } + + public override string ToString() + { + if( !string.IsNullOrWhiteSpace( Images?.RootUrl ) ) + { + return Images.RootUrl; + } + + return "not set"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ApiConfigurationRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ApiConfigurationRequest.cs new file mode 100644 index 00000000..c30d7398 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ApiConfigurationRequest.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.Shims; + +namespace DM.MovieApi.MovieDb.Configuration +{ + internal class ApiConfigurationRequest : ApiRequestBase, IApiConfigurationRequest + { + [ImportingConstructor] + public ApiConfigurationRequest( IApiSettings settings ) + : base( settings ) + { } + + public async Task> GetAsync() + { + ApiQueryResponse response = await base.QueryAsync( "configuration" ); + + return response; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/IApiConfigurationRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/IApiConfigurationRequest.cs new file mode 100644 index 00000000..eea30872 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/IApiConfigurationRequest.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; + +namespace DM.MovieApi.MovieDb.Configuration +{ + /// + /// Interface for retrieving themoviedb.org configuration information. + /// + public interface IApiConfigurationRequest : IApiRequest + { + /// + /// Get themoviedb.org system wide configuration information. Some elements of themoviedb.org + /// API require knowledge of the configuration data. The purpose of the + /// is to try and keep the actual API responses as light as possible. + /// It is recommended you cache this data within your application and check for updates every few days. + /// This method currently holds the data relevant to building image URLs as well as the change key map. + /// + Task> GetAsync(); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ImageConfiguration.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ImageConfiguration.cs new file mode 100644 index 00000000..0cec007f --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Configuration/ImageConfiguration.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Configuration +{ + [DataContract] + public class ImageConfiguration + { + [DataMember( Name = "base_url" )] + public string RootUrl { get; private set; } + + [DataMember( Name = "secure_base_url" )] + public string SecureRootUrl { get; private set; } + + [DataMember( Name = "backdrop_sizes" )] + public IReadOnlyList BackDrops { get; private set; } + + [DataMember( Name = "logo_sizes" )] + public IReadOnlyList Logos { get; private set; } + + [DataMember( Name = "poster_sizes" )] + public IReadOnlyList Posters { get; private set; } + + [DataMember( Name = "profile_sizes" )] + public IReadOnlyList Profiles { get; private set; } + + [DataMember( Name = "still_sizes" )] + public IReadOnlyList Stills { get; private set; } + + public override string ToString() + { + if( !string.IsNullOrWhiteSpace( RootUrl ) ) + { + return RootUrl; + } + + return "not set"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Country.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Country.cs new file mode 100644 index 00000000..7c3d3682 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Country.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb +{ + [DataContract] + public class Country : IEqualityComparer + { + [DataMember( Name = "iso_3166_1" )] + public string Iso3166Code { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + public Country( string iso3166Code, string name ) + { + Iso3166Code = iso3166Code; + Name = name; + } + + public override bool Equals( object obj ) + { + if( obj is not Country country ) + { + return false; + } + + return Equals( this, country ); + } + + public bool Equals( Country x, Country y ) + => x != null && y != null && x.Iso3166Code == y.Iso3166Code && x.Name == y.Name; + + public override int GetHashCode() => + GetHashCode( this ); + + public int GetHashCode( Country obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Iso3166Code.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override string ToString() + { + if( string.IsNullOrWhiteSpace( Name ) ) + { + return "n/a"; + } + + return $"{Name} ({Iso3166Code})"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/ApiGenreRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/ApiGenreRequest.cs new file mode 100644 index 00000000..f2ccc031 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/ApiGenreRequest.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Movies; +using DM.MovieApi.Shims; +using Newtonsoft.Json.Linq; + +namespace DM.MovieApi.MovieDb.Genres +{ + internal class ApiGenreRequest : ApiRequestBase, IApiGenreRequest + { + // ReSharper disable once InconsistentNaming + private static readonly List _allGenres = new(); + + public IReadOnlyList AllGenres + { + get + { + if( _allGenres.Any() == false ) + { + var genres = Task.Run( () => GetAllAsync() ).GetAwaiter().GetResult().Item; + _allGenres.AddRange( genres ); + } + + return _allGenres.AsReadOnly(); + } + } + + [ImportingConstructor] + public ApiGenreRequest( IApiSettings settings ) + : base( settings ) + { } + + public async Task> FindByIdAsync( int genreId, string language = "en" ) + { + var param = new Dictionary + { + {"language", language} + }; + + string command = $"genre/{genreId}"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + EnsureAllGenres( response ); + + return response; + } + + public async Task>> GetAllAsync( string language = "en" ) + { + ApiQueryResponse> tv = await GetTelevisionAsync( language ); + if( tv.Error != null ) + { + return tv; + } + + ApiQueryResponse> movies = await GetMoviesAsync( language ); + if( movies.Error != null ) + { + return movies; + } + + List merged = movies.Item + .Union( tv.Item ) + .OrderBy( x => x.Name ) + .ToList(); + + movies.Item = merged.AsReadOnly(); + + return movies; + } + + public async Task>> GetMoviesAsync( string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + ApiQueryResponse> genres = await base.QueryAsync( "genre/movie/list", param, GenreDeserializer ); + + return genres; + } + + public async Task>> GetTelevisionAsync( string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + ApiQueryResponse> genres = await base.QueryAsync( "genre/tv/list", param, GenreDeserializer ); + + return genres; + } + + public async Task> FindMoviesByIdAsync( int genreId, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + {"include_adult", "false"}, + }; + + string command = $"genre/{genreId}/movies"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( this ); + + return response; + } + + internal void ClearAllGenres() + => _allGenres.Clear(); + + private void EnsureAllGenres( ApiQueryResponse response ) + { + if( response.Error != null ) + { + return; + } + + if( response.Item == null ) + { + return; + } + + if( _allGenres.Contains( response.Item ) == false ) + { + _allGenres.Add( response.Item ); + } + } + + private IReadOnlyList GenreDeserializer( string json ) + { + var obj = JObject.Parse( json ); + + var arr = ( JArray )obj["genres"]; + + // ReSharper disable once PossibleNullReferenceException + var genres = arr.ToObject>(); + + return genres; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/Genre.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/Genre.cs new file mode 100644 index 00000000..2ab6c10b --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/Genre.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Genres +{ + [DataContract] + public class Genre : IEqualityComparer + { + [DataMember( Name = "id" )] + public int Id { get; private set; } + + [DataMember( Name = "name" )] + public string Name { get; private set; } + + public Genre( int id, string name ) + { + Id = id; + Name = name; + } + + public override bool Equals( object obj ) + { + if( obj is not Genre genre ) + { + return false; + } + + return Equals( this, genre ); + } + + public bool Equals( Genre x, Genre y ) + => x != null && y != null && x.Id == y.Id && x.Name == y.Name; + + public override int GetHashCode() + => GetHashCode( this ); + + public int GetHashCode( Genre obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Id.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override string ToString() + => $"{Name} ({Id})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/GenreFactory.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/GenreFactory.cs new file mode 100644 index 00000000..a8a1077c --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/GenreFactory.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +// ReSharper disable UnusedMember.Global + +namespace DM.MovieApi.MovieDb.Genres +{ + public static class GenreFactory + { + public static Genre Action() + => new( 28, "Action" ); + + public static Genre Adventure() + => new( 12, "Adventure" ); + + public static Genre ActionAndAdventure() + => new( 10759, "Action & Adventure" ); + + public static Genre Animation() + => new( 16, "Animation" ); + + public static Genre Comedy() + => new( 35, "Comedy" ); + + public static Genre Crime() + => new( 80, "Crime" ); + + public static Genre Drama() + => new( 18, "Drama" ); + + public static Genre Documentary() + => new( 99, "Documentary" ); + + public static Genre Family() + => new( 10751, "Family" ); + + public static Genre Fantasy() + => new( 14, "Fantasy" ); + + public static Genre History() + => new( 36, "History" ); + + public static Genre Horror() + => new( 27, "Horror" ); + + public static Genre Kids() + => new( 10762, "Kids" ); + + public static Genre Music() + => new( 10402, "Music" ); + + public static Genre Mystery() + => new( 9648, "Mystery" ); + + public static Genre News() + => new( 10763, "News" ); + + public static Genre Reality() + => new( 10764, "Reality" ); + + public static Genre Romance() + => new( 10749, "Romance" ); + + public static Genre ScienceFiction() + => new( 878, "Science Fiction" ); + + public static Genre SciFiAndFantasy() + => new( 10765, "Sci-Fi & Fantasy" ); + + public static Genre Soap() + => new( 10766, "Soap" ); + + public static Genre Talk() + => new( 10767, "Talk" ); + + public static Genre Thriller() + => new( 53, "Thriller" ); + + public static Genre TvMovie() + => new( 10770, "TV Movie" ); + + public static Genre War() + => new( 10752, "War" ); + + public static Genre WarAndPolitics() + => new( 10768, "War & Politics" ); + + public static Genre Western() + => new( 37, "Western" ); + + public static IReadOnlyList GetAll() + => LazyAll.Value; + + + private static readonly Lazy> LazyAll = new( () => + { + var all = typeof( GenreFactory ) + .GetTypeInfo() + .DeclaredMethods + .Where( x => x.IsStatic ) + .Where( x => x.IsPublic ) + .Where( x => x.ReturnType == typeof( Genre ) ) + .Select( x => ( Genre )x.Invoke( null, null ) ) + .ToList(); + + return all.AsReadOnly(); + } ); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/GenreIdCollectionMappingExtensions.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/GenreIdCollectionMappingExtensions.cs new file mode 100644 index 00000000..71027dcb --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/GenreIdCollectionMappingExtensions.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DM.MovieApi.MovieDb.Movies; +using DM.MovieApi.MovieDb.People; +using DM.MovieApi.MovieDb.TV; + +namespace DM.MovieApi.MovieDb.Genres +{ + internal static class GenreIdCollectionMappingExtensions + { + public static void PopulateGenres( this IEnumerable movies, IApiGenreRequest api ) + { + foreach( MovieInfo movie in movies ) + { + movie.Genres = MapGenreIdsToGenres( movie.GenreIds, api ); + } + } + + public static void PopulateGenres( this IEnumerable tvShows, IApiGenreRequest api ) + { + foreach( TVShowInfo tvShow in tvShows ) + { + tvShow.Genres = MapGenreIdsToGenres( tvShow.GenreIds, api ); + } + } + + public static void PopulateGenres( this IEnumerable people, IApiGenreRequest api ) + { + foreach( PersonInfo person in people ) + { + foreach( PersonInfoRole role in person.KnownFor ) + { + role.Genres = MapGenreIdsToGenres( role.GenreIds, api ); + } + } + } + + private static IReadOnlyList MapGenreIdsToGenres( IEnumerable genreIds, IApiGenreRequest api ) + { + IReadOnlyList genres = genreIds + .Select( x => MapGenre( x, api ) ) + .ToList() + .AsReadOnly(); + + return genres; + } + + private static Genre MapGenre( int genreId, IApiGenreRequest api ) + { + Genre genre = api.AllGenres.FirstOrDefault( x => x.Id == genreId ); + + if( genre == null ) + { + genre = Task.Run( () => api.FindByIdAsync( genreId ) ).GetAwaiter().GetResult().Item; + } + + return genre; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/IApiGenreRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/IApiGenreRequest.cs new file mode 100644 index 00000000..b756aaea --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Genres/IApiGenreRequest.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Movies; + +namespace DM.MovieApi.MovieDb.Genres +{ + /// + /// Interface representing Movie and TV genres. + /// + public interface IApiGenreRequest : IApiRequest + { + /// + /// Provides a cache of all the genres (language='en') returned from . + /// As the Genres do not change much, if any, the cache is never evicted. + /// + IReadOnlyList AllGenres { get; } + + /// + /// Gets all the information about a specific Genre. + /// + /// The genre Id is typically found from a more generic Movie or TV query. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> FindByIdAsync( int genreId, string language = "en" ); + + /// + /// It is recommended to use the property, unless a + /// language specific parameter other than 'en' is provided. + /// + /// themoviedb.org api mixes tv and movie genres into their movies and tv titles. + /// Use this method to ensure all genres are accounted for when attempting to join + /// on Genre.Id from a search result; by default, search results only contain genre + /// id and excludes the name. + /// + /// + /// In some rare cases, a genre is not included in the movie or tv genres list; when this + /// occurs, use the method to find a matching genre. + /// + /// + /// The merged set of Movie and TV Genres. + Task>> GetAllAsync( string language = "en" ); + + /// + /// Gets all movie related Genres. + /// + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task>> GetMoviesAsync( string language = "en" ); + + /// + /// Gets all tv related Genres. + /// + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task>> GetTelevisionAsync( string language = "en" ); + + /// + /// Finds all movies related to a genre, where the Id passed to this method is a genre Id, not a movie Id. + /// + /// The genre Id is typically found through from a related Movie request or from any of the Genre API methods. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> FindMoviesByIdAsync( int genreId, int pageNumber = 1, string language = "en" ); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/ApiProfessionRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/ApiProfessionRequest.cs new file mode 100644 index 00000000..a010c244 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/ApiProfessionRequest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.Shims; +using Newtonsoft.Json.Linq; + +namespace DM.MovieApi.MovieDb.IndustryProfessions +{ + internal class ApiProfessionRequest : ApiRequestBase, IApiProfessionRequest + { + [ImportingConstructor] + public ApiProfessionRequest( IApiSettings settings ) + : base( settings ) + { } + + public Task>> GetAllAsync() + { + const string command = "job/list"; + + Task>> response = base.QueryAsync( command, ProfessionDeserializer ); + + return response; + } + + private IReadOnlyList ProfessionDeserializer( string json ) + { + var obj = JObject.Parse( json ); + + var arr = ( JArray )obj["jobs"]; + + // ReSharper disable once PossibleNullReferenceException + var professions = arr.ToObject>(); + + return professions; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/IApiProfessionRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/IApiProfessionRequest.cs new file mode 100644 index 00000000..e83f1e6f --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/IApiProfessionRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; + +namespace DM.MovieApi.MovieDb.IndustryProfessions +{ + /// + /// Interface for retrieving information about Movie/TV industry specific professions. + /// + public interface IApiProfessionRequest : IApiRequest + { + /// + /// Gets all the Movie/TV industry specific professions. + /// + Task>> GetAllAsync(); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/Profession.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/Profession.cs new file mode 100644 index 00000000..813bb5c5 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/IndustryProfessions/Profession.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.IndustryProfessions +{ + [DataContract] + public class Profession + { + [DataMember( Name = "department" )] + public string Department { get; set; } + + [DataMember( Name = "jobs" )] + public IReadOnlyList Jobs { get; set; } + + public override string ToString() + => $"{Department} {Jobs.Count} jobs"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Keywords/Keyword.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Keywords/Keyword.cs new file mode 100644 index 00000000..73cd10d1 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Keywords/Keyword.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Keywords +{ + [DataContract] + public class Keyword : IEqualityComparer + { + /// + /// The keyword Id as identified by theMovieDB.org. + /// + [DataMember( Name = "id" )] + public int Id { get; set; } + + /// + /// The keyword. + /// + [DataMember( Name = "name" )] + public string Name { get; set; } + + public Keyword( int id, string name ) + { + Id = id; + Name = name; + } + + public override bool Equals( object obj ) + { + if( obj is not Keyword genre ) + { + return false; + } + + return Equals( this, genre ); + } + + public bool Equals( Keyword x, Keyword y ) + => x != null && y != null && x.Id == y.Id && x.Name == y.Name; + + public override int GetHashCode() + => GetHashCode( this ); + + public int GetHashCode( Keyword obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Id.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override string ToString() + => $"{Name} ({Id})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Keywords/KeywordConverter.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Keywords/KeywordConverter.cs new file mode 100644 index 00000000..a30773e9 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Keywords/KeywordConverter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DM.MovieApi.MovieDb.Keywords +{ + /// + /// Expected parent json node is "keywords". The child node is variable + /// and should be set as a parameter to the JsonConverter attribute which + /// will use the KeywordConverter .ctor to create the converter with the + /// provided parameter. + /// + internal class KeywordConverter : JsonConverter + { + private readonly string _key; + + public KeywordConverter( string key ) + { + _key = key; + } + + public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) + { + throw new NotImplementedException(); + } + + public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) + { + JToken obj = JToken.Load( reader ); + + var arr = ( JArray )obj[_key]; + + // ReSharper disable once PossibleNullReferenceException + var keywords = arr.ToObject>(); + + return keywords; + } + + public override bool CanConvert( Type objectType ) + => false; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Language.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Language.cs new file mode 100644 index 00000000..a3fe94ed --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Language.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb +{ + [DataContract] + public class Language : IEqualityComparer + { + [DataMember( Name = "iso_639_1" )] + public string Iso639Code { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + public Language( string iso639Code, string name ) + { + Iso639Code = iso639Code; + Name = name; + } + + public override bool Equals( object obj ) + { + if( obj is not Language language ) + { + return false; + } + + return Equals( this, language ); + } + + public bool Equals( Language x, Language y ) + => x != null && y != null && x.Iso639Code == y.Iso639Code && x.Name == y.Name; + + public override int GetHashCode() + => GetHashCode( this ); + + public int GetHashCode( Language obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Iso639Code.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override string ToString() + { + if( string.IsNullOrWhiteSpace( Name ) ) + { + return "n/a"; + } + + return $"{Name} ({Iso639Code})"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/ApiMovieRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/ApiMovieRequest.cs new file mode 100644 index 00000000..e7a1d604 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/ApiMovieRequest.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.Shims; + +namespace DM.MovieApi.MovieDb.Movies +{ + internal class ApiMovieRequest : ApiRequestBase, IApiMovieRequest + { + private readonly IApiGenreRequest _genreApi; + + [ImportingConstructor] + public ApiMovieRequest( IApiSettings settings, IApiGenreRequest genreApi ) + : base( settings ) + { + _genreApi = genreApi; + } + + public async Task> FindByIdAsync( int movieId, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + {"append_to_response", "keywords"}, + }; + + string command = $"movie/{movieId}"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + + public async Task> SearchByTitleAsync( string query, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"query", query}, + {"include_adult", "false"}, + {"language", language}, + }; + + const string command = "search/movie"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetLatestAsync( string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + {"append_to_response", "keywords"}, + }; + + const string command = "movie/latest"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + + public async Task> GetNowPlayingAsync( int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + {"append_to_response", "keywords"}, + }; + + const string command = "movie/now_playing"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + return response; + } + + public async Task> GetUpcomingAsync( int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + {"append_to_response", "keywords"}, + }; + + const string command = "movie/upcoming"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + return response; + } + + public async Task> GetTopRatedAsync( int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + const string command = "movie/top_rated"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetPopularAsync( int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + const string command = "movie/popular"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetCreditsAsync( int movieId, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + string command = $"movie/{movieId}/credits"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/IApiMovieRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/IApiMovieRequest.cs new file mode 100644 index 00000000..f919d5a7 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/IApiMovieRequest.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; + +namespace DM.MovieApi.MovieDb.Movies +{ + /// + /// Interface for retrieving information about Movies. + /// + public interface IApiMovieRequest : IApiRequest + { + /// + /// Gets all the information about a specific Movie. + /// + /// The movie Id is typically found from a more generic Movie query. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> FindByIdAsync( int movieId, string language = "en" ); + + /// + /// Searches for Movies by title. + /// + /// The query to search for Movies. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> SearchByTitleAsync( string query, int pageNumber = 1, string language = "en" ); + + /// + /// Gets the most recent movie that has been added to TheMovieDb.org. + /// + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetLatestAsync( string language = "en" ); + + /// + /// Gets the list of movies playing that have been, or are being released this week. + /// + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetNowPlayingAsync( int pageNumber = 1, string language = "en" ); + + /// + /// Gets the list of upcoming movies by release date. + /// + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetUpcomingAsync( int pageNumber = 1, string language = "en" ); + + /// + /// Gets the list of top rated movies which is refreshed daily. + /// + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetTopRatedAsync( int pageNumber = 1, string language = "en" ); + + /// + /// Gets the list of popular movies which is refreshed daily. + /// + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetPopularAsync( int pageNumber = 1, string language = "en" ); + + /// + /// Get the cast and crew information for a specific movie id. + /// + /// The movie Id is typically found from a more generic Movie query. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetCreditsAsync( int movieId, string language = "en" ); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/Movie.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/Movie.cs new file mode 100644 index 00000000..4dc1f84f --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/Movie.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using DM.MovieApi.MovieDb.Collections; +using DM.MovieApi.MovieDb.Companies; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.MovieDb.Keywords; +using Newtonsoft.Json; + +namespace DM.MovieApi.MovieDb.Movies +{ + [DataContract] + public class Movie + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "title" )] + public string Title { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultThemed { get; set; } + + [DataMember( Name = "backdrop_path" )] + public string BackdropPath { get; set; } + + [DataMember( Name = "belongs_to_collection" )] + public CollectionInfo MovieCollectionInfo { get; set; } + + [DataMember( Name = "budget" )] + public int Budget { get; set; } + + [DataMember( Name = "genres" )] + public IReadOnlyList Genres { get; set; } + + [DataMember( Name = "homepage" )] + public string Homepage { get; set; } + + [DataMember( Name = "imdb_id" )] + public string ImdbId { get; set; } + + /// + /// ISO 3166-1 code. + /// + [DataMember( Name = "original_language" )] + public string OriginalLanguage { get; set; } + + [DataMember( Name = "original_title" )] + public string OriginalTitle { get; set; } + + [DataMember( Name = "overview" )] + public string Overview { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "production_companies" )] + public IReadOnlyList ProductionCompanies { get; set; } + + [DataMember( Name = "production_countries" )] + public IReadOnlyList ProductionCountries { get; set; } + + [DataMember( Name = "release_date" )] + public DateTime ReleaseDate { get; set; } + + [DataMember( Name = "revenue" )] + public decimal Revenue { get; set; } + + [DataMember( Name = "runtime" )] + public int Runtime { get; set; } + + [DataMember( Name = "spoken_languages" )] + public IReadOnlyList SpokenLanguages { get; set; } + + [DataMember( Name = "status" )] + public string Status { get; set; } + + [DataMember( Name = "tagline" )] + public string Tagline { get; set; } + + [DataMember( Name = "video" )] + public bool IsVideo { get; set; } + + [DataMember( Name = "vote_average" )] + public double VoteAverage { get; set; } + + [DataMember( Name = "vote_count" )] + public int VoteCount { get; set; } + + [DataMember( Name = "keywords" )] + [JsonConverter( typeof( KeywordConverter ), "keywords" )] + public IReadOnlyList Keywords { get; set; } + + public Movie() + { + Genres = Array.Empty(); + Keywords = Array.Empty(); + ProductionCompanies = Array.Empty(); + ProductionCountries = Array.Empty(); + SpokenLanguages = Array.Empty(); + } + + public override string ToString() + => $"{Title} ({ReleaseDate:yyyy-MM-dd}) [{Id}]"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/MovieCredit.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/MovieCredit.cs new file mode 100644 index 00000000..11fa133a --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/MovieCredit.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.Movies +{ + [DataContract] + public class MovieCredit + { + [DataMember( Name = "id" )] + public int MovieId { get; set; } + + [DataMember( Name = "cast" )] + public IReadOnlyList CastMembers { get; set; } + + [DataMember( Name = "crew" )] + public IReadOnlyList CrewMembers { get; set; } + } + + [DataContract] + public class MovieCastMember + { + [DataMember( Name = "id" )] + public int PersonId { get; set; } + + [DataMember( Name = "cast_id" )] + public int CastId { get; set; } + + [DataMember( Name = "credit_id" )] + public string CreditId { get; set; } + + [DataMember( Name = "character" )] + public string Character { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "order" )] + public int Order { get; set; } + + [DataMember( Name = "profile_path" )] + public string ProfilePath { get; set; } + + public override string ToString() + => $"{Character}: {Name}"; + } + + [DataContract] + public class MovieCrewMember + { + [DataMember( Name = "id" )] + public int PersonId { get; set; } + + [DataMember( Name = "credit_id" )] + public string CreditId { get; set; } + + [DataMember( Name = "department" )] + public string Department { get; set; } + + [DataMember( Name = "job" )] + public string Job { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "profile_path" )] + public string ProfilePath { get; set; } + + public override string ToString() + => $"{Name} | {Department} | {Job}"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/MovieInfo.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/MovieInfo.cs new file mode 100644 index 00000000..bca1e1fd --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/Movies/MovieInfo.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using DM.MovieApi.MovieDb.Genres; + +namespace DM.MovieApi.MovieDb.Movies +{ + [DataContract] + public class MovieInfo + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "title" )] + public string Title { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultThemed { get; set; } + + [DataMember( Name = "backdrop_path" )] + public string BackdropPath { get; set; } + + [DataMember( Name = "genre_ids" )] + internal IReadOnlyList GenreIds { get; set; } + + public IReadOnlyList Genres { get; set; } + + [DataMember( Name = "original_title" )] + public string OriginalTitle { get; set; } + + [DataMember( Name = "overview" )] + public string Overview { get; set; } + + [DataMember( Name = "release_date" )] + public DateTime ReleaseDate { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; set; } + + [DataMember( Name = "video" )] + public bool Video { get; set; } + + [DataMember( Name = "vote_average" )] + public double VoteAverage { get; set; } + + [DataMember( Name = "vote_count" )] + public int VoteCount { get; set; } + + public MovieInfo() + { + GenreIds = Array.Empty(); + Genres = Array.Empty(); + } + + public override string ToString() + => $"{Title} ({Id} - {ReleaseDate:yyyy-MM-dd})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/ApiPeopleRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/ApiPeopleRequest.cs new file mode 100644 index 00000000..f0b94696 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/ApiPeopleRequest.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.Shims; + +namespace DM.MovieApi.MovieDb.People +{ + internal class ApiPeopleRequest : ApiRequestBase, IApiPeopleRequest + { + private readonly IApiGenreRequest _genreApi; + + [ImportingConstructor] + public ApiPeopleRequest( IApiSettings settings, IApiGenreRequest genreApi ) + : base( settings ) + { + _genreApi = genreApi; + } + + public async Task> FindByIdAsync( int personId, string language = "en" ) + { + var param = new Dictionary + { + {"language", language} + }; + + string command = $"person/{personId}"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + + public async Task> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"query", query}, + {"include_adult", "false"}, + {"language", language}, + }; + + const string command = "search/person"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetMovieCreditsAsync( int personId, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + string command = $"person/{personId}/movie_credits"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + + public async Task> GetTVCreditsAsync( int personId, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + string command = $"person/{personId}/tv_credits"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/Gender.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/Gender.cs new file mode 100644 index 00000000..b49f5126 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/Gender.cs @@ -0,0 +1,9 @@ +namespace DM.MovieApi.MovieDb.People +{ + public enum Gender + { + Unknown = 0, + Female = 1, + Male = 2, + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/IApiPeopleRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/IApiPeopleRequest.cs new file mode 100644 index 00000000..c403666e --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/IApiPeopleRequest.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; + +namespace DM.MovieApi.MovieDb.People +{ + /// + /// Interface for retrieving information about People. + /// + public interface IApiPeopleRequest : IApiRequest + { + /// + /// Gets all the information about a specific Person. + /// + /// The person Id is typically found from a more generic query such as movie or television or search. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> FindByIdAsync( int personId, string language = "en" ); + + /// + /// Searches for People by name. + /// + /// The query to search for People. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" ); + + /// + /// Get the movie credits for a specific person id. Includes movie cast and crew information for the person. + /// + /// The person Id is typically found from a more generic query such as movie or television or search. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetMovieCreditsAsync( int personId, string language = "en" ); + + /// + /// Get the television credits for a specific person id. Includes TV cast and crew information for the person. + /// + /// The person Id is typically found from a more generic query such as movie or television or search. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetTVCreditsAsync( int personId, string language = "en" ); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/Person.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/Person.cs new file mode 100644 index 00000000..5d765be1 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/Person.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.People +{ + [DataContract] + public class Person + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "also_known_as" )] + public IReadOnlyList AlsoKnownAs { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultFilmStar { get; set; } + + [DataMember( Name = "biography" )] + public string Biography { get; set; } + + [DataMember( Name = "birthday" )] + public DateTime Birthday { get; set; } + + [DataMember( Name = "deathday" )] + public DateTime? Deathday { get; set; } + + [DataMember( Name = "gender" )] + public Gender Gender { get; set; } + + [DataMember( Name = "homepage" )] + public string Homepage { get; set; } + + [DataMember( Name = "imdb_id" )] + public string ImdbId { get; set; } + + [DataMember( Name = "place_of_birth" )] + public string PlaceOfBirth { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; set; } + + [DataMember( Name = "profile_path" )] + public string ProfilePath { get; set; } + + public Person() + { + AlsoKnownAs = Array.Empty(); + } + + public override string ToString() + => Name; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonInfo.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonInfo.cs new file mode 100644 index 00000000..ad450b59 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonInfo.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using DM.MovieApi.MovieDb.Genres; + +namespace DM.MovieApi.MovieDb.People +{ + public enum MediaType + { + Unknown, + Movie, + TV, + } + + [DataContract] + public class PersonInfo + { + // TODO: (K. Chase) [2016-07-10] Update all POCO's to explicitly name the Id property, i.e,. PersonId, MovieId, TVShowId. + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultFilmStar { get; set; } + + [DataMember( Name = "known_for" )] + public IReadOnlyList KnownFor { get; set; } + + [DataMember( Name = "profile_path" )] + public string ProfilePath { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; set; } + + public PersonInfo() + { + KnownFor = Array.Empty(); + } + + public override string ToString() + => $"{Name} ({Id})"; + } + + [DataContract] + public class PersonInfoRole + { + // TODO: (K. Chase) [2016-07-10] Break into type for Movie and TV w/ a custom serializer. + // re: see TVShowName v MovieTitle (and related) + + /// + /// The MovieId or TVShowId as defined by the value of . + /// + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "media_type" )] + public MediaType MediaType { get; set; } + + /// + /// Only populated when is TV. + /// + [DataMember( Name = "name" )] + public string TVShowName { get; set; } + + /// + /// Only populated when is TV. + /// + [DataMember( Name = "original_name" )] + public string TVShowOriginalName { get; set; } + + /// + /// Only populated when is Movie. + /// + [DataMember( Name = "title" )] + public string MovieTitle { get; set; } + + /// + /// Only populated when is Movie. + /// + [DataMember( Name = "original_title" )] + public string MovieOriginalTitle { set; get; } + + [DataMember( Name = "backdrop_path" )] + public string BackdropPath { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + /// + /// Only populated when is Movie. + /// + [DataMember( Name = "release_date" )] + public DateTime MovieReleaseDate { get; set; } + + /// + /// Only populated when is TV. + /// + [DataMember( Name = "first_air_date" )] + public DateTime TVShowFirstAirDate { get; set; } + + [DataMember( Name = "overview" )] + public string Overview { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultThemed { get; set; } + + [DataMember( Name = "video" )] + public bool IsVideo { get; set; } + + [DataMember( Name = "genre_ids" )] + internal IReadOnlyList GenreIds { get; set; } + + public IReadOnlyList Genres { get; set; } + + [DataMember( Name = "original_language" )] + public string OriginalLanguage { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; set; } + + [DataMember( Name = "vote_count" )] + public int VoteCount { get; set; } + + [DataMember( Name = "vote_average" )] + public double VoteAverage { get; set; } + + [DataMember( Name = "origin_country" )] + public IReadOnlyList OriginCountry { get; set; } + + public PersonInfoRole() + { + GenreIds = Array.Empty(); + Genres = Array.Empty(); + OriginCountry = Array.Empty(); + } + + public override string ToString() + { + return MediaType == MediaType.Movie + ? $"Movie: {MovieTitle} ({Id} - {MovieReleaseDate:yyyy-MM-dd})" + : $"TV: {TVShowName} ({Id} - {TVShowFirstAirDate:yyyy-MM-dd})"; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonMovieCredit.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonMovieCredit.cs new file mode 100644 index 00000000..8555a650 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonMovieCredit.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.People +{ + [DataContract] + public class PersonMovieCredit + { + [DataMember( Name = "id" )] + public int PersonId { get; set; } + + [DataMember( Name = "cast" )] + public IReadOnlyList CastRoles { get; set; } + + [DataMember( Name = "crew" )] + public IReadOnlyList CrewRoles { get; set; } + + public PersonMovieCredit() + { + CastRoles = Array.Empty(); + CrewRoles = Array.Empty(); + } + } + + [DataContract] + public class PersonMovieCastMember + { + [DataMember( Name = "id" )] + public int MovieId { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultThemed { get; set; } + + [DataMember( Name = "character" )] + public string Character { get; set; } + + [DataMember( Name = "credit_id" )] + public string CreditId { get; set; } + + [DataMember( Name = "original_title" )] + public string OriginalTitle { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "release_date" )] + public DateTime ReleaseDate { get; set; } + + [DataMember( Name = "title" )] + public string Title { get; set; } + } + + [DataContract] + public class PersonMovieCrewMember + { + [DataMember( Name = "id" )] + public int MovieId { get; set; } + + [DataMember( Name = "adult" )] + public bool IsAdultThemed { get; set; } + + [DataMember( Name = "credit_id" )] + public string CreditId { get; set; } + + [DataMember( Name = "department" )] + public string Department { get; set; } + + [DataMember( Name = "job" )] + public string Job { get; set; } + + [DataMember( Name = "original_title" )] + public string OriginalTitle { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "release_date" )] + public DateTime ReleaseDate { get; set; } + + [DataMember( Name = "title" )] + public string Title { get; set; } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonTVCredit.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonTVCredit.cs new file mode 100644 index 00000000..9587162e --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/People/PersonTVCredit.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.People +{ + [DataContract] + public class PersonTVCredit + { + [DataMember( Name = "id" )] + public int PersonId { get; set; } + + [DataMember( Name = "cast" )] + public IReadOnlyList CastRoles { get; set; } + + [DataMember( Name = "crew" )] + public IReadOnlyList CrewRoles { get; set; } + + public PersonTVCredit() + { + CastRoles = Array.Empty(); + CrewRoles = Array.Empty(); + } + } + + [DataContract] + public class PersonTVCastMember + { + [DataMember( Name = "id" )] + public int TVShowId { get; set; } + + [DataMember( Name = "character" )] + public string Character { get; set; } + + [DataMember( Name = "credit_id" )] + public string CreditId { get; set; } + + [DataMember( Name = "episode_count" )] + public int EpisodeCount { get; set; } + + [DataMember( Name = "first_air_date" )] + public DateTime FirstAirDate { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "original_name" )] + public string OriginalName { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + } + + [DataContract] + public class PersonTVCrewMember + { + [DataMember( Name = "id" )] + public int TVShowId { get; set; } + + [DataMember( Name = "credit_id" )] + public string CreditId { get; set; } + + [DataMember( Name = "department" )] + public string Department { get; set; } + + [DataMember( Name = "episode_count" )] + public int EpisodeCount { get; set; } + + [DataMember( Name = "first_air_date" )] + public DateTime FirstAirDate { get; set; } + + [DataMember( Name = "job" )] + public string Job { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "original_name" )] + public string OriginalName { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/ApiTVShowRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/ApiTVShowRequest.cs new file mode 100644 index 00000000..2e69dfbb --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/ApiTVShowRequest.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.Shims; + +namespace DM.MovieApi.MovieDb.TV +{ + internal class ApiTVShowRequest : ApiRequestBase, IApiTVShowRequest + { + private readonly IApiGenreRequest _genreApi; + + [ImportingConstructor] + public ApiTVShowRequest( IApiSettings settings, IApiGenreRequest genreApi ) + : base( settings ) + { + _genreApi = genreApi; + } + + public async Task> FindByIdAsync( int tvShowId, string language = "en" ) + { + var param = new Dictionary + { + { "language", language }, + { "append_to_response", "keywords" }, + }; + + string command = $"tv/{tvShowId}"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + + public async Task> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + { "query", query }, + { "language", language } + }; + + const string command = "search/tv"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetLatestAsync( string language = "en" ) + { + var param = new Dictionary + { + { "language", language }, + { "append_to_response", "keywords" }, + }; + + const string command = "tv/latest"; + + ApiQueryResponse response = await base.QueryAsync( command, param ); + + return response; + } + + public async Task> GetTopRatedAsync( int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + { "language", language } + }; + + const string command = "tv/top_rated"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetPopularAsync( int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + { "language", language } + }; + + const string command = "tv/popular"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/IApiTVShowRequest.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/IApiTVShowRequest.cs new file mode 100644 index 00000000..d2b1c3b8 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/IApiTVShowRequest.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; + +namespace DM.MovieApi.MovieDb.TV +{ + /// + /// Interface for retrieving information about TV shows. + /// + public interface IApiTVShowRequest : IApiRequest + { + /// + /// Gets all the information about a specific TV show. + /// + /// The TV show Id which is typically found from a more generic TV show query. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> FindByIdAsync( int tvShowId, string language = "en" ); + + /// + /// Searches for TV shows by title. + /// + /// The query to search for TV shows. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" ); + + /// + /// Gets the latest TV show added to TheMovieDb.org + /// + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetLatestAsync( string language = "en" ); + + /// + /// Gets the list of top rated TV shows which is refreshed daily. + /// + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetTopRatedAsync( int pageNumber = 1, string language = "en" ); + + /// + /// Gets the list of popular TV shows which is refreshed daily. + /// + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetPopularAsync( int pageNumber = 1, string language = "en" ); + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/Network.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/Network.cs new file mode 100644 index 00000000..ec9a3030 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/Network.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.TV +{ + [DataContract] + public class Network : IEqualityComparer + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + public Network( int id, string name ) + { + Id = id; + Name = name; + } + + public bool Equals( Network x, Network y ) + => x != null && y != null && x.Id == y.Id && x.Name == y.Name; + + public int GetHashCode( Network obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Id.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override bool Equals( object obj ) + { + if( obj is not Network network ) + { + return false; + } + + return Equals( this, network ); + } + + public override int GetHashCode() + => GetHashCode( this ); + + public override string ToString() + => $"{Name} ({Id})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/Season.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/Season.cs new file mode 100644 index 00000000..aeb51fe8 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/Season.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.TV +{ + [DataContract] + public class Season + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "air_date" )] + public DateTime AirDate { get; set; } + + [DataMember( Name = "episode_count" )] + public int EpisodeCount { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "season_number" )] + public int SeasonNumber { get; set; } + + public Season( int id, DateTime airDate, int episodeCount, string posterPath, int seasonNumber ) + { + Id = id; + AirDate = airDate; + EpisodeCount = episodeCount; + PosterPath = posterPath; + SeasonNumber = seasonNumber; + } + + public override string ToString() + => $"({SeasonNumber} - {AirDate:yyyy-MM-dd})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShow.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShow.cs new file mode 100644 index 00000000..8db76c17 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShow.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using DM.MovieApi.MovieDb.Companies; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.MovieDb.Keywords; +using Newtonsoft.Json; + +namespace DM.MovieApi.MovieDb.TV +{ + [DataContract] + public class TVShow + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "backdrop_path" )] + public string BackdropPath { get; set; } + + [DataMember( Name = "created_by" )] + public IReadOnlyList CreatedBy { get; set; } + + [DataMember( Name = "episode_run_time" )] + public IReadOnlyList EpisodeRunTime { get; set; } + + [DataMember( Name = "first_air_date" )] + public DateTime FirstAirDate { get; set; } + + [DataMember( Name = "genres" )] + public IReadOnlyList Genres { get; set; } + + [DataMember( Name = "homepage" )] + public string Homepage { get; set; } + + [DataMember( Name = "in_production" )] + public bool InProduction { get; set; } + + [DataMember( Name = "languages" )] + public IReadOnlyList Languages { get; set; } + + [DataMember( Name = "last_air_date" )] + public DateTime LastAirDate { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "networks" )] + public IReadOnlyList Networks { get; set; } + + [DataMember( Name = "number_of_episodes" )] + public int NumberOfEpisodes { get; set; } + + [DataMember( Name = "number_of_seasons" )] + public int NumberOfSeasons { get; set; } + + [DataMember( Name = "origin_country" )] + public IReadOnlyList OriginCountry { get; set; } + + [DataMember( Name = "original_language" )] + public string OriginalLanguage { get; set; } + + [DataMember( Name = "original_name" )] + public string OriginalName { get; set; } + + [DataMember( Name = "overview" )] + public string Overview { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "production_companies" )] + public IReadOnlyList ProductionCompanies { get; set; } + + [DataMember( Name = "seasons" )] + public IReadOnlyList Seasons { get; set; } + + [DataMember( Name = "keywords" )] + [JsonConverter( typeof( KeywordConverter ), "results" )] + public IReadOnlyList Keywords { get; set; } + + public TVShow() + { + CreatedBy = Array.Empty(); + EpisodeRunTime = Array.Empty(); + Genres = Array.Empty(); + Languages = Array.Empty(); + Networks = Array.Empty(); + OriginCountry = Array.Empty(); + ProductionCompanies = Array.Empty(); + Seasons = Array.Empty(); + Keywords = Array.Empty(); + } + + public override string ToString() + => $"{Name} ({FirstAirDate:yyyy-MM-dd}) [{Id}]"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShowCreator.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShowCreator.cs new file mode 100644 index 00000000..5625e1c7 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShowCreator.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace DM.MovieApi.MovieDb.TV +{ + [DataContract] + public class TVShowCreator : IEqualityComparer + { + [DataMember( Name = "id" )] + public int Id { get; set; } + + [DataMember( Name = "name" )] + public string Name { get; set; } + + [DataMember( Name = "profile_path" )] + public string ProfilePath { get; set; } + + public TVShowCreator( int id, string name, string profilePath ) + { + Id = id; + Name = name; + ProfilePath = profilePath; + } + + public bool Equals( TVShowCreator x, TVShowCreator y ) + => x != null && y != null && x.Id == y.Id && x.Name == y.Name; + + public int GetHashCode( TVShowCreator obj ) + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + obj.Id.GetHashCode(); + hash = hash * 23 + obj.Name.GetHashCode(); + return hash; + } + } + + public override bool Equals( object obj ) + { + if( obj is not TVShowCreator showCreator ) + { + return false; + } + + return Equals( this, showCreator ); + } + + public override int GetHashCode() + => GetHashCode( this ); + + public override string ToString() + => $"{Name} ({Id})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShowInfo.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShowInfo.cs new file mode 100644 index 00000000..7b0abfae --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDb/TV/TVShowInfo.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using DM.MovieApi.MovieDb.Genres; +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace DM.MovieApi.MovieDb.TV +{ + [DataContract] + public class TVShowInfo + { + [DataMember( Name = "id" )] + public int Id { get; private set; } + + [DataMember( Name = "name" )] + public string Name { get; private set; } + + [DataMember( Name = "original_name" )] + public string OriginalName { get; private set; } + + [DataMember( Name = "poster_path" )] + public string PosterPath { get; set; } + + [DataMember( Name = "backdrop_path" )] + public string BackdropPath { get; set; } + + [DataMember( Name = "popularity" )] + public double Popularity { get; private set; } + + [DataMember( Name = "vote_average" )] + public double VoteAverage { get; private set; } + + [DataMember( Name = "vote_count" )] + public int VoteCount { get; private set; } + + [DataMember( Name = "overview" )] + public string Overview { get; private set; } + + [DataMember( Name = "first_air_date" )] + public DateTime FirstAirDate { get; private set; } + + [DataMember( Name = "origin_country" )] + public IReadOnlyList OriginCountry { get; private set; } + + [DataMember( Name = "genre_ids" )] + internal IReadOnlyList GenreIds { get; set; } + + public IReadOnlyList Genres { get; internal set; } + + [DataMember( Name = "original_language" )] + public string OriginalLanguage { get; private set; } + + public TVShowInfo() + { + OriginCountry = Array.Empty(); + GenreIds = Array.Empty(); + Genres = Array.Empty(); + } + + public override string ToString() + => $"{Name} ({Id} - {FirstAirDate:yyyy-MM-dd})"; + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDbFactory.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDbFactory.cs new file mode 100644 index 00000000..dfd24173 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/MovieDbFactory.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.Shims; + +[assembly: InternalsVisibleTo( "DM.MovieApi.IntegrationTests" )] + +namespace DM.MovieApi +{ + /// + /// Note: one of the RegisterSettings must be called before the Factory can Create anything. + /// + public static class MovieDbFactory + { + /// + public const string TheMovieDbApiUrl = "http://api.themoviedb.org/3/"; + + /// + /// Determines if the underlying factory has been created. + /// + public static bool IsFactoryComposed => Settings != null; + + internal static IApiSettings Settings { get; private set; } + + /// + /// Registers themoviedb.org settings for use with the internal DI container. + /// + /// + /// + /// + public static void RegisterSettings( string bearerToken ) + { + ResetFactory(); + + if( bearerToken is null || bearerToken.Length <= 200 ) + { + // v3 access key was approx 33 chars; v4 bearer is approx 212 chars. + throw new ArgumentException( + $"Must provide a valid TheMovieDb.org Bearer token. Invalid: {bearerToken}. " + + "A valid token can be found in your account page, under the API section. " + + "You will see a new key listed under the header \"API Read Access Token\".", bearerToken ); + } + + Settings = new MovieDbSettings( TheMovieDbApiUrl, bearerToken ); + } + + /// + /// Creates the specific API requested. + /// + /// + public static Lazy Create() where T : IApiRequest + { + ContainerGuard(); + + var requestResolver = new ApiRequestResolver(); + + return new Lazy( requestResolver.Get ); + } + + /// + /// Creates a global instance exposing all API interfaces against themoviedb.org + /// that are currently available in this release. Each API is exposed via a Lazy property + /// ensuring no objects are created until they are needed. + /// + /// + public static IMovieDbApi GetAllApiRequests() + { + ContainerGuard(); + + // Note: the concrete implementation is currently excluded from the .csproj, but is still included in source control. + + string msg = $"{nameof( GetAllApiRequests )} has been temporarily disabled due to porting the code base to Asp.Net Core to provide support for portable library projects."; + throw new NotImplementedException( msg ); + } + + /// + /// Clears all factory settings; forces the next call to be RegisterSettings. + /// before can be called. + /// + public static void ResetFactory() + { + Settings = null; + } + + private static void ContainerGuard() + { + if( !IsFactoryComposed ) + { + throw new InvalidOperationException( $"{nameof( RegisterSettings )} must be called before the Factory can Create anything." ); + } + } + + private class MovieDbSettings : IApiSettings + { + public string ApiUrl { get; } + public string BearerToken { get; } + + public MovieDbSettings( string apiUrl, string bearerToken ) + { + ApiUrl = apiUrl; + BearerToken = bearerToken; + } + } + + private class ApiRequestResolver + { + private static readonly IReadOnlyDictionary> SupportedDependencyTypeMap; + private static readonly ConcurrentDictionary TypeCtorMap; + + static ApiRequestResolver() + { + SupportedDependencyTypeMap = new Dictionary> + { + {typeof(IApiSettings), () => Settings}, + {typeof(IApiGenreRequest), () => new ApiGenreRequest( Settings )} + }; + + TypeCtorMap = new ConcurrentDictionary(); + } + + public T Get() where T : IApiRequest + { + ConstructorInfo ctor = TypeCtorMap.GetOrAdd( typeof( T ), GetConstructor ); + + ParameterInfo[] param = ctor.GetParameters(); + + if( param.Length == 0 ) + { + return ( T )ctor.Invoke( null ); + } + + var paramObjects = new List( param.Length ); + foreach( ParameterInfo p in param ) + { + if( SupportedDependencyTypeMap.ContainsKey( p.ParameterType ) == false ) + { + throw new InvalidOperationException( $"{p.ParameterType.FullName} is not a supported dependency type for {typeof( T ).FullName}." ); + } + + Func typeResolver = SupportedDependencyTypeMap[p.ParameterType]; + + paramObjects.Add( typeResolver() ); + } + + return ( T )ctor.Invoke( paramObjects.ToArray() ); + } + + private ConstructorInfo GetConstructor( Type t ) + { + ConstructorInfo[] ctors = GetAvailableConstructors( t.GetTypeInfo() ); + + if( ctors.Length == 0 ) + { + throw new InvalidOperationException( $"No public constructors found for {t.FullName}." ); + } + + if( ctors.Length == 1 ) + { + return ctors[0]; + } + + var importingCtors = ctors + .Where( x => x.IsDefined( typeof( ImportingConstructorAttribute ) ) ) + .ToArray(); + + if( importingCtors.Length != 1 ) + { + throw new InvalidOperationException( "Multiple public constructors found. " + + $"One must be decorated with the {nameof( ImportingConstructorAttribute )}." ); + } + + return importingCtors[0]; + } + + private ConstructorInfo[] GetAvailableConstructors( TypeInfo typeInfo ) + { + TypeInfo[] implementingTypes = typeInfo.Assembly.DefinedTypes + .Where( x => x.IsAbstract == false ) + .Where( x => x.IsInterface == false ) + .Where( typeInfo.IsAssignableFrom ) + .ToArray(); + + if( implementingTypes.Length == 0 ) + { + throw new NotSupportedException( $"{typeInfo.Name} must have a concrete implementation." ); + } + + if( implementingTypes.Length != 1 ) + { + throw new NotSupportedException( $"Requested type: {typeInfo.Name}. " + + "Multiple implementations per request interface is not supported." ); + } + + return implementingTypes[0].DeclaredConstructors.ToArray(); + } + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/Shims/CollectionExtensions.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/Shims/CollectionExtensions.cs new file mode 100644 index 00000000..31bedf08 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/Shims/CollectionExtensions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace DM.MovieApi.Shims +{ + public static class CollectionExtensions + { + public static IReadOnlyList AsReadOnly( this List list ) + { + return new ReadOnlyCollection( list ); + } + } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/Shims/ImportingConstructorAttribute.cs b/MetaNodes/ThirdParty/TheMovieDbWrapper/Shims/ImportingConstructorAttribute.cs new file mode 100644 index 00000000..c288c0f5 --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/Shims/ImportingConstructorAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace DM.MovieApi.Shims +{ + [AttributeUsage( AttributeTargets.Constructor )] + internal sealed class ImportingConstructorAttribute : Attribute + { } +} diff --git a/MetaNodes/ThirdParty/TheMovieDbWrapper/info b/MetaNodes/ThirdParty/TheMovieDbWrapper/info new file mode 100644 index 00000000..16c20d0c --- /dev/null +++ b/MetaNodes/ThirdParty/TheMovieDbWrapper/info @@ -0,0 +1,7 @@ +This code is written by nCubed and was taken from +https://github.com/nCubed/TheMovieDbWrapper/ +On November 22nd 2021 + +No changes have been done to the code, its just easier to include the code in this DLL instead of having to load it separately. + +See the license for this for more information. \ No newline at end of file diff --git a/Plugin.deps.json b/Plugin.deps.json deleted file mode 100644 index dbbdf7b2..00000000 --- a/Plugin.deps.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v6.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v6.0": { - "Plugin/1.0.0": { - "runtime": { - "Plugin.dll": {} - } - } - } - }, - "libraries": { - "Plugin/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - } - } -} \ No newline at end of file diff --git a/Plugin.pdb b/Plugin.pdb index 7baf204c..11726bb1 100644 Binary files a/Plugin.pdb and b/Plugin.pdb differ diff --git a/VideoNodes/VideoNodes.csproj b/VideoNodes/VideoNodes.csproj index 74686b32..a966e680 100644 Binary files a/VideoNodes/VideoNodes.csproj and b/VideoNodes/VideoNodes.csproj differ diff --git a/plugins.json b/plugins.json index e1a6118d..fa4884f6 100644 --- a/plugins.json +++ b/plugins.json @@ -1,12 +1,17 @@ [ { "Name": "BasicNodes", - "Version": "0.0.1.8", + "Version": "0.0.1.9", "Package": "https://github.com/revenz/FileFlowsPlugins/blob/master/Builds/BasicNodes.zip?raw=true" }, + { + "Name": "MetaNodes", + "Version": "0.0.1.9", + "Package": "https://github.com/revenz/FileFlowsPlugins/blob/master/Builds/MetaNodes.zip?raw=true" + }, { "Name": "VideoNodes", - "Version": "0.0.1.8", + "Version": "0.0.1.9", "Package": "https://github.com/revenz/FileFlowsPlugins/blob/master/Builds/VideoNodes.zip?raw=true" } ]