diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3e49d4bca..c02c65295 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -14,6 +14,14 @@ V 1.XX.XX Stable/Early Access Preview/development +#### Media Sever: + +Plex/Emby + +#### Media Server Version: + + + #### Operating System: (Place text here) diff --git a/Ombi.Api/RadarrApi.cs b/Ombi.Api/RadarrApi.cs index 206023f8e..1840f40a0 100644 --- a/Ombi.Api/RadarrApi.cs +++ b/Ombi.Api/RadarrApi.cs @@ -108,7 +108,6 @@ namespace Ombi.Api request.AddHeader("X-Api-Key", apiKey); request.AddJsonBody(options); - RadarrAddMovie result; try { var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] { diff --git a/Ombi.Api/TheMovieDbApi.cs b/Ombi.Api/TheMovieDbApi.cs index ad3f01251..89e6835b2 100644 --- a/Ombi.Api/TheMovieDbApi.cs +++ b/Ombi.Api/TheMovieDbApi.cs @@ -37,6 +37,8 @@ using TMDbLib.Objects.General; using TMDbLib.Objects.Movies; using TMDbLib.Objects.Search; using Movie = TMDbLib.Objects.Movies.Movie; +using TMDbLib.Objects.People; +using System.Linq; namespace Ombi.Api { @@ -69,6 +71,11 @@ namespace Ombi.Api return movies?.Results ?? new List(); } + private async Task GetMovie(int id) + { + return await Client.GetMovie(id); + } + public TmdbMovieDetails GetMovieInformationWithVideos(int tmdbId) { var request = new RestRequest { Resource = "movie/{movieId}", Method = Method.GET }; @@ -100,5 +107,49 @@ namespace Ombi.Api var movies = await Client.GetMovie(imdbId); return movies ?? new Movie(); } + + public async Task> SearchPerson(string searchTerm) + { + return await SearchPerson(searchTerm, null); + } + + public async Task> SearchPerson(string searchTerm, Func> alreadyAvailable) + { + SearchContainer result = await Client.SearchPerson(searchTerm); + + var people = result?.Results ?? new List(); + var person = (people.Count != 0 ? people[0] : null); + var movies = new List(); + var counter = 0; + try + { + if (person != null) + { + var credits = await Client.GetPersonMovieCredits(person.Id); + + // grab results from both cast and crew, prefer items in cast. we can handle directors like this. + List movieResults = (from MovieRole role in credits.Cast select new Movie() { Id = role.Id, Title = role.Title, ReleaseDate = role.ReleaseDate }).ToList(); + movieResults.AddRange((from MovieJob job in credits.Crew select new Movie() { Id = job.Id, Title = job.Title, ReleaseDate = job.ReleaseDate }).ToList()); + + //only get the first 10 movies and delay a bit between each request so we don't overload the API + foreach (var m in movieResults) + { + if (counter == 10) + break; + if (alreadyAvailable == null || !(await alreadyAvailable(m.Id, m.Title, m.ReleaseDate.Value.Year.ToString()))) + { + movies.Add(await GetMovie(m.Id)); + counter++; + } + await Task.Delay(50); + } + } + } + catch (Exception e) + { + Log.Log(LogLevel.Error, e); + } + return movies; + } } } diff --git a/Ombi.Common/EnvironmentInfo/OsInfo.cs b/Ombi.Common/EnvironmentInfo/OsInfo.cs new file mode 100644 index 000000000..3ced6f227 --- /dev/null +++ b/Ombi.Common/EnvironmentInfo/OsInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; + +namespace Ombi.Common.EnvironmentInfo +{ + public class OsInfo + { + public static Os Os { get; } + + public static bool IsNotWindows => !IsWindows; + public static bool IsLinux => Os == Os.Linux; + public static bool IsOsx => Os == Os.Osx; + public static bool IsWindows => Os == Os.Windows; + + static OsInfo() + { + var platform = Environment.OSVersion.Platform; + + switch (platform) + { + case PlatformID.Win32NT: + { + Os = Os.Windows; + break; + } + case PlatformID.MacOSX: + case PlatformID.Unix: + { + // Sometimes Mac OS reports itself as Unix + if (Directory.Exists("/System/Library/CoreServices/") && + (File.Exists("/System/Library/CoreServices/SystemVersion.plist") || + File.Exists("/System/Library/CoreServices/ServerVersion.plist")) + ) + { + Os = Os.Osx; + } + else + { + Os = Os.Linux; + } + break; + } + } + } + + } + + public enum Os + { + Windows, + Linux, + Osx + } +} \ No newline at end of file diff --git a/Ombi.Common/EnvironmentInfo/PlatformInfo.cs b/Ombi.Common/EnvironmentInfo/PlatformInfo.cs new file mode 100644 index 000000000..045dc26e3 --- /dev/null +++ b/Ombi.Common/EnvironmentInfo/PlatformInfo.cs @@ -0,0 +1,42 @@ +using System; + +namespace Ombi.Common.EnvironmentInfo +{ + public enum PlatformType + { + DotNet = 0, + Mono = 1 + } + + public interface IPlatformInfo + { + Version Version { get; } + } + + public abstract class PlatformInfo : IPlatformInfo + { + static PlatformInfo() + { + Platform = Type.GetType("Mono.Runtime") != null ? PlatformType.Mono : PlatformType.DotNet; + } + + public static PlatformType Platform { get; } + public static bool IsMono => Platform == PlatformType.Mono; + public static bool IsDotNet => Platform == PlatformType.DotNet; + + public static string PlatformName + { + get + { + if (IsDotNet) + { + return ".NET"; + } + + return "Mono"; + } + } + + public abstract Version Version { get; } + } +} \ No newline at end of file diff --git a/Ombi.Common/Ombi.Common.csproj b/Ombi.Common/Ombi.Common.csproj new file mode 100644 index 000000000..c8c1a53ef --- /dev/null +++ b/Ombi.Common/Ombi.Common.csproj @@ -0,0 +1,61 @@ + + + + + Debug + AnyCPU + {BFD45569-90CF-47CA-B575-C7B0FF97F67B} + Library + Properties + Ombi.Common + Ombi.Common + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\NLog.4.3.6\lib\net45\NLog.dll + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ombi.Common/Processes/ProcessInfo.cs b/Ombi.Common/Processes/ProcessInfo.cs new file mode 100644 index 000000000..1686f4b80 --- /dev/null +++ b/Ombi.Common/Processes/ProcessInfo.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Common.Processes +{ + public class ProcessInfo + { + public int Id { get; set; } + public string Name { get; set; } + public string StartPath { get; set; } + + public override string ToString() + { + return string.Format("{0}:{1} [{2}]", Id, Name ?? "Unknown", StartPath ?? "Unknown"); + } + } +} diff --git a/Ombi.Common/Processes/ProcessOutput.cs b/Ombi.Common/Processes/ProcessOutput.cs new file mode 100644 index 000000000..dc0edee2d --- /dev/null +++ b/Ombi.Common/Processes/ProcessOutput.cs @@ -0,0 +1,59 @@ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ombi.Common.Processes +{ + public class ProcessOutput + { + public int ExitCode { get; set; } + public List Lines { get; set; } + + public ProcessOutput() + { + Lines = new List(); + } + + public List Standard + { + get + { + return Lines.Where(c => c.Level == ProcessOutputLevel.Standard).ToList(); + } + } + + public List Error + { + get + { + return Lines.Where(c => c.Level == ProcessOutputLevel.Error).ToList(); + } + } + } + + public class ProcessOutputLine + { + public ProcessOutputLevel Level { get; set; } + public string Content { get; set; } + public DateTime Time { get; set; } + + public ProcessOutputLine(ProcessOutputLevel level, string content) + { + Level = level; + Content = content; + Time = DateTime.UtcNow; + } + + public override string ToString() + { + return string.Format("{0} - {1} - {2}", Time, Level, Content); + } + } + + public enum ProcessOutputLevel + { + Standard = 0, + Error = 1 + } +} \ No newline at end of file diff --git a/Ombi.Common/Processes/ProcessProvider.cs b/Ombi.Common/Processes/ProcessProvider.cs new file mode 100644 index 000000000..86d8d808a --- /dev/null +++ b/Ombi.Common/Processes/ProcessProvider.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using NLog; +using Ombi.Common.EnvironmentInfo; + +namespace Ombi.Common.Processes +{ + public interface IProcessProvider + { + int GetCurrentProcessId(); + ProcessInfo GetCurrentProcess(); + ProcessInfo GetProcessById(int id); + List FindProcessByName(string name); + void OpenDefaultBrowser(string url); + void WaitForExit(System.Diagnostics.Process process); + void SetPriority(int processId, ProcessPriorityClass priority); + void KillAll(string processName); + void Kill(int processId); + bool Exists(int processId); + bool Exists(string processName); + ProcessPriorityClass GetCurrentProcessPriority(); + System.Diagnostics.Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action onOutputDataReceived = null, Action onErrorDataReceived = null); + System.Diagnostics.Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null); + ProcessOutput StartAndCapture(string path, string args = null, StringDictionary environmentVariables = null); + } + + public class ProcessProvider : IProcessProvider + { + + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public const string OmbiProcessName = "Ombi"; + + //public ProcessProvider(Logger logger) + //{ + // _logger = logger; + //} + + public int GetCurrentProcessId() + { + return Process.GetCurrentProcess().Id; + } + + public ProcessInfo GetCurrentProcess() + { + return ConvertToProcessInfo(Process.GetCurrentProcess()); + } + + public bool Exists(int processId) + { + return GetProcessById(processId) != null; + } + + public bool Exists(string processName) + { + return GetProcessesByName(processName).Any(); + } + + public ProcessPriorityClass GetCurrentProcessPriority() + { + return Process.GetCurrentProcess().PriorityClass; + } + + public ProcessInfo GetProcessById(int id) + { + _logger.Debug("Finding process with Id:{0}", id); + + var processInfo = ConvertToProcessInfo(Process.GetProcesses().FirstOrDefault(p => p.Id == id)); + + if (processInfo == null) + { + _logger.Warn("Unable to find process with ID {0}", id); + } + else + { + _logger.Debug("Found process {0}", processInfo.ToString()); + } + + return processInfo; + } + + public List FindProcessByName(string name) + { + return GetProcessesByName(name).Select(ConvertToProcessInfo).Where(c => c != null).ToList(); + } + + public void OpenDefaultBrowser(string url) + { + _logger.Info("Opening URL [{0}]", url); + + var process = new Process + { + StartInfo = new ProcessStartInfo(url) + { + UseShellExecute = true + } + }; + + process.Start(); + } + + public Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action onOutputDataReceived = null, Action onErrorDataReceived = null) + { + if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + { + args = GetMonoArgs(path, args); + path = "mono"; + } + + var logger = LogManager.GetLogger(new FileInfo(path).Name); + + var startInfo = new ProcessStartInfo(path, args) + { + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true + }; + + if (environmentVariables != null) + { + foreach (DictionaryEntry environmentVariable in environmentVariables) + { + startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString()); + } + } + + logger.Debug("Starting {0} {1}", path, args); + + var process = new Process + { + StartInfo = startInfo + }; + + process.OutputDataReceived += (sender, eventArgs) => + { + if (string.IsNullOrWhiteSpace(eventArgs.Data)) return; + + logger.Debug(eventArgs.Data); + + onOutputDataReceived?.Invoke(eventArgs.Data); + }; + + process.ErrorDataReceived += (sender, eventArgs) => + { + if (string.IsNullOrWhiteSpace(eventArgs.Data)) return; + + logger.Error(eventArgs.Data); + + onErrorDataReceived?.Invoke(eventArgs.Data); + }; + + process.Start(); + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + return process; + } + + public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null) + { + if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + { + args = GetMonoArgs(path, args); + path = "mono"; + } + + _logger.Debug("Starting {0} {1}", path, args); + + var startInfo = new ProcessStartInfo(path, args); + var process = new Process + { + StartInfo = startInfo + }; + + process.Start(); + + return process; + } + + public ProcessOutput StartAndCapture(string path, string args = null, StringDictionary environmentVariables = null) + { + var output = new ProcessOutput(); + var process = Start(path, args, environmentVariables, s => output.Lines.Add(new ProcessOutputLine(ProcessOutputLevel.Standard, s)), + error => output.Lines.Add(new ProcessOutputLine(ProcessOutputLevel.Error, error))); + + process.WaitForExit(); + output.ExitCode = process.ExitCode; + + return output; + } + + public void WaitForExit(Process process) + { + _logger.Debug("Waiting for process {0} to exit.", process.ProcessName); + + process.WaitForExit(); + } + + public void SetPriority(int processId, ProcessPriorityClass priority) + { + var process = Process.GetProcessById(processId); + + _logger.Info("Updating [{0}] process priority from {1} to {2}", + process.ProcessName, + process.PriorityClass, + priority); + + process.PriorityClass = priority; + } + + public void Kill(int processId) + { + var process = Process.GetProcesses().FirstOrDefault(p => p.Id == processId); + + if (process == null) + { + _logger.Warn("Cannot find process with id: {0}", processId); + return; + } + + process.Refresh(); + + if (process.Id != Process.GetCurrentProcess().Id && process.HasExited) + { + _logger.Debug("Process has already exited"); + return; + } + + _logger.Info("[{0}]: Killing process", process.Id); + process.Kill(); + _logger.Info("[{0}]: Waiting for exit", process.Id); + process.WaitForExit(); + _logger.Info("[{0}]: Process terminated successfully", process.Id); + } + + public void KillAll(string processName) + { + var processes = GetProcessesByName(processName); + + _logger.Debug("Found {0} processes to kill", processes.Count); + + foreach (var processInfo in processes) + { + if (processInfo.Id == Process.GetCurrentProcess().Id) + { + _logger.Debug("Tried killing own process, skipping: {0} [{1}]", processInfo.Id, processInfo.ProcessName); + continue; + } + + _logger.Debug("Killing process: {0} [{1}]", processInfo.Id, processInfo.ProcessName); + Kill(processInfo.Id); + } + } + + private ProcessInfo ConvertToProcessInfo(Process process) + { + if (process == null) return null; + + process.Refresh(); + + ProcessInfo processInfo = null; + + try + { + if (process.Id <= 0) return null; + + processInfo = new ProcessInfo + { + Id = process.Id, + Name = process.ProcessName, + StartPath = GetExeFileName(process) + }; + + if (process.Id != Process.GetCurrentProcess().Id && process.HasExited) + { + processInfo = null; + } + } + catch (Win32Exception e) + { + _logger.Warn(e, "Couldn't get process info for " + process.ProcessName); + } + + return processInfo; + + } + + private static string GetExeFileName(Process process) + { + if (process.MainModule.FileName != "mono.exe") + { + return process.MainModule.FileName; + } + + return process.Modules.Cast().FirstOrDefault(module => module.ModuleName.ToLower().EndsWith(".exe")).FileName; + } + + private List GetProcessesByName(string name) + { + //TODO: move this to an OS specific class + + var monoProcesses = Process.GetProcessesByName("mono") + .Union(Process.GetProcessesByName("mono-sgen")) + .Where(process => + process.Modules.Cast() + .Any(module => + module.ModuleName.ToLower() == name.ToLower() + ".exe")); + + var processes = Process.GetProcessesByName(name) + .Union(monoProcesses).ToList(); + + _logger.Debug("Found {0} processes with the name: {1}", processes.Count, name); + + try + { + foreach (var process in processes) + { + _logger.Debug(" - [{0}] {1}", process.Id, process.ProcessName); + } + } + catch + { + // Don't crash on gettings some log data. + } + + return processes; + } + + private string GetMonoArgs(string path, string args) + { + return string.Format("--debug {0} {1}", path, args); + } + } +} diff --git a/Ombi.Common/Properties/AssemblyInfo.cs b/Ombi.Common/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..af987bb95 --- /dev/null +++ b/Ombi.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Ombi.Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ombi.Common")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bfd45569-90cf-47ca-b575-c7b0ff97f67b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Ombi.Common/ServiceProvider.cs b/Ombi.Common/ServiceProvider.cs new file mode 100644 index 000000000..4441e7291 --- /dev/null +++ b/Ombi.Common/ServiceProvider.cs @@ -0,0 +1,203 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: ServiceProvider.cs +// Created By: Jamie Rees +// +// 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. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Specialized; +using System.Configuration.Install; +using System.Diagnostics; +using System.Linq; +using System.ServiceProcess; +using NLog; +using Ombi.Common.Processes; + +namespace Ombi.Common +{ + public interface IServiceProvider + { + bool ServiceExist(string name); + bool IsServiceRunning(string name); + void Install(string serviceName); + void Run(ServiceBase service); + ServiceController GetService(string serviceName); + void Stop(string serviceName); + void Start(string serviceName); + ServiceControllerStatus GetStatus(string serviceName); + void Restart(string serviceName); + } + + public class ServiceProvider : IServiceProvider + { + public const string OmbiServiceName = "Ombi"; + + private readonly IProcessProvider _processProvider; + + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + + public ServiceProvider(IProcessProvider processProvider) + { + _processProvider = processProvider; + } + + public virtual bool ServiceExist(string name) + { + _logger.Debug("Checking if service {0} exists.", name); + return + ServiceController.GetServices().Any( + s => string.Equals(s.ServiceName, name, StringComparison.InvariantCultureIgnoreCase)); + } + + public virtual bool IsServiceRunning(string name) + { + _logger.Debug("Checking if '{0}' service is running", name); + + var service = ServiceController.GetServices() + .SingleOrDefault(s => string.Equals(s.ServiceName, name, StringComparison.InvariantCultureIgnoreCase)); + + return service != null && ( + service.Status != ServiceControllerStatus.Stopped || + service.Status == ServiceControllerStatus.StopPending || + service.Status == ServiceControllerStatus.Paused || + service.Status == ServiceControllerStatus.PausePending); + } + + public virtual void Install(string serviceName) + { + _logger.Info("Installing service '{0}'", serviceName); + + + var installer = new ServiceProcessInstaller + { + Account = ServiceAccount.LocalSystem + }; + + var serviceInstaller = new ServiceInstaller(); + + + string[] cmdline = { @"/assemblypath=" + Process.GetCurrentProcess().MainModule.FileName }; + + var context = new InstallContext("service_install.log", cmdline); + serviceInstaller.Context = context; + serviceInstaller.DisplayName = serviceName; + serviceInstaller.ServiceName = serviceName; + serviceInstaller.Description = "Ombi Application Server"; + serviceInstaller.StartType = ServiceStartMode.Automatic; + serviceInstaller.ServicesDependedOn = new[] { "EventLog", "Tcpip", "http" }; + + serviceInstaller.Parent = installer; + + serviceInstaller.Install(new ListDictionary()); + + _logger.Info("Service Has installed successfully."); + } + + public virtual void Run(ServiceBase service) + { + ServiceBase.Run(service); + } + + public virtual ServiceController GetService(string serviceName) + { + return ServiceController.GetServices().FirstOrDefault(c => string.Equals(c.ServiceName, serviceName, StringComparison.InvariantCultureIgnoreCase)); + } + + public virtual void Stop(string serviceName) + { + _logger.Info("Stopping {0} Service...", serviceName); + var service = GetService(serviceName); + if (service == null) + { + _logger.Warn("Unable to stop {0}. no service with that name exists.", serviceName); + return; + } + + _logger.Info("Service is currently {0}", service.Status); + + if (service.Status != ServiceControllerStatus.Stopped) + { + service.Stop(); + service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60)); + + service.Refresh(); + if (service.Status == ServiceControllerStatus.Stopped) + { + _logger.Info("{0} has stopped successfully.", serviceName); + } + else + { + _logger.Error("Service stop request has timed out. {0}", service.Status); + } + } + else + { + _logger.Warn("Service {0} is already in stopped state.", service.ServiceName); + } + } + + public ServiceControllerStatus GetStatus(string serviceName) + { + return GetService(serviceName).Status; + } + + public void Start(string serviceName) + { + _logger.Info("Starting {0} Service...", serviceName); + var service = GetService(serviceName); + if (service == null) + { + _logger.Warn("Unable to start '{0}' no service with that name exists.", serviceName); + return; + } + + if (service.Status != ServiceControllerStatus.Paused && service.Status != ServiceControllerStatus.Stopped) + { + _logger.Warn("Service is in a state that can't be started. Current status: {0}", service.Status); + } + + service.Start(); + + service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(60)); + service.Refresh(); + + if (service.Status == ServiceControllerStatus.Running) + { + _logger.Info("{0} has started successfully.", serviceName); + } + else + { + _logger.Error("Service start request has timed out. {0}", service.Status); + } + } + + public void Restart(string serviceName) + { + var args = string.Format("/C net.exe stop \"{0}\" && net.exe start \"{0}\"", serviceName); + + _processProvider.Start("cmd.exe", args); + } + } +} \ No newline at end of file diff --git a/Ombi.Common/packages.config b/Ombi.Common/packages.config new file mode 100644 index 000000000..f05a0e060 --- /dev/null +++ b/Ombi.Common/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Ombi.Core.Migration/Migrations/Version2200.cs b/Ombi.Core.Migration/Migrations/Version2200.cs index de9f0d77e..4422846f0 100644 --- a/Ombi.Core.Migration/Migrations/Version2200.cs +++ b/Ombi.Core.Migration/Migrations/Version2200.cs @@ -27,26 +27,37 @@ #endregion +using System; using System.Data; using NLog; using Ombi.Core.SettingModels; using Ombi.Store; +using Ombi.Store.Models; +using Ombi.Store.Models.Plex; +using Ombi.Store.Repository; +using Quartz.Collection; namespace Ombi.Core.Migration.Migrations { [Migration(22000, "v2.20.0.0")] public class Version2200 : BaseMigration, IMigration { - public Version2200(ISettingsService custom, ISettingsService ps) + public Version2200(ISettingsService custom, ISettingsService ps, IRepository log, + IRepository content, IRepository plexEp) { Customization = custom; PlexSettings = ps; + Log = log; + PlexContent = content; + PlexEpisodes = plexEp; } public int Version => 22000; - private ISettingsService Customization { get; set; } - private ISettingsService PlexSettings { get; set; } - + private ISettingsService Customization { get; } + private ISettingsService PlexSettings { get; } + private IRepository Log { get; } + private IRepository PlexContent { get; } + private IRepository PlexEpisodes { get; } private static Logger Logger = LogManager.GetCurrentClassLogger(); @@ -56,12 +67,46 @@ namespace Ombi.Core.Migration.Migrations UpdateCustomSettings(); AddNewColumns(con); UpdateSchema(con, Version); + UpdateRecentlyAdded(con); + } + + private void UpdateRecentlyAdded(IDbConnection con) + { + var allContent = PlexContent.GetAll(); + + var content = new HashSet(); + foreach (var plexContent in allContent) + { + content.Add(new RecentlyAddedLog + { + AddedAt = DateTime.UtcNow, + ProviderId = plexContent.ProviderId + }); + } + + Log.BatchInsert(content, "RecentlyAddedLog"); + + var allEp = PlexEpisodes.GetAll(); + content.Clear(); + foreach (var ep in allEp) + { + content.Add(new RecentlyAddedLog + { + AddedAt = DateTime.UtcNow, + ProviderId = ep.ProviderId + }); + } + + Log.BatchInsert(content, "RecentlyAddedLog"); } private void AddNewColumns(IDbConnection con) { con.AlterTable("EmbyContent", "ADD", "AddedAt", true, "VARCHAR(50)"); con.AlterTable("EmbyEpisodes", "ADD", "AddedAt", true, "VARCHAR(50)"); + + con.AlterTable("PlexContent", "ADD", "ItemID", true, "VARCHAR(100)"); + con.AlterTable("PlexContent", "ADD", "AddedAt", true, "VARCHAR(100)"); } private void UpdatePlexSettings() diff --git a/Ombi.Core/SettingModels/PlexRequestSettings.cs b/Ombi.Core/SettingModels/PlexRequestSettings.cs index 6c77ba727..026c84c24 100644 --- a/Ombi.Core/SettingModels/PlexRequestSettings.cs +++ b/Ombi.Core/SettingModels/PlexRequestSettings.cs @@ -41,6 +41,7 @@ namespace Ombi.Core.SettingModels public int Port { get; set; } public string BaseUrl { get; set; } public bool SearchForMovies { get; set; } + public bool SearchForActors { get; set; } public bool SearchForTvShows { get; set; } public bool SearchForMusic { get; set; } [Obsolete("Use the user management settings")] diff --git a/Ombi.Core/Setup.cs b/Ombi.Core/Setup.cs index 1eb05837b..1d21e33b9 100644 --- a/Ombi.Core/Setup.cs +++ b/Ombi.Core/Setup.cs @@ -77,6 +77,7 @@ namespace Ombi.Core { SearchForMovies = true, SearchForTvShows = true, + SearchForActors = true, BaseUrl = baseUrl ?? string.Empty, CollectAnalyticData = true, }; diff --git a/Ombi.Core/StatusChecker/StatusChecker.cs b/Ombi.Core/StatusChecker/StatusChecker.cs index d83e7189c..11710f21b 100644 --- a/Ombi.Core/StatusChecker/StatusChecker.cs +++ b/Ombi.Core/StatusChecker/StatusChecker.cs @@ -202,6 +202,7 @@ namespace Ombi.Core.StatusChecker public async Task OAuth(string url, ISession session) { + await Task.Yield(); var csrf = StringCipher.Encrypt(Guid.NewGuid().ToString("N"), "CSRF"); session[SessionKeys.CSRF] = csrf; diff --git a/Ombi.Services/Interfaces/IAvailabilityChecker.cs b/Ombi.Services/Interfaces/IAvailabilityChecker.cs index f2915faa0..cf602e531 100644 --- a/Ombi.Services/Interfaces/IAvailabilityChecker.cs +++ b/Ombi.Services/Interfaces/IAvailabilityChecker.cs @@ -37,15 +37,15 @@ namespace Ombi.Services.Interfaces void Start(); void CheckAndUpdateAll(); IEnumerable GetPlexMovies(IEnumerable content); - bool IsMovieAvailable(PlexContent[] plexMovies, string title, string year, string providerId = null); + bool IsMovieAvailable(IEnumerable plexMovies, string title, string year, string providerId = null); IEnumerable GetPlexTvShows(IEnumerable content); - bool IsTvShowAvailable(PlexContent[] plexShows, string title, string year, string providerId = null, int[] seasons = null); + bool IsTvShowAvailable(IEnumerable plexShows, string title, string year, string providerId = null, int[] seasons = null); IEnumerable GetPlexAlbums(IEnumerable content); - bool IsAlbumAvailable(PlexContent[] plexAlbums, string title, string year, string artist); + bool IsAlbumAvailable(IEnumerable plexAlbums, string title, string year, string artist); bool IsEpisodeAvailable(string theTvDbId, int season, int episode); - PlexContent GetAlbum(PlexContent[] plexAlbums, string title, string year, string artist); - PlexContent GetMovie(PlexContent[] plexMovies, string title, string year, string providerId = null); - PlexContent GetTvShow(PlexContent[] plexShows, string title, string year, string providerId = null, int[] seasons = null); + PlexContent GetAlbum(IEnumerable plexAlbums, string title, string year, string artist); + PlexContent GetMovie(IEnumerable plexMovies, string title, string year, string providerId = null); + PlexContent GetTvShow(IEnumerable plexShows, string title, string year, string providerId = null, int[] seasons = null); /// /// Gets the episode's stored in the cache. /// diff --git a/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs b/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs index 166ed987a..da4a79212 100644 --- a/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs @@ -161,15 +161,15 @@ namespace Ombi.Services.Jobs return content.Where(x => x.Type == EmbyMediaType.Movie); } - public bool IsMovieAvailable(EmbyContent[] embyMovies, string title, string year, string providerId) + public bool IsMovieAvailable(IEnumerable embyMovies, string title, string year, string providerId) { var movie = GetMovie(embyMovies, title, year, providerId); return movie != null; } - public EmbyContent GetMovie(EmbyContent[] embyMovies, string title, string year, string providerId) + public EmbyContent GetMovie(IEnumerable embyMovies, string title, string year, string providerId) { - if (embyMovies.Length == 0) + if (embyMovies.Count() == 0) { return null; } @@ -200,14 +200,14 @@ namespace Ombi.Services.Jobs return content.Where(x => x.Type == EmbyMediaType.Series); } - public bool IsTvShowAvailable(EmbyContent[] embyShows, string title, string year, string providerId, int[] seasons = null) + public bool IsTvShowAvailable(IEnumerable embyShows, string title, string year, string providerId, int[] seasons = null) { var show = GetTvShow(embyShows, title, year, providerId, seasons); return show != null; } - public EmbyContent GetTvShow(EmbyContent[] embyShows, string title, string year, string providerId, + public EmbyContent GetTvShow(IEnumerable embyShows, string title, string year, string providerId, int[] seasons = null) { foreach (var show in embyShows) diff --git a/Ombi.Services/Jobs/EmbyEpisodeCacher.cs b/Ombi.Services/Jobs/EmbyEpisodeCacher.cs index 0135592cc..387a7dc98 100644 --- a/Ombi.Services/Jobs/EmbyEpisodeCacher.cs +++ b/Ombi.Services/Jobs/EmbyEpisodeCacher.cs @@ -111,7 +111,7 @@ namespace Ombi.Services.Jobs } // Insert the new items - var result = Repo.BatchInsert(model, TableName, typeof(EmbyEpisodes).GetPropertyNames()); + var result = Repo.BatchInsert(model, TableName); if (!result) { diff --git a/Ombi.Services/Jobs/Interfaces/IEmbyAvailabilityChecker.cs b/Ombi.Services/Jobs/Interfaces/IEmbyAvailabilityChecker.cs index a954064e7..52e620118 100644 --- a/Ombi.Services/Jobs/Interfaces/IEmbyAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/Interfaces/IEmbyAvailabilityChecker.cs @@ -14,11 +14,11 @@ namespace Ombi.Services.Jobs IEnumerable GetEmbyTvShows(IEnumerable content); Task> GetEpisodes(); Task> GetEpisodes(int theTvDbId); - EmbyContent GetMovie(EmbyContent[] embyMovies, string title, string year, string providerId); - EmbyContent GetTvShow(EmbyContent[] embyShows, string title, string year, string providerId, int[] seasons = null); + EmbyContent GetMovie(IEnumerable embyMovies, string title, string year, string providerId); + EmbyContent GetTvShow(IEnumerable embyShows, string title, string year, string providerId, int[] seasons = null); bool IsEpisodeAvailable(string theTvDbId, int season, int episode); - bool IsMovieAvailable(EmbyContent[] embyMovies, string title, string year, string providerId); - bool IsTvShowAvailable(EmbyContent[] embyShows, string title, string year, string providerId, int[] seasons = null); + bool IsMovieAvailable(IEnumerable embyMovies, string title, string year, string providerId); + bool IsTvShowAvailable(IEnumerable embyShows, string title, string year, string providerId, int[] seasons = null); void Start(); } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/PlexAvailabilityChecker.cs b/Ombi.Services/Jobs/PlexAvailabilityChecker.cs index e9da44eb5..e6da24b14 100644 --- a/Ombi.Services/Jobs/PlexAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/PlexAvailabilityChecker.cs @@ -194,15 +194,15 @@ namespace Ombi.Services.Jobs return content.Where(x => x.Type == Store.Models.Plex.PlexMediaType.Movie); } - public bool IsMovieAvailable(PlexContent[] plexMovies, string title, string year, string providerId = null) + public bool IsMovieAvailable(IEnumerable plexMovies, string title, string year, string providerId = null) { var movie = GetMovie(plexMovies, title, year, providerId); return movie != null; } - public PlexContent GetMovie(PlexContent[] plexMovies, string title, string year, string providerId = null) + public PlexContent GetMovie(IEnumerable plexMovies, string title, string year, string providerId = null) { - if (plexMovies.Length == 0) + if (plexMovies.Count() == 0) { return null; } @@ -236,14 +236,14 @@ namespace Ombi.Services.Jobs return content.Where(x => x.Type == Store.Models.Plex.PlexMediaType.Show); } - public bool IsTvShowAvailable(PlexContent[] plexShows, string title, string year, string providerId = null, int[] seasons = null) + public bool IsTvShowAvailable(IEnumerable plexShows, string title, string year, string providerId = null, int[] seasons = null) { var show = GetTvShow(plexShows, title, year, providerId, seasons); return show != null; } - public PlexContent GetTvShow(PlexContent[] plexShows, string title, string year, string providerId = null, + public PlexContent GetTvShow(IEnumerable plexShows, string title, string year, string providerId = null, int[] seasons = null) { var advanced = !string.IsNullOrEmpty(providerId); @@ -345,14 +345,14 @@ namespace Ombi.Services.Jobs return content.Where(x => x.Type == Store.Models.Plex.PlexMediaType.Artist); } - public bool IsAlbumAvailable(PlexContent[] plexAlbums, string title, string year, string artist) + public bool IsAlbumAvailable(IEnumerable plexAlbums, string title, string year, string artist) { return plexAlbums.Any(x => x.Title.Contains(title) && x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase)); } - public PlexContent GetAlbum(PlexContent[] plexAlbums, string title, string year, string artist) + public PlexContent GetAlbum(IEnumerable plexAlbums, string title, string year, string artist) { return plexAlbums.FirstOrDefault(x => x.Title.Contains(title) && diff --git a/Ombi.Services/Jobs/PlexContentCacher.cs b/Ombi.Services/Jobs/PlexContentCacher.cs index 936a7a60b..041374c6b 100644 --- a/Ombi.Services/Jobs/PlexContentCacher.cs +++ b/Ombi.Services/Jobs/PlexContentCacher.cs @@ -115,7 +115,8 @@ namespace Ombi.Services.Jobs ReleaseYear = video.Year, Title = video.Title, ProviderId = video.ProviderId, - Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, video.RatingKey) + Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, video.RatingKey), + ItemId = video.RatingKey })); } } @@ -145,6 +146,7 @@ namespace Ombi.Services.Jobs ProviderId = x.ProviderId, Seasons = x.Seasons?.Select(d => PlexHelper.GetSeasonNumberFromTitle(d.Title)).ToArray(), Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey), + ItemId= x.RatingKey })); } @@ -271,7 +273,8 @@ namespace Ombi.Services.Jobs ReleaseYear = m.ReleaseYear ?? string.Empty, Title = m.Title, Type = Store.Models.Plex.PlexMediaType.Movie, - Url = m.Url + Url = m.Url, + ItemId = m.ItemId }); } } @@ -311,7 +314,8 @@ namespace Ombi.Services.Jobs Title = t.Title, Type = Store.Models.Plex.PlexMediaType.Show, Url = t.Url, - Seasons = ByteConverterHelper.ReturnBytes(t.Seasons) + Seasons = ByteConverterHelper.ReturnBytes(t.Seasons), + ItemId = t.ItemId }); } } @@ -352,7 +356,7 @@ namespace Ombi.Services.Jobs ReleaseYear = a.ReleaseYear ?? string.Empty, Title = a.Title, Type = Store.Models.Plex.PlexMediaType.Artist, - Url = a.Url + Url = a.Url, }); } } diff --git a/Ombi.Services/Jobs/PlexEpisodeCacher.cs b/Ombi.Services/Jobs/PlexEpisodeCacher.cs index e6d1fc9c9..58ebe7fd3 100644 --- a/Ombi.Services/Jobs/PlexEpisodeCacher.cs +++ b/Ombi.Services/Jobs/PlexEpisodeCacher.cs @@ -134,7 +134,7 @@ namespace Ombi.Services.Jobs Repo.DeleteAll(TableName); // Insert the new items - var result = Repo.BatchInsert(entities.Select(x => x.Key).ToList(), TableName, typeof(PlexEpisodes).GetPropertyNames()); + var result = Repo.BatchInsert(entities.Select(x => x.Key).ToList(), TableName); if (!result) { diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs index b78f64dcc..90e32dcd0 100644 --- a/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs @@ -228,7 +228,7 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter AddParagraph(sb, info.Overview); } - catch (RequestLimitExceededException limit) + catch (Exception limit) { // We have hit a limit, we need to now wait. Thread.Sleep(TimeSpan.FromSeconds(10)); diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/IPlexNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/IPlexNewsletter.cs new file mode 100644 index 000000000..f22ccf519 --- /dev/null +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/IPlexNewsletter.cs @@ -0,0 +1,7 @@ +namespace Ombi.Services.Jobs.RecentlyAddedNewsletter +{ + public interface IPlexNewsletter + { + string GetNewsletterHtml(bool test); + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/PlexRecentlyAddedNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/PlexRecentlyAddedNewsletter.cs new file mode 100644 index 000000000..0b37ae813 --- /dev/null +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/PlexRecentlyAddedNewsletter.cs @@ -0,0 +1,363 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RecentlyAddedModel.cs +// Created By: Jamie Rees +// +// 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. +// ************************************************************************/ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using NLog; +using Ombi.Api; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Emby; +using Ombi.Api.Models.Plex; +using Ombi.Core; +using Ombi.Core.SettingModels; +using Ombi.Helpers; +using Ombi.Services.Jobs.Templates; +using Ombi.Store.Models; +using Ombi.Store.Models.Emby; +using Ombi.Store.Models.Plex; +using Ombi.Store.Repository; +using TMDbLib.Objects.Exceptions; +using EmbyMediaType = Ombi.Store.Models.Plex.EmbyMediaType; +using PlexMediaType = Ombi.Store.Models.Plex.PlexMediaType; + +namespace Ombi.Services.Jobs.RecentlyAddedNewsletter +{ + public class PlexRecentlyAddedNewsletter : HtmlTemplateGenerator, IPlexNewsletter + { + public PlexRecentlyAddedNewsletter(IPlexApi api, ISettingsService plexSettings, + ISettingsService email, + ISettingsService newsletter, IRepository log, + IRepository embyContent, IRepository episodes) + { + Api = api; + PlexSettings = plexSettings; + EmailSettings = email; + NewsletterSettings = newsletter; + Content = embyContent; + MovieApi = new TheMovieDbApi(); + TvApi = new TvMazeApi(); + Episodes = episodes; + RecentlyAddedLog = log; + } + + private IPlexApi Api { get; } + private TheMovieDbApi MovieApi { get; } + private TvMazeApi TvApi { get; } + private ISettingsService PlexSettings { get; } + private ISettingsService EmailSettings { get; } + private ISettingsService NewsletterSettings { get; } + private IRepository Content { get; } + private IRepository Episodes { get; } + private IRepository RecentlyAddedLog { get; } + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public string GetNewsletterHtml(bool test) + { + try + { + return GetHtml(test); + } + catch (Exception e) + { + Log.Error(e); + return string.Empty; + } + } + + private class PlexRecentlyAddedModel + { + public PlexMetadata Metadata { get; set; } + public PlexContent Content { get; set; } + } + + private string GetHtml(bool test) + { + var sb = new StringBuilder(); + var plexSettings = PlexSettings.GetSettings(); + + var plexContent = Content.GetAll().ToList(); + + var series = plexContent.Where(x => x.Type == PlexMediaType.Show).ToList(); + var episodes = Episodes.GetAll().ToList(); + var movie = plexContent.Where(x => x.Type == PlexMediaType.Movie).ToList(); + + var recentlyAdded = RecentlyAddedLog.GetAll().ToList(); + + var firstRun = !recentlyAdded.Any(); + + var filteredMovies = movie.Where(m => recentlyAdded.All(x => x.ProviderId != m.ProviderId)).ToList(); + var filteredEp = episodes.Where(m => recentlyAdded.All(x => x.ProviderId != m.ProviderId)).ToList(); + + + var info = new List(); + foreach (var m in filteredMovies) + { + var i = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, m.ItemId); + info.Add(new PlexRecentlyAddedModel + { + Metadata = i, + Content = m + }); + } + GenerateMovieHtml(info, sb); + + info.Clear(); + foreach (var t in series) + { + var i = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, t.ItemId); + + //var ep = filteredEp.Where(x => x.ShowTitle == t.Title); + info.Add(new PlexRecentlyAddedModel + { + Metadata = i, + Content = t + }); + //if (ep.Any()) + //{ + // var episodeList = new List(); + // foreach (var embyEpisodese in ep) + // { + // var epInfo = Api.GetInformation(embyEpisodese.EmbyId, Ombi.Api.Models.Emby.EmbyMediaType.Episode, + // embySettings.ApiKey, embySettings.AdministratorId, embySettings.FullUri); + // episodeList.Add(epInfo.EpisodeInformation); + // } + // info.Add(new EmbyRecentlyAddedModel + // { + // EmbyContent = t, + // EmbyInformation = i, + // EpisodeInformation = episodeList + // }); + //} + } + GenerateTvHtml(info, sb); + + var template = new RecentlyAddedTemplate(); + var html = template.LoadTemplate(sb.ToString()); + Log.Debug("Loaded the template"); + + if (!test || firstRun) + { + foreach (var a in filteredMovies) + { + RecentlyAddedLog.Insert(new RecentlyAddedLog + { + ProviderId = a.ProviderId, + AddedAt = DateTime.UtcNow + }); + } + foreach (var a in filteredEp) + { + RecentlyAddedLog.Insert(new RecentlyAddedLog + { + ProviderId = a.ProviderId, + AddedAt = DateTime.UtcNow + }); + } + } + + var escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); + Log.Debug(escapedHtml); + return escapedHtml; + } + + private void GenerateMovieHtml(IEnumerable recentlyAddedMovies, StringBuilder sb) + { + var movies = recentlyAddedMovies?.ToList() ?? new List(); + if (!movies.Any()) + { + return; + } + var orderedMovies = movies.OrderByDescending(x => x.Content.AddedAt).ToList(); + sb.Append("

