More xbmc metadata improvements

New: Create/update episode metadata when series is refreshed
Fixed: Episode Metadata when screenshot is not available
Fixed: Episode metadata being stored in database incorrectly
Fixed: Do not create metadata when series folder does not exist
This commit is contained in:
Mark McDowall 2014-02-18 20:51:37 -08:00
parent 1dec725941
commit cbd8e98677
8 changed files with 164 additions and 25 deletions

View File

@ -0,0 +1,27 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(44)]
public class fix_xbmc_episode_metadata : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
//Convert Episode Metadata to proper type
Execute.Sql("UPDATE MetadataFiles " +
"SET Type = 2 " +
"WHERE Consumer = 'XbmcMetadata' " +
"AND EpisodeFileId IS NOT NULL " +
"AND Type = 4 " +
"AND RelativePath LIKE '%.nfo'");
//Convert Episode Images to proper type
Execute.Sql("UPDATE MetadataFiles " +
"SET Type = 5 " +
"WHERE Consumer = 'XbmcMetadata' " +
"AND EpisodeFileId IS NOT NULL " +
"AND Type = 4");
}
}
}

View File

@ -9,6 +9,7 @@ using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -25,6 +26,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
private readonly IMetadataFileService _metadataFileService; private readonly IMetadataFileService _metadataFileService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IHttpProvider _httpProvider; private readonly IHttpProvider _httpProvider;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger; private readonly Logger _logger;
public XbmcMetadata(IEventAggregator eventAggregator, public XbmcMetadata(IEventAggregator eventAggregator,
@ -33,6 +35,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
IMetadataFileService metadataFileService, IMetadataFileService metadataFileService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IHttpProvider httpProvider, IHttpProvider httpProvider,
IEpisodeService episodeService,
Logger logger) Logger logger)
: base(diskProvider, httpProvider, logger) : base(diskProvider, httpProvider, logger)
{ {
@ -42,6 +45,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
_metadataFileService = metadataFileService; _metadataFileService = metadataFileService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_httpProvider = httpProvider; _httpProvider = httpProvider;
_episodeService = episodeService;
_logger = logger; _logger = logger;
} }
@ -51,40 +55,63 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles) public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles)
{ {
if (!_diskProvider.FolderExists(series.Path))
{
_logger.Info("Series folder does not exist, skipping metadata creation");
return;
}
if (Settings.SeriesMetadata) if (Settings.SeriesMetadata)
{ {
EnsureFolder(series.Path);
WriteTvShowNfo(series, existingMetadataFiles); WriteTvShowNfo(series, existingMetadataFiles);
} }
if (Settings.SeriesImages) if (Settings.SeriesImages)
{ {
EnsureFolder(series.Path);
WriteSeriesImages(series, existingMetadataFiles); WriteSeriesImages(series, existingMetadataFiles);
} }
if (Settings.SeasonImages) if (Settings.SeasonImages)
{ {
EnsureFolder(series.Path);
WriteSeasonImages(series, existingMetadataFiles); WriteSeasonImages(series, existingMetadataFiles);
} }
var episodeFiles = GetEpisodeFiles(series.Id);
foreach (var episodeFile in episodeFiles)
{
if (Settings.EpisodeMetadata)
{
WriteEpisodeNfo(series, episodeFile, existingMetadataFiles);
}
}
foreach (var episodeFile in episodeFiles)
{
if (Settings.EpisodeImages)
{
WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
}
}
} }
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
{ {
if (Settings.EpisodeMetadata) if (Settings.EpisodeMetadata)
{ {
WriteEpisodeNfo(series, episodeFile); WriteEpisodeNfo(series, episodeFile, new List<MetadataFile>());
} }
if (Settings.EpisodeImages) if (Settings.EpisodeImages)
{ {
WriteEpisodeImages(series, episodeFile); WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
} }
} }
public override void AfterRename(Series series) public override void AfterRename(Series series)
{ {
//TODO: This should be part of the base class, but could be overwritten if the logic needs to be different
//or it could be done in MetadataService instead of having each metadata consumer do it
var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id);
var episodeFilesMetadata = _metadataFileService.GetFilesBySeries(series.Id).Where(c => c.EpisodeFileId > 0).ToList(); var episodeFilesMetadata = _metadataFileService.GetFilesBySeries(series.Id).Where(c => c.EpisodeFileId > 0).ToList();
@ -305,7 +332,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
} }
} }
private void WriteEpisodeNfo(Series series, EpisodeFile episodeFile) private void WriteEpisodeNfo(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{ {
var filename = episodeFile.Path.Replace(Path.GetExtension(episodeFile.Path), ".nfo"); var filename = episodeFile.Path.Replace(Path.GetExtension(episodeFile.Path), ".nfo");
@ -322,6 +349,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
using (var xw = XmlWriter.Create(sb, xws)) using (var xw = XmlWriter.Create(sb, xws))
{ {
var doc = new XDocument(); var doc = new XDocument();
var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
var details = new XElement("episodedetails"); var details = new XElement("episodedetails");
details.Add(new XElement("title", episode.Title)); details.Add(new XElement("title", episode.Title));
@ -334,7 +362,16 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
details.Add(new XElement("displayseason")); details.Add(new XElement("displayseason"));
details.Add(new XElement("displayepisode")); details.Add(new XElement("displayepisode"));
details.Add(new XElement("thumb", episode.Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot).Url)); if (image == null)
{
details.Add(new XElement("thumb"));
}
else
{
details.Add(new XElement("thumb", image.Url));
}
details.Add(new XElement("watched", "false")); details.Add(new XElement("watched", "false"));
details.Add(new XElement("rating", episode.Ratings.Percentage)); details.Add(new XElement("rating", episode.Ratings.Percentage));
@ -353,19 +390,21 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
_logger.Debug("Saving episodedetails to: {0}", filename); _logger.Debug("Saving episodedetails to: {0}", filename);
_diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
var metadata = new MetadataFile var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
{ c.EpisodeFileId == episodeFile.Id) ??
SeriesId = series.Id, new MetadataFile
EpisodeFileId = episodeFile.Id, {
Consumer = GetType().Name, SeriesId = series.Id,
Type = MetadataType.SeasonImage, EpisodeFileId = episodeFile.Id,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) Consumer = GetType().Name,
}; Type = MetadataType.EpisodeMetadata,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
};
_eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata));
} }
private void WriteEpisodeImages(Series series, EpisodeFile episodeFile) private void WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{ {
var screenshot = episodeFile.Episodes.Value.First().Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot); var screenshot = episodeFile.Episodes.Value.First().Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot);
@ -373,16 +412,32 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
DownloadImage(series, screenshot.Url, filename); DownloadImage(series, screenshot.Url, filename);
var metadata = new MetadataFile var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
c.EpisodeFileId == episodeFile.Id) ??
new MetadataFile
{ {
SeriesId = series.Id, SeriesId = series.Id,
EpisodeFileId = episodeFile.Id, EpisodeFileId = episodeFile.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
Type = MetadataType.SeasonImage, Type = MetadataType.EpisodeImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
}; };
_eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata));
} }
private List<EpisodeFile> GetEpisodeFiles(int seriesId)
{
var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId);
var episodes = _episodeService.GetEpisodeBySeries(seriesId);
foreach (var episodeFile in episodeFiles)
{
var localEpisodeFile = episodeFile;
episodeFile.Episodes = new LazyList<Episode>(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id));
}
return episodeFiles;
}
} }
} }

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Files
{
public interface ICleanMetadataService
{
void Clean(Series series);
}
public class CleanMetadataService : ICleanMetadataService
{
private readonly IMetadataFileService _metadataFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public CleanMetadataService(IMetadataFileService metadataFileService,
IDiskProvider diskProvider,
Logger logger)
{
_metadataFileService = metadataFileService;
_diskProvider = diskProvider;
_logger = logger;
}
public void Clean(Series series)
{
_logger.Trace("Cleaning missing metadata files for series: {0}", series.Title);
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
foreach (var metadataFile in metadataFiles)
{
if (!_diskProvider.FileExists(Path.Combine(series.Path, metadataFile.RelativePath)))
{
_logger.Trace("Deleting metadata file from database: {0}", metadataFile.RelativePath);
_metadataFileService.Delete(metadataFile.Id);
}
}
}
}
}

