From b50e16a4560e475a67393ac050ca3dd044263a71 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 9 Jul 2012 21:37:24 -0700 Subject: [PATCH] Metadata coming together for XBMC --- NzbDrone.Common/DiskProvider.cs | 4 + .../Datastore/Migrations/Migration20120707.cs | 21 ++ NzbDrone.Core/Model/MisnamedEpisodeModel.cs | 2 +- NzbDrone.Core/Providers/BannerProvider.cs | 17 ++ .../Providers/Core/ConfigProvider.cs | 21 ++ .../Providers/Metadata/MetadataBase.cs | 45 ++-- NzbDrone.Core/Providers/Metadata/Xbmc.cs | 199 ++++++++++++++++++ NzbDrone.Core/Providers/MetadataProvider.cs | 60 +++--- NzbDrone.Core/Providers/TvDbProvider.cs | 8 +- ...aseDefinition.cs => MetadataDefinition.cs} | 4 +- 10 files changed, 316 insertions(+), 65 deletions(-) create mode 100644 NzbDrone.Core/Datastore/Migrations/Migration20120707.cs create mode 100644 NzbDrone.Core/Providers/Metadata/Xbmc.cs rename NzbDrone.Core/Repository/{MetabaseDefinition.cs => MetadataDefinition.cs} (79%) diff --git a/NzbDrone.Common/DiskProvider.cs b/NzbDrone.Common/DiskProvider.cs index 4322f3d0d..66b7414bf 100644 --- a/NzbDrone.Common/DiskProvider.cs +++ b/NzbDrone.Common/DiskProvider.cs @@ -200,6 +200,10 @@ namespace NzbDrone.Common return File.ReadAllText(filePath); } + public virtual void WriteAllText(string filename, string contents) + { + File.WriteAllText(filename, contents); + } public static bool PathEquals(string firstPath, string secondPath) { diff --git a/NzbDrone.Core/Datastore/Migrations/Migration20120707.cs b/NzbDrone.Core/Datastore/Migrations/Migration20120707.cs new file mode 100644 index 000000000..e93c4446d --- /dev/null +++ b/NzbDrone.Core/Datastore/Migrations/Migration20120707.cs @@ -0,0 +1,21 @@ +using System.Data; +using Migrator.Framework; + +namespace NzbDrone.Core.Datastore.Migrations +{ + + [Migration(20120707)] + public class Migration20120707 : NzbDroneMigration + { + protected override void MainDbUpgrade() + { + Database.AddTable("MetadataDefinitions", new[] + { + new Column("Id", DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), + new Column("Enable", DbType.Boolean, ColumnProperty.NotNull), + new Column("MetadataProviderType", DbType.String, ColumnProperty.NotNull), + new Column("Name", DbType.String, ColumnProperty.NotNull) + }); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Model/MisnamedEpisodeModel.cs b/NzbDrone.Core/Model/MisnamedEpisodeModel.cs index 8697f6754..a7445d609 100644 --- a/NzbDrone.Core/Model/MisnamedEpisodeModel.cs +++ b/NzbDrone.Core/Model/MisnamedEpisodeModel.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace NzbDrone.Core.Model +namespace NzbDrone.Core.Model.Metadata { public class MisnamedEpisodeModel { diff --git a/NzbDrone.Core/Providers/BannerProvider.cs b/NzbDrone.Core/Providers/BannerProvider.cs index 2176e9d7c..16f091a43 100644 --- a/NzbDrone.Core/Providers/BannerProvider.cs +++ b/NzbDrone.Core/Providers/BannerProvider.cs @@ -81,5 +81,22 @@ namespace NzbDrone.Core.Providers } return true; } + + public virtual void Download(string remotePath, string filename) + { + var url = BANNER_URL_PREFIX + remotePath; + + try + { + _httpProvider.DownloadFile(url, filename); + logger.Trace("Successfully download banner from '{0}' to '{1}'", url, filename); + } + catch (Exception ex) + { + var message = String.Format("Failed to download Banner from '{0}' to '{1}'", url, filename); + logger.DebugException(message, ex); + throw; + } + } } } diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs index 2cdc6be0e..76ad96d42 100644 --- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs @@ -501,6 +501,27 @@ namespace NzbDrone.Core.Providers.Core set { SetValue("PlexPassword", value); } } + public virtual Boolean MetadataEnabled + { + get { return GetValueBoolean("MetadataEnabled"); } + + set { SetValue("MetadataEnabled", value); } + } + + public virtual Boolean MetadataXbmcEnabled + { + get { return GetValueBoolean("MetadataXbmcEnabled"); } + + set { SetValue("MetadataXbmcEnabled", value); } + } + + public virtual Boolean MetadataUseBanners + { + get { return GetValueBoolean("MetadataUseBanners"); } + + set { SetValue("MetadataUseBanners", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/NzbDrone.Core/Providers/Metadata/MetadataBase.cs b/NzbDrone.Core/Providers/Metadata/MetadataBase.cs index c312f4c0f..dc2f84ee2 100644 --- a/NzbDrone.Core/Providers/Metadata/MetadataBase.cs +++ b/NzbDrone.Core/Providers/Metadata/MetadataBase.cs @@ -1,8 +1,10 @@ using System; using NLog; +using NzbDrone.Common; using NzbDrone.Core.Model; using NzbDrone.Core.Providers.Core; using NzbDrone.Core.Repository; +using TvdbLib.Data; namespace NzbDrone.Core.Providers.Metadata { @@ -10,43 +12,42 @@ namespace NzbDrone.Core.Providers.Metadata { protected readonly Logger _logger; protected readonly ConfigProvider _configProvider; + protected readonly DiskProvider _diskProvider; + protected readonly BannerProvider _bannerProvider; + protected readonly EpisodeProvider _episodeProvider; - protected MetadataBase(ConfigProvider configProvider) + protected MetadataBase(ConfigProvider configProvider, DiskProvider diskProvider, + BannerProvider bannerProvider, EpisodeProvider episodeProvider) { _configProvider = configProvider; + _diskProvider = diskProvider; + _bannerProvider = bannerProvider; + _episodeProvider = episodeProvider; _logger = LogManager.GetLogger(GetType().ToString()); } /// - /// Gets the name for the notification provider + /// Gets the name for the metabase provider /// public abstract string Name { get; } /// - /// Performs the on grab action + /// Creates metadata for a series /// - /// The message to send to the receiver - public abstract void OnGrab(string message); + /// The series to create the metadata for + /// Series information from TheTvDb + public abstract void ForSeries(Series series, TvdbSeries tvDbSeries); /// - /// Performs the on download action + /// Creates metadata for the episode file /// - /// The message to send to the receiver - /// The Series for the new download - public abstract void OnDownload(string message, Series series); + /// The episode file to create the metadata + /// Series information from TheTvDb + public abstract void ForEpisodeFile(EpisodeFile episodeFile, TvdbSeries tvDbSeries); - /// - /// Performs the on rename action - /// - /// The message to send to the receiver - /// The Series for the new download - public abstract void OnRename(string message, Series series); - - /// - /// Performs the after rename action, this will be handled after all renaming for episode/season/series - /// - /// The message to send to the receiver - /// The Series for the new download - public abstract void AfterRename(string message, Series series); + public virtual string GetEpisodeGuideUrl(int seriesId) + { + return String.Format("http://www.thetvdb.com/api/{0}/series/{1}/all/en.zip", TvDbProvider.TVDB_APIKEY, seriesId); + } } } diff --git a/NzbDrone.Core/Providers/Metadata/Xbmc.cs b/NzbDrone.Core/Providers/Metadata/Xbmc.cs new file mode 100644 index 000000000..46b9f6f01 --- /dev/null +++ b/NzbDrone.Core/Providers/Metadata/Xbmc.cs @@ -0,0 +1,199 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; +using TvdbLib.Data; +using TvdbLib.Data.Banner; + +namespace NzbDrone.Core.Providers.Metadata +{ + public abstract class Xbmc : MetadataBase + { + protected readonly Logger _logger; + + public Xbmc(ConfigProvider configProvider, DiskProvider diskProvider, BannerProvider bannerProvider, EpisodeProvider episodeProvider) + : base(configProvider, diskProvider, bannerProvider, episodeProvider) + { + } + + public override string Name + { + get { return "XBMC"; } + } + + public override void ForSeries(Series series, TvdbSeries tvDbSeries) + { + //Create tvshow.nfo, fanart.jpg, folder.jpg and searon##.tbn + var episodeGuideUrl = GetEpisodeGuideUrl(series.SeriesId); + + _logger.Debug("Generating tvshow.nfo for: {0}", series.Title); + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = false; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var tvShow = new XElement("tvshow"); + tvShow.Add(new XElement("title", tvDbSeries.SeriesName)); + tvShow.Add(new XElement("rating", tvDbSeries.Rating)); + tvShow.Add(new XElement("plot", tvDbSeries.Overview)); + tvShow.Add(new XElement("episodeguide", new XElement("url"), episodeGuideUrl)); + tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); + tvShow.Add(new XElement("mpaa", tvDbSeries.ContentRating)); + tvShow.Add(new XElement("genre", tvDbSeries.GenreString)); + tvShow.Add(new XElement("premiered", tvDbSeries.FirstAired.ToString("yyyy-MM-dd"))); + tvShow.Add(new XElement("studio", tvDbSeries.Network)); + + foreach(var actor in tvDbSeries.TvdbActors) + { + tvShow.Add(new XElement("actor", + new XElement("name", actor.Name), + new XElement("role", actor.Role), + new XElement("thumb", actor.ActorImage) + )); + } + + var doc = new XDocument(tvShow); + doc.Save(xw); + } + + _logger.Debug("Saving tvshow.nfo for {0}", series.Title); + _diskProvider.WriteAllText(Path.Combine(series.Path, "tvshow.nfo"), sb.ToString()); + + _logger.Debug("Downloading fanart for: {0}", series.Title); + _bannerProvider.Download(tvDbSeries.FanartPath, Path.Combine(series.Path, "fanart.jpg")); + + if (!_configProvider.MetadataUseBanners) + { + _logger.Debug("Downloading series thumbnail for: {0}", series.Title); + _bannerProvider.Download(tvDbSeries.PosterPath, "folder.jpg"); + + _logger.Debug("Downloading Season posters for {0}", series.Title); + DownloadSeasonThumbnails(series, tvDbSeries, TvdbSeasonBanner.Type.season); + } + + else + { + _logger.Debug("Downloading series banner for: {0}", series.Title); + _bannerProvider.Download(tvDbSeries.BannerPath, "folder.jpg"); + + _logger.Debug("Downloading Season banners for {0}", series.Title); + DownloadSeasonThumbnails(series, tvDbSeries, TvdbSeasonBanner.Type.seasonwide); + } + } + + public override void ForEpisodeFile(EpisodeFile episodeFile, TvdbSeries tvDbSeries) + { + //Download filename.tbn and filename.nfo + //Use BannerPath for Thumbnail + var episodes = _episodeProvider.GetEpisodesByFileId(episodeFile.EpisodeFileId); + + if (!episodes.Any()) + { + _logger.Debug("No episodes where found for this episode file: {0}", episodeFile.EpisodeFileId); + return; + } + + var episodeFileThumbnail = tvDbSeries.Episodes.FirstOrDefault( + e => + e.SeasonNumber == episodeFile.SeasonNumber && + e.EpisodeNumber == episodes.First().EpisodeNumber); + + if (episodeFileThumbnail == null || String.IsNullOrWhiteSpace(episodeFileThumbnail.BannerPath)) + { + _logger.Debug("No thumbnail is available for this episode"); + return; + } + + _logger.Debug("Downloading episode thumbnail for: {0}", episodeFile.EpisodeFileId); + _bannerProvider.Download(episodeFileThumbnail.BannerPath, "folder.jpg"); + + _logger.Debug("Generating filename.nfo for: {0}", episodeFile.EpisodeFileId); + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = false; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var doc = new XDocument(); + + foreach (var episode in episodes) + { + var tvdbEpisode = + tvDbSeries.Episodes.FirstOrDefault( + e => + e.SeasonNumber == episode.SeasonNumber && + e.EpisodeNumber == episode.EpisodeNumber); + + if (tvdbEpisode == null) + { + _logger.Debug("Unable to find episode from TvDb - skipping"); + return; + } + + var details = new XElement("episodedetails"); + details.Add(new XElement("title", tvdbEpisode.EpisodeName)); + details.Add(new XElement("season", tvdbEpisode.SeasonNumber)); + details.Add(new XElement("episode", tvdbEpisode.EpisodeNumber)); + details.Add(new XElement("aired", tvdbEpisode.FirstAired)); + details.Add(new XElement("plot", tvDbSeries.Overview)); + details.Add(new XElement("displayseason")); + details.Add(new XElement("displayepisode")); + details.Add(new XElement("thumb", "http://www.thetvdb.com/banners/" + tvdbEpisode.BannerPath)); + details.Add(new XElement("watched", "false")); + details.Add(new XElement("credits", tvdbEpisode.Writer.First())); + details.Add(new XElement("director", tvdbEpisode.Directors.First())); + details.Add(new XElement("rating", tvDbSeries.Rating)); + + foreach(var actor in tvdbEpisode.GuestStars) + { + if (!String.IsNullOrWhiteSpace(actor)) + continue; + + details.Add(new XElement("actor", + new XElement("name", actor) + )); + } + + foreach(var actor in tvDbSeries.TvdbActors) + { + details.Add(new XElement("actor", + new XElement("name", actor.Name), + new XElement("role", actor.Role), + new XElement("thumb", actor.ActorImage) + )); + } + + doc.Add(details); + doc.Save(xw); + } + } + + var filename = Path.GetFileNameWithoutExtension(episodeFile.Path) + ".nfo"; + _logger.Debug("Saving episodedetails to: {0}", filename); + _diskProvider.WriteAllText(filename, sb.ToString()); + } + + private void DownloadSeasonThumbnails(Series series, TvdbSeries tvDbSeries, TvdbSeasonBanner.Type bannerType) + { + var seasons = tvDbSeries.SeasonBanners.Where(s => s.BannerType == bannerType).Select(s => s.Season); + + foreach (var season in seasons) + { + var banner = tvDbSeries.SeasonBanners.FirstOrDefault(b => b.BannerType == bannerType && b.Season == season); + _logger.Debug("Downloading banner for Season: {0} Series: {1}", season, series.Title); + _bannerProvider.Download(banner.BannerPath, + Path.Combine(series.Path, String.Format("season{0:00}.tbn", season))); + } + } + } +} diff --git a/NzbDrone.Core/Providers/MetadataProvider.cs b/NzbDrone.Core/Providers/MetadataProvider.cs index b5200167c..eb0f11d5e 100644 --- a/NzbDrone.Core/Providers/MetadataProvider.cs +++ b/NzbDrone.Core/Providers/MetadataProvider.cs @@ -16,13 +16,15 @@ namespace NzbDrone.Core.Providers private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IDatabase _database; - private IEnumerable _metadataBases; + private IEnumerable _metadataProviders; + private readonly TvDbProvider _tvDbProvider; [Inject] - public MetadataProvider(IDatabase database, IEnumerable metadataBases) + public MetadataProvider(IDatabase database, IEnumerable metadataProviders, TvDbProvider tvDbProvider) { _database = database; - _metadataBases = metadataBases; + _metadataProviders = metadataProviders; + _tvDbProvider = tvDbProvider; } public MetadataProvider() @@ -30,12 +32,12 @@ namespace NzbDrone.Core.Providers } - public virtual List All() + public virtual List All() { - return _database.Fetch(); + return _database.Fetch(); } - public virtual void SaveSettings(MetabaseDefinition settings) + public virtual void SaveSettings(MetadataDefinition settings) { if (settings.Id == 0) { @@ -50,31 +52,31 @@ namespace NzbDrone.Core.Providers } } - public virtual MetabaseDefinition GetSettings(Type type) + public virtual MetadataDefinition GetSettings(Type type) { - return _database.SingleOrDefault("WHERE MetadataProviderType = @0", type.ToString()); + return _database.SingleOrDefault("WHERE MetadataProviderType = @0", type.ToString()); } - public virtual IList GetEnabledExternalNotifiers() + public virtual IList GetEnabledMetabaseProviders() { var all = All(); - return _metadataBases.Where(i => all.Exists(c => c.MetadataProviderType == i.GetType().ToString() && c.Enable)).ToList(); + return _metadataProviders.Where(i => all.Exists(c => c.MetadataProviderType == i.GetType().ToString() && c.Enable)).ToList(); } - public virtual void InitializeNotifiers(IList notifiers) + public virtual void Initialize(IList metabaseProviders) { - Logger.Debug("Initializing notifiers. Count {0}", notifiers.Count); + Logger.Debug("Initializing metabases. Count {0}", metabaseProviders.Count); - _metadataBases = notifiers; + _metadataProviders = metabaseProviders; var currentNotifiers = All(); - foreach (var notificationProvider in notifiers) + foreach (var notificationProvider in metabaseProviders) { MetadataBase metadataProviderLocal = notificationProvider; if (!currentNotifiers.Exists(c => c.MetadataProviderType == metadataProviderLocal.GetType().ToString())) { - var settings = new MetabaseDefinition + var settings = new MetadataDefinition { Enable = false, MetadataProviderType = metadataProviderLocal.GetType().ToString(), @@ -86,35 +88,23 @@ namespace NzbDrone.Core.Providers } } - public virtual void OnGrab(string message) + public virtual void CreateForSeries(Series series) { - foreach (var notifier in _metadataBases.Where(i => GetSettings(i.GetType()).Enable)) - { - notifier.OnGrab(message); - } - } + var tvDbSeries = _tvDbProvider.GetSeries(series.SeriesId, false, true); - public virtual void OnDownload(string message, Series series) - { - foreach (var notifier in _metadataBases.Where(i => GetSettings(i.GetType()).Enable)) + foreach (var provider in _metadataProviders.Where(i => GetSettings(i.GetType()).Enable)) { - notifier.OnDownload(message, series); + provider.ForSeries(series, tvDbSeries); } } - public virtual void OnRename(string message, Series series) + public virtual void CreateForEpisodeFile(EpisodeFile episodeFile) { - foreach (var notifier in _metadataBases.Where(i => GetSettings(i.GetType()).Enable)) - { - notifier.OnRename(message, series); - } - } + var tvDbSeries = _tvDbProvider.GetSeries(episodeFile.SeriesId, true, true); - public virtual void AfterRename(string message, Series series) - { - foreach (var notifier in _metadataBases.Where(i => GetSettings(i.GetType()).Enable)) + foreach (var provider in _metadataProviders.Where(i => GetSettings(i.GetType()).Enable)) { - notifier.AfterRename(message, series); + provider.ForEpisodeFile(episodeFile, tvDbSeries); } } } diff --git a/NzbDrone.Core/Providers/TvDbProvider.cs b/NzbDrone.Core/Providers/TvDbProvider.cs index c6e26be7a..0bbd29b69 100644 --- a/NzbDrone.Core/Providers/TvDbProvider.cs +++ b/NzbDrone.Core/Providers/TvDbProvider.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Providers public class TvDbProvider { private readonly EnvironmentProvider _environmentProvider; - private const string TVDB_APIKEY = "5D2D188E86E07F4F"; + public const string TVDB_APIKEY = "5D2D188E86E07F4F"; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly TvdbHandler _handler; @@ -44,13 +44,12 @@ namespace NzbDrone.Core.Providers } } - - public virtual TvdbSeries GetSeries(int id, bool loadEpisodes) + public virtual TvdbSeries GetSeries(int id, bool loadEpisodes, bool loadActors = false) { lock (_handler) { Logger.Debug("Fetching SeriesId'{0}' from tvdb", id); - var result = _handler.GetSeries(id, TvdbLanguage.DefaultLanguage, loadEpisodes, false, true, true); + var result = _handler.GetSeries(id, TvdbLanguage.DefaultLanguage, loadEpisodes, loadActors, true, true); //Fix American Dad's scene gongshow if (result != null && result.Id == 73141) @@ -86,6 +85,5 @@ namespace NzbDrone.Core.Providers return result; } } - } } \ No newline at end of file diff --git a/NzbDrone.Core/Repository/MetabaseDefinition.cs b/NzbDrone.Core/Repository/MetadataDefinition.cs similarity index 79% rename from NzbDrone.Core/Repository/MetabaseDefinition.cs rename to NzbDrone.Core/Repository/MetadataDefinition.cs index fce0e67b2..9147d4f02 100644 --- a/NzbDrone.Core/Repository/MetabaseDefinition.cs +++ b/NzbDrone.Core/Repository/MetadataDefinition.cs @@ -2,9 +2,9 @@ namespace NzbDrone.Core.Repository { - [TableName("MetabaseDefinitions")] + [TableName("MetadataDefinitions")] [PrimaryKey("Id", autoIncrement = true)] - public class MetabaseDefinition + public class MetadataDefinition { public int Id { get; set; }