mirror of https://github.com/Ombi-app/Ombi
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1076 lines
46 KiB
1076 lines
46 KiB
#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 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.Services.Interfaces;
|
|
using PlexRequests.Services.Notification;
|
|
using PlexRequests.Store;
|
|
using PlexRequests.UI.Helpers;
|
|
using PlexRequests.UI.Models;
|
|
using System.Threading.Tasks;
|
|
|
|
using Nancy.Extensions;
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
using PlexRequests.Api.Models.Tv;
|
|
using PlexRequests.Core.Models;
|
|
using PlexRequests.Helpers.Analytics;
|
|
using PlexRequests.Store.Models;
|
|
using PlexRequests.Store.Repository;
|
|
|
|
using TMDbLib.Objects.General;
|
|
|
|
using Action = PlexRequests.Helpers.Analytics.Action;
|
|
using EpisodesModel = PlexRequests.Store.EpisodesModel;
|
|
|
|
namespace PlexRequests.UI.Modules
|
|
{
|
|
public class SearchModule : BaseAuthModule
|
|
{
|
|
public SearchModule(ICacheProvider cache, ISettingsService<CouchPotatoSettings> cpSettings,
|
|
ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker,
|
|
IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings,
|
|
ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi,
|
|
INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService<HeadphonesSettings> hpService,
|
|
ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi,
|
|
ISettingsService<PlexSettings> plexService, ISettingsService<AuthenticationSettings> auth, IRepository<UsersToNotify> u, ISettingsService<EmailNotificationSettings> email,
|
|
IIssueService issue, IAnalytics a, IRepository<RequestLimit> rl) : base("search", prSettings)
|
|
{
|
|
Auth = auth;
|
|
PlexService = plexService;
|
|
PlexApi = plexApi;
|
|
CpService = cpSettings;
|
|
PrService = prSettings;
|
|
MovieApi = new TheMovieDbApi();
|
|
Cache = cache;
|
|
Checker = checker;
|
|
CpCacher = cpCacher;
|
|
SonarrCacher = sonarrCacher;
|
|
SickRageCacher = sickRageCacher;
|
|
RequestService = request;
|
|
SonarrApi = sonarrApi;
|
|
SonarrService = sonarrSettings;
|
|
CouchPotatoApi = cpApi;
|
|
SickRageService = sickRageService;
|
|
SickrageApi = srApi;
|
|
NotificationService = notify;
|
|
MusicBrainzApi = mbApi;
|
|
HeadphonesApi = hpApi;
|
|
HeadphonesService = hpService;
|
|
UsersToNotifyRepo = u;
|
|
EmailNotificationSettings = email;
|
|
IssueService = issue;
|
|
Analytics = a;
|
|
RequestLimitRepo = rl;
|
|
TvApi = new TvMazeApi();
|
|
|
|
|
|
Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad();
|
|
|
|
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 SearchMusic((string)x.searchTerm);
|
|
Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id);
|
|
|
|
Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies();
|
|
Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies();
|
|
|
|
Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId);
|
|
Post["request/tv", true] = async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
|
|
Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode");
|
|
Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId);
|
|
|
|
Post["/notifyuser", true] = async (x, ct) => await NotifyUser((bool)Request.Form.notify);
|
|
Get["/notifyuser", true] = async (x, ct) => await GetUserNotificationSettings();
|
|
|
|
Get["/seasons"] = x => GetSeasons();
|
|
Get["/episodes", true] = async (x, ct) => await GetEpisodes();
|
|
}
|
|
private TvMazeApi TvApi { get; }
|
|
private IPlexApi PlexApi { get; }
|
|
private TheMovieDbApi MovieApi { get; }
|
|
private INotificationService NotificationService { get; }
|
|
private ICouchPotatoApi CouchPotatoApi { get; }
|
|
private ISonarrApi SonarrApi { get; }
|
|
private ISickRageApi SickrageApi { get; }
|
|
private IRequestService RequestService { get; }
|
|
private ICacheProvider Cache { get; }
|
|
private ISettingsService<AuthenticationSettings> Auth { get; }
|
|
private ISettingsService<PlexSettings> PlexService { get; }
|
|
private ISettingsService<CouchPotatoSettings> CpService { get; }
|
|
private ISettingsService<PlexRequestSettings> PrService { get; }
|
|
private ISettingsService<SonarrSettings> SonarrService { get; }
|
|
private ISettingsService<SickRageSettings> SickRageService { get; }
|
|
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
|
|
private ISettingsService<EmailNotificationSettings> EmailNotificationSettings { get; }
|
|
private IAvailabilityChecker Checker { get; }
|
|
private ICouchPotatoCacher CpCacher { get; }
|
|
private ISonarrCacher SonarrCacher { get; }
|
|
private ISickRageCacher SickRageCacher { get; }
|
|
private IMusicBrainzApi MusicBrainzApi { get; }
|
|
private IHeadphonesApi HeadphonesApi { get; }
|
|
private IRepository<UsersToNotify> UsersToNotifyRepo { get; }
|
|
private IIssueService IssueService { get; }
|
|
private IAnalytics Analytics { get; }
|
|
private IRepository<RequestLimit> RequestLimitRepo { get; }
|
|
private static Logger Log = LogManager.GetCurrentClassLogger();
|
|
|
|
private async Task<Negotiator> RequestLoad()
|
|
{
|
|
|
|
var settings = await PrService.GetSettingsAsync();
|
|
|
|
return View["Search/Index", settings];
|
|
}
|
|
|
|
private async Task<Response> UpcomingMovies()
|
|
{
|
|
Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
return await ProcessMovies(MovieSearchType.Upcoming, string.Empty);
|
|
}
|
|
|
|
private async Task<Response> CurrentlyPlayingMovies()
|
|
{
|
|
Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty);
|
|
}
|
|
|
|
private async Task<Response> SearchMovie(string searchTerm)
|
|
{
|
|
Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
return await ProcessMovies(MovieSearchType.Search, searchTerm);
|
|
}
|
|
|
|
private async Task<Response> ProcessMovies(MovieSearchType searchType, string searchTerm)
|
|
{
|
|
List<MovieResult> apiMovies;
|
|
|
|
switch (searchType)
|
|
{
|
|
case MovieSearchType.Search:
|
|
var movies = await MovieApi.SearchMovie(searchTerm);
|
|
apiMovies = movies.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();
|
|
break;
|
|
case MovieSearchType.CurrentlyPlaying:
|
|
apiMovies = await MovieApi.GetCurrentPlayingMovies();
|
|
break;
|
|
case MovieSearchType.Upcoming:
|
|
apiMovies = await MovieApi.GetUpcomingMovies();
|
|
break;
|
|
default:
|
|
apiMovies = new List<MovieResult>();
|
|
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);
|
|
|
|
|
|
var cpCached = CpCacher.QueuedIds();
|
|
var plexMovies = Checker.GetPlexMovies();
|
|
var settings = await PrService.GetSettingsAsync();
|
|
var viewMovies = new List<SearchMovieViewModel>();
|
|
foreach (var 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
|
|
};
|
|
var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies);
|
|
if (Checker.IsMovieAvailable(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString()))
|
|
{
|
|
viewMovie.Available = true;
|
|
}
|
|
else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db
|
|
{
|
|
var dbm = dbMovies[movie.Id];
|
|
|
|
viewMovie.Requested = true;
|
|
viewMovie.Approved = dbm.Approved;
|
|
viewMovie.Available = dbm.Available;
|
|
}
|
|
else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db
|
|
{
|
|
viewMovie.Requested = true;
|
|
}
|
|
|
|
viewMovies.Add(viewMovie);
|
|
}
|
|
|
|
return Response.AsJson(viewMovies);
|
|
}
|
|
|
|
private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, Dictionary<int, RequestedModel> moviesInDb)
|
|
{
|
|
if (usersCanViewOnlyOwnRequests)
|
|
{
|
|
var result = moviesInDb.FirstOrDefault(x => x.Value.ProviderId == movieId);
|
|
return result.Value == null || result.Value.UserHasRequested(Username);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task<Response> SearchTvShow(string searchTerm)
|
|
{
|
|
Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
var plexSettings = await PlexService.GetSettingsAsync();
|
|
var providerId = string.Empty;
|
|
|
|
var apiTv = new List<TvMazeSearch>();
|
|
await Task.Factory.StartNew(() => new TvMazeApi().Search(searchTerm)).ContinueWith((t) =>
|
|
{
|
|
apiTv = t.Result;
|
|
});
|
|
|
|
var allResults = await RequestService.GetAllAsync();
|
|
allResults = allResults.Where(x => x.Type == RequestType.TvShow);
|
|
|
|
var dbTv = allResults.ToDictionary(x => x.ProviderId);
|
|
|
|
if (!apiTv.Any())
|
|
{
|
|
return Response.AsJson("");
|
|
}
|
|
|
|
var sonarrCached = SonarrCacher.QueuedIds();
|
|
var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays
|
|
var plexTvShows = Checker.GetPlexTvShows();
|
|
|
|
var viewTv = new List<SearchTvShowViewModel>();
|
|
foreach (var t in apiTv)
|
|
{
|
|
var tvInfoTask = Task.Run(() => TvApi.EpisodeLookup(t.show.id));
|
|
|
|
var banner = t.show.image?.medium;
|
|
if (!string.IsNullOrEmpty(banner))
|
|
{
|
|
banner = banner.Replace("http", "https"); // Always use the Https banners
|
|
}
|
|
|
|
var viewT = new SearchTvShowViewModel
|
|
{
|
|
Banner = banner,
|
|
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 (plexSettings.AdvancedSearch)
|
|
{
|
|
providerId = viewT.Id.ToString();
|
|
}
|
|
|
|
if (Checker.IsTvShowAvailable(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId))
|
|
{
|
|
viewT.Available = true;
|
|
}
|
|
else if (t.show?.externals?.thetvdb != null)
|
|
{
|
|
var tvdbid = (int)t.show.externals.thetvdb;
|
|
|
|
if (dbTv.ContainsKey(tvdbid))
|
|
{
|
|
var dbt = dbTv[tvdbid];
|
|
|
|
viewT.Requested = true;
|
|
viewT.Episodes = dbt.Episodes.ToList();
|
|
viewT.Approved = dbt.Approved;
|
|
viewT.Available = dbt.Available;
|
|
}
|
|
else if (sonarrCached.Contains(tvdbid) || sickRageCache.Contains(tvdbid)) // compare to the sonarr/sickrage db
|
|
{
|
|
viewT.Requested = true;
|
|
}
|
|
}
|
|
var tvInfo = await tvInfoTask;
|
|
|
|
// Check if we have every episode in all seasons
|
|
var epModel = tvInfo.Select(tvIn => new Store.EpisodesModel { SeasonNumber = tvIn.season, EpisodeNumber = tvIn.number }).ToList();
|
|
var diff = viewT.Episodes.Except(epModel);
|
|
if (diff.Any())
|
|
{
|
|
viewT.TvFullyAvailable = true;
|
|
}
|
|
viewTv.Add(viewT);
|
|
}
|
|
|
|
return Response.AsJson(viewTv);
|
|
}
|
|
|
|
private async Task<Response> SearchMusic(string searchTerm)
|
|
{
|
|
Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
var apiAlbums = new List<Release>();
|
|
await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) =>
|
|
{
|
|
apiAlbums = t.Result.releases ?? new List<Release>();
|
|
});
|
|
|
|
var allResults = await RequestService.GetAllAsync();
|
|
allResults = allResults.Where(x => x.Type == RequestType.Album);
|
|
|
|
var dbAlbum = allResults.ToDictionary(x => x.MusicBrainzId);
|
|
|
|
var plexAlbums = Checker.GetPlexAlbums();
|
|
|
|
var viewAlbum = new List<SearchMusicViewModel>();
|
|
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
|
|
};
|
|
|
|
DateTime release;
|
|
DateTimeHelper.CustomParse(a.ReleaseEvents?.FirstOrDefault()?.date, out release);
|
|
var artist = a.ArtistCredit?.FirstOrDefault()?.artist;
|
|
if (Checker.IsAlbumAvailable(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name))
|
|
{
|
|
viewA.Available = true;
|
|
}
|
|
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 async Task<Response> RequestMovie(int movieId)
|
|
{
|
|
var settings = await PrService.GetSettingsAsync();
|
|
if (!await CheckRequestLimit(settings, RequestType.Movie))
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = "You have reached your weekly request limit for Movies! Please contact your admin." });
|
|
}
|
|
|
|
Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
var movieInfo = await MovieApi.GetMovieInformation(movieId);
|
|
var fullMovieName = $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}";
|
|
|
|
var existingRequest = await RequestService.CheckRequestAsync(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);
|
|
await RequestService.UpdateRequestAsync(existingRequest);
|
|
}
|
|
|
|
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" });
|
|
}
|
|
|
|
try
|
|
{
|
|
var movies = Checker.GetPlexMovies();
|
|
if (Checker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString()))
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} is already in Plex!" });
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e);
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) });
|
|
}
|
|
//#endif
|
|
|
|
var model = new RequestedModel
|
|
{
|
|
ProviderId = movieInfo.Id,
|
|
Type = RequestType.Movie,
|
|
Overview = movieInfo.Overview,
|
|
ImdbId = movieInfo.ImdbId,
|
|
PosterPath = "https://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<string> { Username },
|
|
Issues = IssueState.None,
|
|
|
|
};
|
|
|
|
if (ShouldAutoApprove(RequestType.Movie, settings))
|
|
{
|
|
var cpSettings = await CpService.GetSettingsAsync();
|
|
model.Approved = true;
|
|
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)
|
|
{
|
|
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
|
|
return Response.AsJson(new JsonResponseModel
|
|
{
|
|
Result = false,
|
|
Message = Resources.UI.Search_CouchPotatoError
|
|
});
|
|
}
|
|
model.Approved = true;
|
|
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
|
|
try
|
|
{
|
|
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Fatal(e);
|
|
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_CouchPotatoError });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requests the tv show.
|
|
/// </summary>
|
|
/// <param name="showId">The show identifier.</param>
|
|
/// <param name="seasons">The seasons.</param>
|
|
/// <returns></returns>
|
|
private async Task<Response> RequestTvShow(int showId, string seasons)
|
|
{
|
|
// Get the JSON from the request
|
|
var req = (Dictionary<string, object>.ValueCollection)Request.Form.Values;
|
|
var json = req.FirstOrDefault()?.ToString();
|
|
var episodeModel = JsonConvert.DeserializeObject<EpisodeRequestModel>(json); // Convert it into the object
|
|
|
|
var episodeRequest = false;
|
|
|
|
var settings = await PrService.GetSettingsAsync();
|
|
if (!await CheckRequestLimit(settings, RequestType.TvShow))
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_WeeklyRequestLimitTVShow });
|
|
}
|
|
Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
|
|
var sonarrSettings = SonarrService.GetSettingsAsync();
|
|
|
|
// This means we are requesting an episode rather than a whole series or season
|
|
if (episodeModel != null)
|
|
{
|
|
episodeRequest = true;
|
|
showId = episodeModel.ShowId;
|
|
var s = await sonarrSettings;
|
|
if (!s.Enabled)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Message = "This is currently only supported with Sonarr, Please enable Sonarr for this feature", Result = false });
|
|
}
|
|
}
|
|
|
|
var showInfo = TvApi.ShowLookupByTheTvDbId(showId);
|
|
DateTime firstAir;
|
|
DateTime.TryParse(showInfo.premiered, out firstAir);
|
|
string fullShowName = $"{showInfo.name} ({firstAir.Year})";
|
|
|
|
if (showInfo.externals?.thetvdb == null)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Our TV Provider (TVMaze) doesn't have a TheTVDBId for this item. Please report this to TVMaze. We cannot add the series sorry." });
|
|
}
|
|
|
|
// check if the show/episodes have already been requested
|
|
|
|
var existingRequest = await RequestService.CheckRequestAsync(showId);
|
|
var difference = new List<Store.EpisodesModel>();
|
|
if (existingRequest != null)
|
|
{
|
|
if (episodeRequest)
|
|
{
|
|
difference = GetListDifferences(existingRequest.Episodes, episodeModel.Episodes).ToList();
|
|
if (!difference.Any())
|
|
{
|
|
return await AddUserToRequest(existingRequest, settings, fullShowName);
|
|
}
|
|
// We have an episode that has not yet been requested, let's continue
|
|
}
|
|
else
|
|
{
|
|
return await AddUserToRequest(existingRequest, settings, fullShowName);
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
var shows = Checker.GetPlexTvShows();
|
|
var providerId = string.Empty;
|
|
var plexSettings = await PlexService.GetSettingsAsync();
|
|
if (plexSettings.AdvancedSearch)
|
|
{
|
|
providerId = showId.ToString();
|
|
}
|
|
if (episodeRequest)
|
|
{
|
|
foreach (var d in difference)
|
|
{
|
|
if (Checker.IsEpisodeAvailable(providerId, d.SeasonNumber, d.EpisodeNumber))
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" });
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId))
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) });
|
|
}
|
|
|
|
|
|
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<string> { Username },
|
|
Issues = IssueState.None,
|
|
ImdbId = showInfo.externals?.imdb ?? string.Empty,
|
|
SeasonCount = showInfo.seasonCount,
|
|
TvDbId = showId.ToString()
|
|
};
|
|
|
|
|
|
var seasonsList = new List<int>();
|
|
switch (seasons)
|
|
{
|
|
case "first":
|
|
seasonsList.Add(1);
|
|
model.SeasonsRequested = "First";
|
|
break;
|
|
case "latest":
|
|
seasonsList.Add(model.SeasonCount);
|
|
model.SeasonsRequested = "Latest";
|
|
break;
|
|
case "all":
|
|
model.SeasonsRequested = "All";
|
|
break;
|
|
case "episode":
|
|
model.Episodes = new List<EpisodesModel>();
|
|
for (var i = 0; i < episodeModel.Episodes.Length; i++)
|
|
{
|
|
model.Episodes[i] = new EpisodesModel { EpisodeNumber = episodeModel.Episodes[i].EpisodeNumber, SeasonNumber = episodeModel.Episodes[i].SeasonNumber };
|
|
}
|
|
break;
|
|
default:
|
|
model.SeasonsRequested = seasons;
|
|
var split = seasons.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
var seasonsCount = new int[split.Length];
|
|
for (var i = 0; i < split.Length; i++)
|
|
{
|
|
int tryInt;
|
|
int.TryParse(split[i], out tryInt);
|
|
seasonsCount[i] = tryInt;
|
|
}
|
|
seasonsList.AddRange(seasonsCount);
|
|
break;
|
|
}
|
|
|
|
model.SeasonList = seasonsList.ToArray();
|
|
|
|
if (ShouldAutoApprove(RequestType.TvShow, settings))
|
|
{
|
|
model.Approved = true;
|
|
var s = await sonarrSettings;
|
|
var sender = new TvSender(SonarrApi, SickrageApi);
|
|
if (s.Enabled)
|
|
{
|
|
var result = await sender.SendToSonarr(s, model);
|
|
if (!string.IsNullOrEmpty(result?.title))
|
|
{
|
|
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
Log.Debug("Error with sending to sonarr.");
|
|
return Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List<string>()));
|
|
}
|
|
|
|
var srSettings = SickRageService.GetSettings();
|
|
if (srSettings.Enabled)
|
|
{
|
|
var result = sender.SendToSickRage(srSettings, model);
|
|
if (result?.result == "success")
|
|
{
|
|
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message ?? Resources.UI.Search_SickrageError });
|
|
}
|
|
|
|
if (!srSettings.Enabled && !s.Enabled)
|
|
{
|
|
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp });
|
|
|
|
}
|
|
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
|
|
private async Task<Response> AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, string fullShowName)
|
|
{
|
|
// 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);
|
|
await RequestService.UpdateRequestAsync(existingRequest);
|
|
}
|
|
return
|
|
Response.AsJson(
|
|
new JsonResponseModel
|
|
{
|
|
Result = true,
|
|
Message =
|
|
settings.UsersCanViewOnlyOwnRequests
|
|
? $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"
|
|
: $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"
|
|
});
|
|
}
|
|
|
|
private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings)
|
|
{
|
|
var sendNotification = ShouldAutoApprove(type, prSettings) ? !prSettings.IgnoreNotifyForAutoApprovedRequests : true;
|
|
var claims = Context.CurrentUser?.Claims;
|
|
if (claims != null)
|
|
{
|
|
var enumerable = claims as string[] ?? claims.ToArray();
|
|
if (enumerable.Contains(UserClaims.Admin) || enumerable.Contains(UserClaims.PowerUser))
|
|
{
|
|
sendNotification = false; // Don't bother sending a notification if the user is an admin
|
|
}
|
|
}
|
|
return sendNotification;
|
|
}
|
|
|
|
|
|
private async Task<Response> RequestAlbum(string releaseId)
|
|
{
|
|
var settings = await PrService.GetSettingsAsync();
|
|
if (!await CheckRequestLimit(settings, RequestType.Album))
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_WeeklyRequestLimitAlbums });
|
|
}
|
|
Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, CookieHelper.GetAnalyticClientId(Cookies));
|
|
var existingRequest = await RequestService.CheckRequestAsync(releaseId);
|
|
|
|
if (existingRequest != null)
|
|
{
|
|
if (!existingRequest.UserHasRequested(Username))
|
|
{
|
|
existingRequest.RequestedUsers.Add(Username);
|
|
await RequestService.UpdateRequestAsync(existingRequest);
|
|
}
|
|
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" });
|
|
}
|
|
|
|
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 = Resources.UI.Search_MusicBrainzError });
|
|
}
|
|
|
|
var albums = Checker.GetPlexAlbums();
|
|
var alreadyInPlex = Checker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), artist.name);
|
|
|
|
if (alreadyInPlex)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel
|
|
{
|
|
Result = false,
|
|
Message = $"{albumInfo.title} {Resources.UI.Search_AlreadyInPlex}"
|
|
});
|
|
}
|
|
|
|
var img = GetMusicBrainzCoverArt(albumInfo.id);
|
|
|
|
var model = new RequestedModel
|
|
{
|
|
Title = albumInfo.title,
|
|
MusicBrainzId = albumInfo.id,
|
|
Overview = albumInfo.disambiguation,
|
|
PosterPath = img,
|
|
Type = RequestType.Album,
|
|
ProviderId = 0,
|
|
RequestedUsers = new List<string> { Username },
|
|
Status = albumInfo.status,
|
|
Issues = IssueState.None,
|
|
RequestedDate = DateTime.UtcNow,
|
|
ReleaseDate = release,
|
|
ArtistName = artist.name,
|
|
ArtistId = artist.id
|
|
};
|
|
|
|
if (ShouldAutoApprove(RequestType.Album, settings))
|
|
{
|
|
model.Approved = true;
|
|
var hpSettings = HeadphonesService.GetSettings();
|
|
|
|
if (!hpSettings.Enabled)
|
|
{
|
|
await RequestService.AddRequestAsync(model);
|
|
return
|
|
Response.AsJson(new JsonResponseModel
|
|
{
|
|
Result = true,
|
|
Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"
|
|
});
|
|
}
|
|
|
|
var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService);
|
|
await sender.AddAlbum(model);
|
|
return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
|
|
return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
private async Task<Response> NotifyUser(bool notify)
|
|
{
|
|
Analytics.TrackEventAsync(Category.Search, Action.Save, "NotifyUser", Username, CookieHelper.GetAnalyticClientId(Cookies), notify ? 1 : 0);
|
|
var authSettings = await Auth.GetSettingsAsync();
|
|
var auth = authSettings.UserAuthentication;
|
|
var emailSettings = await EmailNotificationSettings.GetSettingsAsync();
|
|
var email = emailSettings.EnableUserEmailNotifications;
|
|
if (!auth)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_ErrorPlexAccountOnly });
|
|
}
|
|
if (!email)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_ErrorNotEnabled });
|
|
}
|
|
var username = Username;
|
|
var originalList = await UsersToNotifyRepo.GetAllAsync();
|
|
if (!notify)
|
|
{
|
|
if (originalList == null)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_NotificationError });
|
|
}
|
|
var userToRemove = originalList.FirstOrDefault(x => x.Username == username);
|
|
if (userToRemove != null)
|
|
{
|
|
await UsersToNotifyRepo.DeleteAsync(userToRemove);
|
|
}
|
|
return Response.AsJson(new JsonResponseModel { Result = true });
|
|
}
|
|
|
|
|
|
if (originalList == null)
|
|
{
|
|
var userModel = new UsersToNotify { Username = username };
|
|
var insertResult = await UsersToNotifyRepo.InsertAsync(userModel);
|
|
return Response.AsJson(insertResult != -1 ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = Resources.UI.Common_CouldNotSave });
|
|
}
|
|
|
|
var existingUser = originalList.FirstOrDefault(x => x.Username == username);
|
|
if (existingUser != null)
|
|
{
|
|
return Response.AsJson(new JsonResponseModel { Result = true }); // It's already enabled
|
|
}
|
|
else
|
|
{
|
|
var userModel = new UsersToNotify { Username = username };
|
|
var insertResult = await UsersToNotifyRepo.InsertAsync(userModel);
|
|
return Response.AsJson(insertResult != -1 ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = Resources.UI.Common_CouldNotSave });
|
|
}
|
|
|
|
}
|
|
private async Task<Response> GetUserNotificationSettings()
|
|
{
|
|
var all = await UsersToNotifyRepo.GetAllAsync();
|
|
var retVal = all.FirstOrDefault(x => x.Username == Username);
|
|
return Response.AsJson(retVal != null);
|
|
}
|
|
|
|
private Response GetSeasons()
|
|
{
|
|
var seriesId = (int)Request.Query.tvId;
|
|
var show = TvApi.ShowLookupByTheTvDbId(seriesId);
|
|
var seasons = TvApi.GetSeasons(show.id);
|
|
var model = seasons.Select(x => x.number);
|
|
return Response.AsJson(model);
|
|
}
|
|
|
|
private async Task<Response> GetEpisodes()
|
|
{
|
|
var allResults = await RequestService.GetAllAsync();
|
|
var model = new List<EpisodeListViewModel>();
|
|
var seriesId = (int)Request.Query.tvId;
|
|
var enumerable = allResults as RequestedModel[] ?? allResults.ToArray();
|
|
|
|
var dbDbShow = enumerable.FirstOrDefault(x => x.Type == RequestType.TvShow && x.TvDbId == seriesId.ToString());
|
|
var show = await Task.Run(() => TvApi.ShowLookupByTheTvDbId(seriesId));
|
|
var seasons = await Task.Run(() => TvApi.EpisodeLookup(show.id));
|
|
|
|
|
|
foreach (var ep in seasons)
|
|
{
|
|
var requested = dbDbShow?.Episodes
|
|
.Any(episodesModel =>
|
|
ep.number == episodesModel.EpisodeNumber && ep.season == episodesModel.SeasonNumber) ?? false;
|
|
|
|
var alreadyInPlex = Checker.IsEpisodeAvailable(seriesId.ToString(), ep.season, ep.number);
|
|
|
|
model.Add(new EpisodeListViewModel
|
|
{
|
|
Id = show.id,
|
|
SeasonNumber = ep.season,
|
|
EpisodeNumber = ep.number,
|
|
Requested = requested || alreadyInPlex,
|
|
Name = ep.name,
|
|
EpisodeId = ep.id
|
|
});
|
|
}
|
|
|
|
return Response.AsJson(model);
|
|
}
|
|
|
|
public async Task<bool> CheckRequestLimit(PlexRequestSettings s, RequestType type)
|
|
{
|
|
if (IsAdmin)
|
|
return true;
|
|
|
|
if (s.ApprovalWhiteList.Contains(Username))
|
|
return true;
|
|
|
|
var requestLimit = GetRequestLimitForType(type, s);
|
|
if (requestLimit == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var limit = await RequestLimitRepo.GetAllAsync();
|
|
var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == type);
|
|
if (usersLimit == null)
|
|
{
|
|
// Have not set a requestLimit yet
|
|
return true;
|
|
}
|
|
|
|
return requestLimit > usersLimit.RequestCount;
|
|
}
|
|
|
|
private int GetRequestLimitForType(RequestType type, PlexRequestSettings s)
|
|
{
|
|
int requestLimit;
|
|
switch (type)
|
|
{
|
|
case RequestType.Movie:
|
|
requestLimit = s.MovieWeeklyRequestLimit;
|
|
break;
|
|
case RequestType.TvShow:
|
|
requestLimit = s.TvWeeklyRequestLimit;
|
|
break;
|
|
case RequestType.Album:
|
|
requestLimit = s.AlbumWeeklyRequestLimit;
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
|
}
|
|
return requestLimit;
|
|
}
|
|
|
|
private async Task<Response> AddRequest(RequestedModel model, PlexRequestSettings settings, string message)
|
|
{
|
|
await RequestService.AddRequestAsync(model);
|
|
|
|
if (ShouldSendNotification(model.Type, settings))
|
|
{
|
|
var notificationModel = new NotificationModel
|
|
{
|
|
Title = model.Title,
|
|
User = Username,
|
|
DateTime = DateTime.Now,
|
|
NotificationType = NotificationType.NewRequest,
|
|
RequestType = model.Type
|
|
};
|
|
await NotificationService.Publish(notificationModel);
|
|
}
|
|
|
|
var limit = await RequestLimitRepo.GetAllAsync();
|
|
var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type);
|
|
if (usersLimit == null)
|
|
{
|
|
await RequestLimitRepo.InsertAsync(new RequestLimit
|
|
{
|
|
Username = Username,
|
|
RequestType = model.Type,
|
|
FirstRequestDate = DateTime.UtcNow,
|
|
RequestCount = 1
|
|
});
|
|
}
|
|
else
|
|
{
|
|
usersLimit.RequestCount++;
|
|
await RequestLimitRepo.UpdateAsync(usersLimit);
|
|
}
|
|
|
|
return Response.AsJson(new JsonResponseModel { Result = true, Message = message });
|
|
}
|
|
|
|
private IEnumerable<Store.EpisodesModel> GetListDifferences(IEnumerable<Store.EpisodesModel> model, IEnumerable<Models.EpisodesModel> request)
|
|
{
|
|
var newRequest = request
|
|
.Select(r =>
|
|
new Store.EpisodesModel
|
|
{
|
|
SeasonNumber = r.SeasonNumber,
|
|
EpisodeNumber = r.EpisodeNumber
|
|
}).ToList();
|
|
|
|
return newRequest.Except(model);
|
|
}
|
|
}
|
|
}
|