mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Merge branch 'develop'
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Annotations
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class FieldDefinitionAttribute : Attribute
|
||||
{
|
||||
public FieldDefinitionAttribute(int order)
|
||||
{
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public int Order { get; private set; }
|
||||
public string Label { get; set; }
|
||||
public string HelpText { get; set; }
|
||||
public string HelpLink { get; set; }
|
||||
public FieldType Type { get; set; }
|
||||
public Type SelectOptions { get; set; }
|
||||
}
|
||||
|
||||
public enum FieldType
|
||||
{
|
||||
Textbox,
|
||||
Password,
|
||||
Checkbox,
|
||||
Select
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0"?>
|
||||
<configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
|
||||
@@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public class Config : ModelBase
|
||||
{
|
||||
private string _key;
|
||||
|
||||
public string Key
|
||||
{
|
||||
get { return _key; }
|
||||
set { _key = value.ToLowerInvariant(); }
|
||||
}
|
||||
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>
|
||||
{
|
||||
Dictionary<string, object> GetConfigDictionary();
|
||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||
|
||||
int Port { get; }
|
||||
int SslPort { get; }
|
||||
bool EnableSsl { get; }
|
||||
bool LaunchBrowser { get; }
|
||||
bool AuthenticationEnabled { get; }
|
||||
string Username { get; }
|
||||
string Password { get; }
|
||||
string LogLevel { get; }
|
||||
string Branch { get; }
|
||||
string ApiKey { get; }
|
||||
bool Torrent { get; }
|
||||
string SslCertHash { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
{
|
||||
public const string CONFIG_ELEMENT_NAME = "Config";
|
||||
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ICached<string> _cache;
|
||||
|
||||
private readonly string _configFile;
|
||||
|
||||
public ConfigFileProvider(IAppFolderInfo appFolderInfo, ICacheManger cacheManger, IEventAggregator eventAggregator)
|
||||
{
|
||||
_cache = cacheManger.GetCache<string>(GetType());
|
||||
_eventAggregator = eventAggregator;
|
||||
_configFile = appFolderInfo.GetConfigPath();
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfigDictionary()
|
||||
{
|
||||
var dict = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
var type = GetType();
|
||||
var properties = type.GetProperties();
|
||||
|
||||
foreach (var propertyInfo in properties)
|
||||
{
|
||||
var value = propertyInfo.GetValue(this, null);
|
||||
|
||||
dict.Add(propertyInfo.Name, value);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
public void SaveConfigDictionary(Dictionary<string, object> configValues)
|
||||
{
|
||||
_cache.Clear();
|
||||
|
||||
var allWithDefaults = GetConfigDictionary();
|
||||
|
||||
foreach (var configValue in configValues)
|
||||
{
|
||||
object currentValue;
|
||||
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
|
||||
if (currentValue == null) continue;
|
||||
|
||||
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
|
||||
|
||||
if (!equal)
|
||||
{
|
||||
SetValue(configValue.Key.FirstCharToUpper(), configValue.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new ConfigFileSavedEvent());
|
||||
}
|
||||
|
||||
public int Port
|
||||
{
|
||||
get { return GetValueInt("Port", 8989); }
|
||||
}
|
||||
|
||||
public int SslPort
|
||||
{
|
||||
get { return GetValueInt("SslPort", 9898); }
|
||||
}
|
||||
|
||||
public bool EnableSsl
|
||||
{
|
||||
get { return GetValueBoolean("EnableSsl", false); }
|
||||
}
|
||||
|
||||
public bool LaunchBrowser
|
||||
{
|
||||
get { return GetValueBoolean("LaunchBrowser", true); }
|
||||
}
|
||||
|
||||
public string ApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetValue("ApiKey", Guid.NewGuid().ToString().Replace("-", ""));
|
||||
}
|
||||
}
|
||||
|
||||
public bool Torrent
|
||||
{
|
||||
get { return GetValueBoolean("Torrent", false, persist: false); }
|
||||
}
|
||||
|
||||
public bool AuthenticationEnabled
|
||||
{
|
||||
get { return GetValueBoolean("AuthenticationEnabled", false); }
|
||||
}
|
||||
|
||||
public string Branch
|
||||
{
|
||||
get { return GetValue("Branch", "master"); }
|
||||
}
|
||||
|
||||
public string Username
|
||||
{
|
||||
get { return GetValue("Username", ""); }
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get { return GetValue("Password", ""); }
|
||||
}
|
||||
|
||||
public string LogLevel
|
||||
{
|
||||
get { return GetValue("LogLevel", "Info"); }
|
||||
}
|
||||
|
||||
public string SslCertHash
|
||||
{
|
||||
get { return GetValue("SslCertHash", ""); }
|
||||
}
|
||||
|
||||
public int GetValueInt(string key, int defaultValue)
|
||||
{
|
||||
return Convert.ToInt32(GetValue(key, defaultValue));
|
||||
}
|
||||
|
||||
public bool GetValueBoolean(string key, bool defaultValue, bool persist = true)
|
||||
{
|
||||
return Convert.ToBoolean(GetValue(key, defaultValue, persist));
|
||||
}
|
||||
|
||||
public T GetValueEnum<T>(string key, T defaultValue)
|
||||
{
|
||||
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
|
||||
}
|
||||
|
||||
public string GetValue(string key, object defaultValue, bool persist = true)
|
||||
{
|
||||
return _cache.Get(key, () =>
|
||||
{
|
||||
EnsureDefaultConfigFile();
|
||||
|
||||
var xDoc = LoadConfigFile();
|
||||
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).Single();
|
||||
|
||||
var parentContainer = config;
|
||||
|
||||
var valueHolder = parentContainer.Descendants(key).ToList();
|
||||
|
||||
if (valueHolder.Count() == 1)
|
||||
return valueHolder.First().Value;
|
||||
|
||||
//Save the value
|
||||
if (persist)
|
||||
{
|
||||
SetValue(key, defaultValue);
|
||||
}
|
||||
|
||||
//return the default value
|
||||
return defaultValue.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
public void SetValue(string key, object value)
|
||||
{
|
||||
EnsureDefaultConfigFile();
|
||||
|
||||
var xDoc = LoadConfigFile();
|
||||
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).Single();
|
||||
|
||||
var parentContainer = config;
|
||||
|
||||
var keyHolder = parentContainer.Descendants(key);
|
||||
|
||||
if (keyHolder.Count() != 1)
|
||||
{
|
||||
parentContainer.Add(new XElement(key, value));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
parentContainer.Descendants(key).Single().Value = value.ToString();
|
||||
}
|
||||
|
||||
_cache.Set(key, value.ToString());
|
||||
|
||||
xDoc.Save(_configFile);
|
||||
}
|
||||
|
||||
public void SetValue(string key, Enum value)
|
||||
{
|
||||
SetValue(key, value.ToString().ToLower());
|
||||
}
|
||||
|
||||
private void EnsureDefaultConfigFile()
|
||||
{
|
||||
if (!File.Exists(_configFile))
|
||||
{
|
||||
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
|
||||
xDoc.Save(_configFile);
|
||||
|
||||
SaveConfigDictionary(GetConfigDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteOldValues()
|
||||
{
|
||||
EnsureDefaultConfigFile();
|
||||
|
||||
var xDoc = LoadConfigFile();
|
||||
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).Single();
|
||||
|
||||
var type = GetType();
|
||||
var properties = type.GetProperties();
|
||||
|
||||
foreach (var configValue in config.Descendants().ToList())
|
||||
{
|
||||
var name = configValue.Name.LocalName;
|
||||
|
||||
if (!properties.Any(p => p.Name == name))
|
||||
{
|
||||
config.Descendants(name).Remove();
|
||||
}
|
||||
}
|
||||
|
||||
xDoc.Save(_configFile);
|
||||
}
|
||||
|
||||
private XDocument LoadConfigFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
return XDocument.Load(_configFile);
|
||||
}
|
||||
|
||||
catch (XmlException ex)
|
||||
{
|
||||
throw new InvalidConfigFileException(_configFile + " is invalid, please see the http://wiki.nzbdrone.com for steps to resolve this issue.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleAsync(ApplicationStartedEvent message)
|
||||
{
|
||||
DeleteOldValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public interface IConfigRepository : IBasicRepository<Config>
|
||||
{
|
||||
Config Get(string key);
|
||||
|
||||
}
|
||||
|
||||
public class ConfigRepository : BasicRepository<Config>, IConfigRepository
|
||||
{
|
||||
public ConfigRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public Config Get(string key)
|
||||
{
|
||||
return Query.SingleOrDefault(c => c.Key == key);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients.Nzbget;
|
||||
using NzbDrone.Core.Download.Clients.Sabnzbd;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public enum ConfigKey
|
||||
{
|
||||
DownloadedEpisodesFolder
|
||||
}
|
||||
|
||||
public class ConfigService : IConfigService
|
||||
{
|
||||
private readonly IConfigRepository _repository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
private static Dictionary<string, string> _cache;
|
||||
|
||||
public ConfigService(IConfigRepository repository, IEventAggregator eventAggregator, Logger logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
_cache = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public IEnumerable<Config> All()
|
||||
{
|
||||
return _repository.All();
|
||||
}
|
||||
|
||||
public Dictionary<String, Object> AllWithDefaults()
|
||||
{
|
||||
var dict = new Dictionary<String, Object>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
var type = GetType();
|
||||
var properties = type.GetProperties();
|
||||
|
||||
foreach (var propertyInfo in properties)
|
||||
{
|
||||
var value = propertyInfo.GetValue(this, null);
|
||||
|
||||
dict.Add(propertyInfo.Name, value);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
public void SaveValues(Dictionary<string, object> configValues)
|
||||
{
|
||||
var allWithDefaults = AllWithDefaults();
|
||||
|
||||
foreach (var configValue in configValues)
|
||||
{
|
||||
object currentValue;
|
||||
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
|
||||
if (currentValue == null) continue;
|
||||
|
||||
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
|
||||
|
||||
if (!equal)
|
||||
SetValue(configValue.Key, configValue.Value.ToString());
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new ConfigSavedEvent());
|
||||
}
|
||||
|
||||
public String SabHost
|
||||
{
|
||||
get { return GetValue("SabHost", "localhost"); }
|
||||
|
||||
set { SetValue("SabHost", value); }
|
||||
}
|
||||
|
||||
public int SabPort
|
||||
{
|
||||
get { return GetValueInt("SabPort", 8080); }
|
||||
|
||||
set { SetValue("SabPort", value); }
|
||||
}
|
||||
|
||||
public String SabApiKey
|
||||
{
|
||||
get { return GetValue("SabApiKey"); }
|
||||
|
||||
set { SetValue("SabApiKey", value); }
|
||||
}
|
||||
|
||||
public String SabUsername
|
||||
{
|
||||
get { return GetValue("SabUsername"); }
|
||||
|
||||
set { SetValue("SabUsername", value); }
|
||||
}
|
||||
|
||||
public String SabPassword
|
||||
{
|
||||
get { return GetValue("SabPassword"); }
|
||||
|
||||
set { SetValue("SabPassword", value); }
|
||||
}
|
||||
|
||||
public String SabTvCategory
|
||||
{
|
||||
get { return GetValue("SabTvCategory", "tv"); }
|
||||
|
||||
set { SetValue("SabTvCategory", value); }
|
||||
}
|
||||
|
||||
public SabPriorityType SabRecentTvPriority
|
||||
{
|
||||
get { return GetValueEnum("SabRecentTvPriority", SabPriorityType.Default); }
|
||||
|
||||
set { SetValue("SabRecentTvPriority", value); }
|
||||
}
|
||||
|
||||
public SabPriorityType SabOlderTvPriority
|
||||
{
|
||||
get { return GetValueEnum("SabOlderTvPriority", SabPriorityType.Default); }
|
||||
|
||||
set { SetValue("SabOlderTvPriority", value); }
|
||||
}
|
||||
|
||||
public bool SabUseSsl
|
||||
{
|
||||
get { return GetValueBoolean("SabUseSsl", false); }
|
||||
|
||||
set { SetValue("SabUseSsl", value); }
|
||||
}
|
||||
|
||||
public String DownloadedEpisodesFolder
|
||||
{
|
||||
get { return GetValue(ConfigKey.DownloadedEpisodesFolder.ToString()); }
|
||||
|
||||
set { SetValue(ConfigKey.DownloadedEpisodesFolder.ToString(), value); }
|
||||
}
|
||||
|
||||
public bool UseSeasonFolder
|
||||
{
|
||||
get { return GetValueBoolean("UseSeasonFolder", true); }
|
||||
|
||||
set { SetValue("UseSeasonFolder", value); }
|
||||
}
|
||||
|
||||
public string SeasonFolderFormat
|
||||
{
|
||||
get { return GetValue("SeasonFolderFormat", "Season %s"); }
|
||||
set { SetValue("SeasonFolderFormat", value); }
|
||||
}
|
||||
|
||||
public bool AutoUnmonitorPreviouslyDownloadedEpisodes
|
||||
{
|
||||
get { return GetValueBoolean("AutoUnmonitorPreviouslyDownloadedEpisodes"); }
|
||||
set { SetValue("AutoUnmonitorPreviouslyDownloadedEpisodes", value); }
|
||||
}
|
||||
|
||||
public int Retention
|
||||
{
|
||||
get { return GetValueInt("Retention", 0); }
|
||||
set { SetValue("Retention", value); }
|
||||
}
|
||||
|
||||
public DownloadClientType DownloadClient
|
||||
{
|
||||
get { return GetValueEnum("DownloadClient", DownloadClientType.Blackhole); }
|
||||
|
||||
set { SetValue("DownloadClient", value); }
|
||||
}
|
||||
|
||||
public string BlackholeFolder
|
||||
{
|
||||
get { return GetValue("BlackholeFolder", String.Empty); }
|
||||
set { SetValue("BlackholeFolder", value); }
|
||||
}
|
||||
|
||||
public string PneumaticFolder
|
||||
{
|
||||
get { return GetValue("PneumaticFolder", String.Empty); }
|
||||
set { SetValue("PneumaticFolder", value); }
|
||||
}
|
||||
|
||||
public string RecycleBin
|
||||
{
|
||||
get { return GetValue("RecycleBin", String.Empty); }
|
||||
set { SetValue("RecycleBin", value); }
|
||||
}
|
||||
|
||||
public String NzbgetUsername
|
||||
{
|
||||
get { return GetValue("NzbgetUsername", "nzbget"); }
|
||||
|
||||
set { SetValue("NzbgetUsername", value); }
|
||||
}
|
||||
|
||||
public String NzbgetPassword
|
||||
{
|
||||
get { return GetValue("NzbgetPassword", ""); }
|
||||
|
||||
set { SetValue("NzbgetPassword", value); }
|
||||
}
|
||||
|
||||
public String NzbgetHost
|
||||
{
|
||||
get { return GetValue("NzbgetHost", "localhost"); }
|
||||
|
||||
set { SetValue("NzbgetHost", value); }
|
||||
}
|
||||
|
||||
public Int32 NzbgetPort
|
||||
{
|
||||
get { return GetValueInt("NzbgetPort", 6789); }
|
||||
|
||||
set { SetValue("NzbgetPort", value); }
|
||||
}
|
||||
|
||||
public String NzbgetTvCategory
|
||||
{
|
||||
get { return GetValue("NzbgetTvCategory", "nzbget"); }
|
||||
|
||||
set { SetValue("NzbgetTvCategory", value); }
|
||||
}
|
||||
|
||||
public PriorityType NzbgetRecentTvPriority
|
||||
{
|
||||
get { return GetValueEnum("NzbgetRecentTvPriority", PriorityType.Normal); }
|
||||
|
||||
set { SetValue("NzbgetRecentTvPriority", value); }
|
||||
}
|
||||
|
||||
public PriorityType NzbgetOlderTvPriority
|
||||
{
|
||||
get { return GetValueEnum("NzbgetOlderTvPriority", PriorityType.Normal); }
|
||||
|
||||
set { SetValue("NzbgetOlderTvPriority", value); }
|
||||
}
|
||||
|
||||
public string ReleaseRestrictions
|
||||
{
|
||||
get { return GetValue("ReleaseRestrictions", String.Empty); }
|
||||
set { SetValue("ReleaseRestrictions", value); }
|
||||
}
|
||||
|
||||
public Int32 RssSyncInterval
|
||||
{
|
||||
get { return GetValueInt("RssSyncInterval", 15); }
|
||||
|
||||
set { SetValue("RssSyncInterval", value); }
|
||||
}
|
||||
|
||||
public Boolean AutoDownloadPropers
|
||||
{
|
||||
get { return GetValueBoolean("AutoDownloadPropers", true); }
|
||||
|
||||
set { SetValue("AutoDownloadPropers", value); }
|
||||
}
|
||||
|
||||
public string DownloadClientWorkingFolders
|
||||
{
|
||||
get { return GetValue("DownloadClientWorkingFolders", "_UNPACK_|_FAILED_"); }
|
||||
set { SetValue("DownloadClientWorkingFolders", value); }
|
||||
}
|
||||
|
||||
private string GetValue(string key)
|
||||
{
|
||||
return GetValue(key, String.Empty);
|
||||
}
|
||||
|
||||
private bool GetValueBoolean(string key, bool defaultValue = false)
|
||||
{
|
||||
return Convert.ToBoolean(GetValue(key, defaultValue));
|
||||
}
|
||||
|
||||
private int GetValueInt(string key, int defaultValue = 0)
|
||||
{
|
||||
return Convert.ToInt32(GetValue(key, defaultValue));
|
||||
}
|
||||
|
||||
public T GetValueEnum<T>(string key, T defaultValue)
|
||||
{
|
||||
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
|
||||
}
|
||||
|
||||
public string GetValue(string key, object defaultValue, bool persist = false)
|
||||
{
|
||||
EnsureCache();
|
||||
|
||||
key = key.ToLowerInvariant();
|
||||
string dbValue;
|
||||
|
||||
if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !String.IsNullOrEmpty(dbValue))
|
||||
return dbValue;
|
||||
|
||||
_logger.Trace("Unable to find config key '{0}' defaultValue:'{1}'", key, defaultValue);
|
||||
|
||||
if (persist)
|
||||
{
|
||||
SetValue(key, defaultValue.ToString());
|
||||
}
|
||||
return defaultValue.ToString();
|
||||
}
|
||||
|
||||
private void SetValue(string key, Boolean value)
|
||||
{
|
||||
SetValue(key, value.ToString());
|
||||
}
|
||||
|
||||
private void SetValue(string key, int value)
|
||||
{
|
||||
SetValue(key, value.ToString());
|
||||
}
|
||||
|
||||
public void SetValue(string key, string value)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
|
||||
if (String.IsNullOrEmpty(key))
|
||||
throw new ArgumentOutOfRangeException("key");
|
||||
if (value == null)
|
||||
throw new ArgumentNullException("key");
|
||||
|
||||
_logger.Trace("Writing Setting to file. Key:'{0}' Value:'{1}'", key, value);
|
||||
|
||||
var dbValue = _repository.Get(key);
|
||||
|
||||
if (dbValue == null)
|
||||
{
|
||||
_repository.Insert(new Config { Key = key, Value = value });
|
||||
}
|
||||
else
|
||||
{
|
||||
dbValue.Value = value;
|
||||
_repository.Update(dbValue);
|
||||
}
|
||||
|
||||
ClearCache();
|
||||
}
|
||||
|
||||
public void SetValue(string key, Enum value)
|
||||
{
|
||||
SetValue(key, value.ToString().ToLower());
|
||||
}
|
||||
|
||||
private void EnsureCache()
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
if (!_cache.Any())
|
||||
{
|
||||
_cache = All().ToDictionary(c => c.Key.ToLower(), c => c.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearCache()
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
_cache = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Configuration.Events
|
||||
{
|
||||
public class ConfigFileSavedEvent : IEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Configuration.Events
|
||||
{
|
||||
public class ConfigSavedEvent : IEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients.Nzbget;
|
||||
using NzbDrone.Core.Download.Clients.Sabnzbd;
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public interface IConfigService
|
||||
{
|
||||
IEnumerable<Config> All();
|
||||
Dictionary<String, Object> AllWithDefaults();
|
||||
String SabHost { get; set; }
|
||||
int SabPort { get; set; }
|
||||
String SabApiKey { get; set; }
|
||||
String SabUsername { get; set; }
|
||||
String SabPassword { get; set; }
|
||||
String SabTvCategory { get; set; }
|
||||
SabPriorityType SabRecentTvPriority { get; set; }
|
||||
SabPriorityType SabOlderTvPriority { get; set; }
|
||||
Boolean SabUseSsl { get; set; }
|
||||
String DownloadedEpisodesFolder { get; set; }
|
||||
bool UseSeasonFolder { get; set; }
|
||||
string SeasonFolderFormat { get; set; }
|
||||
bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; }
|
||||
int Retention { get; set; }
|
||||
DownloadClientType DownloadClient { get; set; }
|
||||
string BlackholeFolder { get; set; }
|
||||
string PneumaticFolder { get; set; }
|
||||
string RecycleBin { get; set; }
|
||||
String NzbgetUsername { get; set; }
|
||||
String NzbgetPassword { get; set; }
|
||||
String NzbgetHost { get; set; }
|
||||
Int32 NzbgetPort { get; set; }
|
||||
String NzbgetTvCategory { get; set; }
|
||||
PriorityType NzbgetRecentTvPriority { get; set; }
|
||||
PriorityType NzbgetOlderTvPriority { get; set; }
|
||||
string ReleaseRestrictions { get; set; }
|
||||
Int32 RssSyncInterval { get; set; }
|
||||
Boolean AutoDownloadPropers { get; set; }
|
||||
String DownloadClientWorkingFolders { get; set; }
|
||||
void SaveValues(Dictionary<string, object> configValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public class InvalidConfigFileException : NzbDroneException
|
||||
{
|
||||
public InvalidConfigFileException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.DailySeries
|
||||
{
|
||||
|
||||
public interface IDailySeriesDataProxy
|
||||
{
|
||||
IEnumerable<int> GetDailySeriesIds();
|
||||
bool IsDailySeries(int tvdbid);
|
||||
}
|
||||
|
||||
public class DailySeriesDataProxy : IDailySeriesDataProxy
|
||||
{
|
||||
private readonly IHttpProvider _httpProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DailySeriesDataProxy(IHttpProvider httpProvider, Logger logger)
|
||||
{
|
||||
_httpProvider = httpProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IEnumerable<int> GetDailySeriesIds()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dailySeriesIds = _httpProvider.DownloadString(Services.RootUrl + "/v1/DailySeries");
|
||||
|
||||
var seriesIds = Json.Deserialize<List<int>>(dailySeriesIds);
|
||||
|
||||
return seriesIds;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException("Failed to get Daily Series", ex);
|
||||
return new List<int>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public bool IsDailySeries(int tvdbid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _httpProvider.DownloadString(Services.RootUrl + "/v1/DailySeries?seriesId=" + tvdbid);
|
||||
return Convert.ToBoolean(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException("Failed to check Daily Series status for: " + tvdbid, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.DailySeries
|
||||
{
|
||||
public interface IDailySeriesService
|
||||
{
|
||||
void UpdateDailySeries();
|
||||
bool IsDailySeries(int tvdbid);
|
||||
}
|
||||
|
||||
public class DailySeriesService : IDailySeriesService
|
||||
{
|
||||
//TODO: add timer command
|
||||
|
||||
private readonly IDailySeriesDataProxy _proxy;
|
||||
private readonly ISeriesService _seriesService;
|
||||
|
||||
public DailySeriesService(IDailySeriesDataProxy proxy, ISeriesService seriesService)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_seriesService = seriesService;
|
||||
}
|
||||
|
||||
public void UpdateDailySeries()
|
||||
{
|
||||
var dailySeries = _proxy.GetDailySeriesIds();
|
||||
|
||||
foreach (var tvdbId in dailySeries)
|
||||
{
|
||||
var series = _seriesService.FindByTvdbId(tvdbId);
|
||||
|
||||
if (series != null)
|
||||
{
|
||||
_seriesService.SetSeriesType(series.Id, SeriesTypes.Daily);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDailySeries(int tvdbid)
|
||||
{
|
||||
return _proxy.IsDailySeries(tvdbid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Scene
|
||||
{
|
||||
public class SceneMapping : ModelBase
|
||||
{
|
||||
[JsonProperty("title")]
|
||||
public string ParseTerm { get; set; }
|
||||
|
||||
[JsonProperty("searchTitle")]
|
||||
public string SearchTerm { get; set; }
|
||||
|
||||
public int TvdbId { get; set; }
|
||||
|
||||
[JsonProperty("season")]
|
||||
public int SeasonNumber { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Scene
|
||||
{
|
||||
public interface ISceneMappingProxy
|
||||
{
|
||||
List<SceneMapping> Fetch();
|
||||
}
|
||||
|
||||
public class SceneMappingProxy : ISceneMappingProxy
|
||||
{
|
||||
private readonly IHttpProvider _httpProvider;
|
||||
|
||||
public SceneMappingProxy(IHttpProvider httpProvider)
|
||||
{
|
||||
_httpProvider = httpProvider;
|
||||
}
|
||||
|
||||
public List<SceneMapping> Fetch()
|
||||
{
|
||||
var mappingsJson = _httpProvider.DownloadString(Services.RootUrl + "/v1/SceneMapping");
|
||||
return Json.Deserialize<List<SceneMapping>>(mappingsJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Scene
|
||||
{
|
||||
public interface ISceneMappingRepository : IBasicRepository<SceneMapping>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class SceneMappingRepository : BasicRepository<SceneMapping>, ISceneMappingRepository
|
||||
{
|
||||
public SceneMappingRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Scene
|
||||
{
|
||||
public interface ISceneMappingService
|
||||
{
|
||||
string GetSceneName(int tvdbId);
|
||||
Nullable<int> GetTvDbId(string cleanName);
|
||||
}
|
||||
|
||||
public class SceneMappingService : ISceneMappingService,
|
||||
IHandleAsync<ApplicationStartedEvent>,
|
||||
IExecute<UpdateSceneMappingCommand>
|
||||
{
|
||||
private readonly ISceneMappingRepository _repository;
|
||||
private readonly ISceneMappingProxy _sceneMappingProxy;
|
||||
private readonly Logger _logger;
|
||||
private readonly ICached<SceneMapping> _getSceneNameCache;
|
||||
private readonly ICached<SceneMapping> _gettvdbIdCache;
|
||||
|
||||
public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, ICacheManger cacheManger, Logger logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_sceneMappingProxy = sceneMappingProxy;
|
||||
|
||||
_getSceneNameCache = cacheManger.GetCache<SceneMapping>(GetType(), "scene_name");
|
||||
_gettvdbIdCache = cacheManger.GetCache<SceneMapping>(GetType(), "tvdb_id");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string GetSceneName(int tvdbId)
|
||||
{
|
||||
var mapping = _getSceneNameCache.Find(tvdbId.ToString());
|
||||
|
||||
if (mapping == null) return null;
|
||||
|
||||
return mapping.SearchTerm;
|
||||
}
|
||||
|
||||
public Nullable<Int32> GetTvDbId(string cleanName)
|
||||
{
|
||||
var mapping = _gettvdbIdCache.Find(cleanName.CleanSeriesTitle());
|
||||
|
||||
if (mapping == null)
|
||||
return null;
|
||||
|
||||
return mapping.TvdbId;
|
||||
}
|
||||
|
||||
private void UpdateMappings()
|
||||
{
|
||||
_logger.Info("Updating Scene mapping");
|
||||
|
||||
try
|
||||
{
|
||||
var mappings = _sceneMappingProxy.Fetch();
|
||||
|
||||
if (mappings.Any())
|
||||
{
|
||||
_repository.Purge();
|
||||
|
||||
foreach (var sceneMapping in mappings)
|
||||
{
|
||||
sceneMapping.ParseTerm = sceneMapping.ParseTerm.CleanSeriesTitle();
|
||||
}
|
||||
|
||||
_repository.InsertMany(mappings);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Received empty list of mapping. will not update.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Failed to Update Scene Mappings:", ex);
|
||||
}
|
||||
|
||||
RefreshCache();
|
||||
}
|
||||
|
||||
private void RefreshCache()
|
||||
{
|
||||
var mappings = _repository.All();
|
||||
|
||||
_gettvdbIdCache.Clear();
|
||||
_getSceneNameCache.Clear();
|
||||
|
||||
foreach (var sceneMapping in mappings)
|
||||
{
|
||||
_getSceneNameCache.Set(sceneMapping.TvdbId.ToString(), sceneMapping);
|
||||
_gettvdbIdCache.Set(sceneMapping.ParseTerm.CleanSeriesTitle(), sceneMapping);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void HandleAsync(ApplicationStartedEvent message)
|
||||
{
|
||||
UpdateMappings();
|
||||
}
|
||||
|
||||
public void Execute(UpdateSceneMappingCommand message)
|
||||
{
|
||||
UpdateMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Scene
|
||||
{
|
||||
public class UpdateSceneMappingCommand : Command
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.DataAugmentation.Xem.Model
|
||||
{
|
||||
public class XemResult<T>
|
||||
{
|
||||
public string Result { get; set; }
|
||||
public T Data { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.DataAugmentation.Xem.Model
|
||||
{
|
||||
public class XemSceneTvdbMapping
|
||||
{
|
||||
public XemValues Scene { get; set; }
|
||||
public XemValues Tvdb { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.DataAugmentation.Xem.Model
|
||||
{
|
||||
public class XemValues
|
||||
{
|
||||
public int Season { get; set; }
|
||||
public int Episode { get; set; }
|
||||
public int Absolute { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Xem
|
||||
{
|
||||
public class UpdateXemMappingsCommand : Command
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.DataAugmentation.Xem.Model;
|
||||
using NzbDrone.Core.Rest;
|
||||
using RestSharp;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Xem
|
||||
{
|
||||
public interface IXemProxy
|
||||
{
|
||||
List<int> GetXemSeriesIds();
|
||||
List<XemSceneTvdbMapping> GetSceneTvdbMappings(int id);
|
||||
}
|
||||
|
||||
public class XemProxy : IXemProxy
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
private const string XEM_BASE_URL = "http://thexem.de/map/";
|
||||
|
||||
public XemProxy(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
private static RestRequest BuildRequest(string resource)
|
||||
{
|
||||
var req = new RestRequest(resource, Method.GET);
|
||||
req.AddParameter("origin", "tvdb");
|
||||
return req;
|
||||
}
|
||||
|
||||
public List<int> GetXemSeriesIds()
|
||||
{
|
||||
_logger.Trace("Fetching Series IDs from");
|
||||
|
||||
var restClient = new RestClient(XEM_BASE_URL);
|
||||
|
||||
var request = BuildRequest("havemap");
|
||||
|
||||
var response = restClient.ExecuteAndValidate<XemResult<List<int>>>(request);
|
||||
CheckForFailureResult(response);
|
||||
|
||||
return response.Data.ToList();
|
||||
}
|
||||
|
||||
public List<XemSceneTvdbMapping> GetSceneTvdbMappings(int id)
|
||||
{
|
||||
_logger.Trace("Fetching Mappings for: {0}", id);
|
||||
var url = String.Format("{0}all?id={1}&origin=tvdb", XEM_BASE_URL, id);
|
||||
|
||||
|
||||
var restClient = new RestClient(XEM_BASE_URL);
|
||||
|
||||
var request = BuildRequest("all");
|
||||
request.AddParameter("id", id);
|
||||
|
||||
var response = restClient.ExecuteAndValidate<XemResult<List<XemSceneTvdbMapping>>>(request);
|
||||
CheckForFailureResult(response);
|
||||
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
private static void CheckForFailureResult<T>(XemResult<T> response)
|
||||
{
|
||||
if (response.Result.Equals("failure", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
!response.Message.Contains("no show with the tvdb_id"))
|
||||
{
|
||||
throw new Exception("Error response received from Xem: " + response.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
namespace NzbDrone.Core.DataAugmentation.Xem
|
||||
{
|
||||
public class XemService : IExecute<UpdateXemMappingsCommand>, IHandle<SeriesUpdatedEvent>, IHandleAsync<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly IXemProxy _xemProxy;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly Logger _logger;
|
||||
private readonly ICached<bool> _cache;
|
||||
|
||||
public XemService(IEpisodeService episodeService,
|
||||
IXemProxy xemProxy,
|
||||
ISeriesService seriesService, ICacheManger cacheManger, Logger logger)
|
||||
{
|
||||
if (seriesService == null) throw new ArgumentNullException("seriesService");
|
||||
_episodeService = episodeService;
|
||||
_xemProxy = xemProxy;
|
||||
_seriesService = seriesService;
|
||||
_logger = logger;
|
||||
_logger = logger;
|
||||
_cache = cacheManger.GetCache<bool>(GetType());
|
||||
}
|
||||
|
||||
|
||||
public void Execute(UpdateXemMappingsCommand message)
|
||||
{
|
||||
UpdateMappings();
|
||||
}
|
||||
|
||||
public void Handle(SeriesUpdatedEvent message)
|
||||
{
|
||||
UpdateMappings(message.Series);
|
||||
}
|
||||
|
||||
public void HandleAsync(ApplicationStartedEvent message)
|
||||
{
|
||||
GetXemSeriesIds();
|
||||
}
|
||||
|
||||
private void UpdateMappings()
|
||||
{
|
||||
_logger.Trace("Starting scene numbering update");
|
||||
|
||||
try
|
||||
{
|
||||
var ids = GetXemSeriesIds();
|
||||
var series = _seriesService.GetAllSeries();
|
||||
var wantedSeries = series.Where(s => ids.Contains(s.TvdbId)).ToList();
|
||||
|
||||
foreach (var ser in wantedSeries)
|
||||
{
|
||||
PerformUpdate(ser);
|
||||
}
|
||||
|
||||
_logger.Trace("Completed scene numbering update");
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException("Error updating Scene Mappings", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMappings(Series series)
|
||||
{
|
||||
if (!_cache.Find(series.TvdbId.ToString()))
|
||||
{
|
||||
_logger.Trace("Scene numbering is not available for {0} [{1}]", series.Title, series.TvdbId);
|
||||
return;
|
||||
}
|
||||
|
||||
PerformUpdate(series);
|
||||
}
|
||||
|
||||
private void PerformUpdate(Series series)
|
||||
{
|
||||
_logger.Trace("Updating scene numbering mapping for: {0}", series);
|
||||
try
|
||||
{
|
||||
var episodesToUpdate = new List<Episode>();
|
||||
var mappings = _xemProxy.GetSceneTvdbMappings(series.TvdbId);
|
||||
|
||||
if (!mappings.Any())
|
||||
{
|
||||
_logger.Trace("Mappings for: {0} are empty, skipping", series);
|
||||
_cache.Remove(series.TvdbId.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
|
||||
foreach (var mapping in mappings)
|
||||
{
|
||||
_logger.Trace("Setting scene numbering mappings for {0} S{1:00}E{2:00}", series, mapping.Tvdb.Season, mapping.Tvdb.Episode);
|
||||
|
||||
var episode = episodes.SingleOrDefault(e => e.SeasonNumber == mapping.Tvdb.Season && e.EpisodeNumber == mapping.Tvdb.Episode);
|
||||
|
||||
if (episode == null)
|
||||
{
|
||||
_logger.Trace("Information hasn't been added to TheTVDB yet, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
episode.AbsoluteEpisodeNumber = mapping.Scene.Absolute;
|
||||
episode.SceneSeasonNumber = mapping.Scene.Season;
|
||||
episode.SceneEpisodeNumber = mapping.Scene.Episode;
|
||||
episodesToUpdate.Add(episode);
|
||||
}
|
||||
|
||||
_logger.Trace("Committing scene numbering mappings to database for: {0}", series);
|
||||
_episodeService.UpdateEpisodes(episodesToUpdate);
|
||||
|
||||
_logger.Trace("Setting UseSceneMapping for {0}", series);
|
||||
series.UseSceneNumbering = true;
|
||||
_seriesService.UpdateSeries(series);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error updating scene numbering mappings for: " + series, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<int> GetXemSeriesIds()
|
||||
{
|
||||
_cache.Clear();
|
||||
|
||||
var ids = _xemProxy.GetXemSeriesIds();
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
_cache.Set(id.ToString(), true);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Marr.Data;
|
||||
using Marr.Data.QGen;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IBasicRepository<TModel> where TModel : ModelBase, new()
|
||||
{
|
||||
IEnumerable<TModel> All();
|
||||
int Count();
|
||||
TModel Get(int id);
|
||||
IEnumerable<TModel> Get(IEnumerable<int> ids);
|
||||
TModel SingleOrDefault();
|
||||
TModel Insert(TModel model);
|
||||
TModel Update(TModel model);
|
||||
TModel Upsert(TModel model);
|
||||
void Delete(int id);
|
||||
void Delete(TModel model);
|
||||
void InsertMany(IList<TModel> model);
|
||||
void UpdateMany(IList<TModel> model);
|
||||
void DeleteMany(List<TModel> model);
|
||||
void Purge();
|
||||
bool HasItems();
|
||||
void DeleteMany(IEnumerable<int> ids);
|
||||
void SetFields(TModel model, params Expression<Func<TModel, object>>[] properties);
|
||||
TModel Single();
|
||||
PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec);
|
||||
}
|
||||
|
||||
|
||||
public class BasicRepository<TModel> : IBasicRepository<TModel> where TModel : ModelBase, new()
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
private IDataMapper DataMapper
|
||||
{
|
||||
get { return _database.GetDataMapper(); }
|
||||
}
|
||||
|
||||
public BasicRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
{
|
||||
_database = database;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
protected QueryBuilder<TModel> Query
|
||||
{
|
||||
get { return DataMapper.Query<TModel>(); }
|
||||
}
|
||||
|
||||
protected void Delete(Expression<Func<TModel, bool>> filter)
|
||||
{
|
||||
DataMapper.Delete(filter);
|
||||
}
|
||||
|
||||
public IEnumerable<TModel> All()
|
||||
{
|
||||
return DataMapper.Query<TModel>().ToList();
|
||||
}
|
||||
|
||||
public int Count()
|
||||
{
|
||||
return DataMapper.Query<TModel>().GetRowCount();
|
||||
}
|
||||
|
||||
public TModel Get(int id)
|
||||
{
|
||||
var model = DataMapper.Query<TModel>().SingleOrDefault(c => c.Id == id);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
throw new ModelNotFoundException(typeof(TModel), id);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public IEnumerable<TModel> Get(IEnumerable<int> ids)
|
||||
{
|
||||
var idList = ids.ToList();
|
||||
var query = String.Format("Id IN ({0})", String.Join(",", idList));
|
||||
var result = Query.Where(query).ToList();
|
||||
|
||||
if (result.Count != idList.Count())
|
||||
{
|
||||
throw new ApplicationException("Expected query to return {0} rows but returned {1}".Inject(idList.Count(), result.Count));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public TModel SingleOrDefault()
|
||||
{
|
||||
return All().SingleOrDefault();
|
||||
}
|
||||
|
||||
public TModel Single()
|
||||
{
|
||||
return All().Single();
|
||||
}
|
||||
|
||||
public TModel Insert(TModel model)
|
||||
{
|
||||
if (model.Id != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Can't insert model with existing ID " + model.Id);
|
||||
}
|
||||
|
||||
DataMapper.Insert(model);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public TModel Update(TModel model)
|
||||
{
|
||||
if (model.Id == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Can't update model with ID 0");
|
||||
}
|
||||
|
||||
DataMapper.Update(model, c => c.Id == model.Id);
|
||||
return model;
|
||||
}
|
||||
|
||||
public void Delete(TModel model)
|
||||
{
|
||||
DataMapper.Delete<TModel>(c => c.Id == model.Id);
|
||||
}
|
||||
|
||||
public void InsertMany(IList<TModel> models)
|
||||
{
|
||||
foreach (var model in models)
|
||||
{
|
||||
Insert(model);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMany(IList<TModel> models)
|
||||
{
|
||||
foreach (var model in models)
|
||||
{
|
||||
Update(model);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteMany(List<TModel> models)
|
||||
{
|
||||
models.ForEach(Delete);
|
||||
}
|
||||
|
||||
public TModel Upsert(TModel model)
|
||||
{
|
||||
if (model.Id == 0)
|
||||
{
|
||||
Insert(model);
|
||||
return model;
|
||||
}
|
||||
Update(model);
|
||||
return model;
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
DataMapper.Delete<TModel>(c => c.Id == id);
|
||||
}
|
||||
|
||||
public void DeleteMany(IEnumerable<int> ids)
|
||||
{
|
||||
ids.ToList().ForEach(Delete);
|
||||
}
|
||||
|
||||
public void Purge()
|
||||
{
|
||||
DataMapper.Delete<TModel>(c => c.Id > -1);
|
||||
}
|
||||
|
||||
public bool HasItems()
|
||||
{
|
||||
return Count() > 0;
|
||||
}
|
||||
|
||||
public void SetFields(TModel model, params Expression<Func<TModel, object>>[] properties)
|
||||
{
|
||||
if (model.Id == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Attempted to updated model without ID");
|
||||
}
|
||||
|
||||
DataMapper.Update<TModel>()
|
||||
.Where(c => c.Id == model.Id)
|
||||
.ColumnsIncluding(properties)
|
||||
.Entity(model)
|
||||
.Execute();
|
||||
}
|
||||
|
||||
public virtual PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec)
|
||||
{
|
||||
var pagingQuery = Query.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
|
||||
.Skip(pagingSpec.PagingOffset())
|
||||
.Take(pagingSpec.PageSize);
|
||||
|
||||
pagingSpec.Records = pagingQuery.ToList();
|
||||
|
||||
//TODO: Use the same query for count and records
|
||||
pagingSpec.TotalRecords = Count();
|
||||
|
||||
return pagingSpec;
|
||||
}
|
||||
|
||||
public void DeleteAll()
|
||||
{
|
||||
DataMapper.Delete<TModel>(c => c.Id > 0);
|
||||
}
|
||||
|
||||
protected void ModelCreated(TModel model)
|
||||
{
|
||||
PublishModelEvent(model, ModelAction.Created);
|
||||
}
|
||||
|
||||
protected void ModelUpdated(TModel model)
|
||||
{
|
||||
PublishModelEvent(model, ModelAction.Updated);
|
||||
}
|
||||
|
||||
protected void ModelDeleted(TModel model)
|
||||
{
|
||||
PublishModelEvent(model, ModelAction.Deleted);
|
||||
}
|
||||
|
||||
private void PublishModelEvent(TModel model, ModelAction action)
|
||||
{
|
||||
if (PublishModelEvents)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new ModelEvent<TModel>(model, action));
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool PublishModelEvents
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IConnectionStringFactory
|
||||
{
|
||||
string MainDbConnectionString { get; }
|
||||
string LogDbConnectionString { get; }
|
||||
}
|
||||
|
||||
public class ConnectionStringFactory : IConnectionStringFactory
|
||||
{
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
MainDbConnectionString = GetConnectionString(appFolderInfo.GetNzbDroneDatabase());
|
||||
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
}
|
||||
|
||||
public string MainDbConnectionString { get; private set; }
|
||||
public string LogDbConnectionString { get; private set; }
|
||||
|
||||
private static string GetConnectionString(string dbPath)
|
||||
{
|
||||
var connectionBuilder = new SQLiteConnectionStringBuilder();
|
||||
|
||||
connectionBuilder.DataSource = dbPath;
|
||||
connectionBuilder.CacheSize = (int)-10.Megabytes();
|
||||
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
|
||||
connectionBuilder.JournalMode = SQLiteJournalModeEnum.Wal;
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class BooleanIntConverter : IConverter
|
||||
{
|
||||
public object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return DBNull.Value;
|
||||
}
|
||||
|
||||
var val = (Int64)context.DbValue;
|
||||
|
||||
switch (val)
|
||||
{
|
||||
case 1:
|
||||
return true;
|
||||
case 0:
|
||||
return false;
|
||||
default:
|
||||
throw new ConversionException(string.Format("The BooleanCharConverter could not convert the value '{0}' to a Boolean.", context.DbValue));
|
||||
}
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
var val = (Nullable<bool>)clrValue;
|
||||
|
||||
switch (val)
|
||||
{
|
||||
case true:
|
||||
return 1;
|
||||
case false:
|
||||
return 0;
|
||||
default:
|
||||
return DBNull.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public Type DbType
|
||||
{
|
||||
get
|
||||
{
|
||||
return typeof(int);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class EmbeddedDocumentConverter : IConverter
|
||||
{
|
||||
public virtual object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return DBNull.Value;
|
||||
}
|
||||
|
||||
var stringValue = (string)context.DbValue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Json.Deserialize(stringValue, context.ColumnMap.FieldType);
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
if (clrValue == null) return null;
|
||||
|
||||
return clrValue.ToJson();
|
||||
}
|
||||
|
||||
public Type DbType
|
||||
{
|
||||
get
|
||||
{
|
||||
return typeof(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class EnumIntConverter : IConverter
|
||||
{
|
||||
public Type DbType
|
||||
{
|
||||
get
|
||||
{
|
||||
return typeof(int);
|
||||
}
|
||||
}
|
||||
|
||||
public object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue != null && context.DbValue != DBNull.Value)
|
||||
{
|
||||
return Enum.ToObject(context.ColumnMap.FieldType, (Int64)context.DbValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
if (clrValue != null)
|
||||
{
|
||||
return (int)clrValue;
|
||||
}
|
||||
|
||||
return DBNull.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class Int32Converter : IConverter
|
||||
{
|
||||
public object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return DBNull.Value;
|
||||
}
|
||||
|
||||
if (context.DbValue is Int32)
|
||||
{
|
||||
return context.DbValue;
|
||||
}
|
||||
|
||||
return Convert.ToInt32(context.DbValue);
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
if (dbValue == DBNull.Value)
|
||||
{
|
||||
return DBNull.Value;
|
||||
}
|
||||
|
||||
if (dbValue is Int32)
|
||||
{
|
||||
return dbValue;
|
||||
}
|
||||
|
||||
return Convert.ToInt32(dbValue);
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
return clrValue;
|
||||
}
|
||||
|
||||
public Type DbType { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using NzbDrone.Common.Reflection;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class ProviderSettingConverter : EmbeddedDocumentConverter
|
||||
{
|
||||
public override object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return NullConfig.Instance;
|
||||
}
|
||||
|
||||
var stringValue = (string)context.DbValue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
return NullConfig.Instance;
|
||||
}
|
||||
|
||||
var ordinal = context.DataRecord.GetOrdinal("ConfigContract");
|
||||
var contract = context.DataRecord.GetString(ordinal);
|
||||
|
||||
|
||||
var impType = typeof (IProviderConfig).Assembly.FindTypeByName(contract);
|
||||
|
||||
if (impType == null)
|
||||
{
|
||||
throw new ConfigContractNotFoundException(contract);
|
||||
}
|
||||
|
||||
return Json.Deserialize(stringValue, impType);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class QualityIntConverter : IConverter
|
||||
{
|
||||
public object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return Quality.Unknown;
|
||||
}
|
||||
|
||||
var val = Convert.ToInt32(context.DbValue);
|
||||
|
||||
return (Quality)val;
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
if(clrValue == null) return 0;
|
||||
|
||||
if(clrValue as Quality == null)
|
||||
{
|
||||
throw new InvalidOperationException("Attempted to save a quality that isn't really a quality");
|
||||
}
|
||||
|
||||
var quality = clrValue as Quality;
|
||||
return (int)quality;
|
||||
}
|
||||
|
||||
public Type DbType
|
||||
{
|
||||
get
|
||||
{
|
||||
return typeof(int);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class UtcConverter : IConverter
|
||||
{
|
||||
public object FromDB(ConverterContext context)
|
||||
{
|
||||
return context.DbValue;
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
if (clrValue == DBNull.Value)
|
||||
{
|
||||
return clrValue;
|
||||
}
|
||||
|
||||
var dateTime = (DateTime)clrValue;
|
||||
return dateTime.ToUniversalTime();
|
||||
}
|
||||
|
||||
public Type DbType
|
||||
{
|
||||
get
|
||||
{
|
||||
return typeof(DateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Marr.Data;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IDatabase
|
||||
{
|
||||
IDataMapper GetDataMapper();
|
||||
}
|
||||
|
||||
public class Database : IDatabase
|
||||
{
|
||||
private readonly Func<IDataMapper> _dataMapperFactory;
|
||||
|
||||
public Database(Func<IDataMapper> dataMapperFactory)
|
||||
{
|
||||
_dataMapperFactory = dataMapperFactory;
|
||||
}
|
||||
|
||||
public IDataMapper GetDataMapper()
|
||||
{
|
||||
return _dataMapperFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using Marr.Data;
|
||||
using Marr.Data.Reflection;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IDbFactory
|
||||
{
|
||||
IDatabase Create(MigrationType migrationType = MigrationType.Main);
|
||||
}
|
||||
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
{
|
||||
private readonly IMigrationController _migrationController;
|
||||
private readonly IConnectionStringFactory _connectionStringFactory;
|
||||
|
||||
static DbFactory()
|
||||
{
|
||||
MapRepository.Instance.ReflectionStrategy = new SimpleReflectionStrategy();
|
||||
TableMapping.Map();
|
||||
}
|
||||
|
||||
public static void RegisterDatabase(IContainer container)
|
||||
{
|
||||
container.Resolve<IDbFactory>().Create();
|
||||
|
||||
container.Register(c => c.Resolve<IDbFactory>().Create());
|
||||
|
||||
container.Resolve<IDbFactory>().Create(MigrationType.Log);
|
||||
|
||||
container.Register<ILogRepository>(c =>
|
||||
{
|
||||
var db = c.Resolve<IDbFactory>().Create(MigrationType.Log);
|
||||
return new LogRepository(db, c.Resolve<IEventAggregator>());
|
||||
});
|
||||
}
|
||||
|
||||
public DbFactory(IMigrationController migrationController, IConnectionStringFactory connectionStringFactory)
|
||||
{
|
||||
_migrationController = migrationController;
|
||||
_connectionStringFactory = connectionStringFactory;
|
||||
}
|
||||
|
||||
public IDatabase Create(MigrationType migrationType = MigrationType.Main)
|
||||
{
|
||||
string connectionString;
|
||||
|
||||
|
||||
switch (migrationType)
|
||||
{
|
||||
case MigrationType.Main:
|
||||
{
|
||||
connectionString = _connectionStringFactory.MainDbConnectionString;
|
||||
break;
|
||||
}
|
||||
case MigrationType.Log:
|
||||
{
|
||||
connectionString = _connectionStringFactory.LogDbConnectionString;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new ArgumentException("Invalid MigrationType");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
_migrationController.MigrateToLatest(connectionString, migrationType);
|
||||
|
||||
return new Database(() =>
|
||||
{
|
||||
var dataMapper = new DataMapper(SQLiteFactory.Instance, connectionString)
|
||||
{
|
||||
SqlMode = SqlModes.Text,
|
||||
};
|
||||
|
||||
return dataMapper;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Events
|
||||
{
|
||||
public class ModelEvent <TModel> : IEvent
|
||||
{
|
||||
public TModel Model { get; set; }
|
||||
public ModelAction Action { get; set; }
|
||||
|
||||
public ModelEvent(TModel model, ModelAction action)
|
||||
{
|
||||
Model = model;
|
||||
Action = action;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ModelAction
|
||||
{
|
||||
Unknown = 0,
|
||||
Created = 1,
|
||||
Updated = 2,
|
||||
Deleted = 3,
|
||||
Sync = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IEmbeddedDocument
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using Marr.Data;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class LazyList<T> : LazyLoaded<List<T>>
|
||||
{
|
||||
public LazyList()
|
||||
: this(new List<T>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public LazyList(IEnumerable<T> items)
|
||||
: base(new List<T>(items))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static implicit operator LazyList<T>(List<T> val)
|
||||
{
|
||||
return new LazyList<T>(val);
|
||||
}
|
||||
|
||||
public static implicit operator List<T>(LazyList<T> lazy)
|
||||
{
|
||||
return lazy.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Marr.Data;
|
||||
using Marr.Data.Mapping;
|
||||
using NzbDrone.Common.Reflection;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public static class MappingExtensions
|
||||
{
|
||||
|
||||
public static ColumnMapBuilder<T> MapResultSet<T>(this FluentMappings.MappingsFluentEntity<T> mapBuilder) where T : ResultSet, new()
|
||||
{
|
||||
return mapBuilder
|
||||
.Columns
|
||||
.AutoMapPropertiesWhere(IsMappableProperty);
|
||||
}
|
||||
|
||||
|
||||
public static ColumnMapBuilder<T> RegisterModel<T>(this FluentMappings.MappingsFluentEntity<T> mapBuilder, string tableName = null) where T : ModelBase, new()
|
||||
{
|
||||
return mapBuilder.Table.MapTable(tableName)
|
||||
.Columns
|
||||
.AutoMapPropertiesWhere(IsMappableProperty)
|
||||
.PrefixAltNames(String.Format("{0}_", typeof(T).Name))
|
||||
.For(c => c.Id)
|
||||
.SetPrimaryKey()
|
||||
.SetReturnValue()
|
||||
.SetAutoIncrement();
|
||||
}
|
||||
|
||||
public static RelationshipBuilder<T> AutoMapChildModels<T>(this ColumnMapBuilder<T> mapBuilder)
|
||||
{
|
||||
return mapBuilder.Relationships.AutoMapPropertiesWhere(m =>
|
||||
m.MemberType == MemberTypes.Property &&
|
||||
typeof(ModelBase).IsAssignableFrom(((PropertyInfo) m).PropertyType));
|
||||
}
|
||||
|
||||
public static bool IsMappableProperty(MemberInfo memberInfo)
|
||||
{
|
||||
var propertyInfo = memberInfo as PropertyInfo;
|
||||
|
||||
if (propertyInfo == null) return false;
|
||||
|
||||
|
||||
if (!propertyInfo.IsReadable() || !propertyInfo.IsWritable())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (propertyInfo.PropertyType.IsSimpleType() || MapRepository.Instance.TypeConverters.ContainsKey(propertyInfo.PropertyType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(1)]
|
||||
public class InitialSetup : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("Config")
|
||||
.WithColumn("Key").AsString().Unique()
|
||||
.WithColumn("Value").AsString();
|
||||
|
||||
Create.TableForModel("RootFolders")
|
||||
.WithColumn("Path").AsString().Unique();
|
||||
|
||||
Create.TableForModel("Series")
|
||||
.WithColumn("TvdbId").AsInt32().Unique()
|
||||
.WithColumn("TvRageId").AsInt32().Unique()
|
||||
.WithColumn("ImdbId").AsString().Unique()
|
||||
.WithColumn("Title").AsString()
|
||||
.WithColumn("TitleSlug").AsString().Unique()
|
||||
.WithColumn("CleanTitle").AsString()
|
||||
.WithColumn("Status").AsInt32()
|
||||
.WithColumn("Overview").AsString().Nullable()
|
||||
.WithColumn("AirTime").AsString().Nullable()
|
||||
.WithColumn("Images").AsString()
|
||||
.WithColumn("Path").AsString()
|
||||
.WithColumn("Monitored").AsBoolean()
|
||||
.WithColumn("QualityProfileId").AsInt32()
|
||||
.WithColumn("SeasonFolder").AsBoolean()
|
||||
.WithColumn("LastInfoSync").AsDateTime().Nullable()
|
||||
.WithColumn("LastDiskSync").AsDateTime().Nullable()
|
||||
.WithColumn("Runtime").AsInt32()
|
||||
.WithColumn("SeriesType").AsInt32()
|
||||
.WithColumn("BacklogSetting").AsInt32()
|
||||
.WithColumn("Network").AsString().Nullable()
|
||||
.WithColumn("CustomStartDate").AsDateTime().Nullable()
|
||||
.WithColumn("UseSceneNumbering").AsBoolean()
|
||||
.WithColumn("FirstAired").AsDateTime().Nullable()
|
||||
.WithColumn("NextAiring").AsDateTime().Nullable();
|
||||
|
||||
Create.TableForModel("Seasons")
|
||||
.WithColumn("SeriesId").AsInt32()
|
||||
.WithColumn("SeasonNumber").AsInt32()
|
||||
.WithColumn("Ignored").AsBoolean();
|
||||
|
||||
Create.TableForModel("Episodes")
|
||||
.WithColumn("TvDbEpisodeId").AsInt32().Unique()
|
||||
.WithColumn("SeriesId").AsInt32()
|
||||
.WithColumn("SeasonNumber").AsInt32()
|
||||
.WithColumn("EpisodeNumber").AsInt32()
|
||||
.WithColumn("Title").AsString().Nullable()
|
||||
.WithColumn("Overview").AsString().Nullable()
|
||||
.WithColumn("Ignored").AsBoolean().Nullable()
|
||||
.WithColumn("EpisodeFileId").AsInt32().Nullable()
|
||||
.WithColumn("AirDate").AsDateTime().Nullable()
|
||||
.WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable()
|
||||
.WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable()
|
||||
.WithColumn("SceneSeasonNumber").AsInt32().Nullable()
|
||||
.WithColumn("SceneEpisodeNumber").AsInt32().Nullable();
|
||||
|
||||
Create.TableForModel("EpisodeFiles")
|
||||
.WithColumn("SeriesId").AsInt32()
|
||||
.WithColumn("Path").AsString().Unique()
|
||||
.WithColumn("Quality").AsString()
|
||||
.WithColumn("Size").AsInt64()
|
||||
.WithColumn("DateAdded").AsDateTime()
|
||||
.WithColumn("SeasonNumber").AsInt32()
|
||||
.WithColumn("SceneName").AsString().Nullable()
|
||||
.WithColumn("ReleaseGroup").AsString().Nullable();
|
||||
|
||||
Create.TableForModel("History")
|
||||
.WithColumn("EpisodeId").AsInt32()
|
||||
.WithColumn("SeriesId").AsInt32()
|
||||
.WithColumn("NzbTitle").AsString()
|
||||
.WithColumn("Date").AsDateTime()
|
||||
.WithColumn("Quality").AsString()
|
||||
.WithColumn("Indexer").AsString()
|
||||
.WithColumn("NzbInfoUrl").AsString().Nullable()
|
||||
.WithColumn("ReleaseGroup").AsString().Nullable();
|
||||
|
||||
Create.TableForModel("Notifications")
|
||||
.WithColumn("Name").AsString()
|
||||
.WithColumn("OnGrab").AsBoolean()
|
||||
.WithColumn("OnDownload").AsBoolean()
|
||||
.WithColumn("Settings").AsString()
|
||||
.WithColumn("Implementation").AsString();
|
||||
|
||||
Create.TableForModel("ScheduledTasks")
|
||||
.WithColumn("TypeName").AsString().Unique()
|
||||
.WithColumn("Interval").AsInt32()
|
||||
.WithColumn("LastExecution").AsDateTime();
|
||||
|
||||
Create.TableForModel("Indexers")
|
||||
.WithColumn("Enable").AsBoolean()
|
||||
.WithColumn("Name").AsString().Unique()
|
||||
.WithColumn("Implementation").AsString()
|
||||
.WithColumn("Settings").AsString().Nullable();
|
||||
|
||||
Create.TableForModel("QualityProfiles")
|
||||
.WithColumn("Name").AsString().Unique()
|
||||
.WithColumn("Cutoff").AsInt32()
|
||||
.WithColumn("Allowed").AsString();
|
||||
|
||||
Create.TableForModel("QualitySizes")
|
||||
.WithColumn("QualityId").AsInt32().Unique()
|
||||
.WithColumn("Name").AsString().Unique()
|
||||
.WithColumn("MinSize").AsInt32()
|
||||
.WithColumn("MaxSize").AsInt32();
|
||||
|
||||
Create.TableForModel("SceneMappings")
|
||||
.WithColumn("CleanTitle").AsString()
|
||||
.WithColumn("SceneName").AsString()
|
||||
.WithColumn("TvdbId").AsInt32()
|
||||
.WithColumn("SeasonNumber").AsInt32();
|
||||
|
||||
|
||||
Create.TableForModel("NamingConfig")
|
||||
.WithColumn("UseSceneName").AsBoolean()
|
||||
.WithColumn("Separator").AsString()
|
||||
.WithColumn("NumberStyle").AsInt32()
|
||||
.WithColumn("IncludeSeriesTitle").AsBoolean()
|
||||
.WithColumn("MultiEpisodeStyle").AsInt32()
|
||||
.WithColumn("IncludeEpisodeTitle").AsBoolean()
|
||||
.WithColumn("IncludeQuality").AsBoolean()
|
||||
.WithColumn("ReplaceSpaces").AsBoolean()
|
||||
.WithColumn("SeasonFolderFormat").AsString();
|
||||
}
|
||||
|
||||
protected override void LogDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("Logs")
|
||||
.WithColumn("Message").AsString()
|
||||
.WithColumn("Time").AsDateTime()
|
||||
.WithColumn("Logger").AsString()
|
||||
.WithColumn("Method").AsString().Nullable()
|
||||
.WithColumn("Exception").AsString().Nullable()
|
||||
.WithColumn("ExceptionType").AsString().Nullable()
|
||||
.WithColumn("Level").AsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(2)]
|
||||
public class remove_tvrage_imdb_unique_constraint : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("DROP INDEX IX_Series_TvRageId;");
|
||||
Execute.Sql("DROP INDEX IX_Series_ImdbId;");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(3)]
|
||||
public class remove_renamed_scene_mapping_columns : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.Table("SceneMappings");
|
||||
|
||||
Create.TableForModel("SceneMappings")
|
||||
.WithColumn("TvdbId").AsInt32()
|
||||
.WithColumn("SeasonNumber").AsInt32()
|
||||
.WithColumn("SearchTerm").AsString()
|
||||
.WithColumn("ParseTerm").AsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(4)]
|
||||
public class updated_history : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.Table("History");
|
||||
|
||||
|
||||
Create.TableForModel("History")
|
||||
.WithColumn("EpisodeId").AsInt32()
|
||||
.WithColumn("SeriesId").AsInt32()
|
||||
.WithColumn("SourceTitle").AsString()
|
||||
.WithColumn("Date").AsDateTime()
|
||||
.WithColumn("Quality").AsString()
|
||||
.WithColumn("Data").AsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(5)]
|
||||
public class added_eventtype_to_history : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("History")
|
||||
.AddColumn("EventType")
|
||||
.AsInt32()
|
||||
.Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(6)]
|
||||
public class add_index_to_log_time : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void LogDbUpgrade()
|
||||
{
|
||||
Delete.Table("Logs");
|
||||
|
||||
Create.TableForModel("Logs")
|
||||
.WithColumn("Message").AsString()
|
||||
.WithColumn("Time").AsDateTime().Indexed()
|
||||
.WithColumn("Logger").AsString()
|
||||
.WithColumn("Method").AsString().Nullable()
|
||||
.WithColumn("Exception").AsString().Nullable()
|
||||
.WithColumn("ExceptionType").AsString().Nullable()
|
||||
.WithColumn("Level").AsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(7)]
|
||||
public class add_renameEpisodes_to_naming : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("NamingConfig")
|
||||
.AddColumn("RenameEpisodes")
|
||||
.AsBoolean()
|
||||
.Nullable();
|
||||
|
||||
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes =~ UseSceneName");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(8)]
|
||||
public class remove_backlog : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
SqLiteAlter.DropColumns("Series", new[] { "BacklogSetting" });
|
||||
SqLiteAlter.DropColumns("NamingConfig", new[] { "UseSceneName" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(9)]
|
||||
public class fix_rename_episodes : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
SqLiteAlter.DropColumns("NamingConfig", new[] { "SeasonFolderFormat" });
|
||||
|
||||
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 1 WHERE RenameEpisodes = -1");
|
||||
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 0 WHERE RenameEpisodes = -2");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(10)]
|
||||
public class add_monitored : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Episodes").AddColumn("Monitored").AsBoolean().Nullable();
|
||||
Alter.Table("Seasons").AddColumn("Monitored").AsBoolean().Nullable();
|
||||
|
||||
Execute.Sql("UPDATE Episodes SET Monitored = 1 WHERE Ignored = 0");
|
||||
Execute.Sql("UPDATE Episodes SET Monitored = 0 WHERE Ignored = 1");
|
||||
|
||||
Execute.Sql("UPDATE Seasons SET Monitored = 1 WHERE Ignored = 0");
|
||||
Execute.Sql("UPDATE Seasons SET Monitored = 0 WHERE Ignored = 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(11)]
|
||||
public class remove_ignored : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
SqLiteAlter.DropColumns("Episodes", new[] { "Ignored" });
|
||||
SqLiteAlter.DropColumns("Seasons", new[] { "Ignored" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(12)]
|
||||
public class remove_custom_start_date : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
SqLiteAlter.DropColumns("Series", new[] { "CustomStartDate" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(13)]
|
||||
public class add_air_date_utc : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Episodes").AddColumn("AirDateUtc").AsDateTime().Nullable();
|
||||
|
||||
Execute.Sql("UPDATE Episodes SET AirDateUtc = AirDate");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(14)]
|
||||
public class drop_air_date : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
SqLiteAlter.DropColumns("Episodes", new []{ "AirDate" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(15)]
|
||||
public class add_air_date_as_string : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Episodes").AddColumn("AirDate").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(16)]
|
||||
public class updated_imported_history_item : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql(@"UPDATE HISTORY SET Data = replace( Data, '""Path""', '""ImportedPath""' ) WHERE EventType=3");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(17)]
|
||||
public class reset_scene_names : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
//we were storing new file name as scene name.
|
||||
Execute.Sql(@"UPDATE EpisodeFiles SET SceneName = NULL where SceneName != NULL");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(18)]
|
||||
public class remove_duplicates : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
using (var transaction = MigrationHelper.BeginTransaction())
|
||||
{
|
||||
RemoveDuplicateSeries<int>("TvdbId");
|
||||
RemoveDuplicateSeries<string>("TitleSlug");
|
||||
|
||||
var duplicatedEpisodes = MigrationHelper.GetDuplicates<int>("Episodes", "TvDbEpisodeId");
|
||||
|
||||
foreach (var duplicate in duplicatedEpisodes)
|
||||
{
|
||||
foreach (var episodeId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key))
|
||||
{
|
||||
RemoveEpisodeRows(episodeId);
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDuplicateSeries<T>(string field)
|
||||
{
|
||||
var duplicatedSeries = MigrationHelper.GetDuplicates<T>("Series", field);
|
||||
|
||||
foreach (var duplicate in duplicatedSeries)
|
||||
{
|
||||
foreach (var seriesId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key))
|
||||
{
|
||||
RemoveSeriesRows(seriesId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveSeriesRows(int seriesId)
|
||||
{
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Series WHERE Id = {0}", seriesId.ToString());
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Episodes WHERE SeriesId = {0}", seriesId.ToString());
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Seasons WHERE SeriesId = {0}", seriesId.ToString());
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM History WHERE SeriesId = {0}", seriesId.ToString());
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM EpisodeFiles WHERE SeriesId = {0}", seriesId.ToString());
|
||||
}
|
||||
|
||||
private void RemoveEpisodeRows(int episodeId)
|
||||
{
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Episodes WHERE Id = {0}", episodeId.ToString());
|
||||
MigrationHelper.ExecuteNonQuery("DELETE FROM History WHERE EpisodeId = {0}", episodeId.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(19)]
|
||||
public class restore_unique_constraints : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
SqLiteAlter.AddIndexes("Series",
|
||||
new SQLiteIndex { Column = "TvdbId", Table = "Series", Unique = true },
|
||||
new SQLiteIndex { Column = "TitleSlug", Table = "Series", Unique = true });
|
||||
|
||||
SqLiteAlter.AddIndexes("Episodes",
|
||||
new SQLiteIndex { Column = "TvDbEpisodeId", Table = "Episodes", Unique = true });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(20)]
|
||||
public class add_year_and_seasons_to_series : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Series").AddColumn("Year").AsInt32().Nullable();
|
||||
Alter.Table("Series").AddColumn("Seasons").AsString().Nullable();
|
||||
|
||||
Execute.WithConnection(ConvertSeasons);
|
||||
}
|
||||
|
||||
private void ConvertSeasons(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (IDbCommand allSeriesCmd = conn.CreateCommand())
|
||||
{
|
||||
allSeriesCmd.Transaction = tran;
|
||||
allSeriesCmd.CommandText = @"SELECT Id FROM Series";
|
||||
using (IDataReader allSeriesReader = allSeriesCmd.ExecuteReader())
|
||||
{
|
||||
while (allSeriesReader.Read())
|
||||
{
|
||||
int seriesId = allSeriesReader.GetInt32(0);
|
||||
var seasons = new List<dynamic>();
|
||||
|
||||
using (IDbCommand seasonsCmd = conn.CreateCommand())
|
||||
{
|
||||
seasonsCmd.Transaction = tran;
|
||||
seasonsCmd.CommandText = String.Format(@"SELECT SeasonNumber, Monitored FROM Seasons WHERE SeriesId = {0}", seriesId);
|
||||
|
||||
using (IDataReader seasonReader = seasonsCmd.ExecuteReader())
|
||||
{
|
||||
while (seasonReader.Read())
|
||||
{
|
||||
int seasonNumber = seasonReader.GetInt32(0);
|
||||
bool monitored = seasonReader.GetBoolean(1);
|
||||
|
||||
if (seasonNumber == 0)
|
||||
{
|
||||
monitored = false;
|
||||
}
|
||||
|
||||
seasons.Add(new { seasonNumber, monitored });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (IDbCommand updateCmd = conn.CreateCommand())
|
||||
{
|
||||
var text = String.Format("UPDATE Series SET Seasons = '{0}' WHERE Id = {1}", seasons.ToJson() , seriesId);
|
||||
|
||||
updateCmd.Transaction = tran;
|
||||
updateCmd.CommandText = text;
|
||||
updateCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(21)]
|
||||
public class drop_seasons_table : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.Table("Seasons");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(22)]
|
||||
public class move_indexer_to_generic_provider : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Indexers").AddColumn("ConfigContract").AsString().Nullable();
|
||||
|
||||
//Execute.WithConnection(ConvertSeasons);
|
||||
}
|
||||
|
||||
private void ConvertSeasons(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (IDbCommand allSeriesCmd = conn.CreateCommand())
|
||||
{
|
||||
allSeriesCmd.Transaction = tran;
|
||||
allSeriesCmd.CommandText = @"SELECT Id FROM Series";
|
||||
using (IDataReader allSeriesReader = allSeriesCmd.ExecuteReader())
|
||||
{
|
||||
while (allSeriesReader.Read())
|
||||
{
|
||||
int seriesId = allSeriesReader.GetInt32(0);
|
||||
var seasons = new List<dynamic>();
|
||||
|
||||
using (IDbCommand seasonsCmd = conn.CreateCommand())
|
||||
{
|
||||
seasonsCmd.Transaction = tran;
|
||||
seasonsCmd.CommandText = String.Format(@"SELECT SeasonNumber, Monitored FROM Seasons WHERE SeriesId = {0}", seriesId);
|
||||
|
||||
using (IDataReader seasonReader = seasonsCmd.ExecuteReader())
|
||||
{
|
||||
while (seasonReader.Read())
|
||||
{
|
||||
int seasonNumber = seasonReader.GetInt32(0);
|
||||
bool monitored = seasonReader.GetBoolean(1);
|
||||
|
||||
if (seasonNumber == 0)
|
||||
{
|
||||
monitored = false;
|
||||
}
|
||||
|
||||
seasons.Add(new { seasonNumber, monitored });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (IDbCommand updateCmd = conn.CreateCommand())
|
||||
{
|
||||
var text = String.Format("UPDATE Series SET Seasons = '{0}' WHERE Id = {1}", seasons.ToJson(), seriesId);
|
||||
|
||||
updateCmd.Transaction = tran;
|
||||
updateCmd.CommandText = text;
|
||||
updateCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(23)]
|
||||
public class add_config_contract_to_indexers : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "NewznabSettings" }).Where(new { Implementation = "Newznab" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "OmgwtfnzbsSettings" }).Where(new { Implementation = "Omgwtfnzbs" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "NullConfig" }).Where(new { Implementation = "Wombles" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "NullConfig" }).Where(new { Implementation = "Eztv" });
|
||||
|
||||
Delete.FromTable("Indexers").IsNull("ConfigContract");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public class MigrationContext
|
||||
{
|
||||
public MigrationType MigrationType { get; set; }
|
||||
public ISQLiteAlter SQLiteAlter { get; set; }
|
||||
public ISqLiteMigrationHelper MigrationHelper { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using FluentMigrator.Runner;
|
||||
using FluentMigrator.Runner.Initialization;
|
||||
using FluentMigrator.Runner.Processors.Sqlite;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public interface IMigrationController
|
||||
{
|
||||
void MigrateToLatest(string connectionString, MigrationType migrationType);
|
||||
}
|
||||
|
||||
public class MigrationController : IMigrationController
|
||||
{
|
||||
private readonly IAnnouncer _announcer;
|
||||
private readonly ISQLiteAlter _sqLiteAlter;
|
||||
private readonly ISqLiteMigrationHelper _migrationHelper;
|
||||
|
||||
private static readonly HashSet<string> MigrationCache = new HashSet<string>();
|
||||
|
||||
public MigrationController(IAnnouncer announcer, ISQLiteAlter sqLiteAlter, ISqLiteMigrationHelper migrationHelper)
|
||||
{
|
||||
_announcer = announcer;
|
||||
_sqLiteAlter = sqLiteAlter;
|
||||
_migrationHelper = migrationHelper;
|
||||
}
|
||||
|
||||
public void MigrateToLatest(string connectionString, MigrationType migrationType)
|
||||
{
|
||||
lock (MigrationCache)
|
||||
{
|
||||
if (MigrationCache.Contains(connectionString.ToLower())) return;
|
||||
|
||||
_announcer.Heading("Migrating " + connectionString);
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var migrationContext = new RunnerContext(_announcer)
|
||||
{
|
||||
Namespace = "NzbDrone.Core.Datastore.Migration",
|
||||
ApplicationContext = new MigrationContext
|
||||
{
|
||||
MigrationType = migrationType,
|
||||
SQLiteAlter = _sqLiteAlter,
|
||||
MigrationHelper = _migrationHelper,
|
||||
}
|
||||
};
|
||||
|
||||
var options = new MigrationOptions { PreviewOnly = false, Timeout = 60 };
|
||||
var factory = new SqliteProcessorFactory();
|
||||
var processor = factory.Create(connectionString, _announcer, options);
|
||||
var runner = new MigrationRunner(assembly, migrationContext, processor);
|
||||
runner.MigrateUp(true);
|
||||
|
||||
MigrationCache.Add(connectionString.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator.Builders.Create;
|
||||
using FluentMigrator.Builders.Create.Table;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public static class MigrationExtension
|
||||
{
|
||||
public static ICreateTableColumnOptionOrWithColumnSyntax TableForModel(this ICreateExpressionRoot expressionRoot, string name)
|
||||
{
|
||||
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using FluentMigrator.Runner;
|
||||
using NLog;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public class MigrationLogger : IAnnouncer
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
|
||||
public MigrationLogger(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public void Heading(string message)
|
||||
{
|
||||
_logger.Info("*** {0} ***", message);
|
||||
}
|
||||
|
||||
public void Say(string message)
|
||||
{
|
||||
_logger.Debug(message);
|
||||
}
|
||||
|
||||
public void Emphasize(string message)
|
||||
{
|
||||
_logger.Warn(message);
|
||||
}
|
||||
|
||||
public void Sql(string sql)
|
||||
{
|
||||
_logger.Trace(sql);
|
||||
}
|
||||
|
||||
public void ElapsedTime(TimeSpan timeSpan)
|
||||
{
|
||||
}
|
||||
|
||||
public void Error(string message)
|
||||
{
|
||||
_logger.Error(message);
|
||||
}
|
||||
|
||||
public void Write(string message, bool escaped)
|
||||
{
|
||||
_logger.Info(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using FluentMigrator;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public class MigrationOptions : IMigrationProcessorOptions
|
||||
{
|
||||
public bool PreviewOnly { get; set; }
|
||||
public int Timeout { get; set; }
|
||||
public string ProviderSwitches { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public abstract class NzbDroneMigrationBase : FluentMigrator.Migration
|
||||
{
|
||||
private Logger _logger;
|
||||
|
||||
protected NzbDroneMigrationBase()
|
||||
{
|
||||
_logger = NzbDroneLogger.GetLogger();
|
||||
}
|
||||
|
||||
protected virtual void MainDbUpgrade()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void LogDbUpgrade()
|
||||
{
|
||||
}
|
||||
|
||||
public override void Up()
|
||||
{
|
||||
var context = (MigrationContext)ApplicationContext;
|
||||
|
||||
SqLiteAlter = context.SQLiteAlter;
|
||||
MigrationHelper = context.MigrationHelper;
|
||||
|
||||
switch (context.MigrationType)
|
||||
{
|
||||
case MigrationType.Main:
|
||||
MainDbUpgrade();
|
||||
return;
|
||||
case MigrationType.Log:
|
||||
LogDbUpgrade();
|
||||
return;
|
||||
default:
|
||||
LogDbUpgrade();
|
||||
MainDbUpgrade();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected ISQLiteAlter SqLiteAlter { get; private set; }
|
||||
protected ISqLiteMigrationHelper MigrationHelper { get; private set; }
|
||||
|
||||
public override void Down()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public class SQLiteColumn
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Schema { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] {1}", Name, Schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public class SQLiteIndex : IEquatable<SQLiteIndex>
|
||||
{
|
||||
public string Column { get; set; }
|
||||
public string Table { get; set; }
|
||||
public bool Unique { get; set; }
|
||||
|
||||
public bool Equals(SQLiteIndex other)
|
||||
{
|
||||
return IndexName == other.IndexName;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return IndexName.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] Unique: {1}", Column, Unique);
|
||||
}
|
||||
|
||||
public string IndexName
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("IX_{0}_{1}", Table, Column);
|
||||
}
|
||||
}
|
||||
|
||||
public string CreateSql(string tableName)
|
||||
{
|
||||
return string.Format(@"CREATE UNIQUE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public interface ISQLiteMigrationHelper
|
||||
{
|
||||
Dictionary<String, SQLiteMigrationHelper.SQLiteColumn> GetColumns(string tableName);
|
||||
void CreateTable(string tableName, IEnumerable<SQLiteMigrationHelper.SQLiteColumn> values, IEnumerable<SQLiteMigrationHelper.SQLiteIndex> indexes);
|
||||
void CopyData(string sourceTable, string destinationTable, IEnumerable<SQLiteMigrationHelper.SQLiteColumn> columns);
|
||||
void DropTable(string tableName);
|
||||
void RenameTable(string tableName, string newName);
|
||||
List<T> GetDuplicates<T>(string tableName, string columnName);
|
||||
SQLiteTransaction BeginTransaction();
|
||||
List<SQLiteMigrationHelper.SQLiteIndex> GetIndexes(string tableName);
|
||||
}
|
||||
|
||||
public class SQLiteMigrationHelper : ISQLiteMigrationHelper
|
||||
{
|
||||
private readonly SQLiteConnection _connection;
|
||||
|
||||
private static readonly Regex SchemaRegex = new Regex(@"['\""\[](?<name>\w+)['\""\]]\s(?<schema>[\w-\s]+)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
|
||||
private static readonly Regex IndexRegex = new Regex(@"\(""(?<col>.*)""\s(?<direction>ASC|DESC)\)$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
|
||||
public SQLiteMigrationHelper(IConnectionStringFactory connectionStringFactory, Logger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
_connection = new SQLiteConnection(connectionStringFactory.MainDbConnectionString);
|
||||
_connection.Open();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.ErrorException("Couldn't open database " + connectionStringFactory.MainDbConnectionString, e);
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private string GetOriginalSql(string tableName)
|
||||
{
|
||||
var command =
|
||||
new SQLiteCommand(string.Format("SELECT sql FROM sqlite_master WHERE type='table' AND name ='{0}'",
|
||||
tableName));
|
||||
|
||||
command.Connection = _connection;
|
||||
|
||||
return (string)command.ExecuteScalar();
|
||||
}
|
||||
|
||||
public Dictionary<String, SQLiteColumn> GetColumns(string tableName)
|
||||
{
|
||||
var originalSql = GetOriginalSql(tableName);
|
||||
|
||||
var matches = SchemaRegex.Matches(originalSql);
|
||||
|
||||
return matches.Cast<Match>().ToDictionary(
|
||||
match => match.Groups["name"].Value.Trim(),
|
||||
match => new SQLiteColumn
|
||||
{
|
||||
Name = match.Groups["name"].Value.Trim(),
|
||||
Schema = match.Groups["schema"].Value.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<T> ReadArray<T>(SQLiteDataReader reader)
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
yield return (T)Convert.ChangeType(reader[0], typeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
public List<SQLiteIndex> GetIndexes(string tableName)
|
||||
{
|
||||
var command = new SQLiteCommand(string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name ='{0}'", tableName));
|
||||
command.Connection = _connection;
|
||||
|
||||
var reader = command.ExecuteReader();
|
||||
var sqls = ReadArray<string>(reader).ToList();
|
||||
|
||||
|
||||
var indexes = new List<SQLiteIndex>();
|
||||
|
||||
foreach (var indexSql in sqls)
|
||||
{
|
||||
var newIndex = new SQLiteIndex();
|
||||
var matches = IndexRegex.Match(indexSql);
|
||||
|
||||
newIndex.Column = matches.Groups["col"].Value;
|
||||
newIndex.Unique = indexSql.Contains("UNIQUE");
|
||||
newIndex.Table = tableName;
|
||||
|
||||
indexes.Add(newIndex);
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
public void CreateTable(string tableName, IEnumerable<SQLiteColumn> values, IEnumerable<SQLiteIndex> indexes)
|
||||
{
|
||||
var columns = String.Join(",", values.Select(c => c.ToString()));
|
||||
|
||||
ExecuteNonQuery("CREATE TABLE [{0}] ({1})", tableName, columns);
|
||||
|
||||
foreach (var index in indexes)
|
||||
{
|
||||
ExecuteNonQuery("DROP INDEX {0}", index.IndexName);
|
||||
ExecuteNonQuery(index.CreateSql(tableName));
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyData(string sourceTable, string destinationTable, IEnumerable<SQLiteColumn> columns)
|
||||
{
|
||||
var originalCount = GetRowCount(sourceTable);
|
||||
|
||||
var columnsToTransfer = String.Join(",", columns.Select(c => c.Name));
|
||||
|
||||
var transferCommand = BuildCommand("INSERT INTO {0} SELECT {1} FROM {2};", destinationTable, columnsToTransfer, sourceTable);
|
||||
|
||||
transferCommand.ExecuteNonQuery();
|
||||
|
||||
var transferredRows = GetRowCount(destinationTable);
|
||||
|
||||
|
||||
if (transferredRows != originalCount)
|
||||
{
|
||||
throw new ApplicationException(string.Format("Expected {0} rows to be copied from [{1}] to [{2}]. But only copied {3}", originalCount, sourceTable, destinationTable, transferredRows));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void DropTable(string tableName)
|
||||
{
|
||||
var dropCommand = BuildCommand("DROP TABLE {0};", tableName);
|
||||
dropCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
|
||||
public void RenameTable(string tableName, string newName)
|
||||
{
|
||||
var renameCommand = BuildCommand("ALTER TABLE {0} RENAME TO {1};", tableName, newName);
|
||||
renameCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public Dictionary<int,T> GetDuplicates<T>(string tableName, string columnName)
|
||||
{
|
||||
var dupCommand = BuildCommand("select id, {0} from {1}", columnName, tableName);
|
||||
|
||||
var result = new Dictionary<int, T>();
|
||||
using (var reader = dupCommand.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ReadArray<T>().ToList();
|
||||
}
|
||||
|
||||
public int GetRowCount(string tableName)
|
||||
{
|
||||
var countCommand = BuildCommand("SELECT COUNT(*) FROM {0};", tableName);
|
||||
return Convert.ToInt32(countCommand.ExecuteScalar());
|
||||
}
|
||||
|
||||
|
||||
public SQLiteTransaction BeginTransaction()
|
||||
{
|
||||
return _connection.BeginTransaction();
|
||||
}
|
||||
|
||||
private SQLiteCommand BuildCommand(string format, params string[] args)
|
||||
{
|
||||
var command = new SQLiteCommand(string.Format(format, args));
|
||||
command.Connection = _connection;
|
||||
return command;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void ExecuteNonQuery(string command, params string[] args)
|
||||
{
|
||||
var sqLiteCommand = new SQLiteCommand(string.Format(command, args))
|
||||
{
|
||||
Connection = _connection
|
||||
};
|
||||
|
||||
sqLiteCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
|
||||
public class SQLiteColumn
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Schema { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] {1}", Name, Schema);
|
||||
}
|
||||
}
|
||||
|
||||
public class SQLiteIndex
|
||||
{
|
||||
public string Column { get; set; }
|
||||
public string Table { get; set; }
|
||||
public bool Unique { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] Unique: {1}", Column, Unique);
|
||||
}
|
||||
|
||||
public string IndexName
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("IX_{0}_{1}", Table, Column);
|
||||
}
|
||||
}
|
||||
|
||||
public string CreateSql(string tableName)
|
||||
{
|
||||
return string.Format(@"CREATE UNIQUE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public interface ISqLiteMigrationHelper
|
||||
{
|
||||
Dictionary<String, SQLiteColumn> GetColumns(string tableName);
|
||||
void CreateTable(string tableName, IEnumerable<SQLiteColumn> values, IEnumerable<SQLiteIndex> indexes);
|
||||
void CopyData(string sourceTable, string destinationTable, IEnumerable<SQLiteColumn> columns);
|
||||
void DropTable(string tableName);
|
||||
void RenameTable(string tableName, string newName);
|
||||
IEnumerable<IGrouping<T, KeyValuePair<int, T>>> GetDuplicates<T>(string tableName, string columnName);
|
||||
SQLiteTransaction BeginTransaction();
|
||||
List<SQLiteIndex> GetIndexes(string tableName);
|
||||
int ExecuteScalar(string command, params string[] args);
|
||||
void ExecuteNonQuery(string command, params string[] args);
|
||||
}
|
||||
|
||||
public class SqLiteMigrationHelper : ISqLiteMigrationHelper
|
||||
{
|
||||
private readonly SQLiteConnection _connection;
|
||||
|
||||
private static readonly Regex SchemaRegex = new Regex(@"['\""\[](?<name>\w+)['\""\]]\s(?<schema>[\w-\s]+)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
|
||||
private static readonly Regex IndexRegex = new Regex(@"\(""(?<col>.*)""\s(?<direction>ASC|DESC)\)$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
|
||||
public SqLiteMigrationHelper(IConnectionStringFactory connectionStringFactory, Logger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
_connection = new SQLiteConnection(connectionStringFactory.MainDbConnectionString);
|
||||
_connection.Open();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.ErrorException("Couldn't open database " + connectionStringFactory.MainDbConnectionString, e);
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private string GetOriginalSql(string tableName)
|
||||
{
|
||||
var command =
|
||||
new SQLiteCommand(string.Format("SELECT sql FROM sqlite_master WHERE type='table' AND name ='{0}'",
|
||||
tableName));
|
||||
|
||||
command.Connection = _connection;
|
||||
|
||||
var sql = (string)command.ExecuteScalar();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new TableNotFoundException(tableName);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
public Dictionary<String, SQLiteColumn> GetColumns(string tableName)
|
||||
{
|
||||
var originalSql = GetOriginalSql(tableName);
|
||||
|
||||
var matches = SchemaRegex.Matches(originalSql);
|
||||
|
||||
return matches.Cast<Match>().ToDictionary(
|
||||
match => match.Groups["name"].Value.Trim(),
|
||||
match => new SQLiteColumn
|
||||
{
|
||||
Name = match.Groups["name"].Value.Trim(),
|
||||
Schema = match.Groups["schema"].Value.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<T> ReadArray<T>(SQLiteDataReader reader)
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
yield return (T)Convert.ChangeType(reader[0], typeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
public List<SQLiteIndex> GetIndexes(string tableName)
|
||||
{
|
||||
var command = new SQLiteCommand(string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name ='{0}'", tableName));
|
||||
command.Connection = _connection;
|
||||
|
||||
var reader = command.ExecuteReader();
|
||||
var sqls = ReadArray<string>(reader).ToList();
|
||||
|
||||
|
||||
var indexes = new List<SQLiteIndex>();
|
||||
|
||||
foreach (var indexSql in sqls)
|
||||
{
|
||||
var newIndex = new SQLiteIndex();
|
||||
var matches = IndexRegex.Match(indexSql);
|
||||
|
||||
newIndex.Column = matches.Groups["col"].Value;
|
||||
newIndex.Unique = indexSql.Contains("UNIQUE");
|
||||
newIndex.Table = tableName;
|
||||
|
||||
indexes.Add(newIndex);
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
public void CreateTable(string tableName, IEnumerable<SQLiteColumn> values, IEnumerable<SQLiteIndex> indexes)
|
||||
{
|
||||
var columns = String.Join(",", values.Select(c => c.ToString()));
|
||||
|
||||
ExecuteNonQuery("CREATE TABLE [{0}] ({1})", tableName, columns);
|
||||
|
||||
foreach (var index in indexes)
|
||||
{
|
||||
ExecuteNonQuery("DROP INDEX IF EXISTS {0}", index.IndexName);
|
||||
ExecuteNonQuery(index.CreateSql(tableName));
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyData(string sourceTable, string destinationTable, IEnumerable<SQLiteColumn> columns)
|
||||
{
|
||||
var originalCount = GetRowCount(sourceTable);
|
||||
|
||||
var columnsToTransfer = String.Join(",", columns.Select(c => c.Name));
|
||||
|
||||
var transferCommand = BuildCommand("INSERT INTO {0} SELECT {1} FROM {2};", destinationTable, columnsToTransfer, sourceTable);
|
||||
|
||||
transferCommand.ExecuteNonQuery();
|
||||
|
||||
var transferredRows = GetRowCount(destinationTable);
|
||||
|
||||
|
||||
if (transferredRows != originalCount)
|
||||
{
|
||||
throw new ApplicationException(string.Format("Expected {0} rows to be copied from [{1}] to [{2}]. But only copied {3}", originalCount, sourceTable, destinationTable, transferredRows));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void DropTable(string tableName)
|
||||
{
|
||||
var dropCommand = BuildCommand("DROP TABLE {0};", tableName);
|
||||
dropCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
|
||||
public void RenameTable(string tableName, string newName)
|
||||
{
|
||||
var renameCommand = BuildCommand("ALTER TABLE {0} RENAME TO {1};", tableName, newName);
|
||||
renameCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public IEnumerable<IGrouping<T, KeyValuePair<int, T>>> GetDuplicates<T>(string tableName, string columnName)
|
||||
{
|
||||
var getDuplicates = BuildCommand("select id, {0} from {1}", columnName, tableName);
|
||||
|
||||
var result = new List<KeyValuePair<int, T>>();
|
||||
|
||||
using (var reader = getDuplicates.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
result.Add(new KeyValuePair<int, T>(reader.GetInt32(0), (T)Convert.ChangeType(reader[1], typeof(T))));
|
||||
}
|
||||
}
|
||||
|
||||
return result.GroupBy(c => c.Value).Where(g => g.Count() > 1);
|
||||
}
|
||||
|
||||
public int GetRowCount(string tableName)
|
||||
{
|
||||
var countCommand = BuildCommand("SELECT COUNT(*) FROM {0};", tableName);
|
||||
return Convert.ToInt32(countCommand.ExecuteScalar());
|
||||
}
|
||||
|
||||
|
||||
public SQLiteTransaction BeginTransaction()
|
||||
{
|
||||
return _connection.BeginTransaction();
|
||||
}
|
||||
|
||||
private SQLiteCommand BuildCommand(string format, params string[] args)
|
||||
{
|
||||
var command = new SQLiteCommand(string.Format(format, args));
|
||||
command.Connection = _connection;
|
||||
return command;
|
||||
}
|
||||
|
||||
|
||||
public void ExecuteNonQuery(string command, params string[] args)
|
||||
{
|
||||
var sqLiteCommand = new SQLiteCommand(string.Format(command, args))
|
||||
{
|
||||
Connection = _connection
|
||||
};
|
||||
|
||||
sqLiteCommand.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public int ExecuteScalar(string command, params string[] args)
|
||||
{
|
||||
var sqLiteCommand = new SQLiteCommand(string.Format(command, args))
|
||||
{
|
||||
Connection = _connection
|
||||
};
|
||||
|
||||
return (int)sqLiteCommand.ExecuteScalar();
|
||||
}
|
||||
|
||||
private class TableNotFoundException : NzbDroneException
|
||||
{
|
||||
public TableNotFoundException(string tableName)
|
||||
: base("Table [{0}] not found", tableName)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
public interface ISQLiteAlter
|
||||
{
|
||||
void DropColumns(string tableName, IEnumerable<string> columns);
|
||||
void AddIndexes(string tableName, params SQLiteIndex[] indexes);
|
||||
}
|
||||
|
||||
public class SQLiteAlter : ISQLiteAlter
|
||||
{
|
||||
private readonly ISqLiteMigrationHelper _sqLiteMigrationHelper;
|
||||
|
||||
public SQLiteAlter(ISqLiteMigrationHelper sqLiteMigrationHelper)
|
||||
{
|
||||
_sqLiteMigrationHelper = sqLiteMigrationHelper;
|
||||
}
|
||||
|
||||
public void DropColumns(string tableName, IEnumerable<string> columns)
|
||||
{
|
||||
using (var transaction = _sqLiteMigrationHelper.BeginTransaction())
|
||||
{
|
||||
var originalColumns = _sqLiteMigrationHelper.GetColumns(tableName);
|
||||
var originalIndexes = _sqLiteMigrationHelper.GetIndexes(tableName);
|
||||
|
||||
var newColumns = originalColumns.Where(c => !columns.Contains(c.Key)).Select(c => c.Value).ToList();
|
||||
var newIndexes = originalIndexes.Where(c => !columns.Contains(c.Column));
|
||||
|
||||
CreateTable(tableName, newColumns, newIndexes);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddIndexes(string tableName, params SQLiteIndex[] indexes)
|
||||
{
|
||||
using (var transaction = _sqLiteMigrationHelper.BeginTransaction())
|
||||
{
|
||||
var columns = _sqLiteMigrationHelper.GetColumns(tableName).Select(c => c.Value).ToList();
|
||||
var originalIndexes = _sqLiteMigrationHelper.GetIndexes(tableName);
|
||||
|
||||
var newIndexes = originalIndexes.Union(indexes);
|
||||
|
||||
|
||||
CreateTable(tableName, columns, newIndexes);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateTable(string tableName, List<SQLiteColumn> newColumns, IEnumerable<SQLiteIndex> newIndexes)
|
||||
{
|
||||
var tempTableName = tableName + "_temp";
|
||||
|
||||
_sqLiteMigrationHelper.CreateTable(tempTableName, newColumns, newIndexes);
|
||||
|
||||
_sqLiteMigrationHelper.CopyData(tableName, tempTableName, newColumns);
|
||||
|
||||
_sqLiteMigrationHelper.DropTable(tableName);
|
||||
|
||||
_sqLiteMigrationHelper.RenameTable(tempTableName, tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public enum MigrationType
|
||||
{
|
||||
Main,
|
||||
Log
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
[DebuggerDisplay("{GetType()} ID = {Id}")]
|
||||
public abstract class ModelBase
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class ModelNotFoundException : NzbDroneException
|
||||
{
|
||||
public ModelNotFoundException(Type modelType, int modelId)
|
||||
: base("{0} with ID {1} does not exist", modelType.Name, modelId)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class PagingSpec<TModel>
|
||||
{
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalRecords { get; set; }
|
||||
public string SortKey { get; set; }
|
||||
public SortDirection SortDirection { get; set; }
|
||||
public List<TModel> Records { get; set; }
|
||||
public Expression<Func<TModel, bool>> FilterExpression { get; set; }
|
||||
}
|
||||
|
||||
public enum SortDirection
|
||||
{
|
||||
Ascending,
|
||||
Descending
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public static class PagingSpecExtensions
|
||||
{
|
||||
public static Expression<Func<TModel, object>> OrderByClause<TModel>(this PagingSpec<TModel> pagingSpec)
|
||||
{
|
||||
return CreateExpression<TModel>(pagingSpec.SortKey);
|
||||
}
|
||||
|
||||
public static int PagingOffset<TModel>(this PagingSpec<TModel> pagingSpec)
|
||||
{
|
||||
return (pagingSpec.Page - 1)*pagingSpec.PageSize;
|
||||
}
|
||||
|
||||
public static Marr.Data.QGen.SortDirection ToSortDirection<TModel>(this PagingSpec<TModel> pagingSpec)
|
||||
{
|
||||
if (pagingSpec.SortDirection == SortDirection.Descending) return Marr.Data.QGen.SortDirection.Desc;
|
||||
|
||||
return Marr.Data.QGen.SortDirection.Asc;
|
||||
}
|
||||
|
||||
private static Expression<Func<TModel, object>> CreateExpression<TModel>(string propertyName)
|
||||
{
|
||||
Type type = typeof(TModel);
|
||||
ParameterExpression parameterExpression = Expression.Parameter(type, "x");
|
||||
Expression expressionBody = parameterExpression;
|
||||
|
||||
var splitPropertyName = propertyName.Split('.').ToList();
|
||||
|
||||
foreach (var property in splitPropertyName)
|
||||
{
|
||||
expressionBody = Expression.Property(expressionBody, property);
|
||||
}
|
||||
|
||||
expressionBody = Expression.Convert(expressionBody, typeof(object));
|
||||
return Expression.Lambda<Func<TModel, object>>(expressionBody, parameterExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Marr.Data;
|
||||
using Marr.Data.Mapping;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public static class RelationshipExtensions
|
||||
{
|
||||
public static RelationshipBuilder<TParent> HasOne<TParent, TChild>(this RelationshipBuilder<TParent> relationshipBuilder, Expression<Func<TParent, LazyLoaded<TChild>>> portalExpression, Func<TParent, int> childIdSelector)
|
||||
where TParent : ModelBase
|
||||
where TChild : ModelBase
|
||||
{
|
||||
return relationshipBuilder.For(portalExpression.GetMemberName())
|
||||
.LazyLoad(
|
||||
query: (db, parent) => db.Query<TChild>().SingleOrDefault(c => c.Id == childIdSelector(parent)),
|
||||
condition: parent => childIdSelector(parent) > 0
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public static RelationshipBuilder<TParent> Relationship<TParent>(this ColumnMapBuilder<TParent> mapBuilder)
|
||||
{
|
||||
return mapBuilder.Relationships.AutoMapComplexTypeProperties<ILazyLoaded>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static RelationshipBuilder<TParent> HasMany<TParent, TChild>(this RelationshipBuilder<TParent> relationshipBuilder, Expression<Func<TParent, LazyList<TChild>>> portalExpression, Func<TParent, int> childIdSelector)
|
||||
where TParent : ModelBase
|
||||
where TChild : ModelBase
|
||||
{
|
||||
return relationshipBuilder.For(portalExpression.GetMemberName())
|
||||
.LazyLoad((db, parent) => db.Query<TChild>().Where(c => c.Id == childIdSelector(parent)).ToList());
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static string GetMemberName<T, TMember>(this Expression<Func<T, TMember>> member)
|
||||
{
|
||||
var expression = member.Body as MemberExpression;
|
||||
|
||||
if (expression == null)
|
||||
{
|
||||
expression = (MemberExpression)((UnaryExpression)member.Body).Operand;
|
||||
}
|
||||
|
||||
return expression.Member.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class ResultSet
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Marr.Data;
|
||||
using Marr.Data.Mapping;
|
||||
using NzbDrone.Common.Reflection;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DataAugmentation.Scene;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Jobs;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Notifications;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.SeriesStats;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public static class TableMapping
|
||||
{
|
||||
|
||||
private static readonly FluentMappings Mapper = new FluentMappings(true);
|
||||
|
||||
public static void Map()
|
||||
{
|
||||
RegisterMappers();
|
||||
|
||||
Mapper.Entity<Config>().RegisterModel("Config");
|
||||
Mapper.Entity<RootFolder>().RegisterModel("RootFolders").Ignore(r => r.FreeSpace);
|
||||
|
||||
Mapper.Entity<IndexerDefinition>().RegisterModel("Indexers");
|
||||
Mapper.Entity<ScheduledTask>().RegisterModel("ScheduledTasks");
|
||||
Mapper.Entity<NotificationDefinition>().RegisterModel("Notifications");
|
||||
|
||||
Mapper.Entity<SceneMapping>().RegisterModel("SceneMappings");
|
||||
|
||||
Mapper.Entity<History.History>().RegisterModel("History")
|
||||
.AutoMapChildModels();
|
||||
|
||||
Mapper.Entity<Series>().RegisterModel("Series")
|
||||
.Ignore(s => s.RootFolderPath)
|
||||
.Relationship()
|
||||
.HasOne(s => s.QualityProfile, s => s.QualityProfileId);
|
||||
|
||||
Mapper.Entity<Episode>().RegisterModel("Episodes")
|
||||
.Ignore(e => e.SeriesTitle)
|
||||
.Ignore(e => e.Series)
|
||||
.Ignore(e => e.HasFile)
|
||||
.Relationship()
|
||||
.HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId);
|
||||
|
||||
Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles")
|
||||
.Relationships.AutoMapICollectionOrComplexProperties();
|
||||
|
||||
Mapper.Entity<QualityProfile>().RegisterModel("QualityProfiles");
|
||||
|
||||
Mapper.Entity<QualitySize>().RegisterModel("QualitySizes");
|
||||
|
||||
Mapper.Entity<Log>().RegisterModel("Logs");
|
||||
|
||||
Mapper.Entity<NamingConfig>().RegisterModel("NamingConfig");
|
||||
|
||||
Mapper.Entity<SeriesStatistics>().MapResultSet();
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
{
|
||||
RegisterEmbeddedConverter();
|
||||
RegisterProviderSettingConverter();
|
||||
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Int32), new Int32Converter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(DateTime), new UtcConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Boolean), new BooleanIntConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary<string, string>), new EmbeddedDocumentConverter());
|
||||
}
|
||||
|
||||
private static void RegisterProviderSettingConverter()
|
||||
{
|
||||
var settingTypes = typeof(IProviderConfig).Assembly.ImplementationsOf<IProviderConfig>();
|
||||
|
||||
var providerSettingConverter = new ProviderSettingConverter();
|
||||
foreach (var embeddedType in settingTypes)
|
||||
{
|
||||
MapRepository.Instance.RegisterTypeConverter(embeddedType, providerSettingConverter);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterEmbeddedConverter()
|
||||
{
|
||||
var embeddedTypes = typeof(IEmbeddedDocument).Assembly.ImplementationsOf<IEmbeddedDocument>();
|
||||
|
||||
|
||||
var embeddedConvertor = new EmbeddedDocumentConverter();
|
||||
var genericListDefinition = typeof(List<>).GetGenericTypeDefinition();
|
||||
foreach (var embeddedType in embeddedTypes)
|
||||
{
|
||||
var embeddedListType = genericListDefinition.MakeGenericType(embeddedType);
|
||||
MapRepository.Instance.RegisterTypeConverter(embeddedType, embeddedConvertor);
|
||||
MapRepository.Instance.RegisterTypeConverter(embeddedListType, embeddedConvertor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
public interface IMakeDownloadDecision
|
||||
{
|
||||
List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports);
|
||||
List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase);
|
||||
}
|
||||
|
||||
public class DownloadDecisionMaker : IMakeDownloadDecision
|
||||
{
|
||||
private readonly IEnumerable<IRejectWithReason> _specifications;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DownloadDecisionMaker(IEnumerable<IRejectWithReason> specifications, IParsingService parsingService, Logger logger)
|
||||
{
|
||||
_specifications = specifications;
|
||||
_parsingService = parsingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports)
|
||||
{
|
||||
return GetDecisions(reports).ToList();
|
||||
}
|
||||
|
||||
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
|
||||
{
|
||||
return GetDecisions(reports, searchCriteriaBase).ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<DownloadDecision> GetDecisions(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteria = null)
|
||||
{
|
||||
if (reports.Any())
|
||||
{
|
||||
_logger.ProgressInfo("Processing {0} reports", reports.Count);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_logger.ProgressInfo("No reports found");
|
||||
}
|
||||
|
||||
var reportNumber = 1;
|
||||
|
||||
foreach (var report in reports)
|
||||
{
|
||||
DownloadDecision decision = null;
|
||||
_logger.ProgressTrace("Processing report {0}/{1}", reportNumber, reports.Count);
|
||||
|
||||
try
|
||||
{
|
||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title);
|
||||
|
||||
if (parsedEpisodeInfo != null && !string.IsNullOrWhiteSpace(parsedEpisodeInfo.SeriesTitle))
|
||||
{
|
||||
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvRageId, searchCriteria);
|
||||
remoteEpisode.Release = report;
|
||||
|
||||
if (remoteEpisode.Series != null)
|
||||
{
|
||||
remoteEpisode.DownloadAllowed = true;
|
||||
decision = GetDecisionForReport(remoteEpisode, searchCriteria);
|
||||
}
|
||||
else
|
||||
{
|
||||
decision = new DownloadDecision(remoteEpisode, "Unknown Series");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.ErrorException("Couldn't process report.", e);
|
||||
}
|
||||
|
||||
reportNumber++;
|
||||
|
||||
if (decision != null)
|
||||
{
|
||||
yield return decision;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null)
|
||||
{
|
||||
var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria))
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c));
|
||||
|
||||
return new DownloadDecision(remoteEpisode, reasons.ToArray());
|
||||
}
|
||||
|
||||
private string EvaluateSpec(IRejectWithReason spec, RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteriaBase = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(spec.RejectionReason))
|
||||
{
|
||||
throw new InvalidOperationException("[Need Rejection Text]");
|
||||
}
|
||||
|
||||
var generalSpecification = spec as IDecisionEngineSpecification;
|
||||
if (generalSpecification != null && !generalSpecification.IsSatisfiedBy(remoteEpisode, searchCriteriaBase))
|
||||
{
|
||||
return spec.RejectionReason;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.Data.Add("report", remoteEpisode.Release.ToJson());
|
||||
e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson());
|
||||
_logger.ErrorException("Couldn't evaluate decision on " + remoteEpisode.Release.Title, e);
|
||||
return string.Format("{0}: {1}", spec.GetType().Name, e.Message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
public interface IDecisionEngineSpecification : IRejectWithReason
|
||||
{
|
||||
bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
public interface IRejectWithReason
|
||||
{
|
||||
string RejectionReason { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
public interface IQualityUpgradableSpecification
|
||||
{
|
||||
bool IsUpgradable(QualityModel currentQuality, QualityModel newQuality = null);
|
||||
bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null);
|
||||
bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality);
|
||||
}
|
||||
|
||||
public class QualityUpgradableSpecification : IQualityUpgradableSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QualityUpgradableSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsUpgradable(QualityModel currentQuality, QualityModel newQuality = null)
|
||||
{
|
||||
if (newQuality != null)
|
||||
{
|
||||
if (currentQuality >= newQuality)
|
||||
{
|
||||
_logger.Trace("existing item has better or equal quality. skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsProperUpgrade(currentQuality, newQuality))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null)
|
||||
{
|
||||
if (currentQuality.Quality >= profile.Cutoff)
|
||||
{
|
||||
if (newQuality != null && IsProperUpgrade(currentQuality, newQuality))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.Trace("Existing item meets cut-off. skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality)
|
||||
{
|
||||
if (currentQuality.Quality == newQuality.Quality && newQuality > currentQuality)
|
||||
{
|
||||
_logger.Trace("New quality is a proper for existing quality");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class AcceptableSizeSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IQualitySizeService _qualityTypeProvider;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AcceptableSizeSpecification(IQualitySizeService qualityTypeProvider, IEpisodeService episodeService, Logger logger)
|
||||
{
|
||||
_qualityTypeProvider = qualityTypeProvider;
|
||||
_episodeService = episodeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get { return "File size too big or small"; }
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
|
||||
_logger.Trace("Beginning size check for: {0}", subject);
|
||||
|
||||
var quality = subject.ParsedEpisodeInfo.Quality.Quality;
|
||||
|
||||
if (quality == Quality.RAWHD)
|
||||
{
|
||||
_logger.Trace("Raw-HD release found, skipping size check.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (quality == Quality.Unknown)
|
||||
{
|
||||
_logger.Trace("Unknown quality. skipping size check.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var qualityType = _qualityTypeProvider.Get(quality.Id);
|
||||
|
||||
if (qualityType.MaxSize == 0)
|
||||
{
|
||||
_logger.Trace("Max size is 0 (unlimited) - skipping check.");
|
||||
return true;
|
||||
}
|
||||
|
||||
var maxSize = qualityType.MaxSize.Megabytes();
|
||||
|
||||
//Multiply maxSize by Series.Runtime
|
||||
maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count;
|
||||
|
||||
//Check if there was only one episode parsed and it is the first
|
||||
if (subject.Episodes.Count == 1 && subject.Episodes.First().EpisodeNumber == 1)
|
||||
{
|
||||
maxSize = maxSize * 2;
|
||||
}
|
||||
|
||||
//If the parsed size is greater than maxSize we don't want it
|
||||
if (subject.Release.Size > maxSize)
|
||||
{
|
||||
_logger.Trace("Item: {0}, Size: {1} is greater than maximum allowed size ({2}), rejecting.", subject, subject.Release.Size, maxSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Trace("Item: {0}, meets size constraints.", subject);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class CutoffSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CutoffSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
{
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Cutoff has already been met";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
|
||||
{
|
||||
_logger.Trace("Comparing file quality with report. Existing file is {0}", file.Quality);
|
||||
|
||||
|
||||
if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Series.QualityProfile, file.Quality, subject.ParsedEpisodeInfo.Quality))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class DownloadDecision
|
||||
{
|
||||
public RemoteEpisode RemoteEpisode { get; private set; }
|
||||
public IEnumerable<string> Rejections { get; private set; }
|
||||
|
||||
public bool Approved
|
||||
{
|
||||
get
|
||||
{
|
||||
return !Rejections.Any();
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadDecision(RemoteEpisode episode, params string[] rejections)
|
||||
{
|
||||
RemoteEpisode = episode;
|
||||
Rejections = rejections.ToList();
|
||||
}
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Approved)
|
||||
{
|
||||
return "[OK] " + RemoteEpisode;
|
||||
}
|
||||
|
||||
return "[Rejected " + Rejections.Count() + "]" + RemoteEpisode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class LanguageSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public LanguageSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Not English";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Trace("Checking if report meets language requirements. {0}", subject.ParsedEpisodeInfo.Language);
|
||||
if (subject.ParsedEpisodeInfo.Language != Language.English)
|
||||
{
|
||||
_logger.Trace("Report Language: {0} rejected because it is not English", subject.ParsedEpisodeInfo.Language);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class NotInQueueSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IProvideDownloadClient _downloadClientProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NotInQueueSpecification(IProvideDownloadClient downloadClientProvider, Logger logger)
|
||||
{
|
||||
_downloadClientProvider = downloadClientProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Already in download queue.";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var downloadClient = _downloadClientProvider.GetDownloadClient();
|
||||
|
||||
if (!downloadClient.IsConfigured)
|
||||
{
|
||||
_logger.Warn("Download client {0} isn't configured yet.", downloadClient.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
|
||||
var queue = downloadClient.GetQueue().Select(queueItem => Parser.Parser.ParseTitle(queueItem.Title)).Where(episodeInfo => episodeInfo != null);
|
||||
|
||||
return !IsInQueue(subject, queue);
|
||||
}
|
||||
|
||||
private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable<ParsedEpisodeInfo> queue)
|
||||
{
|
||||
var matchingTitle = queue.Where(q => String.Equals(q.SeriesTitle, newEpisode.Series.CleanTitle, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
var matchingTitleWithQuality = matchingTitle.Where(q => q.Quality >= newEpisode.ParsedEpisodeInfo.Quality);
|
||||
|
||||
if (newEpisode.Series.SeriesType == SeriesTypes.Daily)
|
||||
{
|
||||
return matchingTitleWithQuality.Any(q => q.AirDate.Value.Date == newEpisode.ParsedEpisodeInfo.AirDate.Value.Date);
|
||||
}
|
||||
|
||||
var matchingSeason = matchingTitleWithQuality.Where(q => q.SeasonNumber == newEpisode.ParsedEpisodeInfo.SeasonNumber);
|
||||
|
||||
if (newEpisode.ParsedEpisodeInfo.FullSeason)
|
||||
{
|
||||
return matchingSeason.Any();
|
||||
}
|
||||
|
||||
return matchingSeason.Any(q => q.EpisodeNumbers != null && q.EpisodeNumbers.Any(e => newEpisode.ParsedEpisodeInfo.EpisodeNumbers.Contains(e)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class NotRestrictedReleaseSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NotRestrictedReleaseSpecification(IConfigService configService, Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Contains restricted term.";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Trace("Checking if release contains any restricted terms: {0}", subject);
|
||||
|
||||
var restrictionsString = _configService.ReleaseRestrictions;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(restrictionsString))
|
||||
{
|
||||
_logger.Trace("No restrictions configured, allowing: {0}", subject);
|
||||
return true;
|
||||
}
|
||||
|
||||
var restrictions = restrictionsString.Split('\n');
|
||||
|
||||
foreach (var restriction in restrictions)
|
||||
{
|
||||
if (subject.Release.Title.ToLowerInvariant().Contains(restriction.ToLowerInvariant()))
|
||||
{
|
||||
_logger.Trace("{0} is restricted: {1}", subject, restriction);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Trace("No restrictions apply, allowing: {0}", subject);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class NotSampleSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
public string RejectionReason { get { return "Sample"; } }
|
||||
|
||||
public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (subject.Release.Title.ToLower().Contains("sample") && subject.Release.Size < 70.Megabytes())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class QualityAllowedByProfileSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QualityAllowedByProfileSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Quality rejected by series profile";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Trace("Checking if report meets quality requirements. {0}", subject.ParsedEpisodeInfo.Quality);
|
||||
if (!subject.Series.QualityProfile.Value.Allowed.Contains(subject.ParsedEpisodeInfo.Quality.Quality))
|
||||
{
|
||||
_logger.Trace("Quality {0} rejected by Series' quality profile", subject.ParsedEpisodeInfo.Quality);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class RetentionSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RetentionSpecification(IConfigService configService, Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Report past retention limit.";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var age = subject.Release.Age;
|
||||
|
||||
_logger.Trace("Checking if report meets retention requirements. {0}", age);
|
||||
if (_configService.Retention > 0 && age > _configService.Retention)
|
||||
{
|
||||
_logger.Trace("Report age: {0} rejected by user's retention limit", age);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
{
|
||||
public class MonitoredEpisodeSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MonitoredEpisodeSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Series or Episode is not monitored";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria != null)
|
||||
{
|
||||
_logger.Trace("Skipping monitored check during search");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!subject.Series.Monitored)
|
||||
{
|
||||
_logger.Debug("{0} is present in the DB but not tracked. skipping.", subject.Series);
|
||||
return false;
|
||||
}
|
||||
|
||||
//return monitored if any of the episodes are monitored
|
||||
if (subject.Episodes.Any(episode => episode.Monitored))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.Debug("No episodes are monitored. skipping.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
{
|
||||
public class ProperSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ProperSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, IConfigService configService, Logger logger)
|
||||
{
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Proper for old episode";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
|
||||
{
|
||||
if (_qualityUpgradableSpecification.IsProperUpgrade(file.Quality, subject.ParsedEpisodeInfo.Quality))
|
||||
{
|
||||
if (file.DateAdded < DateTime.Today.AddDays(-7))
|
||||
{
|
||||
_logger.Trace("Proper for old file, skipping: {0}", subject);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_configService.AutoDownloadPropers)
|
||||
{
|
||||
_logger.Trace("Auto downloading of propers is disabled");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
{
|
||||
public class UpgradeHistorySpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UpgradeHistorySpecification(IHistoryService historyService, QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Existing file in history is of equal or higher quality";
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria != null)
|
||||
{
|
||||
_logger.Trace("Skipping history check during search");
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var episode in subject.Episodes)
|
||||
{
|
||||
var bestQualityInHistory = _historyService.GetBestQualityInHistory(episode.Id);
|
||||
if (bestQualityInHistory != null)
|
||||
{
|
||||
_logger.Trace("Comparing history quality with report. History is {0}", bestQualityInHistory);
|
||||
if (!_qualityUpgradableSpecification.IsUpgradable(bestQualityInHistory, subject.ParsedEpisodeInfo.Quality))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||
{
|
||||
public class DailyEpisodeMatchSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
|
||||
public DailyEpisodeMatchSpecification(Logger logger, IEpisodeService episodeService)
|
||||
{
|
||||
_logger = logger;
|
||||
_episodeService = episodeService;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Episode doesn't match";
|
||||
}
|
||||
}
|
||||
public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var dailySearchSpec = searchCriteria as DailyEpisodeSearchCriteria;
|
||||
|
||||
if (dailySearchSpec == null) return true;
|
||||
|
||||
var episode = _episodeService.GetEpisode(dailySearchSpec.Series.Id, dailySearchSpec.Airtime);
|
||||
|
||||
if (!remoteEpisode.ParsedEpisodeInfo.AirDate.HasValue || remoteEpisode.ParsedEpisodeInfo.AirDate.Value.ToString(Episode.AIR_DATE_FORMAT) != episode.AirDate)
|
||||
{
|
||||
_logger.Trace("Episode AirDate does not match searched episode number, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||
{
|
||||
public class SeasonMatchSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SeasonMatchSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Episode doesn't match";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var singleEpisodeSpec = searchCriteria as SeasonSearchCriteria;
|
||||
if (singleEpisodeSpec == null) return true;
|
||||
|
||||
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber)
|
||||
{
|
||||
_logger.Trace("Season number does not match searched season number, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||
{
|
||||
public class SingleEpisodeMatchSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SingleEpisodeMatchSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Episode doesn't match";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchDefinitionBase searchDefinitionBase)
|
||||
{
|
||||
var singleEpisodeSpec = searchDefinitionBase as SingleEpisodeSearchDefinition;
|
||||
if (singleEpisodeSpec == null) return true;
|
||||
|
||||
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber)
|
||||
{
|
||||
_logger.Trace("Season number does not match searched season number, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!remoteEpisode.Episodes.Select(c => c.EpisodeNumber).Contains(singleEpisodeSpec.EpisodeNumber))
|
||||
{
|
||||
_logger.Trace("Episode number does not match searched episode number, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||
{
|
||||
public class SingleEpisodeSearchMatchSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SingleEpisodeSearchMatchSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Episode doesn't match";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var singleEpisodeSpec = searchCriteria as SingleEpisodeSearchCriteria;
|
||||
if (singleEpisodeSpec == null) return true;
|
||||
|
||||
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber)
|
||||
{
|
||||
_logger.Trace("Season number does not match searched season number, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Contains(singleEpisodeSpec.EpisodeNumber))
|
||||
{
|
||||
_logger.Trace("Episode number does not match searched episode number, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user