Ability to manually add a show has been added.

UI cleanup for adding series (new, existing and manual).
pull/4/head
Mark McDowall 14 years ago
parent 3d81bc8770
commit 636f352599

@ -30,9 +30,10 @@ namespace NzbDrone.Core.Test
list.Add(new Indexer { IndexerName = "Test4", RssUrl = "http://www.test4.com/rss.php", Enabled = false, Order = 2 }); list.Add(new Indexer { IndexerName = "Test4", RssUrl = "http://www.test4.com/rss.php", Enabled = false, Order = 2 });
var repo = new Mock<IRepository>(); var repo = new Mock<IRepository>();
var config = new Mock<IConfigProvider>();
repo.Setup(r => r.All<Indexer>()).Returns(list.AsQueryable()); repo.Setup(r => r.All<Indexer>()).Returns(list.AsQueryable());
var target = new IndexerProvider(repo.Object); var target = new IndexerProvider(repo.Object, config.Object);
//Act //Act
var result = target.AllIndexers(); var result = target.AllIndexers();
@ -57,9 +58,10 @@ namespace NzbDrone.Core.Test
list.Add(new Indexer { IndexerName = "Test4", RssUrl = "http://www.test4.com/rss.php", Enabled = false, Order = 2 }); list.Add(new Indexer { IndexerName = "Test4", RssUrl = "http://www.test4.com/rss.php", Enabled = false, Order = 2 });
var repo = new Mock<IRepository>(); var repo = new Mock<IRepository>();
var config = new Mock<IConfigProvider>();
repo.Setup(r => r.All<Indexer>()).Returns(list.AsQueryable()); repo.Setup(r => r.All<Indexer>()).Returns(list.AsQueryable());
var target = new IndexerProvider(repo.Object); var target = new IndexerProvider(repo.Object, config.Object);
//Act //Act
var result = target.EnabledIndexers(); var result = target.EnabledIndexers();

