diff --git a/NzbDrone.Core.Test/Files/Xem/Failure.txt b/NzbDrone.Core.Test/Files/Xem/Failure.txt
new file mode 100644
index 000000000..63d217c10
--- /dev/null
+++ b/NzbDrone.Core.Test/Files/Xem/Failure.txt
@@ -0,0 +1,7 @@
+{
+
+ "result": "failure",
+ "data": [ ],
+ "message": "no show with the tvdb_id 79488 found"
+
+}
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/Files/Xem/Ids.txt b/NzbDrone.Core.Test/Files/Xem/Ids.txt
new file mode 100644
index 000000000..fb005862c
--- /dev/null
+++ b/NzbDrone.Core.Test/Files/Xem/Ids.txt
@@ -0,0 +1,24 @@
+{
+
+ "result": "success",
+ "data": {
+ "220571": [
+ "Is This a Zombie? Of the Dead",
+ "Kore wa Zombie Desuka?",
+ "Kore wa Zombie Desuka? Of the Dead",
+ "Kore wa Zombie Desuka Of the Dead",
+ "Kore wa Zombie Desu ka - Of the Dead",
+ "Kore wa Zombie Desu ka of the Dead"
+ ],
+ "79151": [
+ "Fate Stay Night",
+ "Fate/Zero",
+ "Fate Zero",
+ "Fate/Zero (2012)",
+ "Fate Zero S2",
+ "Fate Zero"
+ ]
+ },
+ "message": ""
+
+}
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/Files/Xem/Mappings.txt b/NzbDrone.Core.Test/Files/Xem/Mappings.txt
new file mode 100644
index 000000000..bc7f223ac
--- /dev/null
+++ b/NzbDrone.Core.Test/Files/Xem/Mappings.txt
@@ -0,0 +1,32 @@
+{
+
+ "result": "success",
+ "data": [
+ {
+ "scene": {
+ "season": 1,
+ "episode": 1,
+ "absolute": 1
+ },
+ "tvdb": {
+ "season": 1,
+ "episode": 1,
+ "absolute": 1
+ }
+ },
+ {
+ "scene": {
+ "season": 1,
+ "episode": 2,
+ "absolute": 2
+ },
+ "tvdb": {
+ "season": 1,
+ "episode": 2,
+ "absolute": 2
+ }
+ }
+ ],
+ "message": "full mapping for 73388 on tvdb. this was a cached version"
+
+}
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
index 42e947a8a..f8b43b823 100644
--- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -191,6 +191,8 @@
+
+
@@ -332,6 +334,15 @@
Designer
Always
+
+ Always
+
+
+ Always
+
+
+ Always
+
Designer
Always
diff --git a/NzbDrone.Core.Test/ProviderTests/XemCommunicationProviderTests/GetSceneTvdbMappingsFixture.cs b/NzbDrone.Core.Test/ProviderTests/XemCommunicationProviderTests/GetSceneTvdbMappingsFixture.cs
new file mode 100644
index 000000000..f5ffc95ae
--- /dev/null
+++ b/NzbDrone.Core.Test/ProviderTests/XemCommunicationProviderTests/GetSceneTvdbMappingsFixture.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common;
+using NzbDrone.Core.Model.Notification;
+using NzbDrone.Core.Providers;
+using NzbDrone.Core.Repository;
+using NzbDrone.Core.Repository.Quality;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Test.Common.AutoMoq;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.ProviderTests.XemCommunicationProviderTests
+{
+ [TestFixture]
+ // ReSharper disable InconsistentNaming
+ public class GetSceneTvdbMappingsFixture : CoreTest
+ {
+ private void WithFailureJson()
+ {
+ Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny()))
+ .Returns(File.ReadAllText(@".\Files\Xem\Failure.txt"));
+ }
+
+ private void WithIdsJson()
+ {
+ Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny()))
+ .Returns(File.ReadAllText(@".\Files\Xem\Ids.txt"));
+ }
+
+ private void WithMappingsJson()
+ {
+ Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny()))
+ .Returns(File.ReadAllText(@".\Files\Xem\Mappings.txt"));
+ }
+
+ [Test]
+ public void should_throw_when_failure_is_found()
+ {
+ WithFailureJson();
+ Assert.Throws(() => Mocker.Resolve().GetSceneTvdbMappings(12345));
+ }
+
+ [Test]
+ public void should_get_list_of_mappings()
+ {
+ WithMappingsJson();
+ Mocker.Resolve().GetSceneTvdbMappings(12345).Should().NotBeEmpty();
+ }
+
+ [Test]
+ public void should_have_two_mappings()
+ {
+ WithMappingsJson();
+ Mocker.Resolve().GetSceneTvdbMappings(12345).Should().HaveCount(2);
+ }
+
+ [Test]
+ public void should_have_expected_results()
+ {
+ WithMappingsJson();
+ var results = Mocker.Resolve().GetSceneTvdbMappings(12345);
+ var first = results.First();
+ first.Scene.Absolute.Should().Be(1);
+ first.Scene.Season.Should().Be(1);
+ first.Scene.Episode.Should().Be(1);
+ first.Tvdb.Absolute.Should().Be(1);
+ first.Tvdb.Season.Should().Be(1);
+ first.Tvdb.Episode.Should().Be(1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/ProviderTests/XemCommunicationProviderTests/GetXemSeriesIdsFixture.cs b/NzbDrone.Core.Test/ProviderTests/XemCommunicationProviderTests/GetXemSeriesIdsFixture.cs
new file mode 100644
index 000000000..55c6885de
--- /dev/null
+++ b/NzbDrone.Core.Test/ProviderTests/XemCommunicationProviderTests/GetXemSeriesIdsFixture.cs
@@ -0,0 +1,63 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common;
+using NzbDrone.Core.Model.Notification;
+using NzbDrone.Core.Providers;
+using NzbDrone.Core.Repository;
+using NzbDrone.Core.Repository.Quality;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Test.Common.AutoMoq;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.ProviderTests.XemCommunicationProviderTests
+{
+ [TestFixture]
+ // ReSharper disable InconsistentNaming
+ public class GetXemSeriesIdsFixture : CoreTest
+ {
+ private void WithFailureJson()
+ {
+ Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny()))
+ .Returns(File.ReadAllText(@".\Files\Xem\Failure.txt"));
+ }
+
+ private void WithIdsJson()
+ {
+ Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny()))
+ .Returns(File.ReadAllText(@".\Files\Xem\Ids.txt"));
+ }
+
+ private void WithMappingsJson()
+ {
+ Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny()))
+ .Returns(File.ReadAllText(@".\Files\Xem\Mappings.txt"));
+ }
+
+ [Test]
+ public void should_throw_when_failure_is_found()
+ {
+ WithFailureJson();
+ Assert.Throws(() => Mocker.Resolve().GetXemSeriesIds());
+ }
+
+ [Test]
+ public void should_get_list_of_int()
+ {
+ WithIdsJson();
+ Mocker.Resolve().GetXemSeriesIds().Should().NotBeEmpty();
+ }
+
+ [Test]
+ public void should_have_two_ids()
+ {
+ WithIdsJson();
+ Mocker.Resolve().GetXemSeriesIds().Should().HaveCount(2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core/Datastore/Migrations/Migration20121016.cs b/NzbDrone.Core/Datastore/Migrations/Migration20121016.cs
new file mode 100644
index 000000000..22aec34fc
--- /dev/null
+++ b/NzbDrone.Core/Datastore/Migrations/Migration20121016.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Data;
+using Migrator.Framework;
+using NzbDrone.Common;
+
+namespace NzbDrone.Core.Datastore.Migrations
+{
+ [Migration(20121016)]
+ public class Migration20121016 : NzbDroneMigration
+ {
+ protected override void MainDbUpgrade()
+ {
+ Database.AddColumn("Episodes", new Column("SceneAbsoluteEpisodeNumber", DbType.Int32, ColumnProperty.Null));
+ Database.AddColumn("Episodes", new Column("SceneSeasonNumber", DbType.Int32, ColumnProperty.Null));
+ Database.AddColumn("Episodes", new Column("SceneEpisodeNumber", DbType.Int32, ColumnProperty.Null));
+ }
+ }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core/Model/Xem/XemResult.cs b/NzbDrone.Core/Model/Xem/XemResult.cs
new file mode 100644
index 000000000..408aa6551
--- /dev/null
+++ b/NzbDrone.Core/Model/Xem/XemResult.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Core.Model.Xem
+{
+ public class XemResult
+ {
+ public string Result { get; set; }
+ public T Data { get; set; }
+ public string Message { get; set; }
+ }
+}
diff --git a/NzbDrone.Core/Model/Xem/XemSceneTvdbMapping.cs b/NzbDrone.Core/Model/Xem/XemSceneTvdbMapping.cs
new file mode 100644
index 000000000..29e242e5b
--- /dev/null
+++ b/NzbDrone.Core/Model/Xem/XemSceneTvdbMapping.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Core.Model.Xem
+{
+ public class XemSceneTvdbMapping
+ {
+ public XemValues Scene { get; set; }
+ public XemValues Tvdb { get; set; }
+ }
+}
diff --git a/NzbDrone.Core/Model/Xem/XemValues.cs b/NzbDrone.Core/Model/Xem/XemValues.cs
new file mode 100644
index 000000000..781fbd5c4
--- /dev/null
+++ b/NzbDrone.Core/Model/Xem/XemValues.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Core.Model.Xem
+{
+ public class XemValues
+ {
+ public int Season { get; set; }
+ public int Episode { get; set; }
+ public int Absolute { get; set; }
+ }
+}
diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj
index 27f6da914..fd55c83f4 100644
--- a/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/NzbDrone.Core/NzbDrone.Core.csproj
@@ -292,6 +292,9 @@
+
+
+
@@ -330,6 +333,7 @@
+
diff --git a/NzbDrone.Core/Providers/EpisodeProvider.cs b/NzbDrone.Core/Providers/EpisodeProvider.cs
index e3e4bfb3e..398938c5e 100644
--- a/NzbDrone.Core/Providers/EpisodeProvider.cs
+++ b/NzbDrone.Core/Providers/EpisodeProvider.cs
@@ -331,7 +331,7 @@ namespace NzbDrone.Core.Providers
episodeToUpdate.TvDbEpisodeId = episode.Id;
episodeToUpdate.EpisodeNumber = episode.EpisodeNumber;
episodeToUpdate.SeasonNumber = episode.SeasonNumber;
- episodeToUpdate.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber;
+ episodeToUpdate.AbsoluteEpisodeNumber = episode.AbsoluteNumber;
episodeToUpdate.Title = episode.EpisodeName;
episodeToUpdate.Overview = episode.Overview.Truncate(3500);
@@ -435,5 +435,10 @@ namespace NzbDrone.Core.Providers
logger.Trace("Updating PostDownloadStatus for all episodeIds in {0}", episodeIdString);
_database.Execute(episodeIdQuery);
}
+
+ public virtual void UpdateEpisodes(List episodes)
+ {
+ _database.UpdateMany(episodes);
+ }
}
}
\ No newline at end of file
diff --git a/NzbDrone.Core/Providers/XemCommunicationProvider.cs b/NzbDrone.Core/Providers/XemCommunicationProvider.cs
new file mode 100644
index 000000000..7c0d97e3a
--- /dev/null
+++ b/NzbDrone.Core/Providers/XemCommunicationProvider.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NLog;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Ninject;
+using NzbDrone.Common;
+using NzbDrone.Core.Model.Xem;
+
+namespace NzbDrone.Core.Providers
+{
+ public class XemCommunicationProvider
+ {
+ private readonly HttpProvider _httpProvider;
+
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ private const string XEM_BASE_URL = "http://thexem.de/map/";
+
+ [Inject]
+ public XemCommunicationProvider(HttpProvider httpProvider)
+ {
+ _httpProvider = httpProvider;
+ }
+
+ public XemCommunicationProvider()
+ {
+ }
+
+ public virtual List GetXemSeriesIds(string origin = "tvdb")
+ {
+ _logger.Trace("Fetching Series IDs from: {0}", origin);
+ var url = String.Format("{0}allNames?origin={1}", XEM_BASE_URL, origin);
+ var response =_httpProvider.DownloadString(url);
+
+ CheckForFailureResult(response);
+
+ var result = JsonConvert.DeserializeObject>>(JObject.Parse(response).SelectToken("data").ToString());
+
+ return result.Keys.ToList();
+ }
+
+ public virtual List 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 response = _httpProvider.DownloadString(url);
+
+ CheckForFailureResult(response);
+
+ var result = JsonConvert.DeserializeObject>(JObject.Parse(response).SelectToken("data").ToString());
+
+ return result;
+ }
+
+ public virtual void CheckForFailureResult(string response)
+ {
+ var result = JsonConvert.DeserializeObject>(response);
+
+ if (result != null && result.Result.Equals("failure", StringComparison.InvariantCultureIgnoreCase))
+ throw new Exception("Error response received from Xem: " + result.Message);
+ }
+ }
+}
diff --git a/NzbDrone.Core/Providers/XemProvider.cs b/NzbDrone.Core/Providers/XemProvider.cs
new file mode 100644
index 000000000..83381fb0a
--- /dev/null
+++ b/NzbDrone.Core/Providers/XemProvider.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NLog;
+using Ninject;
+using NzbDrone.Core.Repository;
+
+namespace NzbDrone.Core.Providers
+{
+ public class XemProvider
+ {
+ private readonly SeriesProvider _seriesProvider;
+ private readonly EpisodeProvider _episodeProvider;
+ private readonly XemCommunicationProvider _xemCommunicationProvider;
+
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ [Inject]
+ public XemProvider(SeriesProvider seriesProvider, EpisodeProvider episodeProvider,
+ XemCommunicationProvider xemCommunicationProvider)
+ {
+ _seriesProvider = seriesProvider;
+ _episodeProvider = episodeProvider;
+ _xemCommunicationProvider = xemCommunicationProvider;
+ }
+
+ public XemProvider()
+ {
+ }
+
+ public virtual void UpdateMappings()
+ {
+ _logger.Trace("Starting scene numbering update");
+ try
+ {
+ var ids = _xemCommunicationProvider.GetXemSeriesIds();
+ var series = _seriesProvider.GetAllSeries();
+ var wantedSeries = series.Where(s => ids.Contains(s.SeriesId));
+
+ foreach(var ser in wantedSeries)
+ {
+ _logger.Trace("Updating scene numbering mapping for: {0}", ser.Title);
+ try
+ {
+ var episodesToUpdate = new List();
+ var mappings = _xemCommunicationProvider.GetSceneTvdbMappings(ser.SeriesId);
+
+ if (mappings == null)
+ {
+ _logger.Trace("Mappings for: {0} are null, skipping", ser.Title);
+ continue;
+ }
+
+ foreach(var mapping in mappings)
+ {
+ _logger.Trace("Setting scene numbering mappings for {0} S{1:00}E{2:00}", ser.Title, mapping.Tvdb.Season, mapping.Tvdb.Episode);
+
+ var episode = _episodeProvider.GetEpisode(ser.SeriesId, mapping.Tvdb.Season, mapping.Tvdb.Episode);
+ 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}", ser.Title);
+ _episodeProvider.UpdateEpisodes(episodesToUpdate);
+ }
+
+ catch(Exception ex)
+ {
+ _logger.WarnException("Error updating scene numbering mappings for: " + ser, ex);
+ }
+ }
+
+ _logger.Trace("Completed scene numbering update");
+ }
+
+ catch(Exception ex)
+ {
+ _logger.WarnException("Error updating Scene Mappings", ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/NzbDrone.Core/Repository/Episode.cs b/NzbDrone.Core/Repository/Episode.cs
index 9c3e38968..f93782fd6 100644
--- a/NzbDrone.Core/Repository/Episode.cs
+++ b/NzbDrone.Core/Repository/Episode.cs
@@ -22,6 +22,9 @@ namespace NzbDrone.Core.Repository
public Boolean Ignored { get; set; }
public PostDownloadStatusType PostDownloadStatus { get; set; }
public int AbsoluteEpisodeNumber { get; set; }
+ public int SceneAbsoluteEpisodeNumber { get; set; }
+ public int SceneSeasonNumber { get; set; }
+ public int SceneEpisodeNumber { get; set; }
///
/// Gets or sets the grab date.