parent
fdecd1fea4
commit
b4ca4908fc
@ -1,150 +0,0 @@
|
||||
using Nancy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
using Ical.Net.Serialization;
|
||||
using NzbDrone.Core.Tv;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Api.Calendar
|
||||
{
|
||||
public class CalendarFeedModule : NzbDroneFeedModule
|
||||
{
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService)
|
||||
: base("calendar")
|
||||
{
|
||||
_episodeService = episodeService;
|
||||
_tagService = tagService;
|
||||
|
||||
Get("/NzbDrone.ics", options => GetCalendarFeed());
|
||||
Get("/Sonarr.ics", options => GetCalendarFeed());
|
||||
}
|
||||
|
||||
private object GetCalendarFeed()
|
||||
{
|
||||
var pastDays = 7;
|
||||
var futureDays = 28;
|
||||
var start = DateTime.Today.AddDays(-pastDays);
|
||||
var end = DateTime.Today.AddDays(futureDays);
|
||||
var unmonitored = false;
|
||||
var premieresOnly = false;
|
||||
var asAllDay = false;
|
||||
var tags = new List<int>();
|
||||
|
||||
// TODO: Remove start/end parameters in v3, they don't work well for iCal
|
||||
var queryStart = Request.Query.Start;
|
||||
var queryEnd = Request.Query.End;
|
||||
var queryPastDays = Request.Query.PastDays;
|
||||
var queryFutureDays = Request.Query.FutureDays;
|
||||
var queryUnmonitored = Request.Query.Unmonitored;
|
||||
var queryPremieresOnly = Request.Query.PremieresOnly;
|
||||
var queryPremiersOnly = Request.Query.PremiersOnly;
|
||||
var queryAsAllDay = Request.Query.AsAllDay;
|
||||
var queryTags = Request.Query.Tags;
|
||||
|
||||
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
|
||||
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
|
||||
|
||||
if (queryPastDays.HasValue)
|
||||
{
|
||||
pastDays = int.Parse(queryPastDays.Value);
|
||||
start = DateTime.Today.AddDays(-pastDays);
|
||||
}
|
||||
|
||||
if (queryFutureDays.HasValue)
|
||||
{
|
||||
futureDays = int.Parse(queryFutureDays.Value);
|
||||
end = DateTime.Today.AddDays(futureDays);
|
||||
}
|
||||
|
||||
if (queryUnmonitored.HasValue)
|
||||
{
|
||||
unmonitored = bool.Parse(queryUnmonitored.Value);
|
||||
}
|
||||
|
||||
if (queryPremieresOnly.HasValue)
|
||||
{
|
||||
premieresOnly = bool.Parse(queryPremieresOnly.Value);
|
||||
}
|
||||
else if (queryPremiersOnly.HasValue)
|
||||
{
|
||||
// There was a typo, recognize mistyped 'premiersOnly' boolean too for background compat.
|
||||
premieresOnly = bool.Parse(queryPremiersOnly.Value);
|
||||
}
|
||||
|
||||
|
||||
if (queryAsAllDay.HasValue)
|
||||
{
|
||||
asAllDay = bool.Parse(queryAsAllDay.Value);
|
||||
}
|
||||
|
||||
if (queryTags.HasValue)
|
||||
{
|
||||
var tagInput = (string)queryTags.Value.ToString();
|
||||
tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
|
||||
}
|
||||
|
||||
var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored);
|
||||
var calendar = new Ical.Net.Calendar
|
||||
{
|
||||
ProductId = "-//sonarr.tv//Sonarr//EN"
|
||||
};
|
||||
|
||||
var calendarName = "Sonarr TV Schedule";
|
||||
calendar.AddProperty(new CalendarProperty("NAME", calendarName));
|
||||
calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName));
|
||||
|
||||
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))
|
||||
{
|
||||
if (premieresOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tags.Any() && tags.None(episode.Series.Tags.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var occurrence = calendar.Create<CalendarEvent>();
|
||||
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
|
||||
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||
occurrence.Description = episode.Overview;
|
||||
occurrence.Categories = new List<string>() { episode.Series.Network };
|
||||
|
||||
if (asAllDay)
|
||||
{
|
||||
occurrence.Start = new CalDateTime(episode.AirDateUtc.Value.ToLocalTime()) { HasTime = false };
|
||||
}
|
||||
else
|
||||
{
|
||||
occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true };
|
||||
occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true };
|
||||
}
|
||||
|
||||
switch (episode.Series.SeriesType)
|
||||
{
|
||||
case SeriesTypes.Daily:
|
||||
occurrence.Summary = $"{episode.Series.Title} - {episode.Title}";
|
||||
break;
|
||||
default:
|
||||
occurrence.Summary =$"{episode.Series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var serializer = (IStringSerializer) new SerializerFactory().Build(calendar.GetType(), new SerializationContext());
|
||||
var icalendar = serializer.SerializeToString(calendar);
|
||||
|
||||
return new TextResponse(icalendar, "text/calendar");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sonarr.Http.Extensions;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ProgressMessaging;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.Validation;
|
||||
|
||||
|
||||
namespace NzbDrone.Api.Commands
|
||||
{
|
||||
public class CommandModule : SonarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IServiceFactory _serviceFactory;
|
||||
private readonly Debouncer _debouncer;
|
||||
private readonly Dictionary<int, CommandResource> _pendingUpdates;
|
||||
|
||||
public CommandModule(IManageCommandQueue commandQueueManager,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IServiceFactory serviceFactory)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_serviceFactory = serviceFactory;
|
||||
|
||||
GetResourceById = GetCommand;
|
||||
CreateResource = StartCommand;
|
||||
GetResourceAll = GetStartedCommands;
|
||||
|
||||
PostValidator.RuleFor(c => c.Name).NotBlank();
|
||||
|
||||
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
|
||||
_pendingUpdates = new Dictionary<int, CommandResource>();
|
||||
|
||||
}
|
||||
|
||||
private CommandResource GetCommand(int id)
|
||||
{
|
||||
return _commandQueueManager.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private int StartCommand(CommandResource commandResource)
|
||||
{
|
||||
var commandType = _serviceFactory.GetImplementations(typeof(Command))
|
||||
.Single(c => c.Name.Replace("Command", "").Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
dynamic command = Request.Body.FromJson(commandType);
|
||||
command.Trigger = CommandTrigger.Manual;
|
||||
|
||||
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
|
||||
return trackedCommand.Id;
|
||||
}
|
||||
|
||||
private List<CommandResource> GetStartedCommands()
|
||||
{
|
||||
return _commandQueueManager.GetStarted().ToResource();
|
||||
}
|
||||
|
||||
public void Handle(CommandUpdatedEvent message)
|
||||
{
|
||||
if (message.Command.Body.SendUpdatesToClient)
|
||||
{
|
||||
lock (_pendingUpdates)
|
||||
{
|
||||
_pendingUpdates[message.Command.Id] = message.Command.ToResource();
|
||||
}
|
||||
_debouncer.Execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendUpdates()
|
||||
{
|
||||
lock (_pendingUpdates)
|
||||
{
|
||||
var pendingUpdates = _pendingUpdates.Values.ToArray();
|
||||
_pendingUpdates.Clear();
|
||||
|
||||
foreach (var pendingUpdate in pendingUpdates)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, pendingUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace NzbDrone.Api.Config
|
||||
{
|
||||
public class HostConfigModule : SonarrRestModule<HostConfigResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService configService, IUserService userService)
|
||||
: base("/config/host")
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_configService = configService;
|
||||
_userService = userService;
|
||||
|
||||
GetResourceSingle = GetHostConfig;
|
||||
GetResourceById = GetHostConfig;
|
||||
UpdateResource = SaveHostConfig;
|
||||
|
||||
SharedValidator.RuleFor(c => c.BindAddress)
|
||||
.ValidIp4Address()
|
||||
.NotListenAllIp4Address()
|
||||
.When(c => c.BindAddress != "*");
|
||||
|
||||
SharedValidator.RuleFor(c => c.Port).ValidPort();
|
||||
|
||||
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||
|
||||
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
|
||||
SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
|
||||
|
||||
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
|
||||
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
|
||||
|
||||
SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder));
|
||||
SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7);
|
||||
SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90);
|
||||
}
|
||||
|
||||
private HostConfigResource GetHostConfig()
|
||||
{
|
||||
var resource = _configFileProvider.ToResource(_configService);
|
||||
resource.Id = 1;
|
||||
|
||||
var user = _userService.FindUser();
|
||||
if (user != null)
|
||||
{
|
||||
resource.Username = user.Username;
|
||||
resource.Password = user.Password;
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private HostConfigResource GetHostConfig(int id)
|
||||
{
|
||||
return GetHostConfig();
|
||||
}
|
||||
|
||||
private void SaveHostConfig(HostConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||
_configService.SaveConfigDictionary(dictionary);
|
||||
|
||||
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_userService.Upsert(resource.Username, resource.Password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
using Sonarr.Http.REST;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
|
||||
namespace NzbDrone.Api.Config
|
||||
{
|
||||
public class HostConfigResource : RestResource
|
||||
{
|
||||
public string BindAddress { get; set; }
|
||||
public int Port { get; set; }
|
||||
public int SslPort { get; set; }
|
||||
public bool EnableSsl { get; set; }
|
||||
public bool LaunchBrowser { get; set; }
|
||||
public AuthenticationType AuthenticationMethod { get; set; }
|
||||
public bool AnalyticsEnabled { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string LogLevel { get; set; }
|
||||
public string ConsoleLogLevel { get; set; }
|
||||
public string Branch { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
public string SslCertHash { get; set; }
|
||||
public string UrlBase { get; set; }
|
||||
public bool UpdateAutomatically { get; set; }
|
||||
public UpdateMechanism UpdateMechanism { get; set; }
|
||||
public string UpdateScriptPath { get; set; }
|
||||
public bool ProxyEnabled { get; set; }
|
||||
public ProxyType ProxyType { get; set; }
|
||||
public string ProxyHostname { get; set; }
|
||||
public int ProxyPort { get; set; }
|
||||
public string ProxyUsername { get; set; }
|
||||
public string ProxyPassword { get; set; }
|
||||
public string ProxyBypassFilter { get; set; }
|
||||
public bool ProxyBypassLocalAddresses { get; set; }
|
||||
public string BackupFolder { get; set; }
|
||||
public int BackupInterval { get; set; }
|
||||
public int BackupRetention { get; set; }
|
||||
}
|
||||
|
||||
public static class HostConfigResourceMapper
|
||||
{
|
||||
public static HostConfigResource ToResource(this IConfigFileProvider model, IConfigService configService)
|
||||
{
|
||||
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead?
|
||||
return new HostConfigResource
|
||||
{
|
||||
BindAddress = model.BindAddress,
|
||||
Port = model.Port,
|
||||
SslPort = model.SslPort,
|
||||
EnableSsl = model.EnableSsl,
|
||||
LaunchBrowser = model.LaunchBrowser,
|
||||
AuthenticationMethod = model.AuthenticationMethod,
|
||||
AnalyticsEnabled = model.AnalyticsEnabled,
|
||||
//Username
|
||||
//Password
|
||||
LogLevel = model.LogLevel,
|
||||
ConsoleLogLevel = model.ConsoleLogLevel,
|
||||
Branch = model.Branch,
|
||||
ApiKey = model.ApiKey,
|
||||
SslCertHash = model.SslCertHash,
|
||||
UrlBase = model.UrlBase,
|
||||
UpdateAutomatically = model.UpdateAutomatically,
|
||||
UpdateMechanism = model.UpdateMechanism,
|
||||
UpdateScriptPath = model.UpdateScriptPath,
|
||||
ProxyEnabled = configService.ProxyEnabled,
|
||||
ProxyType = configService.ProxyType,
|
||||
ProxyHostname = configService.ProxyHostname,
|
||||
ProxyPort = configService.ProxyPort,
|
||||
ProxyUsername = configService.ProxyUsername,
|
||||
ProxyPassword = configService.ProxyPassword,
|
||||
ProxyBypassFilter = configService.ProxyBypassFilter,
|
||||
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
|
||||
BackupFolder = configService.BackupFolder,
|
||||
BackupInterval = configService.BackupInterval,
|
||||
BackupRetention = configService.BackupRetention
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using Sonarr.Http.REST;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Api.Config
|
||||
{
|
||||
public class MediaManagementConfigResource : RestResource
|
||||
{
|
||||
public bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; }
|
||||
public string RecycleBin { get; set; }
|
||||
public ProperDownloadTypes DownloadPropersAndRepacks { get; set; }
|
||||
public bool CreateEmptySeriesFolders { get; set; }
|
||||
public bool DeleteEmptyFolders { get; set; }
|
||||
public FileDateType FileDate { get; set; }
|
||||
|
||||
public bool SetPermissionsLinux { get; set; }
|
||||
public string ChmodFolder { get; set; }
|
||||
public string ChownGroup { get; set; }
|
||||
|
||||
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||
public bool CopyUsingHardlinks { get; set; }
|
||||
public bool ImportExtraFiles { get; set; }
|
||||
public string ExtraFileExtensions { get; set; }
|
||||
public bool EnableMediaInfo { get; set; }
|
||||
}
|
||||
|
||||
public static class MediaManagementConfigResourceMapper
|
||||
{
|
||||
public static MediaManagementConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return new MediaManagementConfigResource
|
||||
{
|
||||
AutoUnmonitorPreviouslyDownloadedEpisodes = model.AutoUnmonitorPreviouslyDownloadedEpisodes,
|
||||
RecycleBin = model.RecycleBin,
|
||||
DownloadPropersAndRepacks = model.DownloadPropersAndRepacks,
|
||||
CreateEmptySeriesFolders = model.CreateEmptySeriesFolders,
|
||||
DeleteEmptyFolders = model.DeleteEmptyFolders,
|
||||
FileDate = model.FileDate,
|
||||
|
||||
SetPermissionsLinux = model.SetPermissionsLinux,
|
||||
ChmodFolder = model.ChmodFolder,
|
||||
ChownGroup = model.ChownGroup,
|
||||
|
||||
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
||||
CopyUsingHardlinks = model.CopyUsingHardlinks,
|
||||
ImportExtraFiles = model.ImportExtraFiles,
|
||||
ExtraFileExtensions = model.ExtraFileExtensions,
|
||||
EnableMediaInfo = model.EnableMediaInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using Sonarr.Http.REST;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
namespace NzbDrone.Api.EpisodeFiles
|
||||
{
|
||||
public class EpisodeFileResource : RestResource
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string Path { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
public string SceneName { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public Language Language { get; set; }
|
||||
public MediaInfoResource MediaInfo { get; set; }
|
||||
public string OriginalFilePath { get; set; }
|
||||
|
||||
public bool QualityCutoffNotMet { get; set; }
|
||||
}
|
||||
|
||||
public static class EpisodeFileResourceMapper
|
||||
{
|
||||
private static EpisodeFileResource ToResource(this EpisodeFile model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new EpisodeFileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
SeriesId = model.SeriesId,
|
||||
SeasonNumber = model.SeasonNumber,
|
||||
RelativePath = model.RelativePath,
|
||||
//Path
|
||||
Size = model.Size,
|
||||
DateAdded = model.DateAdded,
|
||||
SceneName = model.SceneName,
|
||||
Quality = model.Quality,
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
OriginalFilePath = model.OriginalFilePath
|
||||
};
|
||||
}
|
||||
|
||||
public static EpisodeFileResource ToResource(this EpisodeFile model, Core.Tv.Series series, IUpgradableSpecification upgradableSpecification)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new EpisodeFileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
SeriesId = model.SeriesId,
|
||||
SeasonNumber = model.SeasonNumber,
|
||||
RelativePath = model.RelativePath,
|
||||
Path = Path.Combine(series.Path, model.RelativePath),
|
||||
Size = model.Size,
|
||||
DateAdded = model.DateAdded,
|
||||
SceneName = model.SceneName,
|
||||
Quality = model.Quality,
|
||||
Language = model.Language,
|
||||
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality),
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
OriginalFilePath = model.OriginalFilePath
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace NzbDrone.Api.EpisodeFiles
|
||||
{
|
||||
public class MediaInfoResource : RestResource
|
||||
{
|
||||
public decimal AudioChannels { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
}
|
||||
|
||||
public static class MediaInfoResourceMapper
|
||||
{
|
||||
public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MediaInfoResource
|
||||
{
|
||||
AudioChannels = MediaInfoFormatter.FormatAudioChannels(model),
|
||||
AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName),
|
||||
VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Api.EpisodeFiles;
|
||||
using Sonarr.Http.REST;
|
||||
using NzbDrone.Api.Series;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Api.Episodes
|
||||
{
|
||||
public class EpisodeResource : RestResource
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public int EpisodeFileId { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
public int EpisodeNumber { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string AirDate { get; set; }
|
||||
public DateTime? AirDateUtc { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public EpisodeFileResource EpisodeFile { get; set; }
|
||||
|
||||
public bool HasFile { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public int? AbsoluteEpisodeNumber { get; set; }
|
||||
public int? SceneAbsoluteEpisodeNumber { get; set; }
|
||||
public int? SceneEpisodeNumber { get; set; }
|
||||
public int? SceneSeasonNumber { get; set; }
|
||||
public bool UnverifiedSceneNumbering { get; set; }
|
||||
public string SeriesTitle { get; set; }
|
||||
public SeriesResource Series { get; set; }
|
||||
public DateTime? LastSearchTime { get; set; }
|
||||
|
||||
//Hiding this so people don't think its usable (only used to set the initial state)
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool Grabbed { get; set; }
|
||||
}
|
||||
|
||||
public static class EpisodeResourceMapper
|
||||
{
|
||||
public static EpisodeResource ToResource(this Episode model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new EpisodeResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
SeriesId = model.SeriesId,
|
||||
EpisodeFileId = model.EpisodeFileId,
|
||||
SeasonNumber = model.SeasonNumber,
|
||||
EpisodeNumber = model.EpisodeNumber,
|
||||
Title = model.Title,
|
||||
AirDate = model.AirDate,
|
||||
AirDateUtc = model.AirDateUtc,
|
||||
Overview = model.Overview,
|
||||
//EpisodeFile
|
||||
|
||||
HasFile = model.HasFile,
|
||||
Monitored = model.Monitored,
|
||||
AbsoluteEpisodeNumber = model.AbsoluteEpisodeNumber,
|
||||
SceneAbsoluteEpisodeNumber = model.SceneAbsoluteEpisodeNumber,
|
||||
SceneEpisodeNumber = model.SceneEpisodeNumber,
|
||||
SceneSeasonNumber = model.SceneSeasonNumber,
|
||||
UnverifiedSceneNumbering = model.UnverifiedSceneNumbering,
|
||||
SeriesTitle = model.SeriesTitle,
|
||||
//Series = model.Series.MapToResource(),
|
||||
LastSearchTime = model.LastSearchTime
|
||||
};
|
||||
}
|
||||
|
||||
public static List<EpisodeResource> ToResource(this IEnumerable<Episode> models)
|
||||
{
|
||||
if (models == null) return null;
|
||||
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Sonarr.Http.Extensions;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
|
||||
namespace NzbDrone.Api.FileSystem
|
||||
{
|
||||
public class FileSystemModule : NzbDroneApiModule
|
||||
{
|
||||
private readonly IFileSystemLookupService _fileSystemLookupService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IDiskScanService _diskScanService;
|
||||
|
||||
public FileSystemModule(IFileSystemLookupService fileSystemLookupService,
|
||||
IDiskProvider diskProvider,
|
||||
IDiskScanService diskScanService)
|
||||
: base("/filesystem")
|
||||
{
|
||||
_fileSystemLookupService = fileSystemLookupService;
|
||||
_diskProvider = diskProvider;
|
||||
_diskScanService = diskScanService;
|
||||
Get("/", x => GetContents());
|
||||
Get("/type", x => GetEntityType());
|
||||
Get("/mediafiles", x => GetMediaFiles());
|
||||
}
|
||||
|
||||
private object GetContents()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var includeFiles = Request.GetBooleanQueryParameter("includeFiles");
|
||||
var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes");
|
||||
|
||||
return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes);
|
||||
}
|
||||
|
||||
private object GetEntityType()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var path = (string)pathQuery.Value;
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
{
|
||||
return new { type = "file" };
|
||||
}
|
||||
|
||||
//Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system
|
||||
return new { type = "folder" };
|
||||
}
|
||||
|
||||
private object GetMediaFiles()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var path = (string)pathQuery.Value;
|
||||
|
||||
if (!_diskProvider.FolderExists(path))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
return _diskScanService.GetVideoFiles(path).Select(f => new {
|
||||
Path = f,
|
||||
RelativePath = path.GetRelativePath(f),
|
||||
Name = Path.GetFileName(f)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Api.Episodes;
|
||||
using NzbDrone.Api.Series;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.History;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace NzbDrone.Api.History
|
||||
{
|
||||
public class HistoryModule : SonarrRestModule<HistoryResource>
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IUpgradableSpecification _upgradableSpecification;
|
||||
private readonly IFailedDownloadService _failedDownloadService;
|
||||
|
||||
public HistoryModule(IHistoryService historyService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IFailedDownloadService failedDownloadService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_failedDownloadService = failedDownloadService;
|
||||
GetResourcePaged = GetHistory;
|
||||
|
||||
Get("/since", x => GetHistorySince());
|
||||
Post("/failed", x => MarkAsFailed());
|
||||
}
|
||||
|
||||
protected HistoryResource MapToResource(EpisodeHistory model)
|
||||
{
|
||||
var resource = model.ToResource();
|
||||
|
||||
resource.Series = model.Series.ToResource();
|
||||
resource.Episode = model.Episode.ToResource();
|
||||
|
||||
if (model.Series != null)
|
||||
{
|
||||
resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Series.QualityProfile.Value, model.Quality);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private PagingResource<HistoryResource> GetHistory(PagingResource<HistoryResource> pagingResource)
|
||||
{
|
||||
var episodeId = Request.Query.EpisodeId;
|
||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EpisodeHistory>("date", SortDirection.Descending);
|
||||
var filter = pagingResource.Filters.FirstOrDefault();
|
||||
|
||||
if (filter != null && filter.Key == "eventType")
|
||||
{
|
||||
var filterValue = (EpisodeHistoryEventType)Convert.ToInt32(filter.Value);
|
||||
pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue);
|
||||
}
|
||||
|
||||
if (episodeId.HasValue)
|
||||
{
|
||||
int i = (int)episodeId;
|
||||
pagingSpec.FilterExpressions.Add(h => h.EpisodeId == i);
|
||||
}
|
||||
|
||||
return ApplyToPage(_historyService.Paged, pagingSpec, MapToResource);
|
||||
}
|
||||
|
||||
private List<HistoryResource> GetHistorySince()
|
||||
{
|
||||
var queryDate = Request.Query.Date;
|
||||
var queryEventType = Request.Query.EventType;
|
||||
|
||||
if (!queryDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("date is missing");
|
||||
}
|
||||
|
||||
DateTime date = DateTime.Parse(queryDate.Value);
|
||||
EpisodeHistoryEventType? eventType = null;
|
||||
|
||||
if (queryEventType.HasValue)
|
||||
{
|
||||
eventType = (EpisodeHistoryEventType)Convert.ToInt32(queryEventType.Value);
|
||||
}
|
||||
|
||||
return _historyService.Since(date, eventType).Select(MapToResource).ToList();
|
||||
}
|
||||
|
||||
private object MarkAsFailed()
|
||||
{
|
||||
var id = (int)Request.Form.Id;
|
||||
_failedDownloadService.MarkAsFailed(id);
|
||||
return new object();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Api.Indexers
|
||||
{
|
||||
public class IndexerModule : ProviderModuleBase<IndexerResource, IIndexer, IndexerDefinition>
|
||||
{
|
||||
public IndexerModule(IndexerFactory indexerFactory)
|
||||
: base(indexerFactory, "indexer")
|
||||
{
|
||||
}
|
||||
|
||||
protected override void MapToResource(IndexerResource resource, IndexerDefinition definition)
|
||||
{
|
||||
base.MapToResource(resource, definition);
|
||||
|
||||
resource.EnableRss = definition.EnableRss;
|
||||
resource.EnableSearch = definition.EnableAutomaticSearch || definition.EnableInteractiveSearch;
|
||||
resource.SupportsRss = definition.SupportsRss;
|
||||
resource.SupportsSearch = definition.SupportsSearch;
|
||||
resource.Protocol = definition.Protocol;
|
||||
resource.Priority = definition.Priority;
|
||||
}
|
||||
|
||||
protected override void MapToModel(IndexerDefinition definition, IndexerResource resource)
|
||||
{
|
||||
base.MapToModel(definition, resource);
|
||||
|
||||
definition.EnableRss = resource.EnableRss;
|
||||
definition.EnableAutomaticSearch = resource.EnableSearch;
|
||||
definition.EnableInteractiveSearch = resource.EnableSearch;
|
||||
definition.Priority = resource.Priority;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Api.Indexers
|
||||
{
|
||||
public class IndexerResource : ProviderResource
|
||||
{
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableSearch { get; set; }
|
||||
public bool SupportsRss { get; set; }
|
||||
public bool SupportsSearch { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
using NLog;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Nancy.ModelBinding;
|
||||
using NzbDrone.Common.Cache;
|
||||
using HttpStatusCode = System.Net.HttpStatusCode;
|
||||
|
||||
namespace NzbDrone.Api.Indexers
|
||||
{
|
||||
public class ReleaseModule : ReleaseModuleBase
|
||||
{
|
||||
private readonly IFetchAndParseRss _rssFetcherAndParser;
|
||||
private readonly ISearchForReleases _releaseSearchService;
|
||||
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
||||
private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision;
|
||||
private readonly IDownloadService _downloadService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<RemoteEpisode> _remoteEpisodeCache;
|
||||
|
||||
public ReleaseModule(IFetchAndParseRss rssFetcherAndParser,
|
||||
ISearchForReleases releaseSearchService,
|
||||
IMakeDownloadDecision downloadDecisionMaker,
|
||||
IPrioritizeDownloadDecision prioritizeDownloadDecision,
|
||||
IDownloadService downloadService,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_rssFetcherAndParser = rssFetcherAndParser;
|
||||
_releaseSearchService = releaseSearchService;
|
||||
_downloadDecisionMaker = downloadDecisionMaker;
|
||||
_prioritizeDownloadDecision = prioritizeDownloadDecision;
|
||||
_downloadService = downloadService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceAll = GetReleases;
|
||||
Post("/", x => DownloadRelease(this.Bind<ReleaseResource>()));
|
||||
|
||||
PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true);
|
||||
PostValidator.RuleFor(s => s.Guid).NotEmpty();
|
||||
|
||||
_remoteEpisodeCache = cacheManager.GetCache<RemoteEpisode>(GetType(), "remoteEpisodes");
|
||||
}
|
||||
|
||||
private object DownloadRelease(ReleaseResource release)
|
||||
{
|
||||
var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release));
|
||||
|
||||
if (remoteEpisode == null)
|
||||
{
|
||||
_logger.Debug("Couldn't find requested release in cache, cache timeout probably expired.");
|
||||
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_downloadService.DownloadReport(remoteEpisode);
|
||||
}
|
||||
catch (ReleaseDownloadException ex)
|
||||
{
|
||||
_logger.Error(ex, ex.Message);
|
||||
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed");
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetReleases()
|
||||
{
|
||||
if (Request.Query.episodeId != null)
|
||||
{
|
||||
return GetEpisodeReleases(Request.Query.episodeId);
|
||||
}
|
||||
|
||||
return GetRss();
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetEpisodeReleases(int episodeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decisions = _releaseSearchService.EpisodeSearch(episodeId, true, true);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Episode search failed");
|
||||
}
|
||||
|
||||
return new List<ReleaseResource>();
|
||||
}
|
||||
|
||||
private List<ReleaseResource> GetRss()
|
||||
{
|
||||
var reports = _rssFetcherAndParser.Fetch();
|
||||
var decisions = _downloadDecisionMaker.GetRssDecision(reports);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
}
|
||||
|
||||
protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight)
|
||||
{
|
||||
var resource = base.MapDecision(decision, initialWeight);
|
||||
_remoteEpisodeCache.Set(GetCacheKey(resource), decision.RemoteEpisode, TimeSpan.FromMinutes(30));
|
||||
return resource;
|
||||
}
|
||||
|
||||
private string GetCacheKey(ReleaseResource resource)
|
||||
{
|
||||
return string.Concat(resource.IndexerId, "_", resource.Guid);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace NzbDrone.Api.Logs
|
||||
{
|
||||
public class LogModule : SonarrRestModule<LogResource>
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
|
||||
public LogModule(ILogService logService)
|
||||
{
|
||||
_logService = logService;
|
||||
GetResourcePaged = GetLogs;
|
||||
}
|
||||
|
||||
private PagingResource<LogResource> GetLogs(PagingResource<LogResource> pagingResource)
|
||||
{
|
||||
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>();
|
||||
|
||||
if (pageSpec.SortKey == "time")
|
||||
{
|
||||
pageSpec.SortKey = "id";
|
||||
}
|
||||
|
||||
var filter = pagingResource.Filters.FirstOrDefault();
|
||||
|
||||
if (filter != null && filter.Key == "level")
|
||||
{
|
||||
switch (filter.Value)
|
||||
{
|
||||
case "Fatal":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal");
|
||||
break;
|
||||
case "Error":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error");
|
||||
break;
|
||||
case "Warn":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn");
|
||||
break;
|
||||
case "Info":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info");
|
||||
break;
|
||||
case "Debug":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug");
|
||||
break;
|
||||
case "Trace":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.Extensions;
|
||||
|
||||
namespace NzbDrone.Api.ManualImport
|
||||
{
|
||||
public class ManualImportModule : SonarrRestModule<ManualImportResource>
|
||||
{
|
||||
private readonly IManualImportService _manualImportService;
|
||||
|
||||
public ManualImportModule(IManualImportService manualImportService)
|
||||
: base("/manualimport")
|
||||
{
|
||||
_manualImportService = manualImportService;
|
||||
|
||||
GetResourceAll = GetMediaFiles;
|
||||
}
|
||||
|
||||
private List<ManualImportResource> GetMediaFiles()
|
||||
{
|
||||
var folderQuery = Request.Query.folder;
|
||||
var folder = (string)folderQuery.Value;
|
||||
|
||||
var downloadIdQuery = Request.Query.downloadId;
|
||||
var downloadId = (string)downloadIdQuery.Value;
|
||||
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
|
||||
|
||||
return _manualImportService.GetMediaFiles(folder, downloadId, null, filterExistingFiles)
|
||||
.ToResource()
|
||||
.Select(AddQualityWeight).ToList();
|
||||
}
|
||||
|
||||
private ManualImportResource AddQualityWeight(ManualImportResource item)
|
||||
{
|
||||
if (item.Quality != null)
|
||||
{
|
||||
item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight;
|
||||
item.QualityWeight += item.Quality.Revision.Real * 10;
|
||||
item.QualityWeight += item.Quality.Revision.Version;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace NzbDrone.Api.Profiles
|
||||
{
|
||||
public class ProfileModule : SonarrRestModule<ProfileResource>
|
||||
{
|
||||
private readonly IQualityProfileService _qualityProfileService;
|
||||
|
||||
public ProfileModule(IQualityProfileService qualityProfileService)
|
||||
{
|
||||
_qualityProfileService = qualityProfileService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Cutoff).NotNull();
|
||||
SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetById;
|
||||
UpdateResource = Update;
|
||||
CreateResource = Create;
|
||||
DeleteResource = DeleteProfile;
|
||||
}
|
||||
|
||||
private int Create(ProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
return _qualityProfileService.Add(model).Id;
|
||||
}
|
||||
|
||||
private void DeleteProfile(int id)
|
||||
{
|
||||
_qualityProfileService.Delete(id);
|
||||
}
|
||||
|
||||
private void Update(ProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_qualityProfileService.Update(model);
|
||||
}
|
||||
|
||||
private ProfileResource GetById(int id)
|
||||
{
|
||||
return _qualityProfileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<ProfileResource> GetAll()
|
||||
{
|
||||
return _qualityProfileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Sonarr.Http.REST;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Api.Profiles
|
||||
{
|
||||
public class ProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public Quality Cutoff { get; set; }
|
||||
public List<ProfileQualityItemResource> Items { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileQualityItemResource : RestResource
|
||||
{
|
||||
public Quality Quality { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
{
|
||||
public static ProfileResource ToResource(this QualityProfile model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
var cutoffItem = model.Items.First(q =>
|
||||
{
|
||||
if (q.Id == model.Cutoff) return true;
|
||||
|
||||
if (q.Quality == null) return false;
|
||||
|
||||
return q.Quality.Id == model.Cutoff;
|
||||
});
|
||||
|
||||
var cutoff = cutoffItem.Items == null || cutoffItem.Items.Empty()
|
||||
? cutoffItem.Quality
|
||||
: cutoffItem.Items.First().Quality;
|
||||
|
||||
return new ProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Name = model.Name,
|
||||
UpgradeAllowed = model.UpgradeAllowed,
|
||||
Cutoff = cutoff,
|
||||
|
||||
// Flatten groups so things don't explode
|
||||
Items = model.Items.SelectMany(i =>
|
||||
{
|
||||
if (i == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (i.Items.Any())
|
||||
{
|
||||
return i.Items.ConvertAll(ToResource);
|
||||
}
|
||||
|
||||
return new List<ProfileQualityItemResource> {ToResource(i)};
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileQualityItemResource ToResource(this QualityProfileQualityItem model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new ProfileQualityItemResource
|
||||
{
|
||||
Quality = model.Quality,
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfile ToModel(this ProfileResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new QualityProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Name = resource.Name,
|
||||
UpgradeAllowed = resource.UpgradeAllowed,
|
||||
Cutoff = resource.Cutoff.Id,
|
||||
Items = resource.Items.ConvertAll(ToModel)
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfileQualityItem ToModel(this ProfileQualityItemResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new QualityProfileQualityItem
|
||||
{
|
||||
Quality = (Quality)resource.Quality.Id,
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ProfileResource> ToResource(this IEnumerable<QualityProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.Reflection;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.ClientSchema;
|
||||
|
||||
namespace NzbDrone.Api
|
||||
{
|
||||
public abstract class ProviderModuleBase<TProviderResource, TProvider, TProviderDefinition> : SonarrRestModule<TProviderResource>
|
||||
where TProviderDefinition : ProviderDefinition, new()
|
||||
where TProvider : IProvider
|
||||
where TProviderResource : ProviderResource, new()
|
||||
{
|
||||
private readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory;
|
||||
|
||||
protected ProviderModuleBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource)
|
||||
: base(resource)
|
||||
{
|
||||
_providerFactory = providerFactory;
|
||||
|
||||
Get("schema", x => GetTemplates());
|
||||
Post("test", x => Test(ReadResourceFromRequest(true)));
|
||||
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true)));
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetProviderById;
|
||||
CreateResource = CreateProvider;
|
||||
UpdateResource = UpdateProvider;
|
||||
DeleteResource = DeleteProvider;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique");
|
||||
SharedValidator.RuleFor(c => c.Implementation).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty();
|
||||
|
||||
PostValidator.RuleFor(c => c.Fields).NotNull();
|
||||
}
|
||||
|
||||
private TProviderResource GetProviderById(int id)
|
||||
{
|
||||
var definition = _providerFactory.Get(id);
|
||||
_providerFactory.SetProviderCharacteristics(definition);
|
||||
|
||||
var resource = new TProviderResource();
|
||||
MapToResource(resource, definition);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private List<TProviderResource> GetAll()
|
||||
{
|
||||
var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName);
|
||||
|
||||
var result = new List<TProviderResource>(providerDefinitions.Count());
|
||||
|
||||
foreach (var definition in providerDefinitions)
|
||||
{
|
||||
_providerFactory.SetProviderCharacteristics(definition);
|
||||
|
||||
var providerResource = new TProviderResource();
|
||||
MapToResource(providerResource, definition);
|
||||
|
||||
result.Add(providerResource);
|
||||
}
|
||||
|
||||
return result.OrderBy(p => p.Name).ToList();
|
||||
}
|
||||
|
||||
private int CreateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true, false, false);
|
||||
|
||||
if (providerDefinition.Enable)
|
||||
{
|
||||
Test(providerDefinition, false);
|
||||
}
|
||||
|
||||
providerDefinition = _providerFactory.Create(providerDefinition);
|
||||
|
||||
return providerDefinition.Id;
|
||||
}
|
||||
|
||||
private void UpdateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true, false, false);
|
||||
|
||||
_providerFactory.Update(providerDefinition);
|
||||
}
|
||||
|
||||
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate)
|
||||
{
|
||||
var definition = new TProviderDefinition();
|
||||
|
||||
MapToModel(definition, providerResource);
|
||||
|
||||
if (validate && (definition.Enable || forceValidate))
|
||||
{
|
||||
Validate(definition, includeWarnings);
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
protected virtual void MapToResource(TProviderResource resource, TProviderDefinition definition)
|
||||
{
|
||||
resource.Id = definition.Id;
|
||||
|
||||
resource.Name = definition.Name;
|
||||
resource.ImplementationName = definition.ImplementationName;
|
||||
resource.Implementation = definition.Implementation;
|
||||
resource.ConfigContract = definition.ConfigContract;
|
||||
resource.Message = definition.Message;
|
||||
|
||||
resource.Fields = SchemaBuilder.ToSchema(definition.Settings);
|
||||
|
||||
resource.InfoLink = $"https://wiki.servarr.com/sonarr/supported#{definition.Implementation.ToLower()}";
|
||||
}
|
||||
|
||||
protected virtual void MapToModel(TProviderDefinition definition, TProviderResource resource)
|
||||
{
|
||||
definition.Id = resource.Id;
|
||||
|
||||
definition.Name = resource.Name;
|
||||
definition.ImplementationName = resource.ImplementationName;
|
||||
definition.Implementation = resource.Implementation;
|
||||
definition.ConfigContract = resource.ConfigContract;
|
||||
definition.Message = resource.Message;
|
||||
|
||||
var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract);
|
||||
definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract);
|
||||
}
|
||||
|
||||
private void DeleteProvider(int id)
|
||||
{
|
||||
_providerFactory.Delete(id);
|
||||
}
|
||||
|
||||
private object GetTemplates()
|
||||
{
|
||||
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
|
||||
|
||||
var result = new List<TProviderResource>(defaultDefinitions.Count());
|
||||
|
||||
foreach (var providerDefinition in defaultDefinitions)
|
||||
{
|
||||
var providerResource = new TProviderResource();
|
||||
MapToResource(providerResource, providerDefinition);
|
||||
|
||||
var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition);
|
||||
|
||||
providerResource.Presets = presetDefinitions.Select(v =>
|
||||
{
|
||||
var presetResource = new TProviderResource();
|
||||
MapToResource(presetResource, v);
|
||||
|
||||
return presetResource as ProviderResource;
|
||||
}).ToList();
|
||||
|
||||
result.Add(providerResource);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private object Test(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true, true, true);
|
||||
|
||||
Test(providerDefinition, true);
|
||||
|
||||
return "{}";
|
||||
}
|
||||
|
||||
private object RequestAction(string action, TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false, false, false);
|
||||
|
||||
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
|
||||
|
||||
var data = _providerFactory.RequestAction(providerDefinition, action, query);
|
||||
Response resp = data.ToJson();
|
||||
resp.ContentType = "application/json";
|
||||
return resp;
|
||||
}
|
||||
|
||||
private void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = definition.Settings.Validate();
|
||||
|
||||
VerifyValidationResult(validationResult, includeWarnings);
|
||||
}
|
||||
|
||||
protected virtual void Test(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = _providerFactory.Test(definition);
|
||||
|
||||
VerifyValidationResult(validationResult, includeWarnings);
|
||||
}
|
||||
|
||||
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings)
|
||||
{
|
||||
var result = validationResult as NzbDroneValidationResult;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
result = new NzbDroneValidationResult(validationResult.Errors);
|
||||
}
|
||||
|
||||
if (includeWarnings && (!result.IsValid || result.HasWarnings))
|
||||
{
|
||||
throw new ValidationException(result.Failures);
|
||||
}
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
throw new ValidationException(result.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Queue;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace NzbDrone.Api.Queue
|
||||
{
|
||||
public class QueueModule : SonarrRestModuleWithSignalR<QueueResource, Core.Queue.Queue>,
|
||||
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
|
||||
{
|
||||
private readonly IQueueService _queueService;
|
||||
private readonly IPendingReleaseService _pendingReleaseService;
|
||||
|
||||
public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
|
||||
: base(broadcastSignalRMessage)
|
||||
{
|
||||
_queueService = queueService;
|
||||
_pendingReleaseService = pendingReleaseService;
|
||||
GetResourceAll = GetQueue;
|
||||
}
|
||||
|
||||
private List<QueueResource> GetQueue()
|
||||
{
|
||||
return GetQueueItems().ToResource();
|
||||
}
|
||||
|
||||
private IEnumerable<Core.Queue.Queue> GetQueueItems()
|
||||
{
|
||||
var queue = _queueService.GetQueue().Where(q => q.Series != null);
|
||||
var pending = _pendingReleaseService.GetPendingQueue();
|
||||
|
||||
return queue.Concat(pending);
|
||||
}
|
||||
|
||||
public void Handle(QueueUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
|
||||
public void Handle(PendingReleasesUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace NzbDrone.Api.Restrictions
|
||||
{
|
||||
public class RestrictionResource : RestResource
|
||||
{
|
||||
public string Required { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public RestrictionResource()
|
||||
{
|
||||
Tags = new HashSet<int>();
|
||||
}
|
||||
}
|
||||
|
||||
public static class RestrictionResourceMapper
|
||||
{
|
||||
public static RestrictionResource ToResource(this ReleaseProfile model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new RestrictionResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Required = string.Join(",", model.Required),
|
||||
Ignored = string.Join(",", model.Ignored),
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static ReleaseProfile ToModel(this RestrictionResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new ReleaseProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Required = resource.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
Ignored = resource.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<RestrictionResource> ToResource(this IEnumerable<ReleaseProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.Mapping;
|
||||
|
||||
namespace NzbDrone.Api.RootFolders
|
||||
{
|
||||
public class RootFolderModule : SonarrRestModuleWithSignalR<RootFolderResource, RootFolder>
|
||||
{
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
|
||||
public RootFolderModule(IRootFolderService rootFolderService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
RootFolderValidator rootFolderValidator,
|
||||
PathExistsValidator pathExistsValidator,
|
||||
MappedNetworkDriveValidator mappedNetworkDriveValidator,
|
||||
StartupFolderValidator startupFolderValidator,
|
||||
SystemFolderValidator systemFolderValidator,
|
||||
FolderWritableValidator folderWritableValidator
|
||||
)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_rootFolderService = rootFolderService;
|
||||
|
||||
GetResourceAll = GetRootFolders;
|
||||
GetResourceById = GetRootFolder;
|
||||
CreateResource = CreateRootFolder;
|
||||
DeleteResource = DeleteFolder;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Path)
|
||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(startupFolderValidator)
|
||||
.SetValidator(pathExistsValidator)
|
||||
.SetValidator(systemFolderValidator)
|
||||
.SetValidator(folderWritableValidator);
|
||||
}
|
||||
|
||||
private RootFolderResource GetRootFolder(int id)
|
||||
{
|
||||
return _rootFolderService.Get(id, true).ToResource();
|
||||
}
|
||||
|
||||
private int CreateRootFolder(RootFolderResource rootFolderResource)
|
||||
{
|
||||
var model = rootFolderResource.ToModel();
|
||||
|
||||
return _rootFolderService.Add(model).Id;
|
||||
}
|
||||
|
||||
private List<RootFolderResource> GetRootFolders()
|
||||
{
|
||||
return _rootFolderService.AllWithUnmappedFolders().ToResource();
|
||||
}
|
||||
|
||||
private void DeleteFolder(int id)
|
||||
{
|
||||
_rootFolderService.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Sonarr.Http.REST;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
|
||||
namespace NzbDrone.Api.RootFolders
|
||||
{
|
||||
public class RootFolderResource : RestResource
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public long? FreeSpace { get; set; }
|
||||
public long? TotalSpace { get; set; }
|
||||
|
||||
public List<UnmappedFolder> UnmappedFolders { get; set; }
|
||||
}
|
||||
|
||||
public static class RootFolderResourceMapper
|
||||
{
|
||||
public static RootFolderResource ToResource(this RootFolder model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new RootFolderResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Path = model.Path,
|
||||
FreeSpace = model.FreeSpace,
|
||||
TotalSpace = model.TotalSpace,
|
||||
UnmappedFolders = model.UnmappedFolders
|
||||
};
|
||||
}
|
||||
|
||||
public static RootFolder ToModel(this RootFolderResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new RootFolder
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Path = resource.Path,
|
||||
//FreeSpace
|
||||
//UnmappedFolders
|
||||
};
|
||||
}
|
||||
|
||||
public static List<RootFolderResource> ToResource(this IEnumerable<RootFolder> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Tv;
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeasonResource
|
||||
{
|
||||
public int SeasonNumber { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public SeasonStatisticsResource Statistics { get; set; }
|
||||
public List<MediaCover> Images { get; set; }
|
||||
}
|
||||
|
||||
public static class SeasonResourceMapper
|
||||
{
|
||||
public static SeasonResource ToResource(this Season model, bool includeImages = false)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new SeasonResource
|
||||
{
|
||||
SeasonNumber = model.SeasonNumber,
|
||||
Monitored = model.Monitored,
|
||||
Images = includeImages ? model.Images : null
|
||||
};
|
||||
}
|
||||
|
||||
public static Season ToModel(this SeasonResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new Season
|
||||
{
|
||||
SeasonNumber = resource.SeasonNumber,
|
||||
Monitored = resource.Monitored,
|
||||
Images = resource.Images
|
||||
};
|
||||
}
|
||||
|
||||
public static List<SeasonResource> ToResource(this IEnumerable<Season> models, bool includeImages = false)
|
||||
{
|
||||
return models.Select(s => ToResource(s, includeImages)).ToList();
|
||||
}
|
||||
|
||||
public static List<Season> ToModel(this IEnumerable<SeasonResource> resources)
|
||||
{
|
||||
return resources?.Select(ToModel).ToList() ?? new List<Season>();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Profiles.Languages;
|
||||
using Sonarr.Http.Extensions;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeriesEditorModule : NzbDroneApiModule
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly ILanguageProfileService _languageProfileService;
|
||||
|
||||
public SeriesEditorModule(ISeriesService seriesService, ILanguageProfileService languageProfileService)
|
||||
: base("/series/editor")
|
||||
{
|
||||
_seriesService = seriesService;
|
||||
_languageProfileService = languageProfileService;
|
||||
Put("/", series => SaveAll());
|
||||
}
|
||||
|
||||
private object SaveAll()
|
||||
{
|
||||
var resources = Request.Body.FromJson<List<SeriesResource>>();
|
||||
|
||||
var seriesToUpdate = resources.Select(seriesResource =>
|
||||
{
|
||||
var series = _seriesService.GetSeries(seriesResource.Id);
|
||||
var updatedSeries = seriesResource.ToModel(series);
|
||||
|
||||
// If the new language profile doens't exist, keep it the same.
|
||||
// This could happen if a 3rd-party app uses this endpoint to update a
|
||||
// series and doesn't pass the languageProfileI as well.
|
||||
|
||||
if (!_languageProfileService.Exists(updatedSeries.LanguageProfileId))
|
||||
{
|
||||
updatedSeries.LanguageProfileId = series.LanguageProfileId;
|
||||
}
|
||||
|
||||
return updatedSeries;
|
||||
}).ToList();
|
||||
|
||||
return ResponseWithCode(_seriesService.UpdateSeries(seriesToUpdate, true)
|
||||
.ToResource(false)
|
||||
, HttpStatusCode.Accepted);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,268 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.SeriesStats;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.Core.DataAugmentation.Scene;
|
||||
using NzbDrone.Core.Profiles.Languages;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.Extensions;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeriesModule : SonarrRestModuleWithSignalR<SeriesResource, Core.Tv.Series>,
|
||||
IHandle<EpisodeImportedEvent>,
|
||||
IHandle<EpisodeFileDeletedEvent>,
|
||||
IHandle<SeriesUpdatedEvent>,
|
||||
IHandle<SeriesEditedEvent>,
|
||||
IHandle<SeriesDeletedEvent>,
|
||||
IHandle<SeriesRenamedEvent>,
|
||||
IHandle<MediaCoversUpdatedEvent>
|
||||
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IAddSeriesService _addSeriesService;
|
||||
private readonly ISeriesStatisticsService _seriesStatisticsService;
|
||||
private readonly ISceneMappingService _sceneMappingService;
|
||||
private readonly IMapCoversToLocal _coverMapper;
|
||||
private readonly ILanguageProfileService _languageProfileService;
|
||||
|
||||
public SeriesModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
ISeriesService seriesService,
|
||||
IAddSeriesService addSeriesService,
|
||||
ISeriesStatisticsService seriesStatisticsService,
|
||||
ISceneMappingService sceneMappingService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
ILanguageProfileService languageProfileService,
|
||||
RootFolderValidator rootFolderValidator,
|
||||
SeriesPathValidator seriesPathValidator,
|
||||
SeriesExistsValidator seriesExistsValidator,
|
||||
SeriesAncestorValidator seriesAncestorValidator,
|
||||
SystemFolderValidator systemFolderValidator,
|
||||
ProfileExistsValidator profileExistsValidator,
|
||||
LanguageProfileExistsValidator languageProfileExistsValidator
|
||||
)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_seriesService = seriesService;
|
||||
_addSeriesService = addSeriesService;
|
||||
_seriesStatisticsService = seriesStatisticsService;
|
||||
_sceneMappingService = sceneMappingService;
|
||||
|
||||
_coverMapper = coverMapper;
|
||||
_languageProfileService = languageProfileService;
|
||||
|
||||
GetResourceAll = AllSeries;
|
||||
GetResourceById = GetSeries;
|
||||
CreateResource = AddSeries;
|
||||
UpdateResource = UpdateSeries;
|
||||
DeleteResource = DeleteSeries;
|
||||
|
||||
SharedValidator.RuleFor(s => s.ProfileId).ValidId();
|
||||
SharedValidator.RuleFor(s => s.LanguageProfileId);
|
||||
|
||||
SharedValidator.RuleFor(s => s.Path)
|
||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(seriesPathValidator)
|
||||
.SetValidator(seriesAncestorValidator)
|
||||
.SetValidator(systemFolderValidator)
|
||||
.When(s => !s.Path.IsNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
|
||||
|
||||
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator);
|
||||
PostValidator.RuleFor(s => s.LanguageProfileId).SetValidator(languageProfileExistsValidator).When(s => s.LanguageProfileId != 0);
|
||||
|
||||
PutValidator.RuleFor(s => s.Path).IsValidPath();
|
||||
|
||||
// Ensure any editing has a valid LanguageProfile
|
||||
PutValidator.RuleFor(s => s.LanguageProfileId).SetValidator(languageProfileExistsValidator);
|
||||
}
|
||||
|
||||
private SeriesResource GetSeries(int id)
|
||||
{
|
||||
var includeSeasonImages = Context != null && Request.GetBooleanQueryParameter("includeSeasonImages");
|
||||
|
||||
var series = _seriesService.GetSeries(id);
|
||||
return MapToResource(series, includeSeasonImages);
|
||||
}
|
||||
|
||||
private List<SeriesResource> AllSeries()
|
||||
{
|
||||
var includeSeasonImages = Request.GetBooleanQueryParameter("includeSeasonImages");
|
||||
var seriesStats = _seriesStatisticsService.SeriesStatistics();
|
||||
var seriesResources = _seriesService.GetAllSeries().Select(s => s.ToResource(includeSeasonImages)).ToList();
|
||||
|
||||
MapCoversToLocal(seriesResources.ToArray());
|
||||
LinkSeriesStatistics(seriesResources, seriesStats);
|
||||
PopulateAlternateTitles(seriesResources);
|
||||
|
||||
return seriesResources;
|
||||
}
|
||||
|
||||
private int AddSeries(SeriesResource seriesResource)
|
||||
{
|
||||
var model = seriesResource.ToModel();
|
||||
|
||||
// Set a default LanguageProfileId to maintain backwards compatibility with apps using the v2 API
|
||||
if (model.LanguageProfileId == 0 || !_languageProfileService.Exists(model.LanguageProfileId))
|
||||
{
|
||||
model.LanguageProfileId = _languageProfileService.All().First().Id;
|
||||
}
|
||||
|
||||
return _addSeriesService.AddSeries(model).Id;
|
||||
}
|
||||
|
||||
private void UpdateSeries(SeriesResource seriesResource)
|
||||
{
|
||||
var model = seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id));
|
||||
|
||||
_seriesService.UpdateSeries(model);
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, seriesResource);
|
||||
}
|
||||
|
||||
private void DeleteSeries(int id)
|
||||
{
|
||||
var deleteFiles = false;
|
||||
var deleteFilesQuery = Request.Query.deleteFiles;
|
||||
|
||||
if (deleteFilesQuery.HasValue)
|
||||
{
|
||||
deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value);
|
||||
}
|
||||
|
||||
_seriesService.DeleteSeries(id, deleteFiles, false);
|
||||
}
|
||||
|
||||
private SeriesResource MapToResource(Core.Tv.Series series, bool includeSeasonImages)
|
||||
{
|
||||
if (series == null) return null;
|
||||
|
||||
var resource = series.ToResource(includeSeasonImages);
|
||||
MapCoversToLocal(resource);
|
||||
FetchAndLinkSeriesStatistics(resource);
|
||||
PopulateAlternateTitles(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private void MapCoversToLocal(params SeriesResource[] series)
|
||||
{
|
||||
foreach (var seriesResource in series)
|
||||
{
|
||||
_coverMapper.ConvertToLocalUrls(seriesResource.Id, seriesResource.Images);
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchAndLinkSeriesStatistics(SeriesResource resource)
|
||||
{
|
||||
LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id));
|
||||
}
|
||||
|
||||
private void LinkSeriesStatistics(List<SeriesResource> resources, List<SeriesStatistics> seriesStatistics)
|
||||
{
|
||||
var dictSeriesStats = seriesStatistics.ToDictionary(v => v.SeriesId);
|
||||
|
||||
foreach (var series in resources)
|
||||
{
|
||||
var stats = dictSeriesStats.GetValueOrDefault(series.Id);
|
||||
if (stats == null) continue;
|
||||
|
||||
LinkSeriesStatistics(series, stats);
|
||||
}
|
||||
}
|
||||
|
||||
private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seriesStatistics)
|
||||
{
|
||||
resource.TotalEpisodeCount = seriesStatistics.TotalEpisodeCount;
|
||||
resource.EpisodeCount = seriesStatistics.EpisodeCount;
|
||||
resource.EpisodeFileCount = seriesStatistics.EpisodeFileCount;
|
||||
resource.NextAiring = seriesStatistics.NextAiring;
|
||||
resource.PreviousAiring = seriesStatistics.PreviousAiring;
|
||||
resource.SizeOnDisk = seriesStatistics.SizeOnDisk;
|
||||
|
||||
if (seriesStatistics.SeasonStatistics != null)
|
||||
{
|
||||
var dictSeasonStats = seriesStatistics.SeasonStatistics.ToDictionary(v => v.SeasonNumber);
|
||||
|
||||
foreach (var season in resource.Seasons)
|
||||
{
|
||||
season.Statistics = dictSeasonStats.GetValueOrDefault(season.SeasonNumber).ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateAlternateTitles(List<SeriesResource> resources)
|
||||
{
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
PopulateAlternateTitles(resource);
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateAlternateTitles(SeriesResource resource)
|
||||
{
|
||||
var mappings = _sceneMappingService.FindByTvdbId(resource.TvdbId);
|
||||
|
||||
if (mappings == null) return;
|
||||
|
||||
resource.AlternateTitles = mappings.ConvertAll(AlternateTitleResourceMapper.ToResource);
|
||||
}
|
||||
|
||||
public void Handle(EpisodeImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.SeriesId);
|
||||
}
|
||||
|
||||
public void Handle(EpisodeFileDeletedEvent message)
|
||||
{
|
||||
if (message.Reason == DeleteMediaFileReason.Upgrade) return;
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.SeriesId);
|
||||
}
|
||||
|
||||
public void Handle(SeriesUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Series.Id);
|
||||
}
|
||||
|
||||
public void Handle(SeriesEditedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Series.Id);
|
||||
}
|
||||
|
||||
public void Handle(SeriesDeletedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Deleted, message.Series.ToResource());
|
||||
}
|
||||
|
||||
public void Handle(SeriesRenamedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Series.Id);
|
||||
}
|
||||
|
||||
public void Handle(MediaCoversUpdatedEvent message)
|
||||
{
|
||||
if (message.Updated)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Series.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sonarr.Http.REST;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeriesResource : RestResource
|
||||
{
|
||||
public SeriesResource()
|
||||
{
|
||||
Monitored = true;
|
||||
}
|
||||
|
||||
//Todo: Sorters should be done completely on the client
|
||||
//Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
|
||||
//Todo: We should get the entire QualityProfile instead of ID and Name separately
|
||||
|
||||
//View Only
|
||||
public string Title { get; set; }
|
||||
public List<AlternateTitleResource> AlternateTitles { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
|
||||
public int SeasonCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Seasons == null) return 0;
|
||||
|
||||
return Seasons.Where(s => s.SeasonNumber > 0).Count();
|
||||
}
|
||||
}
|
||||
|
||||
public int? TotalEpisodeCount { get; set; }
|
||||
public int? EpisodeCount { get; set; }
|
||||
public int? EpisodeFileCount { get; set; }
|
||||
public long? SizeOnDisk { get; set; }
|
||||
public SeriesStatusType Status { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public DateTime? NextAiring { get; set; }
|
||||
public DateTime? PreviousAiring { get; set; }
|
||||
public string Network { get; set; }
|
||||
public string AirTime { get; set; }
|
||||
public List<MediaCover> Images { get; set; }
|
||||
|
||||
public string RemotePoster { get; set; }
|
||||
public List<SeasonResource> Seasons { get; set; }
|
||||
public int Year { get; set; }
|
||||
|
||||
//View & Edit
|
||||
public string Path { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
public int LanguageProfileId { get; set; }
|
||||
|
||||
//Editing Only
|
||||
public bool SeasonFolder { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
|
||||
public bool UseSceneNumbering { get; set; }
|
||||
public int Runtime { get; set; }
|
||||
public int TvdbId { get; set; }
|
||||
public int TvRageId { get; set; }
|
||||
public int TvMazeId { get; set; }
|
||||
public DateTime? FirstAired { get; set; }
|
||||
public DateTime? LastInfoSync { get; set; }
|
||||
public SeriesTypes SeriesType { get; set; }
|
||||
public string CleanTitle { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public string Certification { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public AddSeriesOptions AddOptions { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
|
||||
//Used to support legacy consumers
|
||||
public int QualityProfileId
|
||||
{
|
||||
get
|
||||
{
|
||||
return ProfileId;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value > 0 && ProfileId == 0)
|
||||
{
|
||||
ProfileId = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SeriesResourceMapper
|
||||
{
|
||||
public static SeriesResource ToResource(this Core.Tv.Series model, bool includeSeasonImages = false)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new SeriesResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Title = model.Title,
|
||||
//AlternateTitles
|
||||
SortTitle = model.SortTitle,
|
||||
|
||||
//TotalEpisodeCount
|
||||
//EpisodeCount
|
||||
//EpisodeFileCount
|
||||
//SizeOnDisk
|
||||
Status = model.Status,
|
||||
Overview = model.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
Network = model.Network,
|
||||
AirTime = model.AirTime,
|
||||
Images = model.Images,
|
||||
|
||||
Seasons = model.Seasons.ToResource(includeSeasonImages),
|
||||
Year = model.Year,
|
||||
|
||||
Path = model.Path,
|
||||
ProfileId = model.QualityProfileId,
|
||||
LanguageProfileId = model.LanguageProfileId,
|
||||
|
||||
SeasonFolder = model.SeasonFolder,
|
||||
Monitored = model.Monitored,
|
||||
|
||||
UseSceneNumbering = model.UseSceneNumbering,
|
||||
Runtime = model.Runtime,
|
||||
TvdbId = model.TvdbId,
|
||||
TvRageId = model.TvRageId,
|
||||
TvMazeId = model.TvMazeId,
|
||||
FirstAired = model.FirstAired,
|
||||
LastInfoSync = model.LastInfoSync,
|
||||
SeriesType = model.SeriesType,
|
||||
CleanTitle = model.CleanTitle,
|
||||
ImdbId = model.ImdbId,
|
||||
TitleSlug = model.TitleSlug,
|
||||
RootFolderPath = model.RootFolderPath,
|
||||
Certification = model.Certification,
|
||||
Genres = model.Genres,
|
||||
Tags = model.Tags,
|
||||
Added = model.Added,
|
||||
AddOptions = model.AddOptions,
|
||||
Ratings = model.Ratings
|
||||
};
|
||||
}
|
||||
|
||||
public static Core.Tv.Series ToModel(this SeriesResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new Core.Tv.Series
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Title = resource.Title,
|
||||
//AlternateTitles
|
||||
SortTitle = resource.SortTitle,
|
||||
|
||||
//TotalEpisodeCount
|
||||
//EpisodeCount
|
||||
//EpisodeFileCount
|
||||
//SizeOnDisk
|
||||
Status = resource.Status,
|
||||
Overview = resource.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
Network = resource.Network,
|
||||
AirTime = resource.AirTime,
|
||||
Images = resource.Images,
|
||||
|
||||
Seasons = resource.Seasons.ToModel(),
|
||||
Year = resource.Year,
|
||||
|
||||
Path = resource.Path,
|
||||
QualityProfileId = resource.ProfileId,
|
||||
LanguageProfileId = resource.LanguageProfileId,
|
||||
|
||||
SeasonFolder = resource.SeasonFolder,
|
||||
Monitored = resource.Monitored,
|
||||
|
||||
UseSceneNumbering = resource.UseSceneNumbering,
|
||||
Runtime = resource.Runtime,
|
||||
TvdbId = resource.TvdbId,
|
||||
TvRageId = resource.TvRageId,
|
||||
TvMazeId = resource.TvMazeId,
|
||||
FirstAired = resource.FirstAired,
|
||||
LastInfoSync = resource.LastInfoSync,
|
||||
SeriesType = resource.SeriesType,
|
||||
CleanTitle = resource.CleanTitle,
|
||||
ImdbId = resource.ImdbId,
|
||||
TitleSlug = resource.TitleSlug,
|
||||
RootFolderPath = resource.RootFolderPath,
|
||||
Certification = resource.Certification,
|
||||
Genres = resource.Genres,
|
||||
Tags = resource.Tags,
|
||||
Added = resource.Added,
|
||||
AddOptions = resource.AddOptions,
|
||||
Ratings = resource.Ratings
|
||||
};
|
||||
}
|
||||
|
||||
public static Core.Tv.Series ToModel(this SeriesResource resource, Core.Tv.Series series)
|
||||
{
|
||||
var updatedSeries = resource.ToModel();
|
||||
|
||||
series.ApplyChanges(updatedSeries);
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
public static List<SeriesResource> ToResource(this IEnumerable<Core.Tv.Series> series, bool includeSeasonImages)
|
||||
{
|
||||
return series.Select(s => ToResource(s, includeSeasonImages)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue