added MetaNodes plugin

This commit is contained in:
reven
2021-11-22 22:38:45 +13:00
parent 2dbd59537f
commit 761b82dbff
79 changed files with 4079 additions and 70 deletions
+3
View File
@@ -4,3 +4,6 @@ Plugin.dll
*/.vs
*.suo
TestStore/
.vs/FileFlowsPlugins/v17/.futdcache.v1
.vs/FileFlowsPlugins/DesignTimeBuild/.dtbcache.v2
.vs/FileFlowsPlugins/project-colors.json
Binary file not shown.
+10 -38
View File
@@ -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;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16 -7
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
namespace MetaNodes
{
internal class Globals
{
public static string MOVIE_INFO = "MovieInfo";
}
}
Binary file not shown.
+24
View File
@@ -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"
}
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace MetaNodes
{
using System.ComponentModel.DataAnnotations;
public class Plugin : FileFlows.Plugin.IPlugin
{
public string Name => "Meta Nodes";
public void Init() { }
}
}
+43
View File
@@ -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<string> Messages = new List<string>();
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);
}
}
}
@@ -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
@@ -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
+105
View File
@@ -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<IApiMovieRequest>().Value;
ApiSearchResponse<MovieInfo> 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;
}
}
}
+80
View File
@@ -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<MovieInfo>(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);
}
}
}
@@ -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<ApiQueryResponse<T>> QueryAsync<T>( string command )
=> await QueryAsync<T>( command, new Dictionary<string, string>() );
public async Task<ApiQueryResponse<T>> QueryAsync<T>( string command, IDictionary<string, string> parameters )
{
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
};
settings.Converters.Add( new IsoDateTimeConverterEx() );
Func<string, T> deserializer = json => JsonConvert.DeserializeObject<T>( json, settings );
return await QueryAsync( command, parameters, deserializer );
}
public async Task<ApiQueryResponse<T>> QueryAsync<T>( string command, Func<string, T> deserializer )
=> await QueryAsync( command, new Dictionary<string, string>(), deserializer );
public async Task<ApiQueryResponse<T>> QueryAsync<T>( string command, IDictionary<string, string> parameters, Func<string, T> 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<T>
{
Error = JsonConvert.DeserializeObject<ApiError>( json ),
CommandText = response.RequestMessage.RequestUri.ToString(),
Json = json,
};
return error;
}
var result = new ApiQueryResponse<T>
{
CommandText = response.RequestMessage.RequestUri.ToString(),
Json = json,
};
T item = deserializer( json );
result.Item = item;
return result;
}
}
public async Task<ApiSearchResponse<T>> SearchAsync<T>( string command )
=> await SearchAsync<T>( command, 1 );
public async Task<ApiSearchResponse<T>> SearchAsync<T>( string command, int pageNumber )
=> await SearchAsync<T>( command, pageNumber, new Dictionary<string, string>() );
public async Task<ApiSearchResponse<T>> SearchAsync<T>( string command, IDictionary<string, string> parameters )
=> await SearchAsync<T>( command, 1, parameters );
public async Task<ApiSearchResponse<T>> SearchAsync<T>( string command, int pageNumber, IDictionary<string, string> 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<T>
{
// 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<ApiError>( json ),
CommandText = response.RequestMessage.RequestUri.ToString(),
Json = json,
};
return error;
}
var result = JsonConvert.DeserializeObject<ApiSearchResponse<T>>( 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<string, string>() );
protected string CreateCommand( string rootCommand, IDictionary<string, string> 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;
}
}
}
@@ -0,0 +1,8 @@
namespace DM.MovieApi.ApiRequest
{
/// <summary>
/// Interface to provide a constraint for all MovieDb Api Request interfaces/classes.
/// </summary>
public interface IApiRequest
{ }
}
@@ -0,0 +1,56 @@
using System;
using System.Diagnostics;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace DM.MovieApi.ApiRequest
{
/// <summary>
/// Extends the native Newtonsoft IsoDateTimeConverter to allow deserializing partial dates.
/// </summary>
public class IsoDateTimeConverterEx : IsoDateTimeConverter
{
/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The Newtonsoft.Json.JsonReader to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>The object value.</returns>
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 = "<empty>";
}
Debug.WriteLine( $"IsoDateTimeConverterEx.JsonReader.Value: {val}" );
}
}
}
@@ -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}";
}
}
@@ -0,0 +1,17 @@
namespace DM.MovieApi.ApiResponse
{
/// <summary>
/// Standard response from an API call returning a single specific result.
/// Multiple item based based results (i.e., searches) are returned with an <see cref="ApiQueryResponse{T}"/>.
/// </summary>
public class ApiQueryResponse<T> : ApiResponseBase
{
/// <summary>
/// The item returned from the API call.
/// </summary>
public T Item { get; internal set; }
public override string ToString()
=> Item.ToString();
}
}
@@ -0,0 +1,26 @@
namespace DM.MovieApi.ApiResponse
{
/// <summary>
/// Base class for all API responses from themoviedb.org.
/// </summary>
public abstract class ApiResponseBase
{
/// <summary>
/// Contains specific error information if an error was encountered during the API call to themoviedb.org.
/// </summary>
public ApiError Error { get; internal set; }
/// <summary>
/// The API command text used for the API call to themoviedb.org.
/// </summary>
public string CommandText { get; internal set; }
/// <summary>
/// The JSON returned from themoviedb.org based on the <see cref="CommandText"/> query.
/// </summary>
public string Json { get; internal set; }
public override string ToString()
=> CommandText;
}
}
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
// ReSharper disable UnusedAutoPropertyAccessor.Local
namespace DM.MovieApi.ApiResponse
{
/// <summary>
/// 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
/// <see cref="DM.MovieApi.ApiResponse.ApiQueryResponse{T}"/>.
/// </summary>
[DataContract]
public class ApiSearchResponse<T> : ApiResponseBase
{
/// <summary>
/// The list of results from the search.
/// </summary>
[DataMember( Name = "results" )]
public IReadOnlyList<T> Results { get; private set; }
/// <summary>
/// The current page number of the search result.
/// </summary>
[DataMember( Name = "page" )]
public int PageNumber { get; private set; }
/// <summary>
/// The total number of pages found from the search result.
/// </summary>
[DataMember( Name = "total_pages" )]
public int TotalPages { get; private set; }
/// <summary>
/// The total number of results from the search.
/// </summary>
[DataMember( Name = "total_results" )]
public int TotalResults { get; private set; }
public override string ToString()
=> $"Page {PageNumber} of {TotalPages} ({TotalResults} total results)";
}
}
@@ -0,0 +1,217 @@
using System.Diagnostics.CodeAnalysis;
namespace DM.MovieApi.ApiResponse
{
/// <summary>
/// themoviedb.org Status Codes as defined by: https://www.themoviedb.org/documentation/api/status-codes
/// </summary>
[SuppressMessage( "ReSharper", "UnusedMember.Global" )]
public enum TmdbStatusCode
{
Unknown = 0,
/// <summary>
/// 200: Success.
/// </summary>
//[Description( "200: Success." )]
Success = 1,
/// <summary>
/// 501: Invalid service: this service does not exist.
/// </summary>
//[Description( "501: Invalid service: this service does not exist." )]
InvalidService = 2,
/// <summary>
/// 401: Authentication failed: You do not have permissions to access the service.
/// </summary>
//[Description( "401: Authentication failed: You do not have permissions to access the service." )]
InsufficientPermissions = 3,
/// <summary>
/// 405: Invalid format: This service doesn't exist in that format.
/// </summary>
//[Description( "405: Invalid format: This service doesn't exist in that format." )]
InvalidFormat = 4,
/// <summary>
/// 422: Invalid parameters: Your request parameters are incorrect.
/// </summary>
//[Description( "422: Invalid parameters: Your request parameters are incorrect." )]
InvalidParameters = 5,
/// <summary>
/// 404: Invalid id: The pre-requisite id is invalid or not found.
/// </summary>
//[Description( "404: Invalid id: The pre-requisite id is invalid or not found." )]
InvalidId = 6,
/// <summary>
/// 401: Invalid API key: You must be granted a valid key.
/// </summary>
//[Description( "401: Invalid API key: You must be granted a valid key." )]
InvalidApiKey = 7,
/// <summary>
/// 403: Duplicate entry: The data you tried to submit already exists.
/// </summary>
//[Description( "403: Duplicate entry: The data you tried to submit already exists." )]
DuplicateEntry = 8,
/// <summary>
/// 503: Service offline: This service is temporarily offline, try again later.
/// </summary>
//[Description( "503: Service offline: This service is temporarily offline, try again later." )]
ServiceOffline = 9,
/// <summary>
/// 503: Service offline: This service is temporarily offline, try again later.
/// </summary>
//[Description( "401: Suspended API key: Access to your account has been suspended, contact TMDb." )]
SuspendedApiKey = 10,
/// <summary>
/// 503: Service offline: This service is temporarily offline, try again later.
/// </summary>
//[Description( "500: Internal error: Something went wrong, contact TMDb." )]
InternalError = 11,
/// <summary>
/// 201: The item/record was updated successfully.
/// </summary>
//[Description( "201: The item/record was updated successfully." )]
SuccessfulUpdate = 12,
/// <summary>
/// 200: The item/record was deleted successfully.
/// </summary>
//[Description( "200: The item/record was deleted successfully." )]
SuccessfulDelete = 13,
/// <summary>
/// 401: Authentication failed.
/// </summary>
//[Description( "401: Authentication failed." )]
AuthenticationFailed = 14,
/// <summary>
/// 500: Failed.
/// </summary>
//[Description( "500: Failed." )]
Failed = 15,
/// <summary>
/// 401: Device denied.
/// </summary>
//[Description( "401: Device denied." )]
DeviceDenied = 16,
/// <summary>
/// 401: Session denied.
/// </summary>
//[Description( "401: Session denied." )]
SessionDenied = 17,
/// <summary>
/// 400: Validation failed.
/// </summary>
//[Description( "400: Validation failed." )]
ValidationFailed = 18,
/// <summary>
/// 406: Invalid accept header.
/// </summary>
//[Description( "406: Invalid accept header." )]
InvalidAcceptHeader = 19,
/// <summary>
/// 422: Invalid date range: Should be a range no longer than 14 days.
/// </summary>
//[Description( "422: Invalid date range: Should be a range no longer than 14 days." )]
InvalidDateRange = 20,
/// <summary>
/// 200: Entry not found: The item you are trying to edit cannot be found.
/// </summary>
//[Description( "200: Entry not found: The item you are trying to edit cannot be found." )]
EntryNotFound = 21,
/// <summary>
/// 400: Invalid page: Pages start at 1 and max at 1000. They are expected to be an integer.
/// </summary>
//[Description( "400: Invalid page: Pages start at 1 and max at 1000. They are expected to be an integer." )]
InvalidPage = 22,
/// <summary>
/// 400: Invalid date: Format needs to be YYYY-MM-DD.
/// </summary>
//[Description( "400: Invalid date: Format needs to be YYYY-MM-DD." )]
InvalidDate = 23,
/// <summary>
/// 400: Invalid date: Format needs to be YYYY-MM-DD.
/// </summary>
//[Description( "504: Your request to the backend server timed out. Try again." )]
ServerTimeout = 24,
/// <summary>
/// 400: Invalid date: Format needs to be YYYY-MM-DD.
/// </summary>
//[Description( "429: Your request count (#) is over the allowed limit of (40)." )]
RequestOverLimit = 25,
/// <summary>
/// "400: You must provide a username and password.
/// </summary>
//[Description( "400: You must provide a username and password." )]
AuthenticationRequired = 26,
/// <summary>
/// 400: Too many append to response objects: The maximum number of remote calls is 20.
/// </summary>
//[Description( "400: Too many append to response objects: The maximum number of remote calls is 20." )]
ResponseObjectOverflow = 27,
/// <summary>
/// 400: Invalid timezone: Please consult the documentation for a valid timezone.
/// </summary>
//[Description( "400: Invalid timezone: Please consult the documentation for a valid timezone." )]
InvalidTimezone = 28,
/// <summary>
/// 400: Invalid timezone: Please consult the documentation for a valid timezone.
/// </summary>
//[Description( "400: You must confirm this action: Please provide a confirm=true parameter." )]
ActionMustBeConfirmed = 29,
/// <summary>
/// 401: Invalid username and/or password: You did not provide a valid login.
/// </summary>
//[Description( "401: Invalid username and/or password: You did not provide a valid login." )]
InvalidAuthentication = 30,
/// <summary>
/// 401: Account disabled: Your account is no longer active. Contact TMDb if this is an error.
/// </summary>
//[Description( "401: Account disabled: Your account is no longer active. Contact TMDb if this is an error." )]
AccountDisabled = 31,
/// <summary>
/// 401: Email not verified: Your email address has not been verified.
/// </summary>
//[Description( "401: Email not verified: Your email address has not been verified." )]
EmailNotVerified = 32,
/// <summary>
/// 401: Invalid request token: The request token is either expired or invalid.
/// </summary>
//[Description( "401: Invalid request token: The request token is either expired or invalid." )]
InvalidRequestToken = 33,
/// <summary>
/// 401: The resource you requested could not be found.
/// </summary>
//[Description( "401: The resource you requested could not be found." )]
ResourceNotFound = 34,
}
}
@@ -0,0 +1,9 @@
namespace DM.MovieApi
{
internal interface IApiSettings
{
string ApiUrl { get; }
string BearerToken { get; }
}
}
+58
View File
@@ -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
{
/// <summary>
/// Global interface exposing all API interfaces against themoviedb.org that are
/// currently available in this release.
/// </summary>
public interface IMovieDbApi
{
/// <summary>
/// Provides access for retrieving production company information.
/// </summary>
IApiCompanyRequest Companies { get; }
/// <summary>
/// Provides access for retrieving themoviedb.org configuration information.
/// </summary>
IApiConfigurationRequest Configuration { get; }
/// <summary>
/// Provides access for retrieving Movie and TV genres.
/// </summary>
IApiGenreRequest Genres { get; }
/// <summary>
/// Provides access for retrieving information about Movie/TV industry specific professions.
/// </summary>
IApiProfessionRequest IndustryProfessions { get; }
/// <summary>
/// Provides access for retrieving information about Movies.
/// </summary>
IApiMovieRequest Movies { get; }
/// <summary>
/// Provides access for retrieving movie rating information.
/// </summary>
IApiMovieRatingRequest MovieRatings { get; }
/// <summary>
/// Provides access for retrieving information about Television shows.
/// </summary>
IApiTVShowRequest Television { get; }
/// <summary>
/// Provides access for retrieving information about People.
/// </summary>
IApiPeopleRequest People { get; }
}
}
+21
View File
@@ -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.
@@ -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<ApiQueryResponse<MovieRatings>> GetMovieRatingsAsync()
{
const string command = "certification/movie/list";
ApiQueryResponse<MovieRatings> 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<MovieRatings>();
Func<IEnumerable<Certification>, IReadOnlyList<Certification>> 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;
}
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using DM.MovieApi.ApiRequest;
using DM.MovieApi.ApiResponse;
namespace DM.MovieApi.MovieDb.Certifications
{
/// <summary>
/// Interface for retrieving movie rating information.
/// </summary>
public interface IApiMovieRatingRequest : IApiRequest
{
/// <summary>
/// Gets the list of supported certifications (movie ratings) for movies.
/// </summary>
Task<ApiQueryResponse<MovieRatings>> GetMovieRatingsAsync();
}
}
@@ -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<Certification> Australia { get; set; }
[DataMember( Name = "CA" )]
public IReadOnlyList<Certification> Canada { get; set; }
[DataMember( Name = "FR" )]
public IReadOnlyList<Certification> France { get; set; }
[DataMember( Name = "DE" )]
public IReadOnlyList<Certification> Germany { get; set; }
[DataMember( Name = "IN" )]
public IReadOnlyList<Certification> India { get; set; }
[DataMember( Name = "NZ" )]
public IReadOnlyList<Certification> NewZealand { get; set; }
[DataMember( Name = "US" )]
public IReadOnlyList<Certification> UnitedStates { get; set; }
[DataMember( Name = "GB" )]
public IReadOnlyList<Certification> UnitedKingdom { get; set; }
public MovieRatings()
{
UnitedStates = Array.Empty<Certification>();
Canada = Array.Empty<Certification>();
Australia = Array.Empty<Certification>();
Germany = Array.Empty<Certification>();
France = Array.Empty<Certification>();
NewZealand = Array.Empty<Certification>();
India = Array.Empty<Certification>();
UnitedKingdom = Array.Empty<Certification>();
}
}
}
@@ -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})";
}
}
}
@@ -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<ApiQueryResponse<ProductionCompany>> FindByIdAsync( int companyId )
{
string command = $"company/{companyId}";
ApiQueryResponse<ProductionCompany> response = await base.QueryAsync<ProductionCompany>( command );
return response;
}
public async Task<ApiSearchResponse<MovieInfo>> GetMoviesAsync( int companyId, int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
string command = $"company/{companyId}/movies";
ApiSearchResponse<MovieInfo> response = await base.SearchAsync<MovieInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
}
}
@@ -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
{
/// <summary>
/// Interface for retrieving information about a production company.
/// </summary>
public interface IApiCompanyRequest : IApiRequest
{
/// <summary>
/// Gets all the basic information about a specific company.
/// </summary>
/// <param name="companyId">The company Id is typically found from a Movie or TV query.</param>
Task<ApiQueryResponse<ProductionCompany>> FindByIdAsync( int companyId );
/// <summary>
/// Get the list of movies associated with a particular company.
/// </summary>
/// <param name="companyId">The company Id is typically found from a Movie or TV query.</param>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
/// <returns></returns>
Task<ApiSearchResponse<MovieInfo>> GetMoviesAsync( int companyId, int pageNumber = 1, string language = "en" );
}
}
@@ -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})";
}
}
}
@@ -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})";
}
}
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb.Companies
{
[DataContract]
public class ProductionCompanyInfo : IEqualityComparer<ProductionCompanyInfo>
{
[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})";
}
}
}
@@ -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<string> ChangeKeys { get; private set; }
public override string ToString()
{
if( !string.IsNullOrWhiteSpace( Images?.RootUrl ) )
{
return Images.RootUrl;
}
return "not set";
}
}
}
@@ -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<ApiQueryResponse<ApiConfiguration>> GetAsync()
{
ApiQueryResponse<ApiConfiguration> response = await base.QueryAsync<ApiConfiguration>( "configuration" );
return response;
}
}
}
@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using DM.MovieApi.ApiRequest;
using DM.MovieApi.ApiResponse;
namespace DM.MovieApi.MovieDb.Configuration
{
/// <summary>
/// Interface for retrieving themoviedb.org configuration information.
/// </summary>
public interface IApiConfigurationRequest : IApiRequest
{
/// <summary>
/// <para>Get themoviedb.org system wide configuration information. Some elements of themoviedb.org
/// API require knowledge of the configuration data. The purpose of the <see cref="ApiConfiguration"/>
/// is to try and keep the actual API responses as light as possible.</para>
/// <para>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.</para>
/// </summary>
Task<ApiQueryResponse<ApiConfiguration>> GetAsync();
}
}
@@ -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<string> BackDrops { get; private set; }
[DataMember( Name = "logo_sizes" )]
public IReadOnlyList<string> Logos { get; private set; }
[DataMember( Name = "poster_sizes" )]
public IReadOnlyList<string> Posters { get; private set; }
[DataMember( Name = "profile_sizes" )]
public IReadOnlyList<string> Profiles { get; private set; }
[DataMember( Name = "still_sizes" )]
public IReadOnlyList<string> Stills { get; private set; }
public override string ToString()
{
if( !string.IsNullOrWhiteSpace( RootUrl ) )
{
return RootUrl;
}
return "not set";
}
}
}
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb
{
[DataContract]
public class Country : IEqualityComparer<Country>
{
[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})";
}
}
}
@@ -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<Genre> _allGenres = new();
public IReadOnlyList<Genre> 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<ApiQueryResponse<Genre>> FindByIdAsync( int genreId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language}
};
string command = $"genre/{genreId}";
ApiQueryResponse<Genre> response = await base.QueryAsync<Genre>( command, param );
EnsureAllGenres( response );
return response;
}
public async Task<ApiQueryResponse<IReadOnlyList<Genre>>> GetAllAsync( string language = "en" )
{
ApiQueryResponse<IReadOnlyList<Genre>> tv = await GetTelevisionAsync( language );
if( tv.Error != null )
{
return tv;
}
ApiQueryResponse<IReadOnlyList<Genre>> movies = await GetMoviesAsync( language );
if( movies.Error != null )
{
return movies;
}
List<Genre> merged = movies.Item
.Union( tv.Item )
.OrderBy( x => x.Name )
.ToList();
movies.Item = merged.AsReadOnly();
return movies;
}
public async Task<ApiQueryResponse<IReadOnlyList<Genre>>> GetMoviesAsync( string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
ApiQueryResponse<IReadOnlyList<Genre>> genres = await base.QueryAsync( "genre/movie/list", param, GenreDeserializer );
return genres;
}
public async Task<ApiQueryResponse<IReadOnlyList<Genre>>> GetTelevisionAsync( string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
ApiQueryResponse<IReadOnlyList<Genre>> genres = await base.QueryAsync( "genre/tv/list", param, GenreDeserializer );
return genres;
}
public async Task<ApiSearchResponse<MovieInfo>> FindMoviesByIdAsync( int genreId, int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
{"include_adult", "false"},
};
string command = $"genre/{genreId}/movies";
ApiSearchResponse<MovieInfo> response = await base.SearchAsync<MovieInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( this );
return response;
}
internal void ClearAllGenres()
=> _allGenres.Clear();
private void EnsureAllGenres( ApiQueryResponse<Genre> response )
{
if( response.Error != null )
{
return;
}
if( response.Item == null )
{
return;
}
if( _allGenres.Contains( response.Item ) == false )
{
_allGenres.Add( response.Item );
}
}
private IReadOnlyList<Genre> GenreDeserializer( string json )
{
var obj = JObject.Parse( json );
var arr = ( JArray )obj["genres"];
// ReSharper disable once PossibleNullReferenceException
var genres = arr.ToObject<IReadOnlyList<Genre>>();
return genres;
}
}
}
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb.Genres
{
[DataContract]
public class Genre : IEqualityComparer<Genre>
{
[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})";
}
}
@@ -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<Genre> GetAll()
=> LazyAll.Value;
private static readonly Lazy<IReadOnlyList<Genre>> 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();
} );
}
}
@@ -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<MovieInfo> movies, IApiGenreRequest api )
{
foreach( MovieInfo movie in movies )
{
movie.Genres = MapGenreIdsToGenres( movie.GenreIds, api );
}
}
public static void PopulateGenres( this IEnumerable<TVShowInfo> tvShows, IApiGenreRequest api )
{
foreach( TVShowInfo tvShow in tvShows )
{
tvShow.Genres = MapGenreIdsToGenres( tvShow.GenreIds, api );
}
}
public static void PopulateGenres( this IEnumerable<PersonInfo> people, IApiGenreRequest api )
{
foreach( PersonInfo person in people )
{
foreach( PersonInfoRole role in person.KnownFor )
{
role.Genres = MapGenreIdsToGenres( role.GenreIds, api );
}
}
}
private static IReadOnlyList<Genre> MapGenreIdsToGenres( IEnumerable<int> genreIds, IApiGenreRequest api )
{
IReadOnlyList<Genre> 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;
}
}
}
@@ -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
{
/// <summary>
/// Interface representing Movie and TV genres.
/// </summary>
public interface IApiGenreRequest : IApiRequest
{
/// <summary>
/// Provides a cache of all the genres (language='en') returned from <see cref="GetAllAsync"/>.
/// As the Genres do not change much, if any, the cache is never evicted.
/// </summary>
IReadOnlyList<Genre> AllGenres { get; }
/// <summary>
/// Gets all the information about a specific Genre.
/// </summary>
/// <param name="genreId">The genre Id is typically found from a more generic Movie or TV query.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<Genre>> FindByIdAsync( int genreId, string language = "en" );
/// <summary>
/// <para>It is recommended to use the <see cref="AllGenres"/> property, unless a
/// language specific parameter other than 'en' is provided.</para>
/// <para>
/// 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.
/// </para>
/// <para>
/// In some rare cases, a genre is not included in the movie or tv genres list; when this
/// occurs, use the <see cref="FindByIdAsync"/> method to find a matching genre.
/// </para>
/// </summary>
/// <returns>The merged set of Movie and TV Genres.</returns>
Task<ApiQueryResponse<IReadOnlyList<Genre>>> GetAllAsync( string language = "en" );
/// <summary>
/// Gets all movie related Genres.
/// </summary>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<IReadOnlyList<Genre>>> GetMoviesAsync( string language = "en" );
/// <summary>
/// Gets all tv related Genres.
/// </summary>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<IReadOnlyList<Genre>>> GetTelevisionAsync( string language = "en" );
/// <summary>
/// Finds all movies related to a genre, where the Id passed to this method is a genre Id, not a movie Id.
/// </summary>
/// <param name="genreId">The genre Id is typically found through from a related Movie request or from any of the Genre API methods.</param>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<MovieInfo>> FindMoviesByIdAsync( int genreId, int pageNumber = 1, string language = "en" );
}
}
@@ -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<ApiQueryResponse<IReadOnlyList<Profession>>> GetAllAsync()
{
const string command = "job/list";
Task<ApiQueryResponse<IReadOnlyList<Profession>>> response = base.QueryAsync( command, ProfessionDeserializer );
return response;
}
private IReadOnlyList<Profession> ProfessionDeserializer( string json )
{
var obj = JObject.Parse( json );
var arr = ( JArray )obj["jobs"];
// ReSharper disable once PossibleNullReferenceException
var professions = arr.ToObject<IReadOnlyList<Profession>>();
return professions;
}
}
}
@@ -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
{
/// <summary>
/// Interface for retrieving information about Movie/TV industry specific professions.
/// </summary>
public interface IApiProfessionRequest : IApiRequest
{
/// <summary>
/// Gets all the Movie/TV industry specific professions.
/// </summary>
Task<ApiQueryResponse<IReadOnlyList<Profession>>> GetAllAsync();
}
}
@@ -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<string> Jobs { get; set; }
public override string ToString()
=> $"{Department} {Jobs.Count} jobs";
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb.Keywords
{
[DataContract]
public class Keyword : IEqualityComparer<Keyword>
{
/// <summary>
/// The keyword Id as identified by theMovieDB.org.
/// </summary>
[DataMember( Name = "id" )]
public int Id { get; set; }
/// <summary>
/// The keyword.
/// </summary>
[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})";
}
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DM.MovieApi.MovieDb.Keywords
{
/// <summary>
/// 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.
/// </summary>
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<IReadOnlyList<Keyword>>();
return keywords;
}
public override bool CanConvert( Type objectType )
=> false;
}
}
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb
{
[DataContract]
public class Language : IEqualityComparer<Language>
{
[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})";
}
}
}
@@ -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<ApiQueryResponse<Movie>> FindByIdAsync( int movieId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
{"append_to_response", "keywords"},
};
string command = $"movie/{movieId}";
ApiQueryResponse<Movie> response = await base.QueryAsync<Movie>( command, param );
return response;
}
public async Task<ApiSearchResponse<MovieInfo>> SearchByTitleAsync( string query, int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"query", query},
{"include_adult", "false"},
{"language", language},
};
const string command = "search/movie";
ApiSearchResponse<MovieInfo> response = await base.SearchAsync<MovieInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
public async Task<ApiQueryResponse<Movie>> GetLatestAsync( string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
{"append_to_response", "keywords"},
};
const string command = "movie/latest";
ApiQueryResponse<Movie> response = await base.QueryAsync<Movie>( command, param );
return response;
}
public async Task<ApiSearchResponse<Movie>> GetNowPlayingAsync( int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
{"append_to_response", "keywords"},
};
const string command = "movie/now_playing";
ApiSearchResponse<Movie> response = await base.SearchAsync<Movie>( command, pageNumber, param );
return response;
}
public async Task<ApiSearchResponse<Movie>> GetUpcomingAsync( int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
{"append_to_response", "keywords"},
};
const string command = "movie/upcoming";
ApiSearchResponse<Movie> response = await base.SearchAsync<Movie>( command, pageNumber, param );
return response;
}
public async Task<ApiSearchResponse<MovieInfo>> GetTopRatedAsync( int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
const string command = "movie/top_rated";
ApiSearchResponse<MovieInfo> response = await base.SearchAsync<MovieInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
public async Task<ApiSearchResponse<MovieInfo>> GetPopularAsync( int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
const string command = "movie/popular";
ApiSearchResponse<MovieInfo> response = await base.SearchAsync<MovieInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
public async Task<ApiQueryResponse<MovieCredit>> GetCreditsAsync( int movieId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
string command = $"movie/{movieId}/credits";
ApiQueryResponse<MovieCredit> response = await base.QueryAsync<MovieCredit>( command, param );
return response;
}
}
}
@@ -0,0 +1,68 @@
using System.Threading.Tasks;
using DM.MovieApi.ApiRequest;
using DM.MovieApi.ApiResponse;
namespace DM.MovieApi.MovieDb.Movies
{
/// <summary>
/// Interface for retrieving information about Movies.
/// </summary>
public interface IApiMovieRequest : IApiRequest
{
/// <summary>
/// Gets all the information about a specific Movie.
/// </summary>
/// <param name="movieId">The movie Id is typically found from a more generic Movie query.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<Movie>> FindByIdAsync( int movieId, string language = "en" );
/// <summary>
/// Searches for Movies by title.
/// </summary>
/// <param name="query">The query to search for Movies.</param>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<MovieInfo>> SearchByTitleAsync( string query, int pageNumber = 1, string language = "en" );
/// <summary>
/// Gets the most recent movie that has been added to TheMovieDb.org.
/// </summary>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<Movie>> GetLatestAsync( string language = "en" );
/// <summary>
/// Gets the list of movies playing that have been, or are being released this week.
/// </summary>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<Movie>> GetNowPlayingAsync( int pageNumber = 1, string language = "en" );
/// <summary>
/// Gets the list of upcoming movies by release date.
/// </summary>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<Movie>> GetUpcomingAsync( int pageNumber = 1, string language = "en" );
/// <summary>
/// Gets the list of top rated movies which is refreshed daily.
/// </summary>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<MovieInfo>> GetTopRatedAsync( int pageNumber = 1, string language = "en" );
/// <summary>
/// Gets the list of popular movies which is refreshed daily.
/// </summary>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<MovieInfo>> GetPopularAsync( int pageNumber = 1, string language = "en" );
/// <summary>
/// Get the cast and crew information for a specific movie id.
/// </summary>
/// <param name="movieId">The movie Id is typically found from a more generic Movie query.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<MovieCredit>> GetCreditsAsync( int movieId, string language = "en" );
}
}
@@ -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<Genre> Genres { get; set; }
[DataMember( Name = "homepage" )]
public string Homepage { get; set; }
[DataMember( Name = "imdb_id" )]
public string ImdbId { get; set; }
/// <summary>
/// ISO 3166-1 code.
/// </summary>
[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<ProductionCompanyInfo> ProductionCompanies { get; set; }
[DataMember( Name = "production_countries" )]
public IReadOnlyList<Country> 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<Language> 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<Keyword> Keywords { get; set; }
public Movie()
{
Genres = Array.Empty<Genre>();
Keywords = Array.Empty<Keyword>();
ProductionCompanies = Array.Empty<ProductionCompanyInfo>();
ProductionCountries = Array.Empty<Country>();
SpokenLanguages = Array.Empty<Language>();
}
public override string ToString()
=> $"{Title} ({ReleaseDate:yyyy-MM-dd}) [{Id}]";
}
}
@@ -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<MovieCastMember> CastMembers { get; set; }
[DataMember( Name = "crew" )]
public IReadOnlyList<MovieCrewMember> 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}";
}
}
@@ -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<int> GenreIds { get; set; }
public IReadOnlyList<Genre> 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<int>();
Genres = Array.Empty<Genre>();
}
public override string ToString()
=> $"{Title} ({Id} - {ReleaseDate:yyyy-MM-dd})";
}
}
@@ -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<ApiQueryResponse<Person>> FindByIdAsync( int personId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language}
};
string command = $"person/{personId}";
ApiQueryResponse<Person> response = await base.QueryAsync<Person>( command, param );
return response;
}
public async Task<ApiSearchResponse<PersonInfo>> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"query", query},
{"include_adult", "false"},
{"language", language},
};
const string command = "search/person";
ApiSearchResponse<PersonInfo> response = await base.SearchAsync<PersonInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
public async Task<ApiQueryResponse<PersonMovieCredit>> GetMovieCreditsAsync( int personId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
string command = $"person/{personId}/movie_credits";
ApiQueryResponse<PersonMovieCredit> response = await base.QueryAsync<PersonMovieCredit>( command, param );
return response;
}
public async Task<ApiQueryResponse<PersonTVCredit>> GetTVCreditsAsync( int personId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{"language", language},
};
string command = $"person/{personId}/tv_credits";
ApiQueryResponse<PersonTVCredit> response = await base.QueryAsync<PersonTVCredit>( command, param );
return response;
}
}
}
@@ -0,0 +1,9 @@
namespace DM.MovieApi.MovieDb.People
{
public enum Gender
{
Unknown = 0,
Female = 1,
Male = 2,
}
}
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using DM.MovieApi.ApiRequest;
using DM.MovieApi.ApiResponse;
namespace DM.MovieApi.MovieDb.People
{
/// <summary>
/// Interface for retrieving information about People.
/// </summary>
public interface IApiPeopleRequest : IApiRequest
{
/// <summary>
/// Gets all the information about a specific Person.
/// </summary>
/// <param name="personId">The person Id is typically found from a more generic query such as movie or television or search.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<Person>> FindByIdAsync( int personId, string language = "en" );
/// <summary>
/// Searches for People by name.
/// </summary>
/// <param name="query">The query to search for People.</param>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<PersonInfo>> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" );
/// <summary>
/// Get the movie credits for a specific person id. Includes movie cast and crew information for the person.
/// </summary>
/// <param name="personId">The person Id is typically found from a more generic query such as movie or television or search.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<PersonMovieCredit>> GetMovieCreditsAsync( int personId, string language = "en" );
/// <summary>
/// Get the television credits for a specific person id. Includes TV cast and crew information for the person.
/// </summary>
/// <param name="personId">The person Id is typically found from a more generic query such as movie or television or search.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<PersonTVCredit>> GetTVCreditsAsync( int personId, string language = "en" );
}
}
@@ -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<string> 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<string>();
}
public override string ToString()
=> Name;
}
}
@@ -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<PersonInfoRole> 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<PersonInfoRole>();
}
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)
/// <summary>
/// The MovieId or TVShowId as defined by the value of <see cref="MediaType"/>.
/// </summary>
[DataMember( Name = "id" )]
public int Id { get; set; }
[DataMember( Name = "media_type" )]
public MediaType MediaType { get; set; }
/// <summary>
/// Only populated when <see cref="MediaType"/> is TV.
/// </summary>
[DataMember( Name = "name" )]
public string TVShowName { get; set; }
/// <summary>
/// Only populated when <see cref="MediaType"/> is TV.
/// </summary>
[DataMember( Name = "original_name" )]
public string TVShowOriginalName { get; set; }
/// <summary>
/// Only populated when <see cref="MediaType"/> is Movie.
/// </summary>
[DataMember( Name = "title" )]
public string MovieTitle { get; set; }
/// <summary>
/// Only populated when <see cref="MediaType"/> is Movie.
/// </summary>
[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; }
/// <summary>
/// Only populated when <see cref="MediaType"/> is Movie.
/// </summary>
[DataMember( Name = "release_date" )]
public DateTime MovieReleaseDate { get; set; }
/// <summary>
/// Only populated when <see cref="MediaType"/> is TV.
/// </summary>
[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<int> GenreIds { get; set; }
public IReadOnlyList<Genre> 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<string> OriginCountry { get; set; }
public PersonInfoRole()
{
GenreIds = Array.Empty<int>();
Genres = Array.Empty<Genre>();
OriginCountry = Array.Empty<string>();
}
public override string ToString()
{
return MediaType == MediaType.Movie
? $"Movie: {MovieTitle} ({Id} - {MovieReleaseDate:yyyy-MM-dd})"
: $"TV: {TVShowName} ({Id} - {TVShowFirstAirDate:yyyy-MM-dd})";
}
}
}
@@ -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<PersonMovieCastMember> CastRoles { get; set; }
[DataMember( Name = "crew" )]
public IReadOnlyList<PersonMovieCrewMember> CrewRoles { get; set; }
public PersonMovieCredit()
{
CastRoles = Array.Empty<PersonMovieCastMember>();
CrewRoles = Array.Empty<PersonMovieCrewMember>();
}
}
[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; }
}
}
@@ -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<PersonTVCastMember> CastRoles { get; set; }
[DataMember( Name = "crew" )]
public IReadOnlyList<PersonTVCrewMember> CrewRoles { get; set; }
public PersonTVCredit()
{
CastRoles = Array.Empty<PersonTVCastMember>();
CrewRoles = Array.Empty<PersonTVCrewMember>();
}
}
[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; }
}
}
@@ -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<ApiQueryResponse<TVShow>> FindByIdAsync( int tvShowId, string language = "en" )
{
var param = new Dictionary<string, string>
{
{ "language", language },
{ "append_to_response", "keywords" },
};
string command = $"tv/{tvShowId}";
ApiQueryResponse<TVShow> response = await base.QueryAsync<TVShow>( command, param );
return response;
}
public async Task<ApiSearchResponse<TVShowInfo>> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{ "query", query },
{ "language", language }
};
const string command = "search/tv";
ApiSearchResponse<TVShowInfo> response = await base.SearchAsync<TVShowInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
public async Task<ApiQueryResponse<TVShow>> GetLatestAsync( string language = "en" )
{
var param = new Dictionary<string, string>
{
{ "language", language },
{ "append_to_response", "keywords" },
};
const string command = "tv/latest";
ApiQueryResponse<TVShow> response = await base.QueryAsync<TVShow>( command, param );
return response;
}
public async Task<ApiSearchResponse<TVShowInfo>> GetTopRatedAsync( int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{ "language", language }
};
const string command = "tv/top_rated";
ApiSearchResponse<TVShowInfo> response = await base.SearchAsync<TVShowInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
public async Task<ApiSearchResponse<TVShowInfo>> GetPopularAsync( int pageNumber = 1, string language = "en" )
{
var param = new Dictionary<string, string>
{
{ "language", language }
};
const string command = "tv/popular";
ApiSearchResponse<TVShowInfo> response = await base.SearchAsync<TVShowInfo>( command, pageNumber, param );
if( response.Error != null )
{
return response;
}
response.Results.PopulateGenres( _genreApi );
return response;
}
}
}
@@ -0,0 +1,47 @@
using System.Threading.Tasks;
using DM.MovieApi.ApiRequest;
using DM.MovieApi.ApiResponse;
namespace DM.MovieApi.MovieDb.TV
{
/// <summary>
/// Interface for retrieving information about TV shows.
/// </summary>
public interface IApiTVShowRequest : IApiRequest
{
/// <summary>
/// Gets all the information about a specific TV show.
/// </summary>
/// <param name="tvShowId">The TV show Id which is typically found from a more generic TV show query.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<TVShow>> FindByIdAsync( int tvShowId, string language = "en" );
/// <summary>
/// Searches for TV shows by title.
/// </summary>
/// <param name="query">The query to search for TV shows.</param>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<TVShowInfo>> SearchByNameAsync( string query, int pageNumber = 1, string language = "en" );
/// <summary>
/// Gets the latest TV show added to TheMovieDb.org
/// </summary>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiQueryResponse<TVShow>> GetLatestAsync( string language = "en" );
/// <summary>
/// Gets the list of top rated TV shows which is refreshed daily.
/// </summary>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<TVShowInfo>> GetTopRatedAsync( int pageNumber = 1, string language = "en" );
/// <summary>
/// Gets the list of popular TV shows which is refreshed daily.
/// </summary>
/// <param name="pageNumber">Default is page 1. The page number to retrieve; the <see cref="ApiSearchResponse{T}"/> will contain the current page returned and the total number of pages available.</param>
/// <param name="language">Default is English. The ISO 639-1 language code to retrieve the result from.</param>
Task<ApiSearchResponse<TVShowInfo>> GetPopularAsync( int pageNumber = 1, string language = "en" );
}
}
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb.TV
{
[DataContract]
public class Network : IEqualityComparer<Network>
{
[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})";
}
}
@@ -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})";
}
}
@@ -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<TVShowCreator> CreatedBy { get; set; }
[DataMember( Name = "episode_run_time" )]
public IReadOnlyList<int> EpisodeRunTime { get; set; }
[DataMember( Name = "first_air_date" )]
public DateTime FirstAirDate { get; set; }
[DataMember( Name = "genres" )]
public IReadOnlyList<Genre> 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<string> 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<Network> 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<string> 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<ProductionCompanyInfo> ProductionCompanies { get; set; }
[DataMember( Name = "seasons" )]
public IReadOnlyList<Season> Seasons { get; set; }
[DataMember( Name = "keywords" )]
[JsonConverter( typeof( KeywordConverter ), "results" )]
public IReadOnlyList<Keyword> Keywords { get; set; }
public TVShow()
{
CreatedBy = Array.Empty<TVShowCreator>();
EpisodeRunTime = Array.Empty<int>();
Genres = Array.Empty<Genre>();
Languages = Array.Empty<string>();
Networks = Array.Empty<Network>();
OriginCountry = Array.Empty<string>();
ProductionCompanies = Array.Empty<ProductionCompanyInfo>();
Seasons = Array.Empty<Season>();
Keywords = Array.Empty<Keyword>();
}
public override string ToString()
=> $"{Name} ({FirstAirDate:yyyy-MM-dd}) [{Id}]";
}
}
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace DM.MovieApi.MovieDb.TV
{
[DataContract]
public class TVShowCreator : IEqualityComparer<TVShowCreator>
{
[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})";
}
}
@@ -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<string> OriginCountry { get; private set; }
[DataMember( Name = "genre_ids" )]
internal IReadOnlyList<int> GenreIds { get; set; }
public IReadOnlyList<Genre> Genres { get; internal set; }
[DataMember( Name = "original_language" )]
public string OriginalLanguage { get; private set; }
public TVShowInfo()
{
OriginCountry = Array.Empty<string>();
GenreIds = Array.Empty<int>();
Genres = Array.Empty<Genre>();
}
public override string ToString()
=> $"{Name} ({Id} - {FirstAirDate:yyyy-MM-dd})";
}
}
+203
View File
@@ -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
{
/// <summary>
/// Note: one of the RegisterSettings must be called before the Factory can Create anything.
/// </summary>
public static class MovieDbFactory
{
/// <include file='ApiDocs.xml' path='Doc/ApiSettings/ApiUrl/*'/>
public const string TheMovieDbApiUrl = "http://api.themoviedb.org/3/";
/// <summary>
/// Determines if the underlying factory has been created.
/// </summary>
public static bool IsFactoryComposed => Settings != null;
internal static IApiSettings Settings { get; private set; }
/// <summary>
/// Registers themoviedb.org settings for use with the internal DI container.
/// </summary>
/// <param name="bearerToken">
/// <include file='ApiDocs.xml' path='Doc/ApiSettings/BearerToken/summary/*'/>
/// </param>
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 );
}
/// <summary>
/// <para>Creates the specific API requested.</para>
/// <para><inheritdoc cref="MovieDbFactory" path="/summary"/></para>
/// </summary>
public static Lazy<T> Create<T>() where T : IApiRequest
{
ContainerGuard();
var requestResolver = new ApiRequestResolver();
return new Lazy<T>( requestResolver.Get<T> );
}
/// <summary>
/// <para>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.</para>
/// <para><inheritdoc cref="MovieDbFactory" path="/summary"/></para>
/// </summary>
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 );
}
/// <summary>
/// Clears all factory settings; forces the next call to be RegisterSettings.
/// before <see cref="Create{T}"/> can be called.
/// </summary>
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<Type, Func<object>> SupportedDependencyTypeMap;
private static readonly ConcurrentDictionary<Type, ConstructorInfo> TypeCtorMap;
static ApiRequestResolver()
{
SupportedDependencyTypeMap = new Dictionary<Type, Func<object>>
{
{typeof(IApiSettings), () => Settings},
{typeof(IApiGenreRequest), () => new ApiGenreRequest( Settings )}
};
TypeCtorMap = new ConcurrentDictionary<Type, ConstructorInfo>();
}
public T Get<T>() 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<object>( 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<object> 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();
}
}
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace DM.MovieApi.Shims
{
public static class CollectionExtensions
{
public static IReadOnlyList<T> AsReadOnly<T>( this List<T> list )
{
return new ReadOnlyCollection<T>( list );
}
}
}
@@ -0,0 +1,8 @@
using System;
namespace DM.MovieApi.Shims
{
[AttributeUsage( AttributeTargets.Constructor )]
internal sealed class ImportingConstructorAttribute : Attribute
{ }
}
+7
View File
@@ -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.
-23
View File
@@ -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": ""
}
}
}
BIN
View File
Binary file not shown.
Binary file not shown.
+7 -2
View File
@@ -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"
}
]