New Movies:



"); + sb.Append( + ""); + foreach (var movie in orderedMovies) + { + // We have a try within a try so we can catch the rate limit without ending the loop (finally block) + try + { + try + { + + var imdbId = PlexHelper.GetProviderIdFromPlexGuid(movie.Metadata.Video.Guid); + var info = MovieApi.GetMovieInformation(imdbId).Result; + if (info == null) + { + throw new Exception($"Movie with Imdb id {imdbId} returned null from the MovieApi"); + } + AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}"); + + sb.Append(""); + sb.Append( + "
"); + + Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/"); + Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}"); + EndTag(sb, "a"); + + if (info.Genres.Any()) + { + AddParagraph(sb, + $"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); + } + + AddParagraph(sb, info.Overview); + } + catch (RequestLimitExceededException limit) + { + // We have hit a limit, we need to now wait. + Thread.Sleep(TimeSpan.FromSeconds(10)); + Log.Info(limit); + } + } + catch (Exception e) + { + Log.Error(e); + Log.Error("Error for movie with IMDB Id = {0}", movie.Metadata.Video.Guid); + } + finally + { + EndLoopHtml(sb); + } + + } + sb.Append("


"); + } + + private class TvModel + { + public EmbySeriesInformation Series { get; set; } + public List Episodes { get; set; } + } + private void GenerateTvHtml(IEnumerable recenetlyAddedTv, StringBuilder sb) + { + var tv = recenetlyAddedTv?.ToList() ?? new List(); + + if (!tv.Any()) + { + return; + } + var orderedTv = tv.OrderByDescending(x => x.Content.AddedAt).ToList(); + + // TV + sb.Append("

New Episodes:



"); + sb.Append( + ""); + foreach (var t in orderedTv) + { + //var seriesItem = t.EmbyInformation.SeriesInformation; + //var relatedEpisodes = t.EpisodeInformation; + + + try + { + var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(t.Metadata.Directory.Guid))); + + var banner = info.image?.original; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + AddImageInsideTable(sb, banner); + + sb.Append(""); + sb.Append( + "
"); + + var title = $"{t.Content.Title} {t.Content.ReleaseYear}"; + + Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); + Header(sb, 3, title); + EndTag(sb, "a"); + + //var results = relatedEpisodes.GroupBy(p => p.ParentIndexNumber, + // (key, g) => new + // { + // ParentIndexNumber = key, + // IndexNumber = g.ToList() + // } + //); + // Group the episodes + //foreach (var embyEpisodeInformation in results.OrderBy(x => x.ParentIndexNumber)) + //{ + // var epSb = new StringBuilder(); + // for (var i = 0; i < embyEpisodeInformation.IndexNumber.Count; i++) + // { + // var ep = embyEpisodeInformation.IndexNumber[i]; + // if (i < embyEpisodeInformation.IndexNumber.Count) + // { + // epSb.Append($"{ep.IndexNumber},"); + // } + // else + // { + // epSb.Append(ep); + // } + // } + // AddParagraph(sb, $"Season: {embyEpisodeInformation.ParentIndexNumber}, Episode: {epSb}"); + //} + + if (info.genres.Any()) + { + AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + } + + AddParagraph(sb, string.IsNullOrEmpty(t.Metadata.Directory.Summary) ? t.Metadata.Directory.Summary : info.summary); + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + EndLoopHtml(sb); + } + } + sb.Append("


"); + } + + + + + private void EndLoopHtml(StringBuilder sb) + { + //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... + //Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag + sb.Append("
"); + sb.Append("
"); + sb.Append("
"); + sb.Append(""); + sb.Append(""); + } + + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs index e2bbf5ede..330adf60c 100644 --- a/Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs @@ -53,18 +53,19 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter public RecentlyAddedNewsletter(IPlexApi api, ISettingsService plexSettings, ISettingsService email, IJobRecord rec, ISettingsService newsletter, - IPlexReadOnlyDatabase db, IUserHelper userHelper, IEmbyAddedNewsletter embyNews, - ISettingsService embyS) + IUserHelper userHelper, IEmbyAddedNewsletter embyNews, + ISettingsService embyS, + IPlexNewsletter plex) { JobRecord = rec; Api = api; PlexSettings = plexSettings; EmailSettings = email; NewsletterSettings = newsletter; - PlexDb = db; UserHelper = userHelper; EmbyNewsletter = embyNews; EmbySettings = embyS; + PlexNewsletter = plex; } private IPlexApi Api { get; } @@ -75,9 +76,9 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter private ISettingsService EmailSettings { get; } private ISettingsService NewsletterSettings { get; } private IJobRecord JobRecord { get; } - private IPlexReadOnlyDatabase PlexDb { get; } private IUserHelper UserHelper { get; } private IEmbyAddedNewsletter EmbyNewsletter { get; } + private IPlexNewsletter PlexNewsletter { get; } private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -144,331 +145,18 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter } else { - var sb = new StringBuilder(); var plexSettings = PlexSettings.GetSettings(); - Log.Debug("Got Plex Settings"); - - var libs = Api.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri); - Log.Debug("Getting Plex Library Sections"); - - var tvSections = libs.Directories.Where(x => x.type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)); // We could have more than 1 lib - Log.Debug("Filtered sections for TV"); - var movieSection = libs.Directories.Where(x => x.type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)); // We could have more than 1 lib - Log.Debug("Filtered sections for Movies"); - - var plexVersion = Api.GetStatus(plexSettings.PlexAuthToken, plexSettings.FullUri).Version; - - var html = string.Empty; - if (plexVersion.StartsWith("1.3")) + if (plexSettings.Enable) { - var tvMetadata = new List(); - var movieMetadata = new List(); - foreach (var tvSection in tvSections) - { - var item = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, - tvSection?.Key); - if (item?.MediaContainer?.Metadata != null) - { - tvMetadata.AddRange(item?.MediaContainer?.Metadata); - } - } - Log.Debug("Got RecentlyAdded TV Shows"); - foreach (var movie in movieSection) - { - var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movie?.Key); - if (recentlyAddedMovies?.MediaContainer?.Metadata != null) - { - movieMetadata.AddRange(recentlyAddedMovies?.MediaContainer?.Metadata); - } - } - Log.Debug("Got RecentlyAdded Movies"); + var html = PlexNewsletter.GetNewsletterHtml(testEmail); - Log.Debug("Started Generating Movie HTML"); - GenerateMovieHtml(movieMetadata, plexSettings, sb); - Log.Debug("Finished Generating Movie HTML"); - Log.Debug("Started Generating TV HTML"); - GenerateTvHtml(tvMetadata, plexSettings, sb); - Log.Debug("Finished Generating TV HTML"); - - var template = new RecentlyAddedTemplate(); - html = template.LoadTemplate(sb.ToString()); - Log.Debug("Loaded the template"); - } - else - { - // Old API - var tvChild = new List(); - var movieChild = new List(); - foreach (var tvSection in tvSections) - { - var recentlyAddedTv = Api.RecentlyAddedOld(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection?.Key); - if (recentlyAddedTv?._children != null) - { - tvChild.AddRange(recentlyAddedTv?._children); - } - } - - Log.Debug("Got RecentlyAdded TV Shows"); - foreach (var movie in movieSection) - { - var recentlyAddedMovies = Api.RecentlyAddedOld(plexSettings.PlexAuthToken, plexSettings.FullUri, movie?.Key); - if (recentlyAddedMovies?._children != null) - { - tvChild.AddRange(recentlyAddedMovies?._children); - } - } - Log.Debug("Got RecentlyAdded Movies"); - - Log.Debug("Started Generating Movie HTML"); - GenerateMovieHtml(movieChild, plexSettings, sb); - Log.Debug("Finished Generating Movie HTML"); - Log.Debug("Started Generating TV HTML"); - GenerateTvHtml(tvChild, plexSettings, sb); - Log.Debug("Finished Generating TV HTML"); - - var template = new RecentlyAddedTemplate(); - html = template.LoadTemplate(sb.ToString()); - Log.Debug("Loaded the template"); - } - string escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); - Log.Debug(escapedHtml); - SendNewsletter(newletterSettings, escapedHtml, testEmail); - } - } - - private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) - { - var orderedMovies = movies.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList() ?? new List(); - sb.Append("

New Movies:



"); - sb.Append( - ""); - foreach (var movie in orderedMovies) - { - var plexGUID = string.Empty; - try - { - var metaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, - movie.ratingKey.ToString()); - - plexGUID = metaData.Video.Guid; - - var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID); - var info = _movieApi.GetMovieInformation(imdbId).Result; - if (info == null) - { - throw new Exception($"Movie with Imdb id {imdbId} returned null from the MovieApi"); - } - AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}"); - - sb.Append(""); - sb.Append( - "
"); - - Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/"); - Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}"); - EndTag(sb, "a"); - - if (info.Genres.Any()) - { - AddParagraph(sb, - $"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); - } - - AddParagraph(sb, info.Overview); - } - catch (Exception e) - { - Log.Error(e); - Log.Error( - "Exception when trying to process a Movie, either in getting the metadata from Plex OR getting the information from TheMovieDB, Plex GUID = {0}", - plexGUID); - } - finally - { - EndLoopHtml(sb); - } - - } - sb.Append("


"); - } - - private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) - { - var orderedMovies = movies.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList() ?? new List(); - sb.Append("

New Movies:



"); - sb.Append( - ""); - foreach (var movie in orderedMovies) - { - var plexGUID = string.Empty; - try - { - var metaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, - movie.ratingKey.ToString()); - - plexGUID = metaData.Video.Guid; - - var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID); - var info = _movieApi.GetMovieInformation(imdbId).Result; - if (info == null) - { - throw new Exception($"Movie with Imdb id {imdbId} returned null from the MovieApi"); - } - AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}"); - - sb.Append(""); - sb.Append( - "
"); - - Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/"); - Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}"); - EndTag(sb, "a"); - - if (info.Genres.Any()) - { - AddParagraph(sb, - $"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); - } - - AddParagraph(sb, info.Overview); - } - catch (Exception e) - { - Log.Error(e); - Log.Error( - "Exception when trying to process a Movie, either in getting the metadata from Plex OR getting the information from TheMovieDB, Plex GUID = {0}", - plexGUID); - } - finally - { - EndLoopHtml(sb); - } - - } - sb.Append("


"); - } - - private void GenerateTvHtml(List tv, PlexSettings plexSettings, StringBuilder sb) - { - var orderedTv = tv.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList(); - // TV - sb.Append("

New Episodes:



"); - sb.Append( - ""); - foreach (var t in orderedTv) - { - var plexGUID = string.Empty; - try - { - - var parentMetaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, - t.parentRatingKey.ToString()); - - plexGUID = parentMetaData.Directory.Guid; - - var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(plexGUID))); - - var banner = info.image?.original; - if (!string.IsNullOrEmpty(banner)) - { - banner = banner.Replace("http", "https"); // Always use the Https banners - } - AddImageInsideTable(sb, banner); - - sb.Append(""); - sb.Append( - "
"); - - var title = $"{t.grandparentTitle} - {t.title} {t.originallyAvailableAt?.Substring(0, 4)}"; - - Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); - Header(sb, 3, title); - EndTag(sb, "a"); - - AddParagraph(sb, $"Season: {t.parentIndex}, Episode: {t.index}"); - if (info.genres.Any()) - { - AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); - } - - AddParagraph(sb, string.IsNullOrEmpty(t.summary) ? info.summary : t.summary); - } - catch (Exception e) - { - Log.Error(e); - Log.Error( - "Exception when trying to process a TV Show, either in getting the metadata from Plex OR getting the information from TVMaze, Plex GUID = {0}", - plexGUID); - } - finally - { - EndLoopHtml(sb); + var escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); + Log.Debug(escapedHtml); + SendNewsletter(newletterSettings, html, testEmail); } } - sb.Append("


"); } - private void GenerateTvHtml(List tv, PlexSettings plexSettings, StringBuilder sb) - { - var orderedTv = tv.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList(); - // TV - sb.Append("

New Episodes:



"); - sb.Append( - ""); - foreach (var t in orderedTv) - { - var plexGUID = string.Empty; - try - { - - var parentMetaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, - t.parentRatingKey.ToString()); - - plexGUID = parentMetaData.Directory.Guid; - - var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(plexGUID))); - - var banner = info.image?.original; - if (!string.IsNullOrEmpty(banner)) - { - banner = banner.Replace("http", "https"); // Always use the Https banners - } - AddImageInsideTable(sb, banner); - - sb.Append(""); - sb.Append( - "
"); - - var title = $"{t.grandparentTitle} - {t.title} {t.originallyAvailableAt?.Substring(0, 4)}"; - - Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); - Header(sb, 3, title); - EndTag(sb, "a"); - - AddParagraph(sb, $"Season: {t.parentIndex}, Episode: {t.index}"); - if (info.genres.Any()) - { - AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); - } - - AddParagraph(sb, string.IsNullOrEmpty(t.summary) ? info.summary : t.summary); - } - catch (Exception e) - { - Log.Error(e); - Log.Error( - "Exception when trying to process a TV Show, either in getting the metadata from Plex OR getting the information from TVMaze, Plex GUID = {0}", - plexGUID); - } - finally - { - EndLoopHtml(sb); - } - } - sb.Append("


"); - } - - private void SendMassEmail(string html, string subject, bool testEmail) { var settings = EmailSettings.GetSettings(); @@ -507,7 +195,7 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter SendMail(settings, message); } - // TODO Emby + private void SendNewsletter(NewletterSettings newletterSettings, string html, bool testEmail = false, string subject = "New Content on Plex!") { Log.Debug("Entering SendNewsletter"); @@ -588,17 +276,5 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter Log.Error(e); } } - - private void EndLoopHtml(StringBuilder sb) - { - //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... - //Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag - sb.Append("
"); - sb.Append("
"); - sb.Append("
"); - sb.Append(""); - sb.Append(""); - } - } } \ No newline at end of file diff --git a/Ombi.Services/Models/PlexMovie.cs b/Ombi.Services/Models/PlexMovie.cs index f0a55e4ce..540055d38 100644 --- a/Ombi.Services/Models/PlexMovie.cs +++ b/Ombi.Services/Models/PlexMovie.cs @@ -7,5 +7,6 @@ public string ReleaseYear { get; set; } public string ProviderId { get; set; } public string Url { get; set; } + public string ItemId { get; set; } } } diff --git a/Ombi.Services/Models/PlexTvShow.cs b/Ombi.Services/Models/PlexTvShow.cs index 60223c233..445296e0f 100644 --- a/Ombi.Services/Models/PlexTvShow.cs +++ b/Ombi.Services/Models/PlexTvShow.cs @@ -8,5 +8,6 @@ public string ProviderId { get; set; } public int[] Seasons { get; set; } public string Url { get; set; } + public string ItemId { get; set; } } } diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index 17093e031..923ea6155 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -108,7 +108,9 @@ + + diff --git a/Ombi.Store/Models/Plex/PlexContent.cs b/Ombi.Store/Models/Plex/PlexContent.cs index 4a24c4e1f..484bb88e0 100644 --- a/Ombi.Store/Models/Plex/PlexContent.cs +++ b/Ombi.Store/Models/Plex/PlexContent.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using System; using System.Data.Linq.Mapping; namespace Ombi.Store.Models.Plex @@ -47,5 +48,8 @@ namespace Ombi.Store.Models.Plex /// Only used for Albums /// public string Artist { get; set; } + + public string ItemId { get; set; } + public DateTime AddedAt { get; set; } } } \ No newline at end of file diff --git a/Ombi.Store/Repository/BaseGenericRepository.cs b/Ombi.Store/Repository/BaseGenericRepository.cs index 48469a8d6..a8593eb2a 100644 --- a/Ombi.Store/Repository/BaseGenericRepository.cs +++ b/Ombi.Store/Repository/BaseGenericRepository.cs @@ -286,7 +286,7 @@ namespace Ombi.Store.Repository } } - public bool BatchInsert(IEnumerable entities, string tableName, params string[] values) + public bool BatchInsert(IEnumerable entities, string tableName) { // If we have nothing to update, then it didn't fail... var enumerable = entities as T[] ?? entities.ToArray(); diff --git a/Ombi.Store/Repository/IRepository.cs b/Ombi.Store/Repository/IRepository.cs index 2901e73c9..618b05133 100644 --- a/Ombi.Store/Repository/IRepository.cs +++ b/Ombi.Store/Repository/IRepository.cs @@ -81,7 +81,7 @@ namespace Ombi.Store.Repository bool UpdateAll(IEnumerable entity); Task UpdateAllAsync(IEnumerable entity); - bool BatchInsert(IEnumerable entities, string tableName, params string[] values); + bool BatchInsert(IEnumerable entities, string tableName); IEnumerable Custom(Func> func); Task> CustomAsync(Func>> func); diff --git a/Ombi.Store/SqlTables.sql b/Ombi.Store/SqlTables.sql index cdf5a2f80..50ce19947 100644 --- a/Ombi.Store/SqlTables.sql +++ b/Ombi.Store/SqlTables.sql @@ -174,7 +174,10 @@ CREATE TABLE IF NOT EXISTS PlexContent Url VARCHAR(100) NOT NULL, Artist VARCHAR(100), Seasons BLOB, - Type INTEGER NOT NULL + Type INTEGER NOT NULL, + ItemID VARCHAR(100) NOT NULL, + + AddedAt VARCHAR(100) NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS PlexContent_Id ON PlexContent (Id); diff --git a/Ombi.UI/Content/requests.js b/Ombi.UI/Content/requests.js index 1d2ad987d..132bb7232 100644 --- a/Ombi.UI/Content/requests.js +++ b/Ombi.UI/Content/requests.js @@ -95,7 +95,10 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { //if ($tvl.mixItUp('isLoaded')) $tvl.mixItUp('destroy'); //$tvl.mixItUp(mixItUpConfig(activeState)); // init or reinit } - if (target === "#MoviesTab") { + if (target === "#MoviesTab" || target === "#ActorsTab") { + if (target === "#ActorsTab") { + actorLoad(); + } $('#approveMovies,#deleteMovies').show(); if ($tvl.mixItUp('isLoaded')) { activeState = $tvl.mixItUp('getState'); @@ -733,6 +736,37 @@ function initLoad() { } + +function actorLoad() { + var $ml = $('#actorMovieList'); + if ($ml.mixItUp('isLoaded')) { + activeState = $ml.mixItUp('getState'); + $ml.mixItUp('destroy'); + } + $ml.html(""); + + var $newOnly = $('#searchNewOnly').val(); + var url = createBaseUrl(base, '/requests/actor' + (!!$newOnly ? '/new' : '')); + $.ajax(url).success(function (results) { + if (results.length > 0) { + results.forEach(function (result) { + var context = buildRequestContext(result, "movie"); + var html = searchTemplate(context); + $ml.append(html); + }); + + + $('.customTooltip').tooltipster({ + contentCloning: true + }); + } + else { + $ml.html(noResultsHtml.format("movie")); + } + $ml.mixItUp(mixItUpConfig()); + }); +}; + function movieLoad() { var $ml = $('#movieList'); if ($ml.mixItUp('isLoaded')) { diff --git a/Ombi.UI/Content/search.js b/Ombi.UI/Content/search.js index bd7690329..ec215b159 100644 --- a/Ombi.UI/Content/search.js +++ b/Ombi.UI/Content/search.js @@ -63,6 +63,26 @@ $(function () { }); + // Type in actor search + $("#actorSearchContent").on("input", function () { + triggerActorSearch(); + }); + + // if they toggle the checkbox, we want to refresh our search + $("#actorsSearchNew").click(function () { + triggerActorSearch(); + }); + + function triggerActorSearch() + { + if (searchTimer) { + clearTimeout(searchTimer); + } + searchTimer = setTimeout(function () { + moviesFromActor(); + }.bind(this), 800); + } + $('#moviesComingSoon').on('click', function (e) { e.preventDefault(); moviesComingSoon(); @@ -300,7 +320,7 @@ $(function () { function movieSearch() { var query = $("#movieSearchContent").val(); var url = createBaseUrl(base, '/search/movie/'); - query ? getMovies(url + query) : resetMovies(); + query ? getMovies(url + query) : resetMovies("#movieList"); } function moviesComingSoon() { @@ -313,6 +333,13 @@ $(function () { getMovies(url); } + function moviesFromActor() { + var query = $("#actorSearchContent").val(); + var $newOnly = $('#actorsSearchNew')[0].checked; + var url = createBaseUrl(base, '/search/actor/' + (!!$newOnly ? 'new/' : '')); + query ? getMovies(url + query, "#actorMovieList", "#actorSearchButton") : resetMovies("#actorMovieList"); + } + function popularShows() { var url = createBaseUrl(base, '/search/tv/popular'); getTvShows(url, true); @@ -330,30 +357,31 @@ $(function () { getTvShows(url, true); } - function getMovies(url) { - resetMovies(); - - $('#movieSearchButton').attr("class", "fa fa-spinner fa-spin"); + function getMovies(url, target, button) { + target = target || "#movieList"; + button = button || "#movieSearchButton"; + resetMovies(target); + $(button).attr("class", "fa fa-spinner fa-spin"); $.ajax(url).success(function (results) { if (results.length > 0) { results.forEach(function (result) { var context = buildMovieContext(result); var html = searchTemplate(context); - $("#movieList").append(html); + $(target).append(html); checkNetflix(context.title, context.id); }); } else { - $("#movieList").html(noResultsHtml); + $(target).html(noResultsHtml); } - $('#movieSearchButton').attr("class", "fa fa-search"); + $(button).attr("class", "fa fa-search"); }); }; - function resetMovies() { - $("#movieList").html(""); + function resetMovies(target) { + $(target).html(""); } function tvSearch() { diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index a11c473b7..388faf926 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -178,7 +178,7 @@ namespace Ombi.UI.Modules.Admin Post["/", true] = async (x, ct) => await SaveAdmin(); - Post["/requestauth"] = _ => RequestAuthToken(); + Post["/requestauth", true] = async (x, ct) => await RequestAuthToken(); Get["/getusers"] = _ => GetUsers(); @@ -319,7 +319,7 @@ namespace Ombi.UI.Modules.Admin : new JsonResponseModel { Result = false, Message = "We could not save to the database, please try again" }); } - private Response RequestAuthToken() + private async Task RequestAuthToken() { var user = this.Bind(); @@ -335,11 +335,11 @@ namespace Ombi.UI.Modules.Admin return Response.AsJson(new { Result = false, Message = "Incorrect username or password!" }); } - var oldSettings = PlexService.GetSettings(); + var oldSettings = await PlexService.GetSettingsAsync(); if (oldSettings != null) { oldSettings.PlexAuthToken = model.user.authentication_token; - PlexService.SaveSettings(oldSettings); + await PlexService.SaveSettingsAsync(oldSettings); } else { @@ -347,10 +347,14 @@ namespace Ombi.UI.Modules.Admin { PlexAuthToken = model.user.authentication_token }; - PlexService.SaveSettings(newModel); + await PlexService.SaveSettingsAsync(newModel); } - return Response.AsJson(new { Result = true, AuthToken = model.user.authentication_token }); + var server = PlexApi.GetServer(model.user.authentication_token); + var machine = + server.Server.FirstOrDefault(x => x.AccessToken == model.user.authentication_token)?.MachineIdentifier; + + return Response.AsJson(new { Result = true, AuthToken = model.user.authentication_token, Identifier = machine }); } diff --git a/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs b/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs index 2fbe77eb0..083eb57d9 100644 --- a/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs +++ b/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs @@ -94,6 +94,8 @@ namespace Ombi.UI.Modules.Admin private async Task ScheduleRun(string key) { + await Task.Yield(); + if (key.Equals(JobNames.PlexCacher, StringComparison.CurrentCultureIgnoreCase)) { PlexContentCacher.CacheContent(); diff --git a/Ombi.UI/Modules/SearchExtensionModule.cs b/Ombi.UI/Modules/SearchExtensionModule.cs index 4c0ee4a6f..d9b34c290 100644 --- a/Ombi.UI/Modules/SearchExtensionModule.cs +++ b/Ombi.UI/Modules/SearchExtensionModule.cs @@ -47,6 +47,8 @@ namespace Ombi.UI.Modules public async Task Netflix(string title) { + await Task.Yield(); + var result = NetflixApi.CheckNetflix(title); if (!string.IsNullOrEmpty(result.Message)) diff --git a/Ombi.UI/Modules/SearchModule.cs b/Ombi.UI/Modules/SearchModule.cs index 76fc5fb62..6e675b959 100644 --- a/Ombi.UI/Modules/SearchModule.cs +++ b/Ombi.UI/Modules/SearchModule.cs @@ -121,6 +121,8 @@ namespace Ombi.UI.Modules Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); + Get["actor/{searchTerm}", true] = async (x, ct) => await SearchPerson((string)x.searchTerm); + Get["actor/new/{searchTerm}", true] = async (x, ct) => await SearchPerson((string)x.searchTerm, true); Get["movie/{searchTerm}", true] = async (x, ct) => await SearchMovie((string)x.searchTerm); Get["tv/{searchTerm}", true] = async (x, ct) => await SearchTvShow((string)x.searchTerm); Get["music/{searchTerm}", true] = async (x, ct) => await SearchAlbum((string)x.searchTerm); @@ -182,9 +184,18 @@ namespace Ombi.UI.Modules private ISettingsService CustomizationSettings { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); + private long _plexMovieCacheTime = 0; + private IEnumerable _plexMovies = null; + + private long _embyMovieCacheTime = 0; + private IEnumerable _embyMovies = null; + + + private long _dbMovieCacheTime = 0; + private Dictionary _dbMovies = null; + private async Task RequestLoad() { - var settings = await PrService.GetSettingsAsync(); var custom = await CustomizationSettings.GetSettingsAsync(); var emby = await EmbySettings.GetSettingsAsync(); @@ -222,6 +233,53 @@ namespace Ombi.UI.Modules return await ProcessMovies(MovieSearchType.Search, searchTerm); } + private async Task SearchPerson(string searchTerm) + { + var movies = TransformMovieListToMovieResultList(await MovieApi.SearchPerson(searchTerm)); + return await TransformMovieResultsToResponse(movies); + } + + private async Task SearchPerson(string searchTerm, bool filterExisting) + { + var movies = TransformMovieListToMovieResultList(await MovieApi.SearchPerson(searchTerm, AlreadyAvailable)); + return await TransformMovieResultsToResponse(movies); + } + + private async Task AlreadyAvailable(int id, string title, string year) + { + var plexSettings = await PlexService.GetSettingsAsync(); + var embySettings = await EmbySettings.GetSettingsAsync(); + + return IsMovieInCache(id, String.Empty) || + (plexSettings.Enable && PlexChecker.IsMovieAvailable(PlexMovies(), title, year)) || + (embySettings.Enable && EmbyChecker.IsMovieAvailable(EmbyMovies(), title, year, String.Empty)); + } + + private IEnumerable PlexMovies() + { long now = DateTime.Now.Ticks; + if(_plexMovies == null || (now - _plexMovieCacheTime) > 10000) + { + var content = PlexContentRepository.GetAll(); + _plexMovies = PlexChecker.GetPlexMovies(content); + _plexMovieCacheTime = now; + } + + return _plexMovies; + } + + private IEnumerable EmbyMovies() + { + long now = DateTime.Now.Ticks; + if (_embyMovies == null || (now - _embyMovieCacheTime) > 10000) + { + var content = EmbyContentRepository.GetAll(); + _embyMovies = EmbyChecker.GetEmbyMovies(content); + _embyMovieCacheTime = now; + } + + return _embyMovies; + } + private Response GetTvPoster(int theTvDbId) { var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); @@ -233,15 +291,10 @@ namespace Ombi.UI.Modules } return banner; } - private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) - { - List apiMovies; - switch (searchType) - { - case MovieSearchType.Search: - var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); - apiMovies = movies.Select(x => + private List TransformSearchMovieListToMovieResultList(List searchMovies) + { + return searchMovies.Select(x => new MovieResult { Adult = x.Adult, @@ -260,6 +313,39 @@ namespace Ombi.UI.Modules VoteCount = x.VoteCount }) .ToList(); + } + + private List TransformMovieListToMovieResultList(List movies) + { + return movies.Select(x => + new MovieResult + { + Adult = x.Adult, + BackdropPath = x.BackdropPath, + GenreIds = x.Genres.Select(y => y.Id).ToList(), + Id = x.Id, + OriginalLanguage = x.OriginalLanguage, + OriginalTitle = x.OriginalTitle, + Overview = x.Overview, + Popularity = x.Popularity, + PosterPath = x.PosterPath, + ReleaseDate = x.ReleaseDate, + Title = x.Title, + Video = x.Video, + VoteAverage = x.VoteAverage, + VoteCount = x.VoteCount + }) + .ToList(); + } + private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) + { + List apiMovies; + + switch (searchType) + { + case MovieSearchType.Search: + var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); + apiMovies = TransformSearchMovieListToMovieResultList(movies); break; case MovieSearchType.CurrentlyPlaying: apiMovies = await MovieApi.GetCurrentPlayingMovies(); @@ -272,20 +358,31 @@ namespace Ombi.UI.Modules break; } - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.Movie); + return await TransformMovieResultsToResponse(apiMovies); + } - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbMovies = distinctResults.ToDictionary(x => x.ProviderId); + private async Task> RequestedMovies() + { + long now = DateTime.Now.Ticks; + if (_dbMovies == null || (now - _dbMovieCacheTime) > 10000) + { + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.Movie); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + _dbMovies = distinctResults.ToDictionary(x => x.ProviderId); + _dbMovieCacheTime = now; + } + return _dbMovies; + } - var cpCached = CpCacher.QueuedIds(); - var watcherCached = WatcherCacher.QueuedIds(); - var radarrCached = RadarrCacher.QueuedIds(); - + private async Task TransformMovieResultsToResponse(List movies) + { + await Task.Yield(); var viewMovies = new List(); var counter = 0; - foreach (var movie in apiMovies) + Dictionary dbMovies = await RequestedMovies(); + foreach (var movie in movies) { var viewMovie = new SearchMovieViewModel { @@ -362,20 +459,11 @@ namespace Ombi.UI.Modules viewMovie.Approved = dbm.Approved; viewMovie.Available = dbm.Available; } - if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db + else if (canSee) { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - if (watcherCached.Contains(viewMovie.ImdbId) && canSee) // compare to the watcher db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - if (radarrCached.Contains(movie.Id) && canSee) - { - viewMovie.Approved = true; - viewMovie.Requested = true; + bool exists = IsMovieInCache(movie, viewMovie.ImdbId); + viewMovie.Approved = exists; + viewMovie.Requested = exists; } viewMovies.Add(viewMovie); } @@ -383,6 +471,19 @@ namespace Ombi.UI.Modules return Response.AsJson(viewMovies); } + private bool IsMovieInCache(MovieResult movie, string imdbId) + { int id = movie.Id; + return IsMovieInCache(id, imdbId); + } + + private bool IsMovieInCache(int id, string imdbId) + { var cpCached = CpCacher.QueuedIds(); + var watcherCached = WatcherCacher.QueuedIds(); + var radarrCached = RadarrCacher.QueuedIds(); + + return cpCached.Contains(id) || watcherCached.Contains(imdbId) || radarrCached.Contains(id); + } + private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, Dictionary moviesInDb) { diff --git a/Ombi.UI/NinjectModules/ServicesModule.cs b/Ombi.UI/NinjectModules/ServicesModule.cs index 210cdfd3e..7ba02e925 100644 --- a/Ombi.UI/NinjectModules/ServicesModule.cs +++ b/Ombi.UI/NinjectModules/ServicesModule.cs @@ -67,6 +67,7 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); + Bind().To(); Bind().To(); diff --git a/Ombi.UI/Resources/UI.resx b/Ombi.UI/Resources/UI.resx index a9f63e0e8..24519826e 100644 --- a/Ombi.UI/Resources/UI.resx +++ b/Ombi.UI/Resources/UI.resx @@ -1,76 +1,96 @@  + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + - + + @@ -89,13 +109,13 @@ text/microsoft-resx - 1.3 + 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Login @@ -191,6 +211,9 @@ Albums + + Don't include titles that are already requested/available + Want to watch something that is not currently on {0}?! No problem! Just search for it below and request it! @@ -473,4 +496,7 @@ If you are an administrator, please use the other login page + + Actors + \ No newline at end of file diff --git a/Ombi.UI/Resources/UI1.Designer.cs b/Ombi.UI/Resources/UI1.Designer.cs index 4920f2e7f..338720039 100644 --- a/Ombi.UI/Resources/UI1.Designer.cs +++ b/Ombi.UI/Resources/UI1.Designer.cs @@ -717,6 +717,15 @@ namespace Ombi.UI.Resources { } } + /// + /// Looks up a localized string similar to Actors. + /// + public static string Search_Actors { + get { + return ResourceManager.GetString("Search_Actors", resourceCulture); + } + } + /// /// Looks up a localized string similar to Albums. /// @@ -879,6 +888,15 @@ namespace Ombi.UI.Resources { } } + /// + /// Looks up a localized string similar to Don't include titles that are already requested/available. + /// + public static string Search_NewOnly { + get { + return ResourceManager.GetString("Search_NewOnly", resourceCulture); + } + } + /// /// Looks up a localized string similar to Not Requested yet. /// diff --git a/Ombi.UI/Views/Admin/Plex.cshtml b/Ombi.UI/Views/Admin/Plex.cshtml index 50ee63e19..ad8702c33 100644 --- a/Ombi.UI/Views/Admin/Plex.cshtml +++ b/Ombi.UI/Views/Admin/Plex.cshtml @@ -222,6 +222,7 @@ if (response.result === true) { generateNotify("Success!", "success"); $('#authToken').val(response.authToken); + $('#MachineIdentifier').val(response.identifier); } else { generateNotify(response.message, "warning"); } diff --git a/Ombi.UI/Views/Admin/Settings.cshtml b/Ombi.UI/Views/Admin/Settings.cshtml index 54e588c20..9a6010964 100644 --- a/Ombi.UI/Views/Admin/Settings.cshtml +++ b/Ombi.UI/Views/Admin/Settings.cshtml @@ -60,6 +60,7 @@ @Html.Checkbox(Model.SearchForMovies,"SearchForMovies","Search for Movies") + @Html.Checkbox(Model.SearchForActors,"SearchForActors","Search for Movies by Actor")
diff --git a/Ombi.UI/Views/Search/Index.cshtml b/Ombi.UI/Views/Search/Index.cshtml index a177fc721..4e0c6df50 100644 --- a/Ombi.UI/Views/Search/Index.cshtml +++ b/Ombi.UI/Views/Search/Index.cshtml @@ -27,6 +27,13 @@ @UI.Search_Movies + @if (Model.Settings.SearchForActors) + { +
  • + @UI.Search_Actors + +
  • + } } @if (Model.Settings.SearchForTvShows) { @@ -72,8 +79,28 @@
    - } + @if (Model.Settings.SearchForActors) + { + +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + } + } @if (Model.Settings.SearchForTvShows) { @@ -123,7 +150,7 @@ } - -