@ -57,9 +57,9 @@ namespace NzbDrone.Core.Test
[Test] [Test]
[Row(@"c:\test\", @"c:\test")] [Row(@"c:\test\", @"c:\test")]
[Row(@"c:\\test\\", @"c:\test")] [Row(@"c:\\test\\", @"c:\test")]
[Row(@"C:\\Test\\", @"c:\test")] [Row(@"C:\\Test\\", @"C:\Test")]
[Row(@"C:\\Test\\Test\", @"c:\test\test")] [Row(@"C:\\Test\\Test\", @"C:\Test\Test")]
[Row(@"\\Testserver\Test\", @"\\testserver\test")] [Row(@"\\Testserver\Test\", @"\\Testserver\Test")]
public void Normalize_Path(string dirty, string clean) public void Normalize_Path(string dirty, string clean)
{ {
var result = Parser.NormalizePath(dirty); var result = Parser.NormalizePath(dirty);

@ -171,15 +171,18 @@ namespace NzbDrone.Core
{ {
//Setup the default providers in the Providers table //Setup the default providers in the Providers table
string nzbMatrixRss = "http://rss.nzbmatrix.com/rss.php?page=download&username={USERNAME}&apikey={APIKEY}&subcat=6&english=1"; string nzbMatrixRss = "http://rss.nzbmatrix.com/rss.php?page=download&username={USERNAME}&apikey={APIKEY}&subcat=6,41&english=1";
string nzbMatrixApi = "http://rss.nzbmatrix.com/rss.php?page=download&username={USERNAME}&apikey={APIKEY}&subcat=6,41&english=1&age={AGE}&term={TERM}";
string nzbsOrgRss = "http://nzbs.org/rss.php?type=1&dl=1&num=100&i={UID}&h={HASH}"; string nzbsOrgRss = "http://nzbs.org/rss.php?type=1&dl=1&num=100&i={UID}&h={HASH}";
string nzbsOrgApi = String.Empty;
string nzbsrusRss = "http://www.nzbsrus.com/rssfeed.php?cat=91,75&i={UID}&h={HASH}"; string nzbsrusRss = "http://www.nzbsrus.com/rssfeed.php?cat=91,75&i={UID}&h={HASH}";
string nzbsrusApi = String.Empty;
var nzbMatrixIndexer = new Indexer var nzbMatrixIndexer = new Indexer
{ {
IndexerName = "NzbMatrix", IndexerName = "NzbMatrix",
RssUrl = nzbMatrixRss, RssUrl = nzbMatrixRss,
ApiUrl = String.Empty, ApiUrl = nzbMatrixApi,
Order = 1 Order = 1
}; };
@ -187,7 +190,7 @@ namespace NzbDrone.Core
{ {
IndexerName = "NzbsOrg", IndexerName = "NzbsOrg",
RssUrl = nzbsOrgRss, RssUrl = nzbsOrgRss,
ApiUrl = String.Empty, ApiUrl = nzbsOrgApi,
Order = 2 Order = 2
}; };
@ -195,7 +198,7 @@ namespace NzbDrone.Core
{ {
IndexerName = "Nzbsrus", IndexerName = "Nzbsrus",
RssUrl = nzbsrusRss, RssUrl = nzbsrusRss,
ApiUrl = String.Empty, ApiUrl = nzbsrusApi,
Order = 3 Order = 3
}; };

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.Helpers
{
public static class SceneNameHelper
{
private static List<SceneNameModel> _sceneNameMappings = new List<SceneNameModel>
{
new SceneNameModel { SeriesId = 72546, Name = "CSI" },
new SceneNameModel { SeriesId = 73696, Name = "CSI New York" },
new SceneNameModel { SeriesId = 73696, Name = "CSI NY" },
new SceneNameModel { SeriesId = 110381, Name = "Archer" },
new SceneNameModel { SeriesId = 83897, Name = "Life After People The Series" },
new SceneNameModel { SeriesId = 83897, Name = "Life After People" },
new SceneNameModel { SeriesId = 80552, Name = "Kitchen Nightmares US" },
new SceneNameModel { SeriesId = 71256, Name = "The Daily Show" },
new SceneNameModel { SeriesId = 71256, Name = "The Daily Show with Jon Stewart" },
new SceneNameModel { SeriesId = 75692, Name = "Law and Order SVU" },
new SceneNameModel { SeriesId = 75692, Name = "Law and Order Special Victims Unit" },
new SceneNameModel { SeriesId = 71489, Name = "Law and Order Criminal Intent" },
new SceneNameModel { SeriesId = 71489, Name = "Law and Order CI" },
new SceneNameModel { SeriesId = 79590, Name = "Dancing With The Stars US" },
new SceneNameModel { SeriesId = 73387, Name = "Craig Ferguson" },
new SceneNameModel { SeriesId = 85355, Name = "Jimmy Fallon" },
new SceneNameModel { SeriesId = 75088, Name = "David Letterman" },
new SceneNameModel { SeriesId = 76706, Name = "Big Brother US" },
new SceneNameModel { SeriesId = 105521, Name = "The Colony" },
new SceneNameModel { SeriesId = 105521, Name = "The Colony US" },
new SceneNameModel { SeriesId = 76235, Name = "Americas Funniest Home Videos" },
new SceneNameModel { SeriesId = 76235, Name = "AFHV" },
new SceneNameModel { SeriesId = 139941, Name = "Childrens Hospital US" },
new SceneNameModel { SeriesId = 139941, Name = "Childrens Hospital" },
new SceneNameModel { SeriesId = 83123, Name = "Merlin" },
new SceneNameModel { SeriesId = 83123, Name = "Merlin 2008" },
new SceneNameModel { SeriesId = 76779, Name = "WWE Monday Night RAW" },
new SceneNameModel { SeriesId = 164951, Name = "Shit My Dad Says" },
new SceneNameModel { SeriesId = 83714, Name = "Genius with Dave Gorman" },
new SceneNameModel { SeriesId = 168161, Name = "Law and Order Los Angeles" },
new SceneNameModel { SeriesId = 168161, Name = "Law and Order LA" },
new SceneNameModel { SeriesId = 77526, Name = "Star Trek TOS" },
new SceneNameModel { SeriesId = 72073, Name = "Star Trek DS9" },
new SceneNameModel { SeriesId = 72194, Name = "Ellen Degeneres" },
new SceneNameModel { SeriesId = 72194, Name = "Ellen Degeneres" },
new SceneNameModel { SeriesId = 195831, Name = "Drinking Made Easy" },
new SceneNameModel { SeriesId = 195831, Name = "Zane Lampreys Drinking Made Easy" },
new SceneNameModel { SeriesId = 76133, Name = "Poirot" },
new SceneNameModel { SeriesId = 76133, Name = "Agatha Christies Poirot" },
new SceneNameModel { SeriesId = 70870, Name = "The Real World Road Rules Challenge" },
new SceneNameModel { SeriesId = 70870, Name = "The Challenge Cutthroat" },
new SceneNameModel { SeriesId = 77444, Name = "This Old House Program" },
new SceneNameModel { SeriesId = 73290, Name = "60 Minutes US" },
new SceneNameModel { SeriesId = 194751, Name = "Conan" },
new SceneNameModel { SeriesId = 194751, Name = "Conan 2010" },
new SceneNameModel { SeriesId = 164451, Name = "Carlos 2010" },
new SceneNameModel { SeriesId = 70726, Name = "Babalon 5" },
new SceneNameModel { SeriesId = 70726, Name = "Babalon5" },
new SceneNameModel { SeriesId = 83714, Name = "Genius" },
new SceneNameModel { SeriesId = 83714, Name = "Genius With Dave Gormand" },
new SceneNameModel { SeriesId = 212571, Name = "Come Fly With Me 2010" },
new SceneNameModel { SeriesId = 81563, Name = "Border Security" },
new SceneNameModel { SeriesId = 81563, Name = "Border Security Australias Frontline" },
new SceneNameModel { SeriesId = 172381, Name = "Silent Library US" },
new SceneNameModel { SeriesId = 131791, Name = "Sci-Fi Science" },
new SceneNameModel { SeriesId = 80646, Name = "Frontline" },
new SceneNameModel { SeriesId = 80646, Name = "Frontline US" },
new SceneNameModel { SeriesId = 189931, Name = "RBT AU" },
new SceneNameModel { SeriesId = 73255, Name = "House" },
new SceneNameModel { SeriesId = 73255, Name = "House MD" },
new SceneNameModel { SeriesId = 73244, Name = "The Office" },
new SceneNameModel { SeriesId = 73244, Name = "The Office US" },
};
public static int FindByName(string seriesName)
{
var map = _sceneNameMappings.Single(s => s.Name == seriesName);
if (map == null)
return 0;
return map.SeriesId;
}
public static List<String> FindById(int seriesId)
{
List<String> results = new List<string>();
var maps = _sceneNameMappings.Where(s => s.SeriesId == seriesId);
foreach (var map in maps)
results.Add(map.Name);
return results;
}
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Model
{
public class SceneNameModel
{
public string Name { get; set; }
public int SeriesId { get; set; }
}
}

@ -158,6 +158,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="Helpers\EpisodeRenameHelper.cs" /> <Compile Include="Helpers\EpisodeRenameHelper.cs" />
<Compile Include="Helpers\EpisodeSortingHelper.cs" /> <Compile Include="Helpers\EpisodeSortingHelper.cs" />
<Compile Include="Helpers\SceneNameHelper.cs" />
<Compile Include="Helpers\ServerHelper.cs" /> <Compile Include="Helpers\ServerHelper.cs" />
<Compile Include="Instrumentation\ILogProvider.cs" /> <Compile Include="Instrumentation\ILogProvider.cs" />
<Compile Include="Instrumentation\LogLevel.cs" /> <Compile Include="Instrumentation\LogLevel.cs" />
@ -174,9 +175,12 @@
<Compile Include="Model\NzbInfoModel.cs" /> <Compile Include="Model\NzbInfoModel.cs" />
<Compile Include="Model\NzbSiteModel.cs" /> <Compile Include="Model\NzbSiteModel.cs" />
<Compile Include="Model\SabnzbdPriorityType.cs" /> <Compile Include="Model\SabnzbdPriorityType.cs" />
<Compile Include="Model\SceneNameModel.cs" />
<Compile Include="Model\SeriesMappingModel.cs" /> <Compile Include="Model\SeriesMappingModel.cs" />
<Compile Include="Providers\BacklogProvider.cs" />
<Compile Include="Providers\ExternalNotificationProvider.cs" /> <Compile Include="Providers\ExternalNotificationProvider.cs" />
<Compile Include="Providers\HistoryProvider.cs" /> <Compile Include="Providers\HistoryProvider.cs" />
<Compile Include="Providers\IBacklogProvider.cs" />
<Compile Include="Providers\IExtenalNotificationProvider.cs" /> <Compile Include="Providers\IExtenalNotificationProvider.cs" />
<Compile Include="Providers\IHistoryProvider.cs" /> <Compile Include="Providers\IHistoryProvider.cs" />
<Compile Include="Providers\IIndexerProvider.cs" /> <Compile Include="Providers\IIndexerProvider.cs" />

@ -0,0 +1,277 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using NLog;
using NzbDrone.Core.Helpers;
using NzbDrone.Core.Model;
using NzbDrone.Core.Model.Notification;
using NzbDrone.Core.Repository;
using Rss;
namespace NzbDrone.Core.Providers
{
public class BacklogProvider : IBacklogProvider
{
private readonly ISeriesProvider _seriesProvider;
private readonly INotificationProvider _notificationProvider;
private readonly IConfigProvider _configProvider;
private readonly IIndexerProvider _indexerProvider;
private readonly IRssProvider _rssProvider;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private List<Series> _seriesList;
private Thread _backlogThread;
private ProgressNotification _backlogSearchNotification;
public BacklogProvider(ISeriesProvider seriesProvider, INotificationProvider notificationProvider,
IConfigProvider configProvider, IIndexerProvider indexerProvider,
IRssProvider rssProvider)
{
_seriesProvider = seriesProvider;
_notificationProvider = notificationProvider;
_configProvider = configProvider;
_indexerProvider = indexerProvider;
_rssProvider = rssProvider;
_seriesList = new List<Series>();
}
#region IBacklogProvider Members
public bool StartSearch()
{
Logger.Debug("Backlog Search Requested");
if (_backlogThread == null || !_backlogThread.IsAlive)
{
Logger.Debug("Initializing Backlog Search");
_backlogThread = new Thread(PerformSearch)
{
Name = "BacklogSearch",
Priority = ThreadPriority.Lowest
};
_seriesList.AddRange(_seriesProvider.GetAllSeries());
_backlogThread.Start();
}
else
{
Logger.Warn("Backlog Search already in progress. Ignoring request.");
//return false if backlog search was already running
return false;
}
//return true if backlog search has started
return true;
}
public bool StartSearch(int seriesId)
{
//Get the series
//Start new Thread if one isn't already started
Logger.Debug("Backlog Search Requested");
if (_backlogThread == null || !_backlogThread.IsAlive)
{
Logger.Debug("Initializing Backlog Search");
_backlogThread = new Thread(PerformSearch)
{
Name = "BacklogSearch",
Priority = ThreadPriority.Lowest
};
var series = _seriesProvider.GetSeries(seriesId);
if (series == null)
{
Logger.Debug("Invalid Series - Not starting Backlog Search");
return false;
}
_seriesList.Add(series);
_backlogThread.Start();
}
else
{
Logger.Warn("Backlog Search already in progress. Ignoring request.");
//return false if backlog search was already running
return false;
}
//return true if backlog search has started
return true;
}
#endregion
private void PerformSearch()
{
try
{
using (_backlogSearchNotification = new ProgressNotification("Series Scan"))
{
_notificationProvider.Register(_backlogSearchNotification);
_backlogSearchNotification.CurrentStatus = "Starting Backlog Search";
_backlogSearchNotification.ProgressMax = _seriesList.Count;
foreach (var series in _seriesList)
{
try
{
//Do the searching here
_backlogSearchNotification.CurrentStatus = String.Format("Backlog Searching For: {0}", series.Title);
var sceneNames = SceneNameHelper.FindById(series.SeriesId);
if (sceneNames.Count < 1)
sceneNames.Add(series.Title);
foreach (var season in series.Seasons)
{
var episodesWithoutFiles = season.Episodes.Where(e => e.EpisodeFileId == 0);
if (season.Episodes.Count() == episodesWithoutFiles.Count())
{
//Whole season needs to be grabbed, look for the whole season first
//Lookup scene name using seriesId
foreach (var sceneName in sceneNames)
{
var searchString = String.Format("{0} Season {1}", sceneName,
season.SeasonNumber);
foreach (var i in _indexerProvider.EnabledIndexers())
{
//Get the users URL
GetUsersUrl(i, searchString);
//If the url still contains '{' & '}' the user probably hasn't configured the indexer settings
if (i.ApiUrl.Contains("{") && i.ApiUrl.Contains("}"))
{
Logger.Debug("Unable to Sync {0}. User Information has not been configured.", i.IndexerName);
continue; //Skip this indexer
}
var indexer = new FeedInfoModel(i.IndexerName, i.ApiUrl);
var feedItems = _rssProvider.GetFeed(indexer);
if (feedItems.Count() == 0)
{
Logger.Debug("Failed to download Backlog Search URL: {0}", indexer.Name);
continue; //No need to process anything else
}
foreach (RssItem item in feedItems)
{
NzbInfoModel nzb = Parser.ParseNzbInfo(indexer, item);
QueueSeasonIfWanted(nzb, i);
}
}
}
}
else
{
//Grab the episodes 1-by-1 (or in smaller chunks)
}
}
//Done searching for each episode
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
}
_backlogSearchNotification.ProgressValue++;
}
_backlogSearchNotification.CurrentStatus = "Backlog Search Completed";
Logger.Info("Backlog Search has successfully completed.");
Thread.Sleep(3000);
_backlogSearchNotification.Status = ProgressNotificationStatus.Completed;
}
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
}
}
private void GetUsersUrl(Indexer indexer, string searchString)
{
if (indexer.IndexerName == "NzbMatrix")
{
var nzbMatrixUsername = _configProvider.GetValue("NzbMatrixUsername", String.Empty, false);
var nzbMatrixApiKey = _configProvider.GetValue("NzbMatrixApiKey", String.Empty, false);
var retention = Convert.ToInt32(_configProvider.GetValue("Retention", String.Empty, false));
if (!String.IsNullOrEmpty(nzbMatrixUsername) && !String.IsNullOrEmpty(nzbMatrixApiKey))
indexer.ApiUrl = indexer.ApiUrl.Replace("{USERNAME}", nzbMatrixUsername).Replace("{APIKEY}", nzbMatrixApiKey).Replace("{AGE}", retention.ToString()).Replace("{TERM}", searchString);
//Todo: Perform validation at the config level so a user is unable to enable a provider until user details are provided
return;
}
if (indexer.IndexerName == "NzbsOrg")
{
var nzbsOrgUId = _configProvider.GetValue("NzbsOrgUId", String.Empty, false);
var nzbsOrgHash = _configProvider.GetValue("NzbsOrgHash", String.Empty, false);
if (!String.IsNullOrEmpty(nzbsOrgUId) && !String.IsNullOrEmpty(nzbsOrgHash))
indexer.RssUrl = indexer.RssUrl.Replace("{UID}", nzbsOrgUId).Replace("{HASH}", nzbsOrgHash);
//Todo: Perform validation at the config level so a user is unable to enable a provider until user details are provided
return;
}
if (indexer.IndexerName == "NzbsOrg")
{
var nzbsrusUId = _configProvider.GetValue("NzbsrusUId", String.Empty, false);
var nzbsrusHash = _configProvider.GetValue("NzbsrusHash", String.Empty, false);
if (!String.IsNullOrEmpty(nzbsrusUId) && !String.IsNullOrEmpty(nzbsrusHash))
indexer.RssUrl = indexer.RssUrl.Replace("{UID}", nzbsrusUId).Replace("{HASH}", nzbsrusHash);
//Todo: Perform validation at the config level so a user is unable to enable a provider until user details are provided
return;
}
return; //Currently other providers do not require user information to be substituted, simply return
}
private void QueueSeasonIfWanted(NzbInfoModel nzb, Indexer indexer)
{
//Do we want this item?
try
{
if (nzb.IsPassworded())
{
Logger.Debug("Skipping Passworded Report {0}", nzb.Title);
return;
}
//Need to get REGEX that will handle "Show Name Season 1 quality"
nzb.TitleFix = String.Empty;
nzb.TitleFix = String.Format("{0} [{1}]", nzb.TitleFix, nzb.Quality); //Add Quality to the titleFix
//Check that we want this quality
var quality = Parser.ParseQuality(nzb.Title);
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
}
}
}
}

@ -49,6 +49,12 @@ namespace NzbDrone.Core.Providers
File.Move(sourcePath, destinationPath); File.Move(sourcePath, destinationPath);
} }
public string GetFolderName(string path)
{
var di = new DirectoryInfo(path);
return di.Name;
}
#endregion #endregion
} }
} }

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Providers
{
public interface IBacklogProvider
{
//Will provide Backlog Search functionality
bool StartSearch();
bool StartSearch(int seriesId);
}
}

@ -13,5 +13,6 @@ namespace NzbDrone.Core.Providers
long GetSize(string path); long GetSize(string path);
void DeleteFile(string path); void DeleteFile(string path);
void RenameFile(string sourcePath, string destinationPath); void RenameFile(string sourcePath, string destinationPath);
string GetFolderName(string path);
} }
} }

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Providers
{ {
bool BeginSyncUnmappedFolders(List<SeriesMappingModel> unmapped); bool BeginSyncUnmappedFolders(List<SeriesMappingModel> unmapped);
bool BeginAddNewSeries(string dir, int seriesId, string seriesName); bool BeginAddNewSeries(string dir, int seriesId, string seriesName);
bool BeginAddExistingSeries(string path, int seriesId);
List<String> GetUnmappedFolders(string path); List<String> GetUnmappedFolders(string path);
} }
} }