View File

@ -7,24 +7,30 @@ using NzbDrone.Core.Metadata.Files;
namespace NzbDrone.Core.Metadata namespace NzbDrone.Core.Metadata
{ {
public class NotificationService public class MetadataService
: IHandle<MediaCoversUpdatedEvent>, : IHandle<MediaCoversUpdatedEvent>,
IHandle<EpisodeImportedEvent>, IHandle<EpisodeImportedEvent>,
IHandle<SeriesRenamedEvent> IHandle<SeriesRenamedEvent>
{ {
private readonly IMetadataFactory _metadataFactory; private readonly IMetadataFactory _metadataFactory;
private readonly IMetadataFileService _metadataFileService; private readonly IMetadataFileService _metadataFileService;
private readonly ICleanMetadataService _cleanMetadataService;
private readonly Logger _logger; private readonly Logger _logger;
public NotificationService(IMetadataFactory metadataFactory, IMetadataFileService metadataFileService, Logger logger) public MetadataService(IMetadataFactory metadataFactory,
IMetadataFileService metadataFileService,
ICleanMetadataService cleanMetadataService,
Logger logger)
{ {
_metadataFactory = metadataFactory; _metadataFactory = metadataFactory;
_metadataFileService = metadataFileService; _metadataFileService = metadataFileService;
_cleanMetadataService = cleanMetadataService;
_logger = logger; _logger = logger;
} }
public void Handle(MediaCoversUpdatedEvent message) public void Handle(MediaCoversUpdatedEvent message)
{ {
_cleanMetadataService.Clean(message.Series);
var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id);
foreach (var consumer in _metadataFactory.Enabled()) foreach (var consumer in _metadataFactory.Enabled())

View File

@ -19,6 +19,7 @@ namespace NzbDrone.Core.Metadata.Files
MetadataFile FindByPath(string path); MetadataFile FindByPath(string path);
List<string> FilterExistingFiles(List<string> files, Series series); List<string> FilterExistingFiles(List<string> files, Series series);
MetadataFile Upsert(MetadataFile metadataFile); MetadataFile Upsert(MetadataFile metadataFile);
void Delete(int id);
} }
public class MetadataFileService : IMetadataFileService, public class MetadataFileService : IMetadataFileService,
@ -72,6 +73,11 @@ namespace NzbDrone.Core.Metadata.Files
return _repository.Upsert(metadataFile); return _repository.Upsert(metadataFile);
} }
public void Delete(int id)
{
_repository.Delete(id);
}
public void HandleAsync(SeriesDeletedEvent message) public void HandleAsync(SeriesDeletedEvent message)
{ {
_logger.Trace("Deleting Metadata from database for series: {0}", message.Series); _logger.Trace("Deleting Metadata from database for series: {0}", message.Series);

View File

@ -55,11 +55,6 @@ namespace NzbDrone.Core.Metadata
} }
} }
protected virtual void EnsureFolder(string path)
{
_diskProvider.CreateFolder(path);
}
protected virtual void DownloadImage(Series series, string url, string path) protected virtual void DownloadImage(Series series, string url, string path)
{ {
try try

View File

@ -200,6 +200,9 @@
<Compile Include="Datastore\Migration\041_fix_xbmc_season_images_metadata.cs" /> <Compile Include="Datastore\Migration\041_fix_xbmc_season_images_metadata.cs" />
<Compile Include="Datastore\Migration\042_add_download_clients_table.cs" /> <Compile Include="Datastore\Migration\042_add_download_clients_table.cs" />
<Compile Include="Datastore\Migration\043_convert_config_to_download_clients.cs" /> <Compile Include="Datastore\Migration\043_convert_config_to_download_clients.cs" />
<Compile Include="Datastore\Migration\044_fix_xbmc_episode_metadata.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" />
@ -334,6 +337,7 @@
<Compile Include="MetadataSource\Trakt\Actor.cs" /> <Compile Include="MetadataSource\Trakt\Actor.cs" />
<Compile Include="MetadataSource\Trakt\People.cs" /> <Compile Include="MetadataSource\Trakt\People.cs" />
<Compile Include="MetadataSource\Trakt\Ratings.cs" /> <Compile Include="MetadataSource\Trakt\Ratings.cs" />
<Compile Include="Metadata\Files\CleanMetadataService.cs" />
<Compile Include="Metadata\Consumers\Fake\Fake.cs" /> <Compile Include="Metadata\Consumers\Fake\Fake.cs" />
<Compile Include="Metadata\Consumers\Fake\FakeSettings.cs" /> <Compile Include="Metadata\Consumers\Fake\FakeSettings.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" /> <Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />

View File

@ -4,6 +4,7 @@
</div> </div>
<div class="modal-body root-folders-modal"> <div class="modal-body root-folders-modal">
<div class="validation-errors"></div> <div class="validation-errors"></div>
<div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div>
<div class="input-prepend input-append x-path control-group"> <div class="input-prepend input-append x-path control-group">
<span class="add-on">&nbsp;<i class="icon-folder-open"></i></span> <span class="add-on">&nbsp;<i class="icon-folder-open"></i></span>
<input class="span9" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> <input class="span9" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows">