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.Interfaces/IEmbyApi.cs b/Ombi.Api.Interfaces/IEmbyApi.cs index ddc85868c..bc4697140 100644 --- a/Ombi.Api.Interfaces/IEmbyApi.cs +++ b/Ombi.Api.Interfaces/IEmbyApi.cs @@ -14,5 +14,6 @@ namespace Ombi.Api.Interfaces EmbyItemContainer ViewLibrary(string apiKey, string userId, Uri baseUri); EmbyInformation GetInformation(string mediaId, EmbyMediaType type, string apiKey, string userId, Uri baseUri); EmbyUser LogIn(string username, string password, string apiKey, Uri baseUri); + EmbySystemInfo GetSystemInformation(string apiKey, Uri baseUrl); } } \ No newline at end of file diff --git a/Ombi.Api.Interfaces/IRadarrApi.cs b/Ombi.Api.Interfaces/IRadarrApi.cs index 88e6d3028..f1b015d31 100644 --- a/Ombi.Api.Interfaces/IRadarrApi.cs +++ b/Ombi.Api.Interfaces/IRadarrApi.cs @@ -11,5 +11,6 @@ namespace Ombi.Api.Interfaces List GetMovies(string apiKey, Uri baseUrl); List GetProfiles(string apiKey, Uri baseUrl); SystemStatus SystemStatus(string apiKey, Uri baseUrl); + List GetRootFolders(string apiKey, Uri baseUrl); } } \ No newline at end of file diff --git a/Ombi.Api.Models/Emby/EmbySystemInfo.cs b/Ombi.Api.Models/Emby/EmbySystemInfo.cs new file mode 100644 index 000000000..e4b6859fc --- /dev/null +++ b/Ombi.Api.Models/Emby/EmbySystemInfo.cs @@ -0,0 +1,63 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: EmbySystemInfo.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 +namespace Ombi.Api.Models.Emby +{ + public class EmbySystemInfo + { + public string SystemUpdateLevel { get; set; } + public string OperatingSystemDisplayName { get; set; } + public bool SupportsRunningAsService { get; set; } + public string MacAddress { get; set; } + public bool HasPendingRestart { get; set; } + public bool SupportsLibraryMonitor { get; set; } + public object[] InProgressInstallations { get; set; } + public int WebSocketPortNumber { get; set; } + public object[] CompletedInstallations { get; set; } + public bool CanSelfRestart { get; set; } + public bool CanSelfUpdate { get; set; } + public object[] FailedPluginAssemblies { get; set; } + public string ProgramDataPath { get; set; } + public string ItemsByNamePath { get; set; } + public string CachePath { get; set; } + public string LogPath { get; set; } + public string InternalMetadataPath { get; set; } + public string TranscodingTempPath { get; set; } + public int HttpServerPortNumber { get; set; } + public bool SupportsHttps { get; set; } + public int HttpsPortNumber { get; set; } + public bool HasUpdateAvailable { get; set; } + public bool SupportsAutoRunAtStartup { get; set; } + public string EncoderLocationType { get; set; } + public string SystemArchitecture { get; set; } + public string LocalAddress { get; set; } + public string WanAddress { get; set; } + public string ServerName { get; set; } + public string Version { get; set; } + public string OperatingSystem { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Api.Models/Ombi.Api.Models.csproj b/Ombi.Api.Models/Ombi.Api.Models.csproj index 3d5fc3460..eb3297999 100644 --- a/Ombi.Api.Models/Ombi.Api.Models.csproj +++ b/Ombi.Api.Models/Ombi.Api.Models.csproj @@ -71,6 +71,7 @@ + diff --git a/Ombi.Api/ApiRequest.cs b/Ombi.Api/ApiRequest.cs index a27d4af28..2de72101d 100644 --- a/Ombi.Api/ApiRequest.cs +++ b/Ombi.Api/ApiRequest.cs @@ -27,6 +27,7 @@ using System; using System.IO; +using System.Net; using System.Xml.Serialization; using Newtonsoft.Json; using NLog; @@ -76,14 +77,7 @@ namespace Ombi.Api var client = new RestClient { BaseUrl = baseUri }; var response = client.Execute(request); - - if (response.ErrorException != null) - { - Log.Error(response.ErrorException); - var message = "Error retrieving response. Check inner details for more info."; - throw new ApiRequestException(message, response.ErrorException); - } - + return response; } diff --git a/Ombi.Api/EmbyApi.cs b/Ombi.Api/EmbyApi.cs index 9625ffa4f..83028153a 100644 --- a/Ombi.Api/EmbyApi.cs +++ b/Ombi.Api/EmbyApi.cs @@ -33,6 +33,7 @@ using NLog; using Ombi.Api.Interfaces; using Ombi.Api.Models.Emby; using Ombi.Helpers; +using Polly; using RestSharp; namespace Ombi.Api @@ -71,6 +72,26 @@ namespace Ombi.Api return obj; } + public EmbySystemInfo GetSystemInformation(string apiKey, Uri baseUrl) + { + var request = new RestRequest + { + Resource = "emby/System/Info", + Method = Method.GET + }; + + AddHeaders(request, apiKey); + + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetSystemInformation for Emby, Retrying {0}", timespan), new[] { + TimeSpan.FromSeconds (1), + TimeSpan.FromSeconds(5) + }); + + var obj = policy.Execute(() => Api.ExecuteJson(request, baseUrl)); + + return obj; + } + public EmbyItemContainer ViewLibrary(string apiKey, string userId, Uri baseUri) { var request = new RestRequest @@ -142,29 +163,71 @@ namespace Ombi.Api TimeSpan.FromSeconds(5) }); - switch (type) + IRestResponse response = null; + try + { + + switch (type) + { + case EmbyMediaType.Movie: + response = policy.Execute(() => Api.Execute(request, baseUri)); + break; + + case EmbyMediaType.Series: + response = policy.Execute(() => Api.Execute(request, baseUri)); + break; + case EmbyMediaType.Music: + break; + case EmbyMediaType.Episode: + response = policy.Execute(() => Api.Execute(request, baseUri)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + var info = new EmbyInformation(); + + switch (type) + { + case EmbyMediaType.Movie: + return new EmbyInformation + { + MovieInformation = JsonConvert.DeserializeObject(response.Content) + }; + case EmbyMediaType.Series: + return new EmbyInformation + { + SeriesInformation = JsonConvert.DeserializeObject(response.Content) + }; + case EmbyMediaType.Music: + break; + case EmbyMediaType.Episode: + return new EmbyInformation + { + EpisodeInformation = JsonConvert.DeserializeObject(response.Content) + }; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + } + catch (Exception e) { - case EmbyMediaType.Movie: - return new EmbyInformation - { - MovieInformation = policy.Execute(() => Api.ExecuteJson(request, baseUri)) - }; - case EmbyMediaType.Series: - return new EmbyInformation - { - SeriesInformation = - policy.Execute(() => Api.ExecuteJson(request, baseUri)) - }; - case EmbyMediaType.Music: - break; - case EmbyMediaType.Episode: - return new EmbyInformation - { - EpisodeInformation = - policy.Execute(() => Api.ExecuteJson(request, baseUri)) - }; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); + Log.Error("Could not get the media item's information"); + Log.Error(e); + Log.Debug("ResponseContent"); + Log.Debug(response?.Content ?? "Empty"); + Log.Debug("ResponseStatusCode"); + Log.Debug(response?.StatusCode ?? HttpStatusCode.PreconditionFailed); + + Log.Debug("ResponseError"); + Log.Debug(response?.ErrorMessage ?? "No Error"); + Log.Debug("ResponseException"); + Log.Debug(response?.ErrorException ?? new Exception()); + + + + throw; } return new EmbyInformation(); } diff --git a/Ombi.Api/RadarrApi.cs b/Ombi.Api/RadarrApi.cs index 7eeb98d3f..1840f40a0 100644 --- a/Ombi.Api/RadarrApi.cs +++ b/Ombi.Api/RadarrApi.cs @@ -62,6 +62,20 @@ namespace Ombi.Api return obj; } + public List GetRootFolders(string apiKey, Uri baseUrl) + { + var request = new RestRequest { Resource = "/api/rootfolder", Method = Method.GET }; + + request.AddHeader("X-Api-Key", apiKey); + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetRootFolders for Radarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (1), + TimeSpan.FromSeconds(2) + }); + + var obj = policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + + return obj; + } public RadarrAddMovie AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, Uri baseUrl, bool searchNow = false) { @@ -94,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.Api/TvMazeApi.cs b/Ombi.Api/TvMazeApi.cs index 5bb534990..4330a3d1e 100644 --- a/Ombi.Api/TvMazeApi.cs +++ b/Ombi.Api/TvMazeApi.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using NLog; using Ombi.Api.Models.Tv; using RestSharp; @@ -90,21 +91,29 @@ namespace Ombi.Api }; request.AddUrlSegment("id", theTvDbId.ToString()); request.AddHeader("Content-Type", "application/json"); + try + { + var result = Api.Execute(request, new Uri(Uri)); + var obj = JsonConvert.DeserializeObject(result.Content); - var obj = Api.Execute(request, new Uri(Uri)); - - var episodes = EpisodeLookup(obj.id).ToList(); + var episodes = EpisodeLookup(obj.id).ToList(); - foreach (var e in episodes) - { - obj.Season.Add(new TvMazeCustomSeason + foreach (var e in episodes) { - SeasonNumber = e.season, - EpisodeNumber = e.number - }); + obj.Season.Add(new TvMazeCustomSeason + { + SeasonNumber = e.season, + EpisodeNumber = e.number + }); + } + + return obj; } - - return obj; + catch (Exception e) + { + Log.Error(e); + return null; + } } public List GetSeasons(int id) 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.Tests/MovieSenderTests.cs b/Ombi.Core.Tests/MovieSenderTests.cs new file mode 100644 index 000000000..8c4e85e39 --- /dev/null +++ b/Ombi.Core.Tests/MovieSenderTests.cs @@ -0,0 +1,169 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: MovieSenderTests.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.Linq.Expressions; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using Ombi.Api; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Radarr; +using Ombi.Api.Models.Sonarr; +using Ombi.Api.Models.Watcher; +using Ombi.Core.SettingModels; +using Ombi.Helpers; +using Ombi.Store; +using Ploeh.AutoFixture; + +namespace Ombi.Core.Tests +{ + public class MovieSenderTests + { + private MovieSender Sender { get; set; } + private Mock> CpMock { get; set; } + private Mock> WatcherMock { get; set; } + private Mock> RadarrMock { get; set; } + private Mock CpApiMock { get; set; } + private Mock WatcherApiMock { get; set; } + private Mock RadarrApiMock { get; set; } + private Mock CacheMock { get; set; } + + private Fixture F { get; set; } + + [SetUp] + public void Setup() + { + F = new Fixture(); + CpMock = new Mock>(); + WatcherMock = new Mock>(); + RadarrApiMock = new Mock(); + RadarrMock = new Mock>(); + CpApiMock = new Mock(); + WatcherApiMock = new Mock(); + CacheMock = new Mock(); + + + RadarrMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, false).Create()); + WatcherMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, false).Create()); + CpMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, false).Create()); + + Sender = new MovieSender(CpMock.Object, WatcherMock.Object, CpApiMock.Object, WatcherApiMock.Object, RadarrApiMock.Object, RadarrMock.Object, CacheMock.Object); + } + + [Test] + public async Task SendRadarrMovie() + { + RadarrMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, true).Create()); + RadarrApiMock.Setup(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())).Returns(new RadarrAddMovie { title = "Abc" }); + + CacheMock.Setup(x => x.GetOrSet>(CacheKeys.RadarrRootFolders, It.IsAny>>(), It.IsAny())) + .Returns(F.CreateMany().ToList()); + + var model = F.Create(); + + var result = await Sender.Send(model, 2.ToString()); + + + Assert.That(result.Result, Is.True); + Assert.That(result.Error, Is.False); + Assert.That(result.MovieSendingEnabled, Is.True); + + RadarrApiMock.Verify(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny(), 2, It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task SendRadarrMovie_SendingFailed() + { + RadarrMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, true).Create()); + RadarrApiMock.Setup(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())).Returns(new RadarrAddMovie { Error = new RadarrError{message = "Movie Already Added"}}); + + CacheMock.Setup(x => x.GetOrSet>(CacheKeys.RadarrRootFolders, It.IsAny>>(), It.IsAny())) + .Returns(F.CreateMany().ToList()); + + var model = F.Create(); + + var result = await Sender.Send(model, 2.ToString()); + + + Assert.That(result.Result, Is.False); + Assert.That(result.Error, Is.True); + Assert.That(result.MovieSendingEnabled, Is.True); + + RadarrApiMock.Verify(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny(), 2, It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task SendCpMovie() + { + CpMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, true).Create()); + CpApiMock.Setup(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())).Returns(true); + + var model = F.Create(); + + var result = await Sender.Send(model); + + Assert.That(result.Result, Is.True); + Assert.That(result.Error, Is.False); + Assert.That(result.MovieSendingEnabled, Is.True); + + CpApiMock.Verify(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task SendWatcherMovie() + { + WatcherMock.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(F.Build().With(x => x.Enabled, true).Create()); + WatcherApiMock.Setup(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny())).Returns(F.Create()); + + var model = F.Create(); + + var result = await Sender.Send(model); + + Assert.That(result.Result, Is.True); + Assert.That(result.Error, Is.False); + Assert.That(result.MovieSendingEnabled, Is.True); + + WatcherApiMock.Verify(x => x.AddMovie(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + } +} \ No newline at end of file diff --git a/Ombi.Core.Tests/Ombi.Core.Tests.csproj b/Ombi.Core.Tests/Ombi.Core.Tests.csproj index 9f4108882..1d5e03e6d 100644 --- a/Ombi.Core.Tests/Ombi.Core.Tests.csproj +++ b/Ombi.Core.Tests/Ombi.Core.Tests.csproj @@ -60,6 +60,7 @@ + @@ -68,6 +69,18 @@ + + {95834072-A675-415D-AA8F-877C91623810} + Ombi.Api.Interfaces + + + {CB37A5F8-6DFC-4554-99D3-A42B502E4591} + Ombi.Api.Models + + + {8CB8D235-2674-442D-9C6A-35FCAEEB160D} + Ombi.Api + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} Ombi.Core @@ -76,6 +89,10 @@ {1252336D-42A3-482A-804C-836E60173DFA} Ombi.Helpers + + {92433867-2B7B-477B-A566-96C382427525} + Ombi.Store + diff --git a/Ombi.Core/CacheKeys.cs b/Ombi.Core/CacheKeys.cs index 32466e897..e78f9a9d0 100644 --- a/Ombi.Core/CacheKeys.cs +++ b/Ombi.Core/CacheKeys.cs @@ -50,5 +50,6 @@ namespace Ombi.Core public const string GetPlexRequestSettings = nameof(GetPlexRequestSettings); public const string LastestProductVersion = nameof(LastestProductVersion); public const string SonarrRootFolders = nameof(SonarrRootFolders); + public const string RadarrRootFolders = nameof(RadarrRootFolders); } } \ No newline at end of file diff --git a/Ombi.Core/HeadphonesSender.cs b/Ombi.Core/HeadphonesSender.cs index f3dab3850..043ea4e2a 100644 --- a/Ombi.Core/HeadphonesSender.cs +++ b/Ombi.Core/HeadphonesSender.cs @@ -62,7 +62,7 @@ namespace Ombi.Core // Artist is now active // Add album - var albumResult = await Api.AddAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + var albumResult = await Api.AddAlbum(Settings.ApiKey, Settings.FullUri, request.ReleaseId); if (!albumResult) { Log.Error("Couldn't add the album to headphones"); diff --git a/Ombi.Core/MovieSender.cs b/Ombi.Core/MovieSender.cs index 37eeee308..c7636ba83 100644 --- a/Ombi.Core/MovieSender.cs +++ b/Ombi.Core/MovieSender.cs @@ -26,10 +26,12 @@ #endregion using System; +using System.Linq; using System.Threading.Tasks; using NLog; using Ombi.Api.Interfaces; using Ombi.Core.SettingModels; +using Ombi.Helpers; using Ombi.Store; namespace Ombi.Core @@ -37,7 +39,8 @@ namespace Ombi.Core public class MovieSender : IMovieSender { public MovieSender(ISettingsService cp, ISettingsService watcher, - ICouchPotatoApi cpApi, IWatcherApi watcherApi, IRadarrApi radarrApi, ISettingsService radarrSettings) + ICouchPotatoApi cpApi, IWatcherApi watcherApi, IRadarrApi radarrApi, ISettingsService radarrSettings, + ICacheProvider cache) { CouchPotatoSettings = cp; WatcherSettings = watcher; @@ -45,6 +48,7 @@ namespace Ombi.Core WatcherApi = watcherApi; RadarrSettings = radarrSettings; RadarrApi = radarrApi; + Cache = cache; } private ISettingsService CouchPotatoSettings { get; } @@ -53,6 +57,7 @@ namespace Ombi.Core private IRadarrApi RadarrApi { get; } private ICouchPotatoApi CpApi { get; } private IWatcherApi WatcherApi { get; } + private ICacheProvider Cache { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); public async Task Send(RequestedModel model, string qualityId = "") @@ -73,7 +78,7 @@ namespace Ombi.Core if (radarrSettings.Enabled) { - return SendToRadarr(model, radarrSettings); + return SendToRadarr(model, radarrSettings, qualityId); } return new MovieSenderResult { Result = false, MovieSendingEnabled = false }; @@ -102,16 +107,26 @@ namespace Ombi.Core return new MovieSenderResult { Result = result, MovieSendingEnabled = true }; } - private MovieSenderResult SendToRadarr(RequestedModel model, RadarrSettings settings) + private MovieSenderResult SendToRadarr(RequestedModel model, RadarrSettings settings, string qualityId) { var qualityProfile = 0; - int.TryParse(settings.QualityProfile, out qualityProfile); - var result = RadarrApi.AddMovie(model.ProviderId, model.Title, model.ReleaseDate.Year, qualityProfile, settings.RootPath, settings.ApiKey, settings.FullUri, true); + if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality + { + int.TryParse(qualityId, out qualityProfile); + } + + if (qualityProfile <= 0) + { + int.TryParse(settings.QualityProfile, out qualityProfile); + } + + var rootFolderPath = model.RootFolderSelected <= 0 ? settings.FullRootPath : GetRootPath(model.RootFolderSelected, settings); + var result = RadarrApi.AddMovie(model.ProviderId, model.Title, model.ReleaseDate.Year, qualityProfile, rootFolderPath, settings.ApiKey, settings.FullUri, true); if (!string.IsNullOrEmpty(result.Error?.message)) { Log.Error(result.Error.message); - return new MovieSenderResult { Result = false, Error = true}; + return new MovieSenderResult { Result = false, Error = true , MovieSendingEnabled = true}; } if (!string.IsNullOrEmpty(result.title)) { @@ -119,5 +134,16 @@ namespace Ombi.Core } return new MovieSenderResult { Result = false, MovieSendingEnabled = true }; } + + private string GetRootPath(int pathId, RadarrSettings sonarrSettings) + { + var rootFoldersResult = Cache.GetOrSet(CacheKeys.RadarrRootFolders, () => RadarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { + return r.path; + } + return string.Empty; + } } } \ No newline at end of file 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/SettingModels/RadarrSettings.cs b/Ombi.Core/SettingModels/RadarrSettings.cs index b8a6287f7..f5d994535 100644 --- a/Ombi.Core/SettingModels/RadarrSettings.cs +++ b/Ombi.Core/SettingModels/RadarrSettings.cs @@ -32,6 +32,6 @@ namespace Ombi.Core.SettingModels public string ApiKey { get; set; } public string QualityProfile { get; set; } public string RootPath { get; set; } - + public string FullRootPath { get; set; } } } \ No newline at end of file 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.Helpers.Tests/TypeHelperTests.cs b/Ombi.Helpers.Tests/TypeHelperTests.cs index cff7d16d5..0390f4087 100644 --- a/Ombi.Helpers.Tests/TypeHelperTests.cs +++ b/Ombi.Helpers.Tests/TypeHelperTests.cs @@ -48,7 +48,7 @@ namespace Ombi.Helpers.Tests var consts = typeof(UserClaims).GetConstantsValues(); Assert.That(consts.Contains("Admin"),Is.True); Assert.That(consts.Contains("PowerUser"),Is.True); - Assert.That(consts.Contains("User"),Is.True); + Assert.That(consts.Contains("RegularUser"),Is.True); } private static IEnumerable TypeData @@ -59,14 +59,7 @@ namespace Ombi.Helpers.Tests yield return new TestCaseData(typeof(int)).Returns(new string[0]).SetName("NoPropeties Class"); yield return new TestCaseData(typeof(IEnumerable<>)).Returns(new string[0]).SetName("Interface"); yield return new TestCaseData(typeof(string)).Returns(new[] { "Chars", "Length" }).SetName("String"); - yield return new TestCaseData(typeof(RequestedModel)).Returns( - new[] - { - "ProviderId", "ImdbId", "TvDbId", "Overview", "Title", "PosterPath", "ReleaseDate", "Type", - "Status", "Approved", "RequestedBy", "RequestedDate", "Available", "Issues", "OtherMessage", "AdminNote", - "SeasonList", "SeasonCount", "SeasonsRequested", "MusicBrainzId", "RequestedUsers","ArtistName", - "ArtistId","IssueId","Episodes", "Denied", "DeniedReason", "AllUsers","CanApprove","Id", - }).SetName("Requested Model"); + } } 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")) - { - 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"); - - 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); - } - } - 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 + if (plexSettings.Enable) { + var html = PlexNewsletter.GetNewsletterHtml(testEmail); - 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 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/RequestedModel.cs b/Ombi.Store/RequestedModel.cs index c54d68e5c..c2130d277 100644 --- a/Ombi.Store/RequestedModel.cs +++ b/Ombi.Store/RequestedModel.cs @@ -68,6 +68,8 @@ namespace Ombi.Store [JsonIgnore] public bool CanApprove => !Approved && !Available; + public string ReleaseId { get; set; } + public bool UserHasRequested(string username) { return AllUsers.Any(x => x.Equals(username, StringComparison.OrdinalIgnoreCase)); 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.Tests/AdminModuleTests.cs b/Ombi.UI.Tests/AdminModuleTests.cs index 62e4bf514..30da77591 100644 --- a/Ombi.UI.Tests/AdminModuleTests.cs +++ b/Ombi.UI.Tests/AdminModuleTests.cs @@ -48,6 +48,7 @@ using Ombi.UI.Modules.Admin; namespace Ombi.UI.Tests { [TestFixture] + [Ignore("Needs rework")] public class AdminModuleTests { private Mock> PlexRequestMock { get; set; } diff --git a/Ombi.UI.Tests/UserLoginModuleTests.cs b/Ombi.UI.Tests/UserLoginModuleTests.cs index f7e59e86a..a5f68d06b 100644 --- a/Ombi.UI.Tests/UserLoginModuleTests.cs +++ b/Ombi.UI.Tests/UserLoginModuleTests.cs @@ -44,6 +44,7 @@ using Ombi.UI.Modules; namespace Ombi.UI.Tests { [TestFixture] + [Ignore("Needs rewrite")] public class UserLoginModuleTests { private Mock> AuthMock { get; set; } diff --git a/Ombi.UI/Content/requests.js b/Ombi.UI/Content/requests.js index 1d2ad987d..b902c1560 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'); @@ -564,16 +567,21 @@ $(document).on("click", ".change-root-folder", function (e) { e.preventDefault(); var $this = $(this); var $button = $this.parents('.btn-split').children('.change').first(); - var rootFolderId = e.target.id + var rootFolderId = e.target.id; var $form = $this.parents('form').first(); + var requestId = $button.attr('id'); + if ($button.text() === " Loading...") { return; } - loadingButton($button.attr('id'), "success"); + loadingButton(requestId, "success"); changeRootFolder($form, rootFolderId, function () { + if ($('#' + requestId + "rootPathMain").length) { + $('#' + requestId + "currentRootPath").text($this.text); + } }); }); @@ -733,6 +741,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/IntegrationModule.cs b/Ombi.UI/Modules/Admin/IntegrationModule.cs index 4076f9756..57eccfeff 100644 --- a/Ombi.UI/Modules/Admin/IntegrationModule.cs +++ b/Ombi.UI/Modules/Admin/IntegrationModule.cs @@ -69,6 +69,7 @@ namespace Ombi.UI.Modules.Admin Post["/sonarrrootfolders"] = _ => GetSonarrRootFolders(); + Post["/radarrrootfolders"] = _ => GetSonarrRootFolders(); Get["/watcher", true] = async (x, ct) => await Watcher(); Post["/watcher", true] = async (x, ct) => await SaveWatcher(); @@ -191,7 +192,22 @@ namespace Ombi.UI.Modules.Admin { var settings = this.Bind(); - var rootFolders = SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); + var rootFolders = SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); + + // set the cache + if (rootFolders != null) + { + Cache.Set(CacheKeys.SonarrRootFolders, rootFolders); + } + + return Response.AsJson(rootFolders); + } + + private Response GetRadarrRootFolders() + { + var settings = this.Bind(); + + var rootFolders = RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); // set the cache if (rootFolders != null) 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/LandingPageModule.cs b/Ombi.UI/Modules/LandingPageModule.cs index 5f1076be8..d544af2c3 100644 --- a/Ombi.UI/Modules/LandingPageModule.cs +++ b/Ombi.UI/Modules/LandingPageModule.cs @@ -40,12 +40,15 @@ namespace Ombi.UI.Modules public class LandingPageModule : BaseModule { public LandingPageModule(ISettingsService settingsService, ISettingsService landing, - ISettingsService ps, IPlexApi pApi, IResourceLinker linker, ISecurityExtensions security) : base("landing", settingsService, security) + ISettingsService ps, IPlexApi pApi, IResourceLinker linker, ISecurityExtensions security, ISettingsService emby, + IEmbyApi embyApi) : base("landing", settingsService, security) { LandingSettings = landing; PlexSettings = ps; PlexApi = pApi; Linker = linker; + EmbySettings = emby; + EmbyApi = embyApi; Get["LandingPageIndex","/", true] = async (x, ct) => { @@ -75,26 +78,49 @@ namespace Ombi.UI.Modules private ISettingsService LandingSettings { get; } private ISettingsService PlexSettings { get; } + private ISettingsService EmbySettings { get; } private IPlexApi PlexApi { get; } + private IEmbyApi EmbyApi { get; } private IResourceLinker Linker { get; } private async Task CheckStatus() { var plexSettings = await PlexSettings.GetSettingsAsync(); - if (string.IsNullOrEmpty(plexSettings.PlexAuthToken) || string.IsNullOrEmpty(plexSettings.Ip)) + if (plexSettings.Enable) { - return Response.AsJson(false); - } - try - { - var status = PlexApi.GetStatus(plexSettings.PlexAuthToken, plexSettings.FullUri); - return Response.AsJson(status != null); + if (string.IsNullOrEmpty(plexSettings.PlexAuthToken) || string.IsNullOrEmpty(plexSettings.Ip)) + { + return Response.AsJson(false); + } + try + { + var status = PlexApi.GetStatus(plexSettings.PlexAuthToken, plexSettings.FullUri); + return Response.AsJson(status != null); + } + catch (Exception) + { + return Response.AsJson(false); + } } - catch (Exception) + + var emby = await EmbySettings.GetSettingsAsync(); + if (emby.Enable) { - return Response.AsJson(false); + if (string.IsNullOrEmpty(emby.AdministratorId) || string.IsNullOrEmpty(emby.Ip)) + { + return Response.AsJson(false); + } + try + { + var status = EmbyApi.GetSystemInformation(emby.ApiKey, emby.FullUri); + return Response.AsJson(status?.Version != null); + } + catch (Exception) + { + return Response.AsJson(false); + } } - + return Response.AsJson(false); } } } \ No newline at end of file diff --git a/Ombi.UI/Modules/RequestsModule.cs b/Ombi.UI/Modules/RequestsModule.cs index 897986635..ae6c7778d 100644 --- a/Ombi.UI/Modules/RequestsModule.cs +++ b/Ombi.UI/Modules/RequestsModule.cs @@ -69,7 +69,9 @@ namespace Ombi.UI.Modules IEmbyNotificationEngine embyEngine, ISecurityExtensions security, ISettingsService customSettings, - ISettingsService embyS) : base("requests", prSettings, security) + ISettingsService embyS, + ISettingsService radarr, + IRadarrApi radarrApi) : base("requests", prSettings, security) { Service = service; PrSettings = prSettings; @@ -87,6 +89,8 @@ namespace Ombi.UI.Modules EmbyNotificationEngine = embyEngine; CustomizationSettings = customSettings; EmbySettings = embyS; + Radarr = radarr; + RadarrApi = radarrApi; Get["/", true] = async (x, ct) => await LoadRequests(); Get["/movies", true] = async (x, ct) => await GetMovies(); @@ -115,8 +119,10 @@ namespace Ombi.UI.Modules private ISettingsService SickRageSettings { get; } private ISettingsService CpSettings { get; } private ISettingsService CustomizationSettings { get; } + private ISettingsService Radarr { get; } private ISettingsService EmbySettings { get; } private ISonarrApi SonarrApi { get; } + private IRadarrApi RadarrApi { get; } private ISickRageApi SickRageApi { get; } private ICouchPotatoApi CpApi { get; } private ICacheProvider Cache { get; } @@ -144,28 +150,58 @@ namespace Ombi.UI.Modules } List qualities = new List(); + var rootFolders = new List(); + var radarr = await Radarr.GetSettingsAsync(); if (IsAdmin) { - var cpSettings = CpSettings.GetSettings(); - if (cpSettings.Enabled) + try { - try + var cpSettings = await CpSettings.GetSettingsAsync(); + if (cpSettings.Enabled) { - var result = await Cache.GetOrSetAsync(CacheKeys.CouchPotatoQualityProfiles, async () => + try { - return await Task.Run(() => CpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey)).ConfigureAwait(false); - }); - if (result != null) + var result = await Cache.GetOrSetAsync(CacheKeys.CouchPotatoQualityProfiles, async () => + { + return + await Task.Run(() => CpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey)) + .ConfigureAwait(false); + }); + if (result != null) + { + qualities = + result.list.Select(x => new QualityModel {Id = x._id, Name = x.label}).ToList(); + } + } + catch (Exception e) { - qualities = result.list.Select(x => new QualityModel { Id = x._id, Name = x.label }).ToList(); + Log.Info(e); } } - catch (Exception e) + if (radarr.Enabled) { - Log.Info(e); + var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.RadarrRootFolders, async () => + { + return await Task.Run(() => RadarrApi.GetRootFolders(radarr.ApiKey, radarr.FullUri)); + }); + + rootFolders = + rootFoldersResult.Select( + x => new RootFolderModel {Id = x.id.ToString(), Path = x.path, FreeSpace = x.freespace}) + .ToList(); + + var result = await Cache.GetOrSetAsync(CacheKeys.RadarrQualityProfiles, async () => + { + return await Task.Run(() => RadarrApi.GetProfiles(radarr.ApiKey, radarr.FullUri)); + }); + qualities = result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList(); } } + catch (Exception e) + { + Log.Error(e); + } } @@ -194,6 +230,9 @@ namespace Ombi.UI.Modules Denied = movie.Denied, DeniedReason = movie.DeniedReason, Qualities = qualities.ToArray(), + HasRootFolders = rootFolders.Any(), + RootFolders = rootFolders.ToArray(), + CurrentRootPath = radarr.Enabled ? GetRootPath(movie.RootFolderSelected, radarr).Result : null }).ToList(); return Response.AsJson(viewModel); @@ -260,7 +299,7 @@ namespace Ombi.UI.Modules Status = tv.Status, ImdbId = tv.ImdbId, Id = tv.Id, - PosterPath = tv.PosterPath.Contains("http:") ? tv.PosterPath.Replace("http:", "https:") : tv.PosterPath, // We make the poster path https on request, but this is just incase + PosterPath = tv.PosterPath?.Contains("http:") ?? false ? tv.PosterPath?.Replace("http:", "https:") : tv.PosterPath ?? string.Empty, // We make the poster path https on request, but this is just incase ReleaseDate = tv.ReleaseDate, ReleaseDateTicks = tv.ReleaseDate.Ticks, RequestedDate = tv.RequestedDate, @@ -313,6 +352,32 @@ namespace Ombi.UI.Modules } } + private async Task GetRootPath(int pathId, RadarrSettings radarrSettings) + { + var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.RadarrRootFolders, async () => + { + return await Task.Run(() => RadarrApi.GetRootFolders(radarrSettings.ApiKey, radarrSettings.FullUri)); + }); + + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { + return r.path; + } + + int outRoot; + var defaultPath = int.TryParse(radarrSettings.RootPath, out outRoot); + + if (defaultPath) + { + // Return default path + return rootFoldersResult.FirstOrDefault(x => x.id.Equals(outRoot))?.path ?? string.Empty; + } + else + { + return rootFoldersResult.FirstOrDefault()?.path ?? string.Empty; + } + } + private async Task GetAlbumRequests() { var settings = PrSettings.GetSettings(); 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 f11e41b55..d1cf52473 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); - - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbMovies = distinctResults.ToDictionary(x => x.ProviderId); + return await TransformMovieResultsToResponse(apiMovies); + } + 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 cpCached = CpCacher.QueuedIds(); - var watcherCached = WatcherCacher.QueuedIds(); - var radarrCached = RadarrCacher.QueuedIds(); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + _dbMovies = distinctResults.ToDictionary(x => x.ProviderId); + _dbMovieCacheTime = now; + } + return _dbMovies; + } + 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 { @@ -354,7 +451,7 @@ namespace Ombi.UI.Modules viewMovie.Available = true; } } - else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db + if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db { var dbm = dbMovies[movie.Id]; @@ -362,20 +459,11 @@ namespace Ombi.UI.Modules viewMovie.Approved = dbm.Approved; viewMovie.Available = dbm.Available; } - else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - else if (watcherCached.Contains(viewMovie.ImdbId) && canSee) // compare to the watcher db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - else if (radarrCached.Contains(movie.Id) && canSee) + else if (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) { @@ -437,6 +538,11 @@ namespace Ombi.UI.Modules { var show = anticipatedShow.Show; var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + if (result == null) + { + continue; + } var model = new SearchTvShowViewModel { @@ -466,6 +572,12 @@ namespace Ombi.UI.Modules { var show = watched.Show; var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + if (result == null) + { + continue; + } + var model = new SearchTvShowViewModel { FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), @@ -494,6 +606,12 @@ namespace Ombi.UI.Modules { var show = watched.Show; var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + if (result == null) + { + continue; + } + var model = new SearchTvShowViewModel { FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), @@ -1395,6 +1513,7 @@ namespace Ombi.UI.Modules { Title = albumInfo.title, MusicBrainzId = albumInfo.id, + ReleaseId = releaseId, Overview = albumInfo.disambiguation, PosterPath = img, Type = RequestType.Album, 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/Validators/RadarrValidator.cs b/Ombi.UI/Validators/RadarrValidator.cs index 75550c787..cfa1945c6 100644 --- a/Ombi.UI/Validators/RadarrValidator.cs +++ b/Ombi.UI/Validators/RadarrValidator.cs @@ -37,7 +37,7 @@ namespace Ombi.UI.Validators RuleFor(request => request.ApiKey).NotEmpty().WithMessage("You must specify a Api Key."); RuleFor(request => request.Ip).NotEmpty().WithMessage("You must specify a IP/Host name."); RuleFor(request => request.Port).NotEmpty().WithMessage("You must specify a Port."); - RuleFor(request => request.QualityProfile).NotEmpty().WithMessage("You must specify a Quality Profile."); + RuleFor(request => request.QualityProfile).NotEmpty().NotNull().WithMessage("You must specify a Quality Profile."); } } } \ No newline at end of file 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/Admin/Sonarr.cshtml b/Ombi.UI/Views/Admin/Sonarr.cshtml index 72498b036..92c667708 100644 --- a/Ombi.UI/Views/Admin/Sonarr.cshtml +++ b/Ombi.UI/Views/Admin/Sonarr.cshtml @@ -103,13 +103,6 @@
- @*
- -
- - -
-
*@
diff --git a/Ombi.UI/Views/Integration/Radarr.cshtml b/Ombi.UI/Views/Integration/Radarr.cshtml index 1ccccc40f..00a9d4968 100644 --- a/Ombi.UI/Views/Integration/Radarr.cshtml +++ b/Ombi.UI/Views/Integration/Radarr.cshtml @@ -11,11 +11,20 @@ { port = Model.Port; } + + var rootFolder = string.Empty; + if (!string.IsNullOrEmpty(Model.RootPath)) + + { + rootFolder = Model.RootPath.Replace("/", "//"); + } }
Radarr Settings + + @Html.Checkbox(Model.Enabled, "Enabled", "Enabled") @@ -64,10 +73,17 @@
-
- - + + +
+ +
+
+ +
+ +
@@ -128,6 +144,39 @@ } } + @if (!string.IsNullOrEmpty(Model.RootPath)) + { + + + console.log('Hit root folders..'); + + var rootFolderSelected = '@rootFolder'; + if (!rootFolderSelected) { + return; + } + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: "sonarrrootfolders", + dataType: "json", + success: function(response) { + response.forEach(function(result) { + $('#selectedRootFolder').html(""); + if (result.id == rootFolderSelected) { + $("#selectRootFolder").append(""); + } else { + $("#selectRootFolder").append(""); + } + }); + }, + error: function(e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + + } $('#save').click(function(e) { @@ -138,11 +187,14 @@ return; } var qualityProfile = $("#profiles option:selected").val(); + var rootFolder = $("#rootFolders option:selected").val(); + var rootFolderPath = $('#rootFolders option:selected').text(); + $('#fullRootPath').val(rootFolderPath); var $form = $("#mainForm"); var data = $form.serialize(); - data = data + "&qualityProfile=" + qualityProfile; + data = data + "&qualityProfile=" + qualityProfile + "&rootPath=" + rootFolder; $.ajax({ type: $form.prop("method"), @@ -202,6 +254,45 @@ }); }); + $('#getRootFolders').click(function (e) { + + $('#getRootFolderSpinner').attr("class", "fa fa-spinner fa-spin"); + e.preventDefault(); + if (!$('#Ip').val()) { + generateNotify("Please enter a valid IP/Hostname.", "warning"); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + return; + } + if (!$('#portNumber').val()) { + generateNotify("Please enter a valid Port Number.", "warning"); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + return; + } + if (!$('#ApiKey').val()) { + generateNotify("Please enter a valid ApiKey.", "warning"); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + return; + } + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: "radarrrootfolders", + dataType: "json", + success: function (response) { + response.forEach(function (result) { + $('#getRootFolderSpinner').attr("class", "fa fa-check"); + $("#selectRootFolder").append(""); + }); + }, + error: function (e) { + console.log(e); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + generateNotify("Something went wrong!", "danger"); + } + }); + }); + var base = '@Html.GetBaseUrl()'; $('#testRadarr').click(function (e) { @@ -213,7 +304,7 @@ var data = $form.serialize(); data = data + "&qualityProfile=" + qualityProfile; - + var url = createBaseUrl(base, '/test/radarr'); $.ajax({ type: $form.prop("method"), diff --git a/Ombi.UI/Views/Requests/Index.cshtml b/Ombi.UI/Views/Requests/Index.cshtml index e94decd85..710339fe4 100644 --- a/Ombi.UI/Views/Requests/Index.cshtml +++ b/Ombi.UI/Views/Requests/Index.cshtml @@ -245,7 +245,7 @@
@UI.Requests_RequestedDate: {{requestedDate}}
{{#if admin}} {{#if currentRootPath}} -
Root Path: {{currentRootPath}}
+
Root Path: {{currentRootPath}}
{{/if}} {{/if}}
@@ -285,14 +285,14 @@ {{#if_eq hasRootFolders true}}
- +
diff --git a/Ombi.UI/Views/Search/Index.cshtml b/Ombi.UI/Views/Search/Index.cshtml index 07f9ac358..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 @@ } - -