@ -14,13 +14,15 @@ namespace NzbDrone.Core.Providers
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IRepository _sonicRepo; private readonly IRepository _sonicRepo;
private readonly IConfigProvider _configProvider;
public IndexerProvider(IRepository sonicRepo) public IndexerProvider(IRepository sonicRepo, IConfigProvider configProvider)
{ {
_sonicRepo = sonicRepo; _sonicRepo = sonicRepo;
_configProvider = configProvider;
} }
#region IIndexerProvider #region IIndexerProvider Members
public List<Indexer> AllIndexers() public List<Indexer> AllIndexers()
{ {

@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using NLog; using NLog;
using NzbDrone.Core.Helpers;
using NzbDrone.Core.Model; using NzbDrone.Core.Model;
using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Model.Notification;
using NzbDrone.Core.Repository; using NzbDrone.Core.Repository;
@ -146,8 +147,14 @@ namespace NzbDrone.Core.Providers
if (series == null) if (series == null)
{ {
Logger.Debug("Show is not being watched: {0}", episodeParseResults[0].SeriesTitle); //If we weren't able to find a title using the clean name, lets try again looking for a scene name
return; series = _series.GetSeries(SceneNameHelper.FindByName(episodeParseResults[0].SeriesTitle));
if (series == null)
{
Logger.Debug("Show is not being watched: {0}", episodeParseResults[0].SeriesTitle);
return;
}
} }
Logger.Debug("Show is being watched: {0}", series.Title); Logger.Debug("Show is being watched: {0}", series.Title);

@ -126,6 +126,37 @@ namespace NzbDrone.Core.Providers
return true; return true;
} }
public bool BeginAddExistingSeries(string path, int seriesId)
{
Logger.Debug("User has requested adding of new series");
if (_seriesSyncThread == null || !_seriesSyncThread.IsAlive)
{
Logger.Debug("Initializing background add of of series folder.");
_seriesSyncThread = new Thread(SyncUnmappedFolders)
{
Name = "SyncUnmappedFolders",
Priority = ThreadPriority.Lowest
};
_syncList = new List<SeriesMappingModel>();
//Add it to the list so it will be processed
_syncList.Add(new SeriesMappingModel { Path = path, TvDbId = seriesId });
_seriesSyncThread.Start();
}
else
{
Logger.Warn("Series folder scan already in progress. Ignoring request.");
//return false if sync was already running, then we can tell the user to try again later
return false;
}
//return true if sync has started
return true;
}
private void SyncUnmappedFolders() private void SyncUnmappedFolders()
{ {
Logger.Info("Starting Series folder scan"); Logger.Info("Starting Series folder scan");
@ -167,6 +198,7 @@ namespace NzbDrone.Core.Providers
_episodeProvider.RefreshEpisodeInfo(mappedSeries.Id); _episodeProvider.RefreshEpisodeInfo(mappedSeries.Id);
_seriesSyncNotification.CurrentStatus = String.Format("{0}: finding episodes on disk...", mappedSeries.SeriesName); _seriesSyncNotification.CurrentStatus = String.Format("{0}: finding episodes on disk...", mappedSeries.SeriesName);
_mediaFileProvider.Scan(_seriesProvider.GetSeries(mappedSeries.Id)); _mediaFileProvider.Scan(_seriesProvider.GetSeries(mappedSeries.Id));
//Todo: Launch Backlog search for this series _backlogProvider.StartSearch(mappedSeries.Id);
} }
else else
{ {

@ -234,4 +234,11 @@ input[type="text"]:hover
{ {
border: 1px solid #f00; border: 1px solid #f00;
background: #eef; background: #eef;
}
/* Add Series */
.tvDbSearchResults
{
width: 400px;
} }

@ -28,6 +28,7 @@ namespace NzbDrone.Web.Controllers
private readonly IRenameProvider _renameProvider; private readonly IRenameProvider _renameProvider;
private readonly IRootDirProvider _rootDirProvider; private readonly IRootDirProvider _rootDirProvider;
private readonly ITvDbProvider _tvDbProvider; private readonly ITvDbProvider _tvDbProvider;
private readonly IDiskProvider _diskProvider;
// //
// GET: /Series/ // GET: /Series/
@ -36,7 +37,7 @@ namespace NzbDrone.Web.Controllers
IEpisodeProvider episodeProvider, IRssSyncProvider rssSyncProvider, IEpisodeProvider episodeProvider, IRssSyncProvider rssSyncProvider,
IQualityProvider qualityProvider, IMediaFileProvider mediaFileProvider, IQualityProvider qualityProvider, IMediaFileProvider mediaFileProvider,
IRenameProvider renameProvider, IRootDirProvider rootDirProvider, IRenameProvider renameProvider, IRootDirProvider rootDirProvider,
ITvDbProvider tvDbProvider) ITvDbProvider tvDbProvider, IDiskProvider diskProvider)
{ {
_seriesProvider = seriesProvider; _seriesProvider = seriesProvider;
_episodeProvider = episodeProvider; _episodeProvider = episodeProvider;
@ -47,6 +48,7 @@ namespace NzbDrone.Web.Controllers
_renameProvider = renameProvider; _renameProvider = renameProvider;
_rootDirProvider = rootDirProvider; _rootDirProvider = rootDirProvider;
_tvDbProvider = tvDbProvider; _tvDbProvider = tvDbProvider;
_diskProvider = diskProvider;
} }
public ActionResult Index() public ActionResult Index()
@ -67,7 +69,25 @@ namespace NzbDrone.Web.Controllers
public ActionResult AddNew() public ActionResult AddNew()
{ {
return View(); ViewData["RootDirs"] = _rootDirProvider.GetAll();
ViewData["DirSep"] = Path.DirectorySeparatorChar;
var model = new AddNewSeriesModel
{
DirectorySeparatorChar = Path.DirectorySeparatorChar.ToString(),
RootDirectories = _rootDirProvider.GetAll()
};
return View(model);
}
public ActionResult AddExistingManual(string path)
{
var model = new AddExistingManualModel();
model.Path = path;
model.FolderName = _diskProvider.GetFolderName(path);
return View(model);
} }
public ActionResult RssSync() public ActionResult RssSync()
@ -137,6 +157,7 @@ namespace NzbDrone.Web.Controllers
{ {
IsWanted = true, IsWanted = true,
Path = unmappedFolder, Path = unmappedFolder,
PathEncoded = Url.Encode(unmappedFolder),
TvDbId = tvDbSeries.Id, TvDbId = tvDbSeries.Id,
TvDbName = tvDbSeries.SeriesName TvDbName = tvDbSeries.SeriesName
}); });
@ -181,26 +202,35 @@ namespace NzbDrone.Web.Controllers
return Content("Unable to add new series, please wait for previous scans to complete first."); return Content("Unable to add new series, please wait for previous scans to complete first.");
} }
public ActionResult AddExistingSeries(string path, int seriesId)
{
//Get TVDB Series Name
//Create new folder for series
//Add the new series to the Database
if (_syncProvider.BeginAddExistingSeries(path, seriesId))
return Content("Manual adding of existing series has started");
return Content("Unable to add existing series, please wait for previous scans to complete first.");
}
public ActionResult SearchForSeries(string seriesName) public ActionResult SearchForSeries(string seriesName)
{ {
var model = new List<SeriesSearchResultModel>(); var model = new List<SeriesSearchResultModel>();
//Get Results from TvDb and convert them to something we can use. //Get Results from TvDb and convert them to something we can use.
//foreach (var tvdbSearchResult in _tvDbProvider.SearchSeries(seriesName)) foreach (var tvdbSearchResult in _tvDbProvider.SearchSeries(seriesName))
//{ {
// model.Add(new SeriesSearchResultModel model.Add(new SeriesSearchResultModel
// { {
// TvDbId = tvdbSearchResult.Id, TvDbId = tvdbSearchResult.Id,
// TvDbName = tvdbSearchResult.SeriesName, TvDbName = tvdbSearchResult.SeriesName,
// FirstAired = tvdbSearchResult.FirstAired FirstAired = tvdbSearchResult.FirstAired
// }); });
//} }
ViewData["RootDirs"] = _rootDirProvider.GetAll();
ViewData["DirSep"] = Path.DirectorySeparatorChar;
model.Add(new SeriesSearchResultModel{ TvDbId = 12345, TvDbName = "30 Rock", FirstAired = DateTime.Today }); //model.Add(new SeriesSearchResultModel{ TvDbId = 12345, TvDbName = "30 Rock", FirstAired = DateTime.Today });
model.Add(new SeriesSearchResultModel { TvDbId = 65432, TvDbName = "The Office (US)", FirstAired = DateTime.Today.AddDays(-100) }); //model.Add(new SeriesSearchResultModel { TvDbId = 65432, TvDbName = "The Office (US)", FirstAired = DateTime.Today.AddDays(-100) });
return PartialView("SeriesSearchResults", model); return PartialView("SeriesSearchResults", model);
} }

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace NzbDrone.Web.Models
{
public class AddExistingManualModel
{
public string Path { get; set; }
public string FolderName { get; set; }
}
}

