Merge branch 'develop'

This commit is contained in:
Mark McDowall
2013-10-08 16:39:14 -07:00
1531 changed files with 4323 additions and 2918 deletions
@@ -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
}
}
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
+17
View File
@@ -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);
}
}
}
}
+25
View File
@@ -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();
}
}
}
+89
View File
@@ -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
{
}
}
+30
View File
@@ -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);
}
}
}
@@ -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
}
}
+10
View File
@@ -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)
{
}
}
}
+23
View File
@@ -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;
}
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace NzbDrone.Core.Datastore
{
public class ResultSet
{
}
}
+110
View File
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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