#region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees // File: SearchModule.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.Globalization; using System.Linq; using Nancy; using Nancy.Responses.Negotiation; using Nancy.Security; using NLog; using PlexRequests.Api; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Music; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Exceptions; using PlexRequests.Services; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; using System.Threading.Tasks; using TMDbLib.Objects.Search; using PlexRequests.Api.Models.Tv; using TMDbLib.Objects.General; namespace PlexRequests.UI.Modules { public class SearchModule : BaseModule { public SearchModule(ICacheProvider cache, ISettingsService cpSettings, ISettingsService prSettings, IAvailabilityChecker checker, IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService) : base("search") { CpService = cpSettings; PrService = prSettings; MovieApi = new TheMovieDbApi(); TvApi = new TheTvDbApi(); Cache = cache; Checker = checker; RequestService = request; SonarrApi = sonarrApi; SonarrService = sonarrSettings; CouchPotatoApi = cpApi; SickRageService = sickRageService; SickrageApi = srApi; NotificationService = notify; MusicBrainzApi = mbApi; HeadphonesApi = hpApi; HeadphonesService = hpService; Get["/"] = parameters => RequestLoad(); Get["movie/{searchTerm}"] = parameters => SearchMovie((string)parameters.searchTerm); Get["tv/{searchTerm}"] = parameters => SearchTvShow((string)parameters.searchTerm); Get["music/{searchTerm}"] = parameters => SearchMusic((string)parameters.searchTerm); Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); Get["movie/upcoming"] = parameters => UpcomingMovies(); Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId); Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId); } private TheMovieDbApi MovieApi { get; } private INotificationService NotificationService { get; } private ICouchPotatoApi CouchPotatoApi { get; } private ISonarrApi SonarrApi { get; } private TheTvDbApi TvApi { get; } private ISickRageApi SickrageApi { get; } private IRequestService RequestService { get; } private ICacheProvider Cache { get; } private ISettingsService CpService { get; } private ISettingsService PrService { get; } private ISettingsService SonarrService { get; } private ISettingsService SickRageService { get; } private ISettingsService HeadphonesService { get; } private IAvailabilityChecker Checker { get; } private IMusicBrainzApi MusicBrainzApi { get; } private IHeadphonesApi HeadphonesApi { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); private bool IsAdmin { get { return Context.CurrentUser.IsAuthenticated(); } } private Negotiator RequestLoad() { var settings = PrService.GetSettings(); Log.Trace("Loading Index"); return View["Search/Index", settings]; } private Response UpcomingMovies() { Log.Trace("Loading upcoming movies"); return ProcessMovies(MovieSearchType.Upcoming, string.Empty); } private Response CurrentlyPlayingMovies() { Log.Trace("Loading currently playing movies"); return ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty); } private Response SearchMovie(string searchTerm) { Log.Trace("Searching for Movie {0}", searchTerm); return ProcessMovies(MovieSearchType.Search, searchTerm); } private Response ProcessMovies(MovieSearchType searchType, string searchTerm) { List taskList = new List(); List apiMovies = new List(); taskList.Add(Task.Factory.StartNew>(() => { switch(searchType) { case MovieSearchType.Search: return MovieApi.SearchMovie(searchTerm).Result.Select(x => new MovieResult() { Adult = x.Adult, BackdropPath = x.BackdropPath, GenreIds = x.GenreIds, 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(); case MovieSearchType.CurrentlyPlaying: return MovieApi.GetCurrentPlayingMovies().Result.ToList(); case MovieSearchType.Upcoming: return MovieApi.GetUpcomingMovies().Result.ToList(); default: return new List(); } }).ContinueWith((t) => { apiMovies = t.Result; })); Dictionary dbMovies = new Dictionary(); taskList.Add(Task.Factory.StartNew(() => { return RequestService.GetAll().Where(x => x.Type == RequestType.Movie); }).ContinueWith((t) => { dbMovies = t.Result.ToDictionary(x => x.ProviderId); })); Task.WaitAll(taskList.ToArray()); List viewMovies = new List(); foreach (MovieResult movie in apiMovies) { var viewMovie = new SearchMovieViewModel() { Adult = movie.Adult, BackdropPath = movie.BackdropPath, GenreIds = movie.GenreIds, Id = movie.Id, OriginalLanguage = movie.OriginalLanguage, OriginalTitle = movie.OriginalTitle, Overview = movie.Overview, Popularity = movie.Popularity, PosterPath = movie.PosterPath, ReleaseDate = movie.ReleaseDate, Title = movie.Title, Video = movie.Video, VoteAverage = movie.VoteAverage, VoteCount = movie.VoteCount }; if (dbMovies.ContainsKey(movie.Id)) { var dbm = dbMovies[movie.Id]; viewMovie.Requested = true; viewMovie.Approved = dbm.Approved; viewMovie.Available = dbm.Available; } viewMovies.Add(viewMovie); } return Response.AsJson(viewMovies); } private Response SearchTvShow(string searchTerm) { Log.Trace("Searching for TV Show {0}", searchTerm); List taskList = new List(); List apiTv = new List(); taskList.Add(Task.Factory.StartNew(() => { return new TvMazeApi().Search(searchTerm); }).ContinueWith((t) => { apiTv = t.Result; })); Dictionary dbTv = new Dictionary(); taskList.Add(Task.Factory.StartNew(() => { return RequestService.GetAll().Where(x => x.Type == RequestType.TvShow); }).ContinueWith((t) => { dbTv = t.Result.ToDictionary(x => x.ProviderId); })); Task.WaitAll(taskList.ToArray()); if (!apiTv.Any()) { Log.Trace("TV Show data is null"); return Response.AsJson(""); } var viewTv = new List(); foreach (var t in apiTv) { var viewT = new SearchTvShowViewModel { // We are constructing the banner with the id: // http://thetvdb.com/banners/_cache/posters/ID-1.jpg Banner = t.show.image?.medium, FirstAired = t.show.premiered, Id = t.show.externals?.thetvdb ?? 0, ImdbId = t.show.externals?.imdb, Network = t.show.network?.name, NetworkId = t.show.network?.id.ToString(), Overview = t.show.summary.RemoveHtml(), Rating = t.score.ToString(CultureInfo.CurrentUICulture), Runtime = t.show.runtime.ToString(), SeriesId = t.show.id, SeriesName = t.show.name, Status = t.show.status }; if (t.show.externals.thetvdb != null && dbTv.ContainsKey((int)t.show.externals.thetvdb)) { var dbt = dbTv[(int)t.show.externals.thetvdb]; viewT.Requested = true; viewT.Approved = dbt.Approved; viewT.Available = dbt.Available; } viewTv.Add(viewT); } Log.Trace("Returning TV Show results: "); Log.Trace(viewTv.DumpJson()); return Response.AsJson(viewTv); } private Response SearchMusic(string searchTerm) { List taskList = new List(); List apiAlbums = new List(); taskList.Add(Task.Factory.StartNew(() => { return MusicBrainzApi.SearchAlbum(searchTerm); }).ContinueWith((t) => { apiAlbums = t.Result.releases ?? new List(); })); Dictionary dbAlbum = new Dictionary(); taskList.Add(Task.Factory.StartNew(() => { return RequestService.GetAll().Where(x => x.Type == RequestType.Album); }).ContinueWith((t) => { dbAlbum = t.Result.ToDictionary(x => x.MusicBrainzId); })); Task.WaitAll(taskList.ToArray()); var viewAlbum = new List(); foreach (var a in apiAlbums) { var viewA = new SearchMusicViewModel { Title = a.title, Id = a.id, Artist = a.ArtistCredit?.Select(x => x.artist?.name).FirstOrDefault(), Overview = a.disambiguation, ReleaseDate = a.date, TrackCount = a.TrackCount, ReleaseType = a.status, Country = a.country }; if (!string.IsNullOrEmpty(a.id) && dbAlbum.ContainsKey(a.id)) { var dba = dbAlbum[a.id]; viewA.Requested = true; viewA.Approved = dba.Approved; viewA.Available = dba.Available; } viewAlbum.Add(viewA); } return Response.AsJson(viewAlbum); } private Response RequestMovie(int movieId) { var movieApi = new TheMovieDbApi(); var movieInfo = movieApi.GetMovieInformation(movieId).Result; var fullMovieName = $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; Log.Trace("Getting movie info from TheMovieDb"); Log.Trace(movieInfo.DumpJson); //#if !DEBUG var settings = PrService.GetSettings(); // check if the movie has already been requested Log.Info("Requesting movie with id {0}", movieId); var existingRequest = RequestService.CheckRequest(movieId); if (existingRequest != null) { // check if the current user is already marked as a requester for this movie, if not, add them if (!existingRequest.UserHasRequested(Username)) { existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} was successfully added!" : $"{fullMovieName} has already been requested!" }); } Log.Debug("movie with id {0} doesnt exists", movieId); try { if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString(), null, PlexType.Movie)) { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} is already in Plex!" }); } } catch (ApplicationSettingsException) { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {fullMovieName} is in Plex, are you sure it's correctly setup?" }); } //#endif var model = new RequestedModel { ProviderId = movieInfo.Id, Type = RequestType.Movie, Overview = movieInfo.Overview, ImdbId = movieInfo.ImdbId, PosterPath = "http://image.tmdb.org/t/p/w150/" + movieInfo.PosterPath, Title = movieInfo.Title, ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, Status = movieInfo.Status, RequestedDate = DateTime.UtcNow, Approved = false, RequestedUsers = new List() { Username }, Issues = IssueState.None, }; Log.Trace(settings.DumpJson()); if (ShouldAutoApprove(RequestType.Movie, settings)) { var cpSettings = CpService.GetSettings(); Log.Trace("Settings: "); Log.Trace(cpSettings.DumpJson); if (cpSettings.Enabled) { Log.Info("Adding movie to CP (No approval required)"); var result = CouchPotatoApi.AddMovie(model.ImdbId, cpSettings.ApiKey, model.Title, cpSettings.FullUri, cpSettings.ProfileId); Log.Debug("Adding movie to CP result {0}", result); if (result) { model.Approved = true; Log.Debug("Adding movie to database requests (No approval required)"); RequestService.AddRequest(model); var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to CouchPotato! Please check your settings." }); } else { model.Approved = true; Log.Debug("Adding movie to database requests (No approval required)"); RequestService.AddRequest(model); var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } } try { Log.Debug("Adding movie to database requests"); var id = RequestService.AddRequest(model); var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } catch (Exception e) { Log.Fatal(e); return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to CouchPotato! Please check your settings." }); } } /// /// Requests the tv show. /// /// The show identifier. /// The seasons. /// private Response RequestTvShow(int showId, string seasons) { var tvApi = new TvMazeApi(); var showInfo = tvApi.ShowLookupByTheTvDbId(showId); DateTime firstAir; DateTime.TryParse(showInfo.premiered, out firstAir); string fullShowName = $"{showInfo.name} ({firstAir.Year})"; //#if !DEBUG var settings = PrService.GetSettings(); // check if the show has already been requested Log.Info("Requesting tv show with id {0}", showId); var existingRequest = RequestService.CheckRequest(showId); if (existingRequest != null) { // check if the current user is already marked as a requester for this show, if not, add them if (!existingRequest.UserHasRequested(Username)) { existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullShowName} was successfully added!" : $"{fullShowName} has already been requested!" }); } try { if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4), null, PlexType.TvShow)) // Take only the year Format = 2014-01-01 { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} is already in Plex!" }); } } catch (ApplicationSettingsException) { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {fullShowName} is in Plex, are you sure it's correctly setup?" }); } //#endif var model = new RequestedModel { ProviderId = showInfo.externals?.thetvdb ?? 0, Type = RequestType.TvShow, Overview = showInfo.summary.RemoveHtml(), PosterPath = showInfo.image?.medium, Title = showInfo.name, ReleaseDate = firstAir, Status = showInfo.status, RequestedDate = DateTime.UtcNow, Approved = false, RequestedUsers = new List() { Username }, Issues = IssueState.None, ImdbId = showInfo.externals?.imdb ?? string.Empty, SeasonCount = showInfo.seasonCount }; var seasonsList = new List(); switch (seasons) { case "first": seasonsList.Add(1); model.SeasonsRequested = "First"; break; case "latest": seasonsList.Add(model.SeasonCount); model.SeasonsRequested = "Latest"; break; default: model.SeasonsRequested = "All"; break; } model.SeasonList = seasonsList.ToArray(); if (ShouldAutoApprove(RequestType.TvShow, settings)) { var sonarrSettings = SonarrService.GetSettings(); var sender = new TvSender(SonarrApi, SickrageApi); if (sonarrSettings.Enabled) { var result = sender.SendToSonarr(sonarrSettings, model); if (result != null && !string.IsNullOrEmpty(result.title)) { model.Approved = true; Log.Debug("Adding tv to database requests (No approval required & Sonarr)"); RequestService.AddRequest(model); var notify1 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notify1); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } return Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages)); } var srSettings = SickRageService.GetSettings(); if (srSettings.Enabled) { var result = sender.SendToSickRage(srSettings, model); if (result?.result == "success") { model.Approved = true; Log.Debug("Adding tv to database requests (No approval required & SickRage)"); RequestService.AddRequest(model); var notify2 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notify2); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message != null ? "Message From SickRage: " + result.message : "Something went wrong adding the movie to SickRage! Please check your settings." }); } return Response.AsJson("The request of TV Shows is not correctly set up. Please contact your admin."); } RequestService.AddRequest(model); var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } private bool CheckIfTitleExistsInPlex(string title, string year, string artist, PlexType type) { var result = Checker.IsAvailable(title, year, artist, type); return result; } private Response RequestAlbum(string releaseId) { var settings = PrService.GetSettings(); var existingRequest = RequestService.CheckRequest(releaseId); Log.Debug("Checking for an existing request"); if (existingRequest != null) { Log.Debug("We do have an existing album request"); if (!existingRequest.UserHasRequested(Username)) { Log.Debug("Not in the requested list so adding them and updating the request. User: {0}", Username); existingRequest.RequestedUsers.Add(Username); RequestService.UpdateRequest(existingRequest); } return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{existingRequest.Title} was successfully added!" : $"{existingRequest.Title} has already been requested!" }); } Log.Debug("This is a new request"); var albumInfo = MusicBrainzApi.GetAlbum(releaseId); DateTime release; DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; if (artist == null) { return Response.AsJson(new JsonResponseModel { Result = false, Message = "We could not find the artist on MusicBrainz. Please try again later or contact your admin" }); } var alreadyInPlex = CheckIfTitleExistsInPlex(albumInfo.title, release.ToString("yyyy"), artist.name, PlexType.Music); if (alreadyInPlex) { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{albumInfo.title} is already in Plex!" }); } var img = GetMusicBrainzCoverArt(albumInfo.id); Log.Trace("Album Details:"); Log.Trace(albumInfo.DumpJson()); Log.Trace("CoverArt Details:"); Log.Trace(img.DumpJson()); var model = new RequestedModel { Title = albumInfo.title, MusicBrainzId = albumInfo.id, Overview = albumInfo.disambiguation, PosterPath = img, Type = RequestType.Album, ProviderId = 0, RequestedUsers = new List { Username }, Status = albumInfo.status, Issues = IssueState.None, RequestedDate = DateTime.UtcNow, ReleaseDate = release, ArtistName = artist.name, ArtistId = artist.id }; if (ShouldAutoApprove(RequestType.Album, settings)) { Log.Debug("We don't require approval OR the user is in the whitelist"); var hpSettings = HeadphonesService.GetSettings(); Log.Trace("Headphone Settings:"); Log.Trace(hpSettings.DumpJson()); if (!hpSettings.Enabled) { RequestService.AddRequest(model); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{model.Title} was successfully added!" }); } var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); sender.AddAlbum(model); model.Approved = true; RequestService.AddRequest(model); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{model.Title} was successfully added!" }); } var result = RequestService.AddRequest(model); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{model.Title} was successfully added!" }); } private string GetMusicBrainzCoverArt(string id) { var coverArt = MusicBrainzApi.GetCoverArt(id); var firstImage = coverArt?.images?.FirstOrDefault(); var img = string.Empty; if (firstImage != null) { img = firstImage.thumbnails?.small ?? firstImage.image; } return img; } private bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings) { // if the user is an admin or they are whitelisted, they go ahead and allow auto-approval if (IsAdmin || prSettings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase))) return true; // check by request type if the category requires approval or not switch (requestType) { case RequestType.Movie: return !prSettings.RequireMovieApproval; case RequestType.TvShow: return !prSettings.RequireTvShowApproval; case RequestType.Album: return !prSettings.RequireMusicApproval; default: return false; } } } }