@ -9,6 +9,7 @@ namespace NzbDrone.Web.Models
{ {
public bool IsWanted { get; set; } public bool IsWanted { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string PathEncoded { get; set; }
public int TvDbId { get; set; } public int TvDbId { get; set; }
public string TvDbName { get; set; } public string TvDbName { get; set; }
} }

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Web; using System.Web;
using NzbDrone.Core.Repository;
namespace NzbDrone.Web.Models namespace NzbDrone.Web.Models
{ {
@ -14,5 +15,9 @@ namespace NzbDrone.Web.Models
[DisplayName("Single Series Path")] [DisplayName("Single Series Path")]
[DisplayFormat(ConvertEmptyStringToNull = false)] [DisplayFormat(ConvertEmptyStringToNull = false)]
public string SeriesName { get; set; } public string SeriesName { get; set; }
public string DirectorySeparatorChar { get; set; }
public List<RootDir> RootDirectories { get; set; }
} }
} }

@ -90,6 +90,7 @@
<Compile Include="Helpers\HtmlPrefixScopeExtensions.cs" /> <Compile Include="Helpers\HtmlPrefixScopeExtensions.cs" />
<Compile Include="Helpers\IsCurrentActionHelper.cs" /> <Compile Include="Helpers\IsCurrentActionHelper.cs" />
<Compile Include="Models\AccountModels.cs" /> <Compile Include="Models\AccountModels.cs" />
<Compile Include="Models\AddExistingManualModel.cs" />
<Compile Include="Models\AddExistingSeriesModel.cs" /> <Compile Include="Models\AddExistingSeriesModel.cs" />
<Compile Include="Models\AddNewSeriesModel.cs" /> <Compile Include="Models\AddNewSeriesModel.cs" />
<Compile Include="Models\DownloadSettingsModel.cs" /> <Compile Include="Models\DownloadSettingsModel.cs" />
@ -278,6 +279,7 @@
<Content Include="Views\Home\Test.aspx" /> <Content Include="Views\Home\Test.aspx" />
<Content Include="Views\Log\Index.aspx" /> <Content Include="Views\Log\Index.aspx" />
<Content Include="Views\Series\AddExisting.aspx" /> <Content Include="Views\Series\AddExisting.aspx" />
<Content Include="Views\Series\AddExistingManual.aspx" />
<Content Include="Views\Series\AddNew.aspx" /> <Content Include="Views\Series\AddNew.aspx" />
<Content Include="Views\Series\Details.aspx" /> <Content Include="Views\Series\Details.aspx" />
<Content Include="Views\Series\Edit.aspx" /> <Content Include="Views\Series\Edit.aspx" />

@ -4,7 +4,7 @@
<%@ Import Namespace="NzbDrone.Web.Models" %> <%@ Import Namespace="NzbDrone.Web.Models" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Add Existing Add Existing Series
</asp:Content> </asp:Content>
<asp:Content ID="Menu" ContentPlaceHolderID="ActionMenu" runat="server"> <asp:Content ID="Menu" ContentPlaceHolderID="ActionMenu" runat="server">
<% <%
@ -16,6 +16,7 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$('#mastercheckbox').attr("checked", "checked"); $('#mastercheckbox').attr("checked", "checked");
document.getElementById('unmappedGrid').style.display = 'block';
}); });
function Grid_onRowDataBound(e) { function Grid_onRowDataBound(e) {
@ -31,32 +32,43 @@
//You can use the OnRowDataBound event to customize the way data is presented on the client-side //You can use the OnRowDataBound event to customize the way data is presented on the client-side
}; };
function Grid_onLoad(e) {
$('.t-no-data').text("Loading...");
};
</script> </script>
<%
Html.Telerik().Grid<AddExistingSeriesModel>().Name("Unmapped_Series_Folders")
.TableHtmlAttributes(new { id = "UnmappedSeriesGrid" })
.Columns(columns =>
{
columns.Bound(c => c.IsWanted).ClientTemplate("<input type='checkbox' name='<#= Path #>' class='checkedSeries' value='<#= TvDbId #>' checked='true'/>")
.Width(20).Title("<input id='mastercheckbox' type='checkbox' />")
.HtmlAttributes(new { style = "text-align:center" });
columns.Bound(c => c.Path);
columns.Bound(c => c.TvDbName);
})
.DataBinding(d => d.Ajax().Select("_AjaxUnmappedFoldersGrid", "Series"))
.ClientEvents(events => events.OnRowDataBound("Grid_onRowDataBound"))
.Footer(false)
.Render();
%>
<p> <div id="unmappedGrid" style="display:none">
<button class="t.button" onclick="syncSelected ()">Sync Selected Series</button> <%
</p> Html.Telerik().Grid<AddExistingSeriesModel>().Name("Unmapped_Series_Folders")
.TableHtmlAttributes(new { id = "UnmappedSeriesGrid" })
.Columns(columns =>
{
columns.Bound(c => c.IsWanted).ClientTemplate("<input type='checkbox' name='<#= Path #>' class='checkedSeries' value='<#= TvDbId #>' checked='true'/>")
.Width(20).Title("<input id='mastercheckbox' type='checkbox' style='margin-left:5px'/>")
.HtmlAttributes(new { style = "text-align:center" });
columns.Bound(c => c.Path).ClientTemplate("<a href=" + Url.Action("AddExistingManual", "Series", new { path = "<#= PathEncoded #>" }) + "><#= Path #></a>")
.Template(c =>
{ %>
<%:Html.ActionLink(c.Path, "AddExistingManual", new { path = c.Path })%>
<% }).Title("Path");
columns.Bound(c => c.TvDbName);
})
.DataBinding(d => d.Ajax().Select("_AjaxUnmappedFoldersGrid", "Series"))
.ClientEvents(events => events.OnRowDataBound("Grid_onRowDataBound"))
.ClientEvents(events => events.OnLoad("Grid_onLoad"))
.Footer(false)
.Render();
%>
<p>
<button class="t.button" onclick="syncSelected ()">Sync Selected Series</button>
</p>
</div>
<div id="result"></div> <div id="result"></div>
<div id="tester"></div>
<script type="text/javascript" language="javascript"> <script type="text/javascript" language="javascript">

@ -0,0 +1,88 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<NzbDrone.Web.Models.AddExistingManualModel>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Add Series Manually
<script type="text/javascript">
jQuery(document).ready(function () {
$('#searchButton').click();
$('#searchButton').attr('disabled', '');
});
</script>
</asp:Content>
<asp:Content ID="Menu" ContentPlaceHolderID="ActionMenu" runat="server">
<%
Html.RenderPartial("SubMenu");
%>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<div>
<h4><%= Html.Label(Model.Path) %></h4>
</div>
<%= Html.Label("Enter a Series Name") %>
<%= Html.TextBoxFor(m => m.FolderName, new { id="existing_series_id" }) %>
<%= Html.TextBoxFor(m => m.Path, new { id ="series_path", style="display:none" }) %>
<p>
<button class="t.button" id="searchButton" disabled="disabled" onclick="searchSeries ()">Search</button>
</p>
<div id="result"></div>
<div id="addSeriesControls" style="display:none">
<button class="t.button" onclick="addSeries ()">Add Series</button>
</div>
<div id="addResult"></div>
<script type="text/javascript" language="javascript">
$('#existing_series_id').bind('keydown', function (e) {
if (e.keyCode == 13) {
$('#searchButton').click();
}
});
function searchSeries() {
var seriesSearch = $('#existing_series_id');
$("#result").text("Searching..."); //Tell the user that we're performing the search
document.getElementById('addSeriesControls').style.display = 'none'; //Hide the add button
$("#result").load('<%=Url.Action("SearchForSeries", "Series") %>', {
seriesName: seriesSearch.val()
});
}
$(".searchRadio").live("change", function () {
var checked = $(this).attr('checked');
if (checked) {
document.getElementById('addSeriesControls').style.display = 'inline';
}
});
function addSeries() {
//Get the selected tvdbid + selected root folder
//jquery bit below doesn't want to work...
var checkedSeries = $("input[name='selectedSeries']:checked").val();
var id = "#" + checkedSeries + "_text";
var seriesName = $(id).val();
var pathTest = $('#series_path').val();
$('#tester').text(pathTest);
$("#addResult").load('<%=Url.Action("AddExistingSeries", "Series") %>', {
path: pathTest,
seriesId: checkedSeries
});
}
</script>
<div id="tester"></div>
</asp:Content>

@ -1,9 +1,17 @@
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<AddNewSeriesModel>" %> <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<AddNewSeriesModel>" %>
<%@ Import Namespace="NzbDrone.Web.Models" %> <%@ Import Namespace="NzbDrone.Web.Models" %>
<%@ Import Namespace="Telerik.Web.Mvc.UI" %> <%@ Import Namespace="Telerik.Web.Mvc.UI" %>
<%@ Import Namespace="NzbDrone.Core.Repository" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Add New Series Add New Series
<script type="text/javascript">
jQuery(document).ready(function () {
$('#searchButton').attr('disabled', '');
});
</script>
</asp:Content> </asp:Content>
<asp:Content ID="Menu" ContentPlaceHolderID="ActionMenu" runat="server"> <asp:Content ID="Menu" ContentPlaceHolderID="ActionMenu" runat="server">
<% <%
@ -16,30 +24,95 @@
<%= Html.TextBox("new_series_name", String.Empty, new { id="new_series_id" }) %> <%= Html.TextBox("new_series_name", String.Empty, new { id="new_series_id" }) %>
<p> <p>
<button class="t.button" onclick="searchSeries ()">Search</button> <button class="t.button" id="searchButton" disabled="disabled" onclick="searchSeries ()">Search</button>
</p> </p>
<div id="result"></div> <div id="result"></div>
<div id="RootDirectories" style="display:none">
<fieldset>
<legend>Root TV Folders</legend>
<% int d = 0; %>
<% foreach (var dir in Model.RootDirectories)
{ %>
<%: Html.RadioButton("selectedRootDir", dir.Path, dir.Default, new { @class="dirList examplePart", id="dirRadio_" + d }) %>
<%: Html.Label(dir.Path) %>
<% if (dir.Default) { %> * <% } %>
<% d++;%>
<br />
<% } %>
</fieldset>
<div id="example"></div>
<button class="t.button" onclick="addSeries ()">Add New Series</button>
</div>
<div id="addResult"></div>
<script type="text/javascript" language="javascript"> <script type="text/javascript" language="javascript">
function searchSeries() { $('#new_series_id').bind('keydown', function (e) {
var seriesSearch = $('#new_series_id'); if (e.keyCode == 13) {
$('#searchButton').click();
}
});
// if ($seriesSearch.length < 1) { function searchSeries() {
// alert("Enter a valid name to search for"); var seriesSearch = $('#new_series_id');
// return;
// }
//Setup a function to handle the results... Or return a partial... $("#result").text("Searching...");
document.getElementById('RootDirectories').style.display = 'none';
$("#result").load('<%=Url.Action("SearchForSeries", "Series") %>', { $("#result").load('<%=Url.Action("SearchForSeries", "Series") %>', {
seriesName: seriesSearch.val() seriesName: seriesSearch.val()
});
}
$(".searchRadio").live("change", function () {
var checked = $(this).attr('checked');
if (checked) {
document.getElementById('RootDirectories').style.display = 'inline';
} }
});
function addSeries() {
//Get the selected tvdbid + selected root folder
//jquery bit below doesn't want to work...
//this.window.location = '<%= Url.Action("Index", "Series") %>'; var checkedSeries = $("input[name='selectedSeries']:checked").val();
//var checkedSeries = $('input.searchRadio:checked').val();
//var checkedSeries = $('input:radio[name=selectedSeries]:checked').val();
//var checkedSeries = $('input:radio[class=searchRadio]:checked').val();
); var checkedDir = $("input[name='selectedRootDir']:checked").val();
var id = "#" + checkedSeries + "_text";
var seriesName = $(id).val();
$("#addResult").load('<%=Url.Action("AddNewSeries", "Series") %>', {
dir: checkedDir,
seriesId: checkedSeries,
seriesName: seriesName
});
} }
//Need to figure out how to use 'ViewData["DirSep"]' instead of hardcoding '\'
$(".examplePart").live("change", function () {
var dir = $("input[name='selectedRootDir']:checked").val();
var series = $("input[name='selectedSeries']:checked").val();
var id = "#" + series + "_text";
var seriesName = $(id).val();
var sep = "\\";
var str = "Example: " + dir + sep + seriesName;
$('#example').text(str);
});
</script> </script>
<div id="tester"></div> <div id="tester"></div>

@ -2,9 +2,16 @@
<%@ Import Namespace="NzbDrone.Core.Repository" %> <%@ Import Namespace="NzbDrone.Core.Repository" %>
<div id="searchResults"> <div id="searchResults">
<fieldset> <fieldset class="tvDbSearchResults">
<legend>Search Results</legend> <legend>Search Results</legend>
<% if (Model.Count == 0)
{ %>
<b>No results found for the series name</b>
<% }
%>
<% int r = 0; %> <% int r = 0; %>
<% foreach (var result in Model) <% foreach (var result in Model)
{ %> { %>
@ -17,75 +24,4 @@
<% <%
} %> } %>
</fieldset> </fieldset>
</div> </div>
<div id="RootDirectories" style="display:none">
<fieldset>
<legend>Root TV Folders</legend>
<% int d = 0; %>
<% foreach (var dir in (List<RootDir>)ViewData["RootDirs"])
{ %>
<%: Html.RadioButton("selectedRootDir", dir.Path, dir.Default, new { @class="dirList examplePart", id="dirRadio_" + d }) %>
<%: Html.Label(dir.Path) %>
<% if (dir.Default) { %> * <% } %>
<% d++;%>
<br />
<% } %>
</fieldset>
<div id="example"></div>
<button class="t.button" onclick="addSeries ()">Add New Series</button>
</div>
<div id="addResult"></div>
<script type="text/javascript">
$(".searchRadio").live("change", function () {
var checked = $(this).attr('checked');
if (checked) {
document.getElementById('RootDirectories').style.display = 'inline';
}
});
function addSeries() {
//Get the selected tvdbid + selected root folder
//jquery bit below doesn't want to work...
var checkedSeries = $("input[name='selectedSeries']:checked").val();
//var checkedSeries = $('input.searchRadio:checked').val();
//var checkedSeries = $('input:radio[name=selectedSeries]:checked').val();
//var checkedSeries = $('input:radio[class=searchRadio]:checked').val();
var checkedDir = $("input[name='selectedRootDir']:checked").val();
var id = "#" + checkedSeries + "_text";
var seriesName = $(id).val();
$("#addResult").load('<%=Url.Action("AddNewSeries", "Series") %>', {
dir: checkedDir,
seriesId: checkedSeries,
seriesName: seriesName
});
}
//Need to figure out how to use 'ViewData["DirSep"]' instead of hardcoding '\'
$(".examplePart").live("change", function () {
var dir = $("input[name='selectedRootDir']:checked").val();
var series = $("input[name='selectedSeries']:checked").val();
var id = "#" + series + "_text";
var seriesName = $(id).val();
//var sep = '<%= ViewData["DirSep"] %>';
var sep = "\\";
var str = "Example: " + dir + sep + seriesName;
$('#example').text(str);
});
</script>
Loading…
Cancel
Save