Refactor ExtraFile/MetadataFile Services (#104)

* Preliminary Work for Extras for Music

* DB Migration for ExtraFiles, Other Cleanup

* More Extras Work, Add Album Metadata Type

* Update Housekeeps for Music Extras

* Fix HouseKeeper and add new Tests

* Final round of Cleanup
pull/110/head
Qstick 7 years ago committed by GitHub
parent 4016d359ac
commit 38cbb2114f

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -11,7 +11,7 @@ using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck.Checks namespace NzbDrone.Core.Test.HealthCheck.Checks
@ -20,27 +20,27 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public class DeleteBadMediaCoversFixture : CoreTest<DeleteBadMediaCovers> public class DeleteBadMediaCoversFixture : CoreTest<DeleteBadMediaCovers>
{ {
private List<MetadataFile> _metadata; private List<MetadataFile> _metadata;
private List<Series> _series; private List<Artist> _artist;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_series = Builder<Series>.CreateListOfSize(1) _artist = Builder<Artist>.CreateListOfSize(1)
.All() .All()
.With(c => c.Path = "C:\\TV\\".AsOsAgnostic()) .With(c => c.Path = "C:\\Music\\".AsOsAgnostic())
.Build().ToList(); .Build().ToList();
_metadata = Builder<MetadataFile>.CreateListOfSize(1) _metadata = Builder<MetadataFile>.CreateListOfSize(1)
.Build().ToList(); .Build().ToList();
Mocker.GetMock<ISeriesService>() Mocker.GetMock<IArtistService>()
.Setup(c => c.GetAllSeries()) .Setup(c => c.GetAllArtists())
.Returns(_series); .Returns(_artist);
Mocker.GetMock<IMetadataFileService>() Mocker.GetMock<IMetadataFileService>()
.Setup(c => c.GetFilesBySeries(_series.First().Id)) .Setup(c => c.GetFilesByArtist(_artist.First().Id))
.Returns(_metadata); .Returns(_metadata);
@ -51,8 +51,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_not_process_non_image_files() public void should_not_process_non_image_files()
{ {
_metadata.First().RelativePath = "season\\file.xml".AsOsAgnostic(); _metadata.First().RelativePath = "album\\file.xml".AsOsAgnostic();
_metadata.First().Type = MetadataType.EpisodeMetadata; _metadata.First().Type = MetadataType.TrackMetadata;
Subject.Clean(); Subject.Clean();
@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Clean(); Subject.Clean();
Mocker.GetMock<IConfigService>().VerifySet(c => c.CleanupMetadataImages = true, Times.Never()); Mocker.GetMock<IConfigService>().VerifySet(c => c.CleanupMetadataImages = true, Times.Never());
Mocker.GetMock<ISeriesService>().Verify(c => c.GetAllSeries(), Times.Never()); Mocker.GetMock<IArtistService>().Verify(c => c.GetAllArtists(), Times.Never());
AssertImageWasNotRemoved(); AssertImageWasNotRemoved();
} }
@ -101,10 +101,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public void should_delete_html_images() public void should_delete_html_images()
{ {
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); var imagePath = "C:\\Music\\Album\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29); _metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); _metadata.First().RelativePath = "Album\\image.jpg".AsOsAgnostic();
_metadata.First().Type = MetadataType.SeriesImage; _metadata.First().Type = MetadataType.ArtistImage;
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath)) .Setup(c => c.OpenReadStream(imagePath))
@ -123,10 +123,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public void should_delete_empty_images() public void should_delete_empty_images()
{ {
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); var imagePath = "C:\\Music\\Album\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29); _metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().Type = MetadataType.SeasonImage; _metadata.First().Type = MetadataType.AlbumImage;
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); _metadata.First().RelativePath = "Album\\image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath)) .Setup(c => c.OpenReadStream(imagePath))
@ -144,9 +144,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public void should_not_delete_non_html_files() public void should_not_delete_non_html_files()
{ {
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); var imagePath = "C:\\Music\\Album\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29); _metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); _metadata.First().RelativePath = "Album\\image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath)) .Setup(c => c.OpenReadStream(imagePath))

@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
@ -12,12 +12,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
public class CleanupDuplicateMetadataFilesFixture : DbTest<CleanupDuplicateMetadataFiles, MetadataFile> public class CleanupDuplicateMetadataFilesFixture : DbTest<CleanupDuplicateMetadataFiles, MetadataFile>
{ {
[Test] [Test]
public void should_not_delete_metadata_files_when_they_are_for_the_same_series_but_different_consumers() public void should_not_delete_metadata_files_when_they_are_for_the_same_artist_but_different_consumers()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.SeriesMetadata) .With(m => m.Type = MetadataType.ArtistMetadata)
.With(m => m.SeriesId = 1) .With(m => m.ArtistId = 1)
.BuildListOfNew(); .BuildListOfNew();
Db.InsertMany(files); Db.InsertMany(files);
@ -26,11 +26,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_for_different_series() public void should_not_delete_metadata_files_for_different_artist()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.SeriesMetadata) .With(m => m.Type = MetadataType.ArtistMetadata)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -40,12 +40,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_delete_metadata_files_when_they_are_for_the_same_series_and_consumer() public void should_delete_metadata_files_when_they_are_for_the_same_artist_and_consumer()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.SeriesMetadata) .With(m => m.Type = MetadataType.ArtistMetadata)
.With(m => m.SeriesId = 1) .With(m => m.ArtistId = 1)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer() public void should_not_delete_metadata_files_when_there_is_only_one_for_that_artist_and_consumer()
{ {
var file = Builder<MetadataFile>.CreateNew() var file = Builder<MetadataFile>.CreateNew()
.BuildNew(); .BuildNew();
@ -66,12 +66,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_when_they_are_for_the_same_episode_but_different_consumers() public void should_not_delete_metadata_files_when_they_are_for_the_same_track_but_different_consumers()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.TrackMetadata)
.With(m => m.EpisodeFileId = 1) .With(m => m.TrackFileId = 1)
.BuildListOfNew(); .BuildListOfNew();
Db.InsertMany(files); Db.InsertMany(files);
@ -80,11 +80,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_for_different_episode() public void should_not_delete_metadata_files_for_different_track()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.TrackMetadata)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -94,12 +94,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_delete_metadata_files_when_they_are_for_the_same_episode_and_consumer() public void should_delete_metadata_files_when_they_are_for_the_same_track_and_consumer()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.TrackMetadata)
.With(m => m.EpisodeFileId = 1) .With(m => m.TrackFileId = 1)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_episode_and_consumer() public void should_not_delete_metadata_files_when_there_is_only_one_for_that_track_and_consumer()
{ {
var file = Builder<MetadataFile>.CreateNew() var file = Builder<MetadataFile>.CreateNew()
.BuildNew(); .BuildNew();
@ -120,12 +120,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_image_when_they_are_for_the_same_episode_but_different_consumers() public void should_not_delete_image_when_they_are_for_the_same_track_but_different_consumers()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.TrackImage)
.With(m => m.EpisodeFileId = 1) .With(m => m.TrackFileId = 1)
.BuildListOfNew(); .BuildListOfNew();
Db.InsertMany(files); Db.InsertMany(files);
@ -134,11 +134,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_image_for_different_episode() public void should_not_delete_image_for_different_track()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.TrackImage)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -148,12 +148,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_delete_image_when_they_are_for_the_same_episode_and_consumer() public void should_delete_image_when_they_are_for_the_same_track_and_consumer()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.TrackImage)
.With(m => m.EpisodeFileId = 1) .With(m => m.TrackFileId = 1)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -163,7 +163,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_image_when_there_is_only_one_for_that_episode_and_consumer() public void should_not_delete_image_when_there_is_only_one_for_that_track_and_consumer()
{ {
var file = Builder<MetadataFile>.CreateNew() var file = Builder<MetadataFile>.CreateNew()
.BuildNew(); .BuildNew();

@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
@ -7,7 +7,7 @@ using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{ {
@ -15,10 +15,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
public class CleanupOrphanedMetadataFilesFixture : DbTest<CleanupOrphanedMetadataFiles, MetadataFile> public class CleanupOrphanedMetadataFilesFixture : DbTest<CleanupOrphanedMetadataFiles, MetadataFile>
{ {
[Test] [Test]
public void should_delete_metadata_files_that_dont_have_a_coresponding_series() public void should_delete_metadata_files_that_dont_have_a_coresponding_artist()
{ {
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.EpisodeFileId = null) .With(m => m.TrackFileId = null)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -27,16 +27,59 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_that_have_a_coresponding_series() public void should_delete_metadata_files_that_dont_have_a_coresponding_album()
{ {
var series = Builder<Series>.CreateNew() var artist = Builder<Artist>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(artist);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.ArtistId = artist.Id)
.With(m => m.EpisodeFileId = null) .With(m => m.TrackFileId = null)
.BuildNew();
Db.Insert(metadataFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_metadata_files_that_have_a_coresponding_artist()
{
var artist = Builder<Artist>.CreateNew()
.BuildNew();
Db.Insert(artist);
var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.ArtistId = artist.Id)
.With(m => m.AlbumId = null)
.With(m => m.TrackFileId = null)
.BuildNew();
Db.Insert(metadataFile);
var countMods = AllStoredModels.Count;
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_not_delete_metadata_files_that_have_a_coresponding_album()
{
var artist = Builder<Artist>.CreateNew()
.BuildNew();
var album = Builder<Album>.CreateNew()
.BuildNew();
Db.Insert(artist);
Db.Insert(album);
var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.ArtistId = artist.Id)
.With(m => m.AlbumId = album.Id)
.With(m => m.TrackFileId = null)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -45,16 +88,16 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_delete_metadata_files_that_dont_have_a_coresponding_episode_file() public void should_delete_metadata_files_that_dont_have_a_coresponding_track_file()
{ {
var series = Builder<Series>.CreateNew() var artist = Builder<Artist>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(artist);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.ArtistId = artist.Id)
.With(m => m.EpisodeFileId = 10) .With(m => m.TrackFileId = 10)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -63,21 +106,26 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_not_delete_metadata_files_that_have_a_coresponding_episode_file() public void should_not_delete_metadata_files_that_have_a_coresponding_track_file()
{ {
var series = Builder<Series>.CreateNew() var artist = Builder<Artist>.CreateNew()
.BuildNew();
var album = Builder<Album>.CreateNew()
.BuildNew(); .BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew() var trackFile = Builder<TrackFile>.CreateNew()
.With(h => h.Quality = new QualityModel()) .With(h => h.Quality = new QualityModel())
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(artist);
Db.Insert(episodeFile); Db.Insert(album);
Db.Insert(trackFile);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.ArtistId = artist.Id)
.With(m => m.EpisodeFileId = episodeFile.Id) .With(m => m.AlbumId = album.Id)
.With(m => m.TrackFileId = trackFile.Id)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -86,17 +134,17 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_delete_episode_metadata_files_that_have_episodefileid_of_zero() public void should_delete_track_metadata_files_that_have_trackfileid_of_zero()
{ {
var series = Builder<Series>.CreateNew() var artist = Builder<Artist>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(artist);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.ArtistId = artist.Id)
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.TrackMetadata)
.With(m => m.EpisodeFileId = 0) .With(m => m.TrackFileId = 0)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -105,17 +153,17 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[Test] [Test]
public void should_delete_episode_image_files_that_have_episodefileid_of_zero() public void should_delete_track_image_files_that_have_trackfileid_of_zero()
{ {
var series = Builder<Series>.CreateNew() var artist = Builder<Artist>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(artist);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.ArtistId = artist.Id)
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.TrackImage)
.With(m => m.EpisodeFileId = 0) .With(m => m.TrackFileId = 0)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);

@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -11,7 +11,7 @@ namespace NzbDrone.Core.Test.MediaFiles
public class MediaFileRepositoryFixture : DbTest<MediaFileRepository, TrackFile> public class MediaFileRepositoryFixture : DbTest<MediaFileRepository, TrackFile>
{ {
[Test] [Test]
public void get_files_by_series() public void get_files_by_artist()
{ {
var files = Builder<TrackFile>.CreateListOfSize(10) var files = Builder<TrackFile>.CreateListOfSize(10)
.All() .All()
@ -24,11 +24,11 @@ namespace NzbDrone.Core.Test.MediaFiles
Db.InsertMany(files); Db.InsertMany(files);
var seriesFiles = Subject.GetFilesByArtist(12); var artistFiles = Subject.GetFilesByArtist(12);
seriesFiles.Should().HaveCount(4); artistFiles.Should().HaveCount(4);
seriesFiles.Should().OnlyContain(c => c.ArtistId == 12); artistFiles.Should().OnlyContain(c => c.ArtistId == 12);
} }
} }
} }

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.MediaFiles
} }
[Test] [Test]
public void should_delete_files_that_dont_belong_to_any_episodes() public void should_delete_files_that_dont_belong_to_any_tracks()
{ {
var trackFiles = Builder<TrackFile>.CreateListOfSize(10) var trackFiles = Builder<TrackFile>.CreateListOfSize(10)
.Random(10) .Random(10)
@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.MediaFiles
} }
[Test] [Test]
public void should_unlink_episode_when_episodeFile_does_not_exist() public void should_unlink_track_when_trackFile_does_not_exist()
{ {
GivenTrackFiles(new List<TrackFile>()); GivenTrackFiles(new List<TrackFile>());

@ -1,11 +1,11 @@
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Consumers.Roksbox; using NzbDrone.Core.Extras.Metadata.Consumers.Roksbox;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox
@ -13,12 +13,12 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox
[TestFixture] [TestFixture]
public class FindMetadataFileFixture : CoreTest<RoksboxMetadata> public class FindMetadataFileFixture : CoreTest<RoksboxMetadata>
{ {
private Series _series; private Artist _series;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_series = Builder<Series>.CreateNew() _series = Builder<Artist>.CreateNew()
.With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic())
.Build(); .Build();
} }
@ -38,11 +38,11 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox
{ {
var path = Path.Combine(_series.Path, folder, folder + ".jpg"); var path = Path.Combine(_series.Path, folder, folder + ".jpg");
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.AlbumImage);
} }
[TestCase(".xml", MetadataType.EpisodeMetadata)] [TestCase(".xml", MetadataType.TrackMetadata)]
[TestCase(".jpg", MetadataType.EpisodeImage)] [TestCase(".jpg", MetadataType.TrackImage)]
public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type)
{ {
var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension);
@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox
{ {
var path = Path.Combine(_series.Path, new DirectoryInfo(_series.Path).Name + ".jpg"); var path = Path.Combine(_series.Path, new DirectoryInfo(_series.Path).Name + ".jpg");
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.ArtistImage);
} }
} }
} }

@ -1,11 +1,11 @@
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Consumers.Wdtv; using NzbDrone.Core.Extras.Metadata.Consumers.Wdtv;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv
@ -13,12 +13,12 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv
[TestFixture] [TestFixture]
public class FindMetadataFileFixture : CoreTest<WdtvMetadata> public class FindMetadataFileFixture : CoreTest<WdtvMetadata>
{ {
private Series _series; private Artist _series;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_series = Builder<Series>.CreateNew() _series = Builder<Artist>.CreateNew()
.With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic())
.Build(); .Build();
} }
@ -38,11 +38,11 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv
{ {
var path = Path.Combine(_series.Path, folder, "folder.jpg"); var path = Path.Combine(_series.Path, folder, "folder.jpg");
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.AlbumImage);
} }
[TestCase(".xml", MetadataType.EpisodeMetadata)] [TestCase(".xml", MetadataType.TrackMetadata)]
[TestCase(".metathumb", MetadataType.EpisodeImage)] [TestCase(".metathumb", MetadataType.TrackImage)]
public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type)
{ {
var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension);
@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv
{ {
var path = Path.Combine(_series.Path, "folder.jpg"); var path = Path.Combine(_series.Path, "folder.jpg");
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.ArtistImage);
} }
} }
} }

@ -0,0 +1,44 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(123)]
public class music_extras : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("ExtraFiles")
.AddColumn("ArtistId").AsInt32().NotNullable().WithDefaultValue(0)
.AddColumn("AlbumId").AsInt32().NotNullable().WithDefaultValue(0)
.AddColumn("TrackFileId").AsInt32().NotNullable().WithDefaultValue(0);
Delete.Column("SeriesId").FromTable("ExtraFiles");
Delete.Column("SeasonNumber").FromTable("ExtraFiles");
Delete.Column("EpisodeFileId").FromTable("ExtraFiles");
Alter.Table("SubtitleFiles")
.AddColumn("ArtistId").AsInt32().NotNullable().WithDefaultValue(0)
.AddColumn("AlbumId").AsInt32().NotNullable().WithDefaultValue(0)
.AddColumn("TrackFileId").AsInt32().NotNullable().WithDefaultValue(0);
Delete.Column("SeriesId").FromTable("SubtitleFiles");
Delete.Column("SeasonNumber").FromTable("SubtitleFiles");
Delete.Column("EpisodeFileId").FromTable("SubtitleFiles");
Alter.Table("MetadataFiles")
.AddColumn("ArtistId").AsInt32().NotNullable().WithDefaultValue(0)
.AddColumn("AlbumId").AsInt32().Nullable()
.AddColumn("TrackFileId").AsInt32().Nullable();
Delete.Column("SeriesId").FromTable("MetadataFiles");
Delete.Column("SeasonNumber").FromTable("MetadataFiles");
Delete.Column("EpisodeFileId").FromTable("MetadataFiles");
}
}
}

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -48,14 +48,12 @@ namespace NzbDrone.Core.Extras
foreach (var existingExtraFileImporter in _existingExtraFileImporters) foreach (var existingExtraFileImporter in _existingExtraFileImporters)
{ {
// TODO Implement existingExtraFileImporter for Audio Files var imported = existingExtraFileImporter.ProcessFiles(artist, filteredFiles, importedFiles);
//var imported = existingExtraFileImporter.ProcessFiles(artist, filteredFiles, importedFiles); importedFiles.AddRange(imported.Select(f => Path.Combine(artist.Path, f.RelativePath)));
//importedFiles.AddRange(imported.Select(f => Path.Combine(artist.Path, f.RelativePath)));
} }
_logger.Info("Found {0} extra files", extraFiles.Count); _logger.Info("Found {0} extra files", extraFiles.Count);
} }
} }
} }

@ -12,50 +12,55 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras namespace NzbDrone.Core.Extras
{ {
// NOTE: Majora: ExtraService can be reserved for Music Videos, lyric files, etc for Plex. TODO: Implement Extras for Music
public interface IExtraService public interface IExtraService
{ {
void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); void ImportExtraFiles(LocalTrack localEpisode, TrackFile episodeFile, bool isReadOnly);
} }
public class ExtraService : IExtraService, public class ExtraService : IExtraService,
IHandle<MediaCoversUpdatedEvent>, IHandle<MediaCoversUpdatedEvent>,
IHandle<EpisodeFolderCreatedEvent>, IHandle<TrackFolderCreatedEvent>,
IHandle<SeriesRenamedEvent> IHandle<ArtistRenamedEvent>
{ {
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IEpisodeService _episodeService; //private readonly IEpisodeService _episodeService;
private readonly IAlbumService _albumService;
private readonly ITrackService _trackService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly List<IManageExtraFiles> _extraFileManagers; private readonly List<IManageExtraFiles> _extraFileManagers;
private readonly Logger _logger; private readonly Logger _logger;
public ExtraService(IMediaFileService mediaFileService, public ExtraService(IMediaFileService mediaFileService,
IEpisodeService episodeService, //IEpisodeService episodeService,
IAlbumService albumService,
ITrackService trackService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IConfigService configService, IConfigService configService,
List<IManageExtraFiles> extraFileManagers, List<IManageExtraFiles> extraFileManagers,
Logger logger) Logger logger)
{ {
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_episodeService = episodeService; //_episodeService = episodeService;
_albumService = albumService;
_trackService = trackService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configService = configService; _configService = configService;
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
_logger = logger; _logger = logger;
} }
public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) public void ImportExtraFiles(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly)
{ {
var series = localEpisode.Series; var artist = localTrack.Artist;
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
extraFileManager.CreateAfterEpisodeImport(series, episodeFile); extraFileManager.CreateAfterTrackImport(artist, trackFile);
} }
if (!_configService.ImportExtraFiles) if (!_configService.ImportExtraFiles)
@ -63,7 +68,7 @@ namespace NzbDrone.Core.Extras
return; return;
} }
var sourcePath = localEpisode.Path; var sourcePath = localTrack.Path;
var sourceFolder = _diskProvider.GetParentFolder(sourcePath); var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly); var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly);
@ -88,7 +93,7 @@ namespace NzbDrone.Core.Extras
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
var extension = Path.GetExtension(matchingFilename); var extension = Path.GetExtension(matchingFilename);
var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, extension, isReadOnly); var extraFile = extraFileManager.Import(artist, trackFile, matchingFilename, extension, isReadOnly);
if (extraFile != null) if (extraFile != null)
{ {
@ -105,49 +110,49 @@ namespace NzbDrone.Core.Extras
public void Handle(MediaCoversUpdatedEvent message) public void Handle(MediaCoversUpdatedEvent message)
{ {
//var artist = message.Artist; var artist = message.Artist;
//var episodeFiles = GetEpisodeFiles(artist.Id); var albums = _albumService.GetAlbumsByArtist(artist.Id);
var trackFiles = GetTrackFiles(artist.Id);
//foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
//{ {
// extraFileManager.CreateAfterSeriesScan(artist, episodeFiles); extraFileManager.CreateAfterArtistScan(artist, albums, trackFiles);
//} }
} }
public void Handle(EpisodeFolderCreatedEvent message) public void Handle(TrackFolderCreatedEvent message)
{ {
var series = message.Series; var artist = message.Artist;
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
extraFileManager.CreateAfterEpisodeImport(series, message.SeriesFolder, message.SeasonFolder); extraFileManager.CreateAfterTrackImport(artist, message.ArtistFolder, message.AlbumFolder);
} }
} }
public void Handle(SeriesRenamedEvent message) public void Handle(ArtistRenamedEvent message)
{ {
var series = message.Series; var artist = message.Artist;
var episodeFiles = GetEpisodeFiles(series.Id); var trackFiles = GetTrackFiles(artist.Id);
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
extraFileManager.MoveFilesAfterRename(series, episodeFiles); extraFileManager.MoveFilesAfterRename(artist, trackFiles);
} }
} }
private List<EpisodeFile> GetEpisodeFiles(int seriesId) private List<TrackFile> GetTrackFiles(int artistId)
{ {
//var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); var trackFiles = _mediaFileService.GetFilesByArtist(artistId);
//var episodes = _episodeService.GetEpisodeBySeries(seriesId); var tracks = _trackService.GetTracksByArtist(artistId);
//foreach (var episodeFile in episodeFiles) foreach (var trackFile in trackFiles)
//{ {
// var localEpisodeFile = episodeFile; var localTrackFile = trackFile;
// episodeFile.Episodes = new LazyList<Episode>(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); trackFile.Tracks = new LazyList<Track>(tracks.Where(e => e.TrackFileId == localTrackFile.Id));
//} }
//return episodeFiles; return trackFiles;
return new List<EpisodeFile>();
} }
} }
} }

@ -1,13 +1,13 @@
using System; using System;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Extras.Files namespace NzbDrone.Core.Extras.Files
{ {
public abstract class ExtraFile : ModelBase public abstract class ExtraFile : ModelBase
{ {
public int SeriesId { get; set; } public int ArtistId { get; set; }
public int? EpisodeFileId { get; set; } public int? TrackFileId { get; set; }
public int? SeasonNumber { get; set; } public int? AlbumId { get; set; }
public string RelativePath { get; set; } public string RelativePath { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public DateTime LastUpdated { get; set; } public DateTime LastUpdated { get; set; }

@ -7,18 +7,18 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Files namespace NzbDrone.Core.Extras.Files
{ {
public interface IManageExtraFiles public interface IManageExtraFiles
{ {
int Order { get; } int Order { get; }
IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles); IEnumerable<ExtraFile> CreateAfterArtistScan(Artist artist, List<Album> albums, List<TrackFile> trackFiles);
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, TrackFile trackFile);
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder);
IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles); IEnumerable<ExtraFile> MoveFilesAfterRename(Artist artist, List<TrackFile> trackFiles);
ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly);
} }
public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
@ -42,16 +42,16 @@ namespace NzbDrone.Core.Extras.Files
} }
public abstract int Order { get; } public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles); public abstract IEnumerable<ExtraFile> CreateAfterArtistScan(Artist artist, List<Album> albums, List<TrackFile> trackFiles);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); public abstract IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, TrackFile trackFile);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); public abstract IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder);
public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles); public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Artist artist, List<TrackFile> trackFiles);
public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); public abstract ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly);
protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, bool readOnly, string extension, string fileNameSuffix = null) protected TExtraFile ImportFile(Artist artist, TrackFile trackFile, string path, bool readOnly, string extension, string fileNameSuffix = null)
{ {
var newFolder = Path.GetDirectoryName(Path.Combine(series.Path, episodeFile.RelativePath)); var newFolder = Path.GetDirectoryName(Path.Combine(artist.Path, trackFile.RelativePath));
var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(episodeFile.RelativePath)); var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.RelativePath));
if (fileNameSuffix.IsNotNullOrWhiteSpace()) if (fileNameSuffix.IsNotNullOrWhiteSpace())
{ {
@ -72,18 +72,18 @@ namespace NzbDrone.Core.Extras.Files
return new TExtraFile return new TExtraFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
SeasonNumber = episodeFile.SeasonNumber, AlbumId = trackFile.AlbumId,
EpisodeFileId = episodeFile.Id, TrackFileId = trackFile.Id,
RelativePath = series.Path.GetRelativePath(newFileName), RelativePath = artist.Path.GetRelativePath(newFileName),
Extension = extension Extension = extension
}; };
} }
protected TExtraFile MoveFile(Series series, EpisodeFile episodeFile, TExtraFile extraFile, string fileNameSuffix = null) protected TExtraFile MoveFile(Artist artist, TrackFile trackFile, TExtraFile extraFile, string fileNameSuffix = null)
{ {
var newFolder = Path.GetDirectoryName(Path.Combine(series.Path, episodeFile.RelativePath)); var newFolder = Path.GetDirectoryName(Path.Combine(artist.Path, trackFile.RelativePath));
var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(episodeFile.RelativePath)); var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.RelativePath));
if (fileNameSuffix.IsNotNullOrWhiteSpace()) if (fileNameSuffix.IsNotNullOrWhiteSpace())
{ {
@ -92,7 +92,7 @@ namespace NzbDrone.Core.Extras.Files
filenameBuilder.Append(extraFile.Extension); filenameBuilder.Append(extraFile.Extension);
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath); var existingFileName = Path.Combine(artist.Path, extraFile.RelativePath);
var newFileName = Path.Combine(newFolder, filenameBuilder.ToString()); var newFileName = Path.Combine(newFolder, filenameBuilder.ToString());
if (newFileName.PathNotEquals(existingFileName)) if (newFileName.PathNotEquals(existingFileName))
@ -100,7 +100,7 @@ namespace NzbDrone.Core.Extras.Files
try try
{ {
_diskProvider.MoveFile(existingFileName, newFileName); _diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = series.Path.GetRelativePath(newFileName); extraFile.RelativePath = artist.Path.GetRelativePath(newFileName);
return extraFile; return extraFile;
} }

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -7,12 +7,12 @@ namespace NzbDrone.Core.Extras.Files
{ {
public interface IExtraFileRepository<TExtraFile> : IBasicRepository<TExtraFile> where TExtraFile : ExtraFile, new() public interface IExtraFileRepository<TExtraFile> : IBasicRepository<TExtraFile> where TExtraFile : ExtraFile, new()
{ {
void DeleteForSeries(int seriesId); void DeleteForArtist(int artistId);
void DeleteForSeason(int seriesId, int seasonNumber); void DeleteForAlbum(int artistId, int albumId);
void DeleteForEpisodeFile(int episodeFileId); void DeleteForTrackFile(int trackFileId);
List<TExtraFile> GetFilesBySeries(int seriesId); List<TExtraFile> GetFilesByArtist(int artistId);
List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber); List<TExtraFile> GetFilesByAlbum(int artistId, int albumId);
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId); List<TExtraFile> GetFilesByTrackFile(int trackFileId);
TExtraFile FindByPath(string path); TExtraFile FindByPath(string path);
} }
@ -24,34 +24,34 @@ namespace NzbDrone.Core.Extras.Files
{ {
} }
public void DeleteForSeries(int seriesId) public void DeleteForArtist(int artistId)
{ {
Delete(c => c.SeriesId == seriesId); Delete(c => c.ArtistId == artistId);
} }
public void DeleteForSeason(int seriesId, int seasonNumber) public void DeleteForAlbum(int artistId, int albumId)
{ {
Delete(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); Delete(c => c.ArtistId == artistId && c.AlbumId == albumId);
} }
public void DeleteForEpisodeFile(int episodeFileId) public void DeleteForTrackFile(int trackFileId)
{ {
Delete(c => c.EpisodeFileId == episodeFileId); Delete(c => c.TrackFileId == trackFileId);
} }
public List<TExtraFile> GetFilesBySeries(int seriesId) public List<TExtraFile> GetFilesByArtist(int artistId)
{ {
return Query.Where(c => c.SeriesId == seriesId); return Query.Where(c => c.ArtistId == artistId);
} }
public List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber) public List<TExtraFile> GetFilesByAlbum(int artistId, int albumId)
{ {
return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); return Query.Where(c => c.ArtistId == artistId && c.AlbumId == albumId);
} }
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId) public List<TExtraFile> GetFilesByTrackFile(int trackFileId)
{ {
return Query.Where(c => c.EpisodeFileId == episodeFileId); return Query.Where(c => c.TrackFileId == trackFileId);
} }
public TExtraFile FindByPath(string path) public TExtraFile FindByPath(string path)

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -8,16 +8,16 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.Extras.Files namespace NzbDrone.Core.Extras.Files
{ {
public interface IExtraFileService<TExtraFile> public interface IExtraFileService<TExtraFile>
where TExtraFile : ExtraFile, new() where TExtraFile : ExtraFile, new()
{ {
List<TExtraFile> GetFilesBySeries(int seriesId); List<TExtraFile> GetFilesByArtist(int artistId);
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId); List<TExtraFile> GetFilesByTrackFile(int trackFileId);
TExtraFile FindByPath(string path); TExtraFile FindByPath(string path);
void Upsert(TExtraFile extraFile); void Upsert(TExtraFile extraFile);
void Upsert(List<TExtraFile> extraFiles); void Upsert(List<TExtraFile> extraFiles);
@ -26,24 +26,24 @@ namespace NzbDrone.Core.Extras.Files
} }
public abstract class ExtraFileService<TExtraFile> : IExtraFileService<TExtraFile>, public abstract class ExtraFileService<TExtraFile> : IExtraFileService<TExtraFile>,
IHandleAsync<SeriesDeletedEvent>, IHandleAsync<ArtistDeletedEvent>,
IHandleAsync<EpisodeFileDeletedEvent> IHandleAsync<TrackFileDeletedEvent>
where TExtraFile : ExtraFile, new() where TExtraFile : ExtraFile, new()
{ {
private readonly IExtraFileRepository<TExtraFile> _repository; private readonly IExtraFileRepository<TExtraFile> _repository;
private readonly ISeriesService _seriesService; private readonly IArtistService _artistService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IRecycleBinProvider _recycleBinProvider; private readonly IRecycleBinProvider _recycleBinProvider;
private readonly Logger _logger; private readonly Logger _logger;
public ExtraFileService(IExtraFileRepository<TExtraFile> repository, public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
ISeriesService seriesService, IArtistService artistService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRecycleBinProvider recycleBinProvider, IRecycleBinProvider recycleBinProvider,
Logger logger) Logger logger)
{ {
_repository = repository; _repository = repository;
_seriesService = seriesService; _artistService = artistService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_logger = logger; _logger = logger;
@ -51,14 +51,14 @@ namespace NzbDrone.Core.Extras.Files
public virtual bool PermanentlyDelete => false; public virtual bool PermanentlyDelete => false;
public List<TExtraFile> GetFilesBySeries(int seriesId) public List<TExtraFile> GetFilesByArtist(int artistId)
{ {
return _repository.GetFilesBySeries(seriesId); return _repository.GetFilesByArtist(artistId);
} }
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId) public List<TExtraFile> GetFilesByTrackFile(int trackFileId)
{ {
return _repository.GetFilesByEpisodeFile(episodeFileId); return _repository.GetFilesByTrackFile(trackFileId);
} }
public TExtraFile FindByPath(string path) public TExtraFile FindByPath(string path)
@ -97,28 +97,28 @@ namespace NzbDrone.Core.Extras.Files
_repository.DeleteMany(ids); _repository.DeleteMany(ids);
} }
public void HandleAsync(SeriesDeletedEvent message) public void HandleAsync(ArtistDeletedEvent message)
{ {
_logger.Debug("Deleting Extra from database for series: {0}", message.Series); _logger.Debug("Deleting Extra from database for artist: {0}", message.Artist);
_repository.DeleteForSeries(message.Series.Id); _repository.DeleteForArtist(message.Artist.Id);
} }
public void HandleAsync(EpisodeFileDeletedEvent message) public void HandleAsync(TrackFileDeletedEvent message)
{ {
var episodeFile = message.EpisodeFile; var trackFile = message.TrackFile;
if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes) if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes)
{ {
_logger.Debug("Removing episode file from DB as part of cleanup routine, not deleting extra files from disk."); _logger.Debug("Removing track file from DB as part of cleanup routine, not deleting extra files from disk.");
} }
else else
{ {
var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId); var artist = _artistService.GetArtist(message.TrackFile.ArtistId);
foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id)) foreach (var extra in _repository.GetFilesByTrackFile(trackFile.Id))
{ {
var path = Path.Combine(series.Path, extra.RelativePath); var path = Path.Combine(artist.Path, extra.RelativePath);
if (_diskProvider.FileExists(path)) if (_diskProvider.FileExists(path))
{ {
@ -130,15 +130,15 @@ namespace NzbDrone.Core.Extras.Files
else else
{ {
// Send extra files to the recycling bin so they can be recovered if necessary // Send extra files to the recycling bin so they can be recovered if necessary
var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(path)); var subfolder = _diskProvider.GetParentFolder(artist.Path).GetRelativePath(_diskProvider.GetParentFolder(path));
_recycleBinProvider.DeleteFile(path, subfolder); _recycleBinProvider.DeleteFile(path, subfolder);
} }
} }
} }
} }
_logger.Debug("Deleting Extra from database for episode file: {0}", episodeFile); _logger.Debug("Deleting Extra from database for track file: {0}", trackFile);
_repository.DeleteForEpisodeFile(episodeFile.Id); _repository.DeleteForTrackFile(trackFile.Id);
} }
} }
} }

@ -1,12 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras namespace NzbDrone.Core.Extras
{ {
public interface IImportExistingExtraFiles public interface IImportExistingExtraFiles
{ {
int Order { get; } int Order { get; }
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles); IEnumerable<ExtraFile> ProcessFiles(Artist artist, List<string> filesOnDisk, List<string> importedFiles);
} }
} }

@ -1,10 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras namespace NzbDrone.Core.Extras
{ {
@ -19,21 +19,21 @@ namespace NzbDrone.Core.Extras
} }
public abstract int Order { get; } public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles); public abstract IEnumerable<ExtraFile> ProcessFiles(Artist artist, List<string> filesOnDisk, List<string> importedFiles);
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles) public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Artist artist, List<string> filesOnDisk, List<string> importedFiles)
{ {
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); var artistFiles = _extraFileService.GetFilesByArtist(artist.Id);
Clean(series, filesOnDisk, importedFiles, seriesFiles); Clean(artist, filesOnDisk, importedFiles, artistFiles);
return Filter(series, filesOnDisk, importedFiles, seriesFiles); return Filter(artist, filesOnDisk, importedFiles, artistFiles);
} }
private ImportExistingExtraFileFilterResult<TExtraFile> Filter(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles) private ImportExistingExtraFileFilterResult<TExtraFile> Filter(Artist artist, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> artistFiles)
{ {
var previouslyImported = seriesFiles.IntersectBy(s => Path.Combine(series.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList(); var previouslyImported = artistFiles.IntersectBy(s => Path.Combine(artist.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList();
var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance) var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance)
.Except(importedFiles, PathEqualityComparer.Instance) .Except(importedFiles, PathEqualityComparer.Instance)
.ToList(); .ToList();
@ -42,12 +42,12 @@ namespace NzbDrone.Core.Extras
return new ImportExistingExtraFileFilterResult<TExtraFile>(previouslyImported, filteredFiles); return new ImportExistingExtraFileFilterResult<TExtraFile>(previouslyImported, filteredFiles);
} }
private void Clean(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles) private void Clean(Artist artist, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> artistFiles)
{ {
var alreadyImportedFileIds = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance) var alreadyImportedFileIds = artistFiles.IntersectBy(f => Path.Combine(artist.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance)
.Select(f => f.Id); .Select(f => f.Id);
var deletedFiles = seriesFiles.ExceptBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance) var deletedFiles = artistFiles.ExceptBy(f => Path.Combine(artist.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance)
.Select(f => f.Id); .Select(f => f.Id);
_extraFileService.DeleteMany(alreadyImportedFileIds); _extraFileService.DeleteMany(alreadyImportedFileIds);

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -9,7 +9,7 @@ using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
{ {
@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
public override string Name => "Emby (Legacy)"; public override string Name => "Emby (Legacy)";
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Artist artist, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -33,28 +33,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = artist.Path.GetRelativePath(path)
}; };
if (filename.Equals("series.xml", StringComparison.InvariantCultureIgnoreCase)) if (filename.Equals("artist.xml", StringComparison.InvariantCultureIgnoreCase))
{ {
metadata.Type = MetadataType.SeriesMetadata; metadata.Type = MetadataType.ArtistMetadata;
return metadata; return metadata;
} }
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult ArtistMetadata(Artist artist)
{ {
if (!Settings.SeriesMetadata) if (!Settings.ArtistMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating series.xml for: {0}", series.Title); _logger.Debug("Generating artist.xml for: {0}", artist.Name);
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
@ -62,85 +62,73 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
using (var xw = XmlWriter.Create(sb, xws)) using (var xw = XmlWriter.Create(sb, xws))
{ {
var tvShow = new XElement("Series"); var artistElement = new XElement("Artist");
tvShow.Add(new XElement("id", series.TvdbId)); artistElement.Add(new XElement("id", artist.ForeignArtistId));
tvShow.Add(new XElement("Status", series.Status)); artistElement.Add(new XElement("Status", artist.Status));
tvShow.Add(new XElement("Network", series.Network));
tvShow.Add(new XElement("Airs_Time", series.AirTime));
if (series.FirstAired.HasValue) artistElement.Add(new XElement("Added", artist.Added.ToString("MM/dd/yyyy HH:mm:ss tt")));
{ artistElement.Add(new XElement("LockData", "false"));
tvShow.Add(new XElement("FirstAired", series.FirstAired.Value.ToString("yyyy-MM-dd"))); artistElement.Add(new XElement("Overview", artist.Overview));
} artistElement.Add(new XElement("LocalTitle", artist.Name));
tvShow.Add(new XElement("ContentRating", series.Certification));
tvShow.Add(new XElement("Added", series.Added.ToString("MM/dd/yyyy HH:mm:ss tt")));
tvShow.Add(new XElement("LockData", "false"));
tvShow.Add(new XElement("Overview", series.Overview));
tvShow.Add(new XElement("LocalTitle", series.Title));
if (series.FirstAired.HasValue)
{
tvShow.Add(new XElement("PremiereDate", series.FirstAired.Value.ToString("yyyy-MM-dd")));
}
tvShow.Add(new XElement("Rating", series.Ratings.Value)); artistElement.Add(new XElement("Rating", artist.Ratings.Value));
tvShow.Add(new XElement("ProductionYear", series.Year)); artistElement.Add(new XElement("Genres", artist.Genres.Select(genre => new XElement("Genre", genre))));
tvShow.Add(new XElement("RunningTime", series.Runtime));
tvShow.Add(new XElement("IMDB", series.ImdbId));
tvShow.Add(new XElement("TVRageId", series.TvRageId));
tvShow.Add(new XElement("Genres", series.Genres.Select(genre => new XElement("Genre", genre))));
var persons = new XElement("Persons"); var persons = new XElement("Persons");
foreach (var person in series.Actors) foreach (var person in artist.Members)
{ {
persons.Add(new XElement("Person", persons.Add(new XElement("Person",
new XElement("Name", person.Name), new XElement("Name", person.Name),
new XElement("Type", "Actor"), new XElement("Type", "Actor"),
new XElement("Role", person.Character) new XElement("Role", person.Instrument)
)); ));
} }
tvShow.Add(persons); artistElement.Add(persons);
var doc = new XDocument(tvShow); var doc = new XDocument(artistElement);
doc.Save(xw); doc.Save(xw);
_logger.Debug("Saving series.xml for {0}", series.Title); _logger.Debug("Saving artist.xml for {0}", artist.Name);
return new MetadataFileResult("series.xml", doc.ToString()); return new MetadataFileResult("artist.xml", doc.ToString());
} }
} }
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) public override MetadataFileResult AlbumMetadata(Artist artist, Album album)
{
return null;
}
public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile)
{ {
return null; return null;
} }
public override List<ImageFileResult> SeriesImages(Series series) public override List<ImageFileResult> ArtistImages(Artist artist)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) public override List<ImageFileResult> AlbumImages(Artist artist, Album season)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) public override List<ImageFileResult> TrackImages(Artist artist, TrackFile trackFile)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series) private IEnumerable<ImageFileResult> ProcessArtistImages(Artist artist)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) private IEnumerable<ImageFileResult> ProcessAlbumImages(Artist artist, Album album)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
@ -155,4 +143,4 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
return null; return null;
} }
} }
} }

@ -18,11 +18,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
public MediaBrowserMetadataSettings() public MediaBrowserMetadataSettings()
{ {
SeriesMetadata = true; ArtistMetadata = true;
} }
[FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox, HelpText = "series.xml")] [FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox, HelpText = "artist.xml")]
public bool SeriesMetadata { get; set; } public bool ArtistMetadata { get; set; }
public bool IsValid => true; public bool IsValid => true;

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -12,7 +12,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
{ {
@ -36,25 +36,25 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
public override string Name => "Roksbox"; public override string Name => "Roksbox";
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile)
{ {
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath);
if (metadataFile.Type == MetadataType.EpisodeImage) if (metadataFile.Type == MetadataType.TrackImage)
{ {
return GetEpisodeImageFilename(episodeFilePath); return GetTrackImageFilename(trackFilePath);
} }
if (metadataFile.Type == MetadataType.EpisodeMetadata) if (metadataFile.Type == MetadataType.TrackMetadata)
{ {
return GetEpisodeMetadataFilename(episodeFilePath); return GetTrackMetadataFilename(trackFilePath);
} }
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath); return Path.Combine(artist.Path, metadataFile.RelativePath);
} }
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Artist artist, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -63,9 +63,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = artist.Path.GetRelativePath(path)
}; };
//Series and season images are both named folder.jpg, only season ones sit in season folders //Series and season images are both named folder.jpg, only season ones sit in season folders
@ -75,22 +75,22 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
if (seasonMatch.Success) if (seasonMatch.Success)
{ {
metadata.Type = MetadataType.SeasonImage; metadata.Type = MetadataType.AlbumImage;
if (seasonMatch.Groups["specials"].Success) if (seasonMatch.Groups["specials"].Success)
{ {
metadata.SeasonNumber = 0; metadata.AlbumId = 0;
} }
else else
{ {
metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); metadata.AlbumId = Convert.ToInt32(seasonMatch.Groups["season"].Value);
} }
return metadata; return metadata;
} }
metadata.Type = MetadataType.SeriesImage; metadata.Type = MetadataType.ArtistImage;
return metadata; return metadata;
} }
@ -103,7 +103,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
if (extension == ".xml") if (extension == ".xml")
{ {
metadata.Type = MetadataType.EpisodeMetadata; metadata.Type = MetadataType.TrackMetadata;
return metadata; return metadata;
} }
@ -111,7 +111,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
{ {
if (!Path.GetFileNameWithoutExtension(filename).EndsWith("-thumb")) if (!Path.GetFileNameWithoutExtension(filename).EndsWith("-thumb"))
{ {
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.TrackImage;
return metadata; return metadata;
} }
} }
@ -120,23 +120,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult ArtistMetadata(Artist artist)
{ {
//Series metadata is not supported //Artist metadata is not supported
return null; return null;
} }
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) public override MetadataFileResult AlbumMetadata(Artist artist, Album album)
{
return null;
}
public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile)
{ {
if (!Settings.EpisodeMetadata) if (!Settings.EpisodeMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating Episode Metadata for: {0}", episodeFile.RelativePath); _logger.Debug("Generating Track Metadata for: {0}", trackFile.RelativePath);
var xmlResult = string.Empty; var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value) foreach (var track in trackFile.Tracks.Value)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
@ -148,24 +153,10 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
var doc = new XDocument(); var doc = new XDocument();
var details = new XElement("video"); var details = new XElement("video");
details.Add(new XElement("title", string.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); details.Add(new XElement("title", string.Format("{0} - {1} - {2}", artist.Name, track.TrackNumber, track.Title)));
details.Add(new XElement("year", episode.AirDate)); details.Add(new XElement("genre", string.Join(" / ", artist.Genres)));
details.Add(new XElement("genre", string.Join(" / ", series.Genres))); var actors = string.Join(" , ", artist.Members.ConvertAll(c => c.Name + " - " + c.Instrument).GetRange(0, Math.Min(3, artist.Members.Count)));
var actors = string.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count)));
details.Add(new XElement("actors", actors)); details.Add(new XElement("actors", actors));
details.Add(new XElement("description", episode.Overview));
details.Add(new XElement("length", series.Runtime));
if (series.Certification.IsNotNullOrWhiteSpace() &&
ValidCertification.Contains(series.Certification.ToUpperInvariant()))
{
details.Add(new XElement("mpaa", series.Certification.ToUpperInvariant()));
}
else
{
details.Add(new XElement("mpaa", "UNRATED"));
}
doc.Add(details); doc.Add(details);
doc.Save(xw); doc.Save(xw);
@ -175,77 +166,78 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
} }
} }
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); return new MetadataFileResult(GetTrackMetadataFilename(trackFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeriesImages(Series series) public override List<ImageFileResult> ArtistImages(Artist artist)
{ {
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); var image = artist.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? artist.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); _logger.Trace("Failed to find suitable Artist image for artist {0}.", artist.Name);
return null; return null;
} }
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); var source = _mediaCoverService.GetCoverPath(artist.Id, image.CoverType);
var destination = Path.GetFileName(series.Path) + Path.GetExtension(source); var destination = Path.GetFileName(artist.Path) + Path.GetExtension(source);
return new List<ImageFileResult>{ new ImageFileResult(destination, source) }; return new List<ImageFileResult>{ new ImageFileResult(destination, source) };
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) public override List<ImageFileResult> AlbumImages(Artist artist, Album album)
{ {
var seasonFolders = GetSeasonFolders(series); var seasonFolders = GetAlbumFolders(artist);
string seasonFolder; string seasonFolder;
if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder)) if (!seasonFolders.TryGetValue(album.ArtistId, out seasonFolder))
{ {
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); _logger.Trace("Failed to find season folder for series {0}, season {1}.", artist.Name, album.Title);
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
//Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); var image = album.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? album.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", artist.Name, album.Title);
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
var filename = Path.GetFileName(seasonFolder) + ".jpg"; var filename = Path.GetFileName(seasonFolder) + ".jpg";
var path = series.Path.GetRelativePath(Path.Combine(series.Path, seasonFolder, filename)); var path = artist.Path.GetRelativePath(Path.Combine(artist.Path, seasonFolder, filename));
return new List<ImageFileResult> { new ImageFileResult(path, image.Url) }; return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) public override List<ImageFileResult> TrackImages(Artist artist, TrackFile trackFile)
{ {
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); //var screenshot = episodeFile.Tracks.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null) //if (screenshot == null)
{ //{
_logger.Trace("Episode screenshot not available"); // _logger.Trace("Episode screenshot not available");
return new List<ImageFileResult>(); // return new List<ImageFileResult>();
} //}
return new List<ImageFileResult> {new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)}; //return new List<ImageFileResult> {new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)};
return new List<ImageFileResult>();
} }
private string GetEpisodeMetadataFilename(string episodeFilePath) private string GetTrackMetadataFilename(string trackFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "xml"); return Path.ChangeExtension(trackFilePath, "xml");
} }
private string GetEpisodeImageFilename(string episodeFilePath) private string GetTrackImageFilename(string trackFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "jpg"); return Path.ChangeExtension(trackFilePath, "jpg");
} }
private Dictionary<int, string> GetSeasonFolders(Series series) private Dictionary<int, string> GetAlbumFolders(Artist artist)
{ {
var seasonFolderMap = new Dictionary<int, string>(); var seasonFolderMap = new Dictionary<int, string>();
foreach (var folder in _diskProvider.GetDirectories(series.Path)) foreach (var folder in _diskProvider.GetDirectories(artist.Path))
{ {
var directoryinfo = new DirectoryInfo(folder); var directoryinfo = new DirectoryInfo(folder);
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
@ -267,13 +259,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
} }
else else
{ {
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); _logger.Debug("Failed to parse season number from {0} for artist {1}.", folder, artist.Name);
} }
} }
} }
else else
{ {
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); _logger.Debug("Rejecting folder {0} for artist {1}.", Path.GetDirectoryName(folder), artist.Name);
} }
} }

@ -19,19 +19,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
public RoksboxMetadataSettings() public RoksboxMetadataSettings()
{ {
EpisodeMetadata = true; EpisodeMetadata = true;
SeriesImages = true; ArtistImages = true;
SeasonImages = true; AlbumImages = true;
EpisodeImages = true; EpisodeImages = true;
} }
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, HelpText = "Season##\\filename.xml")] [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, HelpText = "Season##\\filename.xml")]
public bool EpisodeMetadata { get; set; } public bool EpisodeMetadata { get; set; }
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox, HelpText = "Series Title.jpg")] [FieldDefinition(1, Label = "Artist Images", Type = FieldType.Checkbox, HelpText = "Artist Title.jpg")]
public bool SeriesImages { get; set; } public bool ArtistImages { get; set; }
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox, HelpText = "Season ##.jpg")] [FieldDefinition(2, Label = "Album Images", Type = FieldType.Checkbox, HelpText = "Album Title.jpg")]
public bool SeasonImages { get; set; } public bool AlbumImages { get; set; }
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, HelpText = "Season##\\filename.jpg")] [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, HelpText = "Season##\\filename.jpg")]
public bool EpisodeImages { get; set; } public bool EpisodeImages { get; set; }

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -12,7 +12,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
{ {
@ -35,26 +35,26 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
public override string Name => "WDTV"; public override string Name => "WDTV";
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile)
{ {
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath);
if (metadataFile.Type == MetadataType.EpisodeImage) if (metadataFile.Type == MetadataType.TrackImage)
{ {
return GetEpisodeImageFilename(episodeFilePath); return GetTrackImageFilename(trackFilePath);
} }
if (metadataFile.Type == MetadataType.EpisodeMetadata) if (metadataFile.Type == MetadataType.TrackMetadata)
{ {
return GetEpisodeMetadataFilename(episodeFilePath); return GetTrackMetadataFilename(trackFilePath);
} }
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath); return Path.Combine(artist.Path, metadataFile.RelativePath);
} }
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Artist artist, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -62,9 +62,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = artist.Path.GetRelativePath(path)
}; };
//Series and season images are both named folder.jpg, only season ones sit in season folders //Series and season images are both named folder.jpg, only season ones sit in season folders
@ -74,22 +74,22 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
if (seasonMatch.Success) if (seasonMatch.Success)
{ {
metadata.Type = MetadataType.SeasonImage; metadata.Type = MetadataType.AlbumImage;
if (seasonMatch.Groups["specials"].Success) if (seasonMatch.Groups["specials"].Success)
{ {
metadata.SeasonNumber = 0; metadata.AlbumId = 0;
} }
else else
{ {
metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); metadata.AlbumId = Convert.ToInt32(seasonMatch.Groups["season"].Value);
} }
return metadata; return metadata;
} }
metadata.Type = MetadataType.SeriesImage; metadata.Type = MetadataType.ArtistImage;
return metadata; return metadata;
} }
@ -101,10 +101,10 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
switch (Path.GetExtension(filename).ToLowerInvariant()) switch (Path.GetExtension(filename).ToLowerInvariant())
{ {
case ".xml": case ".xml":
metadata.Type = MetadataType.EpisodeMetadata; metadata.Type = MetadataType.TrackMetadata;
return metadata; return metadata;
case ".metathumb": case ".metathumb":
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.TrackImage;
return metadata; return metadata;
} }
@ -113,23 +113,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult ArtistMetadata(Artist artist)
{ {
//Series metadata is not supported //Artist metadata is not supported
return null; return null;
} }
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) public override MetadataFileResult AlbumMetadata(Artist artist, Album album)
{
return null;
}
public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile)
{ {
if (!Settings.EpisodeMetadata) if (!Settings.EpisodeMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); _logger.Debug("Generating Track Metadata for: {0}", Path.Combine(artist.Path, trackFile.RelativePath));
var xmlResult = string.Empty; var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value) foreach (var track in trackFile.Tracks.Value)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
@ -141,16 +146,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
var doc = new XDocument(); var doc = new XDocument();
var details = new XElement("details"); var details = new XElement("details");
details.Add(new XElement("id", series.Id)); details.Add(new XElement("id", artist.Id));
details.Add(new XElement("title", string.Format("{0} - {1}x{2:00} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); details.Add(new XElement("title", string.Format("{0} - {1} - {2}", artist.Name, track.TrackNumber, track.Title)));
details.Add(new XElement("series_name", series.Title)); details.Add(new XElement("artist_name", artist.Name));
details.Add(new XElement("episode_name", episode.Title)); details.Add(new XElement("track_name", track.Title));
details.Add(new XElement("season_number", episode.SeasonNumber.ToString("00"))); details.Add(new XElement("track_number", track.TrackNumber.ToString("00")));
details.Add(new XElement("episode_number", episode.EpisodeNumber.ToString("00"))); details.Add(new XElement("genre", string.Join(" / ", artist.Genres)));
details.Add(new XElement("firstaired", episode.AirDate)); details.Add(new XElement("member", string.Join(" / ", artist.Members.ConvertAll(c => c.Name + " - " + c.Instrument))));
details.Add(new XElement("genre", string.Join(" / ", series.Genres)));
details.Add(new XElement("actor", string.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character))));
details.Add(new XElement("overview", episode.Overview));
//Todo: get guest stars, writer and director //Todo: get guest stars, writer and director
@ -165,27 +167,27 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
} }
} }
var filename = GetEpisodeMetadataFilename(episodeFile.RelativePath); var filename = GetTrackMetadataFilename(trackFile.RelativePath);
return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeriesImages(Series series) public override List<ImageFileResult> ArtistImages(Artist artist)
{ {
if (!Settings.SeriesImages) if (!Settings.ArtistImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first //Because we only support one image, attempt to get the Poster type, then if that fails grab the first
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); var image = artist.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? artist.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); _logger.Trace("Failed to find suitable Artist image for artist {0}.", artist.Name);
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); var source = _mediaCoverService.GetCoverPath(artist.Id, image.CoverType);
var destination = "folder" + Path.GetExtension(source); var destination = "folder" + Path.GetExtension(source);
return new List<ImageFileResult> return new List<ImageFileResult>
@ -194,28 +196,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
}; };
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) public override List<ImageFileResult> AlbumImages(Artist artist, Album album)
{ {
if (!Settings.SeasonImages) if (!Settings.AlbumImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
var seasonFolders = GetSeasonFolders(series); var seasonFolders = GetAlbumFolders(artist);
//Work out the path to this season - if we don't have a matching path then skip this season. //Work out the path to this season - if we don't have a matching path then skip this season.
string seasonFolder; string seasonFolder;
if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder)) if (!seasonFolders.TryGetValue(album.Id, out seasonFolder))
{ {
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); _logger.Trace("Failed to find album folder for artist {0}, album {1}.", artist.Name, album.Title);
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
//WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); var image = album.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? album.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); _logger.Trace("Failed to find suitable album image for artist {0}, album {1}.", artist.Name, album.Title);
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
@ -224,39 +226,27 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
return new List<ImageFileResult>{ new ImageFileResult(path, image.Url) }; return new List<ImageFileResult>{ new ImageFileResult(path, image.Url) };
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) public override List<ImageFileResult> TrackImages(Artist artist, TrackFile trackFile)
{ {
if (!Settings.EpisodeImages)
{
return new List<ImageFileResult>();
}
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null)
{
_logger.Trace("Episode screenshot not available");
return new List<ImageFileResult>();
}
return new List<ImageFileResult>{ new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url) }; return new List<ImageFileResult>();
} }
private string GetEpisodeMetadataFilename(string episodeFilePath) private string GetTrackMetadataFilename(string trackFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "xml"); return Path.ChangeExtension(trackFilePath, "xml");
} }
private string GetEpisodeImageFilename(string episodeFilePath) private string GetTrackImageFilename(string trackFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "metathumb"); return Path.ChangeExtension(trackFilePath, "metathumb");
} }
private Dictionary<int, string> GetSeasonFolders(Series series) private Dictionary<int, string> GetAlbumFolders(Artist artist)
{ {
var seasonFolderMap = new Dictionary<int, string>(); var seasonFolderMap = new Dictionary<int, string>();
foreach (var folder in _diskProvider.GetDirectories(series.Path)) foreach (var folder in _diskProvider.GetDirectories(artist.Path))
{ {
var directoryinfo = new DirectoryInfo(folder); var directoryinfo = new DirectoryInfo(folder);
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
@ -278,14 +268,14 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
} }
else else
{ {
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); _logger.Debug("Failed to parse season number from {0} for artist {1}.", folder, artist.Name);
} }
} }
} }
else else
{ {
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); _logger.Debug("Rejecting folder {0} for artist {1}.", Path.GetDirectoryName(folder), artist.Name);
} }
} }

@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -19,19 +19,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
public WdtvMetadataSettings() public WdtvMetadataSettings()
{ {
EpisodeMetadata = true; EpisodeMetadata = true;
SeriesImages = true; ArtistImages = true;
SeasonImages = true; AlbumImages = true;
EpisodeImages = true; EpisodeImages = true;
} }
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
public bool EpisodeMetadata { get; set; } public bool EpisodeMetadata { get; set; }
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] [FieldDefinition(1, Label = "Artist Images", Type = FieldType.Checkbox)]
public bool SeriesImages { get; set; } public bool ArtistImages { get; set; }
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] [FieldDefinition(2, Label = "Album Images", Type = FieldType.Checkbox)]
public bool SeasonImages { get; set; } public bool AlbumImages { get; set; }
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
public bool EpisodeImages { get; set; } public bool EpisodeImages { get; set; }

@ -11,7 +11,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{ {
@ -27,31 +27,31 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
_logger = logger; _logger = logger;
} }
private static readonly Regex SeriesImagesRegex = new Regex(@"^(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ArtistImagesRegex = new Regex(@"^(?<type>poster|banner|fanart|logo)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?<season>\d{2,}|-all|-specials)-(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex AlbumImagesRegex = new Regex(@"^season(?<season>\d{2,}|-all|-specials)-(?<type>poster|banner|fanart|cover)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override string Name => "Kodi (XBMC) / Emby"; public override string Name => "Kodi (XBMC) / Emby";
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile)
{ {
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath);
if (metadataFile.Type == MetadataType.EpisodeImage) if (metadataFile.Type == MetadataType.TrackImage)
{ {
return GetEpisodeImageFilename(episodeFilePath); return GetEpisodeImageFilename(trackFilePath);
} }
if (metadataFile.Type == MetadataType.EpisodeMetadata) if (metadataFile.Type == MetadataType.TrackMetadata)
{ {
return GetEpisodeMetadataFilename(episodeFilePath); return GetEpisodeMetadataFilename(trackFilePath);
} }
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath); return Path.Combine(artist.Path, metadataFile.RelativePath);
} }
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Artist artist, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -59,34 +59,34 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = artist.Path.GetRelativePath(path)
}; };
if (SeriesImagesRegex.IsMatch(filename)) if (ArtistImagesRegex.IsMatch(filename))
{ {
metadata.Type = MetadataType.SeriesImage; metadata.Type = MetadataType.ArtistImage;
return metadata; return metadata;
} }
var seasonMatch = SeasonImagesRegex.Match(filename); var seasonMatch = AlbumImagesRegex.Match(filename);
if (seasonMatch.Success) if (seasonMatch.Success)
{ {
metadata.Type = MetadataType.SeasonImage; metadata.Type = MetadataType.AlbumImage;
var seasonNumberMatch = seasonMatch.Groups["season"].Value; var seasonNumberMatch = seasonMatch.Groups["season"].Value;
int seasonNumber; int seasonNumber;
if (seasonNumberMatch.Contains("specials")) if (seasonNumberMatch.Contains("specials"))
{ {
metadata.SeasonNumber = 0; metadata.AlbumId = 0;
} }
else if (int.TryParse(seasonNumberMatch, out seasonNumber)) else if (int.TryParse(seasonNumberMatch, out seasonNumber))
{ {
metadata.SeasonNumber = seasonNumber; metadata.AlbumId = seasonNumber;
} }
else else
@ -99,13 +99,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
if (EpisodeImageRegex.IsMatch(filename)) if (EpisodeImageRegex.IsMatch(filename))
{ {
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.TrackImage;
return metadata; return metadata;
} }
if (filename.Equals("tvshow.nfo", StringComparison.InvariantCultureIgnoreCase)) if (filename.Equals("artist.nfo", StringComparison.InvariantCultureIgnoreCase))
{ {
metadata.Type = MetadataType.SeriesMetadata; metadata.Type = MetadataType.ArtistMetadata;
return metadata;
}
if (filename.Equals("album.nfo", StringComparison.InvariantCultureIgnoreCase))
{
metadata.Type = MetadataType.AlbumMetadata;
return metadata; return metadata;
} }
@ -115,91 +121,120 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
!parseResult.FullSeason && !parseResult.FullSeason &&
Path.GetExtension(filename) == ".nfo") Path.GetExtension(filename) == ".nfo")
{ {
metadata.Type = MetadataType.EpisodeMetadata; metadata.Type = MetadataType.TrackMetadata;
return metadata; return metadata;
} }
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult ArtistMetadata(Artist artist)
{ {
if (!Settings.SeriesMetadata) if (!Settings.ArtistMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating tvshow.nfo for: {0}", series.Title); _logger.Debug("Generating artist.nfo for: {0}", artist.Name);
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
xws.Indent = false; xws.Indent = false;
var episodeGuideUrl = string.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/en.zip", series.TvdbId);
using (var xw = XmlWriter.Create(sb, xws)) using (var xw = XmlWriter.Create(sb, xws))
{ {
var tvShow = new XElement("tvshow"); var artistElement = new XElement("artist");
tvShow.Add(new XElement("title", series.Title)); artistElement.Add(new XElement("title", artist.Name));
if (series.Ratings != null && series.Ratings.Votes > 0) if (artist.Ratings != null && artist.Ratings.Votes > 0)
{ {
tvShow.Add(new XElement("rating", series.Ratings.Value)); artistElement.Add(new XElement("rating", artist.Ratings.Value));
} }
tvShow.Add(new XElement("plot", series.Overview)); artistElement.Add(new XElement("musicbrainzartistid", artist.ForeignArtistId));
tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); artistElement.Add(new XElement("biography", artist.Overview));
tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); artistElement.Add(new XElement("outline", artist.Overview));
tvShow.Add(new XElement("mpaa", series.Certification)); //tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl)));
tvShow.Add(new XElement("id", series.TvdbId)); //tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl));
//foreach (var genre in artist.Genres)
//{
// tvShow.Add(new XElement("genre", genre));
//}
//foreach (var actor in artist.Members)
//{
// var xmlActor = new XElement("actor",
// new XElement("name", actor.Name),
// new XElement("role", actor.Instrument));
// if (actor.Images.Any())
// {
// xmlActor.Add(new XElement("thumb", actor.Images.First().Url));
// }
// tvShow.Add(xmlActor);
//}
var doc = new XDocument(artistElement);
doc.Save(xw);
foreach (var genre in series.Genres) _logger.Debug("Saving artist.nfo for {0}", artist.Name);
{
tvShow.Add(new XElement("genre", genre));
}
if (series.FirstAired.HasValue) return new MetadataFileResult("artist.nfo", doc.ToString());
{ }
tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd"))); }
}
tvShow.Add(new XElement("studio", series.Network)); public override MetadataFileResult AlbumMetadata(Artist artist, Album album)
{
if (!Settings.AlbumMetadata)
{
return null;
}
foreach (var actor in series.Actors) _logger.Debug("Generating album.nfo for: {0}", album.Title);
{ var sb = new StringBuilder();
var xmlActor = new XElement("actor", var xws = new XmlWriterSettings();
new XElement("name", actor.Name), xws.OmitXmlDeclaration = true;
new XElement("role", actor.Character)); xws.Indent = false;
if (actor.Images.Any()) using (var xw = XmlWriter.Create(sb, xws))
{ {
xmlActor.Add(new XElement("thumb", actor.Images.First().Url)); var albumElement = new XElement("album");
}
tvShow.Add(xmlActor); albumElement.Add(new XElement("title", album.Title));
if (album.Ratings != null && album.Ratings.Votes > 0)
{
albumElement.Add(new XElement("rating", album.Ratings.Value));
} }
var doc = new XDocument(tvShow); albumElement.Add(new XElement("musicbrainzalbumid", album.ForeignAlbumId));
albumElement.Add(new XElement("artistdesc", artist.Overview));
albumElement.Add(new XElement("releasedate", album.ReleaseDate.Value.ToShortDateString()));
var doc = new XDocument(albumElement);
doc.Save(xw); doc.Save(xw);
_logger.Debug("Saving tvshow.nfo for {0}", series.Title); _logger.Debug("Saving album.nfo for {0}", artist.Name);
return new MetadataFileResult("tvshow.nfo", doc.ToString()); return new MetadataFileResult("album.nfo", doc.ToString());
} }
} }
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile)
{ {
if (!Settings.EpisodeMetadata) if (!Settings.TrackMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); _logger.Debug("Generating Track Metadata for: {0}", Path.Combine(artist.Path, trackFile.RelativePath));
var xmlResult = string.Empty; var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value) foreach (var episode in trackFile.Tracks.Value)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
@ -209,29 +244,15 @@ namespace NzbDrone.Core.Extras.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));
details.Add(new XElement("season", episode.SeasonNumber)); details.Add(new XElement("episode", episode.TrackNumber));
details.Add(new XElement("episode", episode.EpisodeNumber));
details.Add(new XElement("aired", episode.AirDate));
details.Add(new XElement("plot", episode.Overview));
//If trakt ever gets airs before information for specials we should add set it //If trakt ever gets airs before information for specials we should add set it
details.Add(new XElement("displayseason")); details.Add(new XElement("displayseason"));
details.Add(new XElement("displayepisode")); details.Add(new XElement("displayepisode"));
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"));
if (episode.Ratings != null && episode.Ratings.Votes > 0) if (episode.Ratings != null && episode.Ratings.Votes > 0)
@ -239,39 +260,39 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
details.Add(new XElement("rating", episode.Ratings.Value)); details.Add(new XElement("rating", episode.Ratings.Value));
} }
if (episodeFile.MediaInfo != null) if (trackFile.MediaInfo != null)
{ {
var fileInfo = new XElement("fileinfo"); var fileInfo = new XElement("fileinfo");
var streamDetails = new XElement("streamdetails"); var streamDetails = new XElement("streamdetails");
var video = new XElement("video"); var video = new XElement("video");
video.Add(new XElement("aspect", (float)episodeFile.MediaInfo.Width / (float)episodeFile.MediaInfo.Height)); video.Add(new XElement("aspect", (float)trackFile.MediaInfo.Width / (float)trackFile.MediaInfo.Height));
video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate)); video.Add(new XElement("bitrate", trackFile.MediaInfo.VideoBitrate));
video.Add(new XElement("codec", episodeFile.MediaInfo.VideoCodec)); video.Add(new XElement("codec", trackFile.MediaInfo.VideoCodec));
video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps)); video.Add(new XElement("framerate", trackFile.MediaInfo.VideoFps));
video.Add(new XElement("height", episodeFile.MediaInfo.Height)); video.Add(new XElement("height", trackFile.MediaInfo.Height));
video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType)); video.Add(new XElement("scantype", trackFile.MediaInfo.ScanType));
video.Add(new XElement("width", episodeFile.MediaInfo.Width)); video.Add(new XElement("width", trackFile.MediaInfo.Width));
if (episodeFile.MediaInfo.RunTime != null) if (trackFile.MediaInfo.RunTime != null)
{ {
video.Add(new XElement("duration", episodeFile.MediaInfo.RunTime.TotalMinutes)); video.Add(new XElement("duration", trackFile.MediaInfo.RunTime.TotalMinutes));
video.Add(new XElement("durationinseconds", episodeFile.MediaInfo.RunTime.TotalSeconds)); video.Add(new XElement("durationinseconds", trackFile.MediaInfo.RunTime.TotalSeconds));
} }
streamDetails.Add(video); streamDetails.Add(video);
var audio = new XElement("audio"); var audio = new XElement("audio");
audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate)); audio.Add(new XElement("bitrate", trackFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", episodeFile.MediaInfo.AudioChannels)); audio.Add(new XElement("channels", trackFile.MediaInfo.AudioChannels));
audio.Add(new XElement("codec", GetAudioCodec(episodeFile.MediaInfo.AudioFormat))); audio.Add(new XElement("codec", GetAudioCodec(trackFile.MediaInfo.AudioFormat)));
audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages)); audio.Add(new XElement("language", trackFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio); streamDetails.Add(audio);
if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Length > 0) if (trackFile.MediaInfo.Subtitles != null && trackFile.MediaInfo.Subtitles.Length > 0)
{ {
var subtitle = new XElement("subtitle"); var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", episodeFile.MediaInfo.Subtitles)); subtitle.Add(new XElement("language", trackFile.MediaInfo.Subtitles));
streamDetails.Add(subtitle); streamDetails.Add(subtitle);
} }
@ -291,80 +312,52 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
} }
} }
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); return new MetadataFileResult(GetEpisodeMetadataFilename(trackFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeriesImages(Series series) public override List<ImageFileResult> ArtistImages(Artist artist)
{ {
if (!Settings.SeriesImages) if (!Settings.ArtistImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
return ProcessSeriesImages(series).ToList(); return ProcessArtistImages(artist).ToList();
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) public override List<ImageFileResult> AlbumImages(Artist artist, Album album)
{ {
if (!Settings.SeasonImages) if (!Settings.AlbumImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
return ProcessSeasonImages(series, season).ToList(); return ProcessAlbumImages(artist, album).ToList();
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) public override List<ImageFileResult> TrackImages(Artist artist, TrackFile trackFile)
{ {
if (!Settings.EpisodeImages)
{
return new List<ImageFileResult>();
}
try return new List<ImageFileResult>();
{
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null)
{
_logger.Debug("Episode screenshot not available");
return new List<ImageFileResult>();
}
return new List<ImageFileResult>
{
new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)
};
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to process episode image for file: {0}", Path.Combine(series.Path, episodeFile.RelativePath));
return new List<ImageFileResult>();
}
} }
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series) private IEnumerable<ImageFileResult> ProcessArtistImages(Artist artist)
{ {
foreach (var image in series.Images) foreach (var image in artist.Images)
{ {
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); var source = _mediaCoverService.GetCoverPath(artist.Id, image.CoverType);
var destination = image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source); var destination = image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source);
yield return new ImageFileResult(destination, source); yield return new ImageFileResult(destination, source);
} }
} }
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) private IEnumerable<ImageFileResult> ProcessAlbumImages(Artist artist, Album album)
{ {
foreach (var image in season.Images) foreach (var image in album.Images)
{ {
var filename = string.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower()); var destination = Path.GetFileName(album.Path);
var filename = string.Format("{0}\\{1}{2}", destination, image.CoverType.ToString().ToLower(), Path.GetExtension(image.Url));
if (season.SeasonNumber == 0)
{
filename = string.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower());
}
yield return new ImageFileResult(filename, image.Url); yield return new ImageFileResult(filename, image.Url);
} }

@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -18,28 +18,32 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
public XbmcMetadataSettings() public XbmcMetadataSettings()
{ {
SeriesMetadata = true; ArtistMetadata = true;
EpisodeMetadata = true; AlbumMetadata = true;
SeriesImages = true; TrackMetadata = true;
SeasonImages = true; ArtistImages = true;
AlbumImages = true;
EpisodeImages = true; EpisodeImages = true;
} }
[FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] [FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox)]
public bool SeriesMetadata { get; set; } public bool ArtistMetadata { get; set; }
[FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox)] [FieldDefinition(1, Label = "Album Metadata", Type = FieldType.Checkbox)]
public bool EpisodeMetadata { get; set; } public bool AlbumMetadata { get; set; }
[FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox)] [FieldDefinition(2, Label = "Track Metadata", Type = FieldType.Checkbox)]
public bool SeriesImages { get; set; } public bool TrackMetadata { get; set; }
[FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox)] [FieldDefinition(3, Label = "Artist Images", Type = FieldType.Checkbox)]
public bool SeasonImages { get; set; } public bool ArtistImages { get; set; }
[FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox)] [FieldDefinition(4, Label = "Album Images", Type = FieldType.Checkbox)]
public bool AlbumImages { get; set; }
[FieldDefinition(5, Label = "Episode Images", Type = FieldType.Checkbox)]
public bool EpisodeImages { get; set; } public bool EpisodeImages { get; set; }
public bool IsValid => true; public bool IsValid => true;
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -7,7 +7,7 @@ using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
@ -32,12 +32,12 @@ namespace NzbDrone.Core.Extras.Metadata
public override int Order => 0; public override int Order => 0;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) public override IEnumerable<ExtraFile> ProcessFiles(Artist artist, List<string> filesOnDisk, List<string> importedFiles)
{ {
_logger.Debug("Looking for existing metadata in {0}", series.Path); _logger.Debug("Looking for existing metadata in {0}", artist.Path);
var metadataFiles = new List<MetadataFile>(); var metadataFiles = new List<MetadataFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles);
foreach (var possibleMetadataFile in filterResult.FilesOnDisk) foreach (var possibleMetadataFile in filterResult.FilesOnDisk)
{ {
@ -50,38 +50,38 @@ namespace NzbDrone.Core.Extras.Metadata
foreach (var consumer in _consumers) foreach (var consumer in _consumers)
{ {
var metadata = consumer.FindMetadataFile(series, possibleMetadataFile); var metadata = consumer.FindMetadataFile(artist, possibleMetadataFile);
if (metadata == null) if (metadata == null)
{ {
continue; continue;
} }
if (metadata.Type == MetadataType.EpisodeImage || if (metadata.Type == MetadataType.TrackImage ||
metadata.Type == MetadataType.EpisodeMetadata) metadata.Type == MetadataType.TrackMetadata)
{ {
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series); var localTrack = _parsingService.GetLocalTrack(possibleMetadataFile, artist);
if (localEpisode == null) if (localTrack == null)
{ {
_logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile); _logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) if (localTrack.Tracks.Empty())
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile); _logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile);
continue; continue;
} }
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1)
{ {
_logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile); _logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile);
continue; continue;
} }
metadata.SeasonNumber = localEpisode.SeasonNumber; metadata.AlbumId = localTrack.Album.Id;
metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId; metadata.TrackFileId = localTrack.Tracks.First().TrackFileId;
} }
metadata.Extension = Path.GetExtension(possibleMetadataFile); metadata.Extension = Path.GetExtension(possibleMetadataFile);

@ -1,13 +1,13 @@
using System.IO; using System.IO;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata.Files namespace NzbDrone.Core.Extras.Metadata.Files
{ {
public interface ICleanMetadataService public interface ICleanMetadataService
{ {
void Clean(Series series); void Clean(Artist artist);
} }
public class CleanExtraFileService : ICleanMetadataService public class CleanExtraFileService : ICleanMetadataService
@ -25,15 +25,15 @@ namespace NzbDrone.Core.Extras.Metadata.Files
_logger = logger; _logger = logger;
} }
public void Clean(Series series) public void Clean(Artist artist)
{ {
_logger.Debug("Cleaning missing metadata files for series: {0}", series.Title); _logger.Debug("Cleaning missing metadata files for artist: {0}", artist.Name);
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id);
foreach (var metadataFile in metadataFiles) foreach (var metadataFile in metadataFiles)
{ {
if (!_diskProvider.FileExists(Path.Combine(series.Path, metadataFile.RelativePath))) if (!_diskProvider.FileExists(Path.Combine(artist.Path, metadataFile.RelativePath)))
{ {
_logger.Debug("Deleting metadata file from database: {0}", metadataFile.RelativePath); _logger.Debug("Deleting metadata file from database: {0}", metadataFile.RelativePath);
_metadataFileService.Delete(metadataFile.Id); _metadataFileService.Delete(metadataFile.Id);

@ -1,8 +1,8 @@
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata.Files namespace NzbDrone.Core.Extras.Metadata.Files
{ {
@ -12,8 +12,8 @@ namespace NzbDrone.Core.Extras.Metadata.Files
public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService
{ {
public MetadataFileService(IExtraFileRepository<MetadataFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) public MetadataFileService(IExtraFileRepository<MetadataFile> repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger) : base(repository, artistService, diskProvider, recycleBinProvider, logger)
{ {
} }

@ -1,19 +1,20 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
public interface IMetadata : IProvider public interface IMetadata : IProvider
{ {
string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile); string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile);
MetadataFile FindMetadataFile(Series series, string path); MetadataFile FindMetadataFile(Artist artist, string path);
MetadataFileResult SeriesMetadata(Series series); MetadataFileResult ArtistMetadata(Artist artist);
MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); MetadataFileResult AlbumMetadata(Artist artist, Album album);
List<ImageFileResult> SeriesImages(Series series); MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile);
List<ImageFileResult> SeasonImages(Series series, Season season); List<ImageFileResult> ArtistImages(Artist artist);
List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile); List<ImageFileResult> AlbumImages(Artist artist, Album album);
List<ImageFileResult> TrackImages(Artist artist, TrackFile trackFile);
} }
} }

@ -1,11 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using FluentValidation.Results; using FluentValidation.Results;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
@ -26,22 +26,23 @@ namespace NzbDrone.Core.Extras.Metadata
return new ValidationResult(); return new ValidationResult();
} }
public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public virtual string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile)
{ {
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); var existingFilename = Path.Combine(artist.Path, metadataFile.RelativePath);
var extension = Path.GetExtension(existingFilename).TrimStart('.'); var extension = Path.GetExtension(existingFilename).TrimStart('.');
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension); var newFileName = Path.ChangeExtension(Path.Combine(artist.Path, trackFile.RelativePath), extension);
return newFileName; return newFileName;
} }
public abstract MetadataFile FindMetadataFile(Series series, string path); public abstract MetadataFile FindMetadataFile(Artist artist, string path);
public abstract MetadataFileResult SeriesMetadata(Series series); public abstract MetadataFileResult ArtistMetadata(Artist artist);
public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); public abstract MetadataFileResult AlbumMetadata(Artist artist, Album album);
public abstract List<ImageFileResult> SeriesImages(Series series); public abstract MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile);
public abstract List<ImageFileResult> SeasonImages(Series series, Season season); public abstract List<ImageFileResult> ArtistImages(Artist artist);
public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile); public abstract List<ImageFileResult> AlbumImages(Artist artist, Album album);
public abstract List<ImageFileResult> TrackImages(Artist artist, TrackFile trackFile);
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; } public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -11,7 +11,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
@ -24,6 +24,7 @@ namespace NzbDrone.Core.Extras.Metadata
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IMetadataFileService _metadataFileService; private readonly IMetadataFileService _metadataFileService;
private readonly IAlbumService _albumService;
private readonly Logger _logger; private readonly Logger _logger;
public MetadataService(IConfigService configService, public MetadataService(IConfigService configService,
@ -34,6 +35,7 @@ namespace NzbDrone.Core.Extras.Metadata
IHttpClient httpClient, IHttpClient httpClient,
IMediaFileAttributeService mediaFileAttributeService, IMediaFileAttributeService mediaFileAttributeService,
IMetadataFileService metadataFileService, IMetadataFileService metadataFileService,
IAlbumService albumService,
Logger logger) Logger logger)
: base(configService, diskProvider, diskTransferService, logger) : base(configService, diskProvider, diskTransferService, logger)
{ {
@ -44,19 +46,20 @@ namespace NzbDrone.Core.Extras.Metadata
_httpClient = httpClient; _httpClient = httpClient;
_mediaFileAttributeService = mediaFileAttributeService; _mediaFileAttributeService = mediaFileAttributeService;
_metadataFileService = metadataFileService; _metadataFileService = metadataFileService;
_albumService = albumService;
_logger = logger; _logger = logger;
} }
public override int Order => 0; public override int Order => 0;
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> CreateAfterArtistScan(Artist artist, List<Album> albums, List<TrackFile> trackFiles)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id);
_cleanMetadataService.Clean(series); _cleanMetadataService.Clean(artist);
if (!_diskProvider.FolderExists(series.Path)) if (!_diskProvider.FolderExists(artist.Path))
{ {
_logger.Info("Series folder does not exist, skipping metadata creation"); _logger.Info("Artist folder does not exist, skipping metadata creation");
return Enumerable.Empty<MetadataFile>(); return Enumerable.Empty<MetadataFile>();
} }
@ -66,14 +69,20 @@ namespace NzbDrone.Core.Extras.Metadata
{ {
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); files.AddIfNotNull(ProcessArtistMetadata(consumer, artist, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); files.AddRange(ProcessArtistImages(consumer, artist, consumerFiles));
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); files.AddRange(ProcessAlbumImages(consumer, artist, consumerFiles));
foreach (var episodeFile in episodeFiles) foreach (var album in albums)
{ {
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles)); album.Artist = artist;
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, consumerFiles)); files.AddIfNotNull(ProcessAlbumMetadata(consumer, album, consumerFiles));
}
foreach (var trackFile in trackFiles)
{
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, artist, trackFile, consumerFiles));
files.AddRange(ProcessEpisodeImages(consumer, artist, trackFile, consumerFiles));
} }
} }
@ -82,15 +91,15 @@ namespace NzbDrone.Core.Extras.Metadata
return files; return files;
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) public override IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, TrackFile trackFile)
{ {
var files = new List<MetadataFile>(); var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled()) foreach (var consumer in _metadataFactory.Enabled())
{ {
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List<MetadataFile>())); files.AddIfNotNull(ProcessEpisodeMetadata(consumer, artist, trackFile, new List<MetadataFile>()));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List<MetadataFile>())); files.AddRange(ProcessEpisodeImages(consumer, artist, trackFile, new List<MetadataFile>()));
} }
_metadataFileService.Upsert(files); _metadataFileService.Upsert(files);
@ -98,11 +107,11 @@ namespace NzbDrone.Core.Extras.Metadata
return files; return files;
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id);
if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace()) if (artistFolder.IsNullOrWhiteSpace() && albumFolder.IsNullOrWhiteSpace())
{ {
return new List<MetadataFile>(); return new List<MetadataFile>();
} }
@ -113,15 +122,15 @@ namespace NzbDrone.Core.Extras.Metadata
{ {
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
if (seriesFolder.IsNotNullOrWhiteSpace()) if (artistFolder.IsNotNullOrWhiteSpace())
{ {
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); files.AddIfNotNull(ProcessArtistMetadata(consumer, artist, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); files.AddRange(ProcessArtistImages(consumer, artist, consumerFiles));
} }
if (seasonFolder.IsNotNullOrWhiteSpace()) if (albumFolder.IsNotNullOrWhiteSpace())
{ {
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); files.AddRange(ProcessAlbumImages(consumer, artist, consumerFiles));
} }
} }
@ -130,9 +139,9 @@ namespace NzbDrone.Core.Extras.Metadata
return files; return files;
} }
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Artist artist, List<TrackFile> trackFiles)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id);
var movedFiles = new List<MetadataFile>(); var movedFiles = new List<MetadataFile>();
// TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it // TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it
@ -140,21 +149,21 @@ namespace NzbDrone.Core.Extras.Metadata
foreach (var consumer in _metadataFactory.GetAvailableProviders()) foreach (var consumer in _metadataFactory.GetAvailableProviders())
{ {
foreach (var episodeFile in episodeFiles) foreach (var trackFile in trackFiles)
{ {
var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.TrackFileId == trackFile.Id).ToList();
foreach (var metadataFile in metadataFilesForConsumer) foreach (var metadataFile in metadataFilesForConsumer)
{ {
var newFileName = consumer.GetFilenameAfterMove(series, episodeFile, metadataFile); var newFileName = consumer.GetFilenameAfterMove(artist, trackFile, metadataFile);
var existingFileName = Path.Combine(series.Path, metadataFile.RelativePath); var existingFileName = Path.Combine(artist.Path, metadataFile.RelativePath);
if (newFileName.PathNotEquals(existingFileName)) if (newFileName.PathNotEquals(existingFileName))
{ {
try try
{ {
_diskProvider.MoveFile(existingFileName, newFileName); _diskProvider.MoveFile(existingFileName, newFileName);
metadataFile.RelativePath = series.Path.GetRelativePath(newFileName); metadataFile.RelativePath = artist.Path.GetRelativePath(newFileName);
movedFiles.Add(metadataFile); movedFiles.Add(metadataFile);
} }
catch (Exception ex) catch (Exception ex)
@ -171,40 +180,84 @@ namespace NzbDrone.Core.Extras.Metadata
return movedFiles; return movedFiles;
} }
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly)
{ {
return null; return null;
} }
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata) private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> artistMetadata)
{ {
return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); return artistMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList();
}
private MetadataFile ProcessArtistMetadata(IMetadata consumer, Artist artist, List<MetadataFile> existingMetadataFiles)
{
var artistMetadata = consumer.ArtistMetadata(artist);
if (artistMetadata == null)
{
return null;
}
var hash = artistMetadata.Contents.SHA256Hash();
var metadata = GetMetadataFile(artist, existingMetadataFiles, e => e.Type == MetadataType.ArtistMetadata) ??
new MetadataFile
{
ArtistId = artist.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.ArtistMetadata
};
if (hash == metadata.Hash)
{
if (artistMetadata.RelativePath != metadata.RelativePath)
{
metadata.RelativePath = artistMetadata.RelativePath;
return metadata;
}
return null;
}
var fullPath = Path.Combine(artist.Path, artistMetadata.RelativePath);
_logger.Debug("Writing Artist Metadata to: {0}", fullPath);
SaveMetadataFile(fullPath, artistMetadata.Contents);
metadata.Hash = hash;
metadata.RelativePath = artistMetadata.RelativePath;
metadata.Extension = Path.GetExtension(fullPath);
return metadata;
} }
private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles) private MetadataFile ProcessAlbumMetadata(IMetadata consumer, Album album, List<MetadataFile> existingMetadataFiles)
{ {
var seriesMetadata = consumer.SeriesMetadata(series); var albumMetadata = consumer.AlbumMetadata(album.Artist, album);
if (seriesMetadata == null) if (albumMetadata == null)
{ {
return null; return null;
} }
var hash = seriesMetadata.Contents.SHA256Hash(); var hash = albumMetadata.Contents.SHA256Hash();
var metadata = GetMetadataFile(series, existingMetadataFiles, e => e.Type == MetadataType.SeriesMetadata) ?? var metadata = GetMetadataFile(album.Artist, existingMetadataFiles, e => e.Type == MetadataType.AlbumMetadata && e.AlbumId == album.Id) ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, ArtistId = album.ArtistId,
AlbumId = album.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.SeriesMetadata Type = MetadataType.AlbumMetadata
}; };
if (hash == metadata.Hash) if (hash == metadata.Hash)
{ {
if (seriesMetadata.RelativePath != metadata.RelativePath) if (albumMetadata.RelativePath != metadata.RelativePath)
{ {
metadata.RelativePath = seriesMetadata.RelativePath; metadata.RelativePath = albumMetadata.RelativePath;
return metadata; return metadata;
} }
@ -212,35 +265,35 @@ namespace NzbDrone.Core.Extras.Metadata
return null; return null;
} }
var fullPath = Path.Combine(series.Path, seriesMetadata.RelativePath); var fullPath = Path.Combine(album.Path, albumMetadata.RelativePath);
_logger.Debug("Writing Series Metadata to: {0}", fullPath); _logger.Debug("Writing Album Metadata to: {0}", fullPath);
SaveMetadataFile(fullPath, seriesMetadata.Contents); SaveMetadataFile(fullPath, albumMetadata.Contents);
metadata.Hash = hash; metadata.Hash = hash;
metadata.RelativePath = seriesMetadata.RelativePath; metadata.RelativePath = albumMetadata.RelativePath;
metadata.Extension = Path.GetExtension(fullPath); metadata.Extension = Path.GetExtension(fullPath);
return metadata; return metadata;
} }
private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles) private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Artist artist, TrackFile trackFile, List<MetadataFile> existingMetadataFiles)
{ {
var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile); var episodeMetadata = consumer.TrackMetadata(artist, trackFile);
if (episodeMetadata == null) if (episodeMetadata == null)
{ {
return null; return null;
} }
var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath); var fullPath = Path.Combine(artist.Path, episodeMetadata.RelativePath);
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeMetadata && var existingMetadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.TrackMetadata &&
c.EpisodeFileId == episodeFile.Id); c.TrackFileId == trackFile.Id);
if (existingMetadata != null) if (existingMetadata != null)
{ {
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); var existingFullPath = Path.Combine(artist.Path, existingMetadata.RelativePath);
if (fullPath.PathNotEquals(existingFullPath)) if (fullPath.PathNotEquals(existingFullPath))
{ {
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
@ -253,11 +306,11 @@ namespace NzbDrone.Core.Extras.Metadata
var metadata = existingMetadata ?? var metadata = existingMetadata ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
SeasonNumber = episodeFile.SeasonNumber, AlbumId = trackFile.AlbumId,
EpisodeFileId = episodeFile.Id, TrackFileId = trackFile.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeMetadata, Type = MetadataType.TrackMetadata,
RelativePath = episodeMetadata.RelativePath, RelativePath = episodeMetadata.RelativePath,
Extension = Path.GetExtension(fullPath) Extension = Path.GetExtension(fullPath)
}; };
@ -267,7 +320,7 @@ namespace NzbDrone.Core.Extras.Metadata
return null; return null;
} }
_logger.Debug("Writing Episode Metadata to: {0}", fullPath); _logger.Debug("Writing Track Metadata to: {0}", fullPath);
SaveMetadataFile(fullPath, episodeMetadata.Contents); SaveMetadataFile(fullPath, episodeMetadata.Contents);
metadata.Hash = hash; metadata.Hash = hash;
@ -275,32 +328,32 @@ namespace NzbDrone.Core.Extras.Metadata
return metadata; return metadata;
} }
private List<MetadataFile> ProcessSeriesImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles) private List<MetadataFile> ProcessArtistImages(IMetadata consumer, Artist artist, List<MetadataFile> existingMetadataFiles)
{ {
var result = new List<MetadataFile>(); var result = new List<MetadataFile>();
foreach (var image in consumer.SeriesImages(series)) foreach (var image in consumer.ArtistImages(artist))
{ {
var fullPath = Path.Combine(series.Path, image.RelativePath); var fullPath = Path.Combine(artist.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath)) if (_diskProvider.FileExists(fullPath))
{ {
_logger.Debug("Series image already exists: {0}", fullPath); _logger.Debug("Artist image already exists: {0}", fullPath);
continue; continue;
} }
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage && var metadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.ArtistImage &&
c.RelativePath == image.RelativePath) ?? c.RelativePath == image.RelativePath) ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.SeriesImage, Type = MetadataType.ArtistImage,
RelativePath = image.RelativePath, RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath) Extension = Path.GetExtension(fullPath)
}; };
DownloadImage(series, image); DownloadImage(artist, image);
result.Add(metadata); result.Add(metadata);
} }
@ -308,36 +361,38 @@ namespace NzbDrone.Core.Extras.Metadata
return result; return result;
} }
private List<MetadataFile> ProcessSeasonImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles) private List<MetadataFile> ProcessAlbumImages(IMetadata consumer, Artist artist, List<MetadataFile> existingMetadataFiles)
{ {
var result = new List<MetadataFile>(); var result = new List<MetadataFile>();
foreach (var season in series.Seasons) var albums = _albumService.GetAlbumsByArtist(artist.Id);
foreach (var album in albums)
{ {
foreach (var image in consumer.SeasonImages(series, season)) foreach (var image in consumer.AlbumImages(artist, album))
{ {
var fullPath = Path.Combine(series.Path, image.RelativePath); var fullPath = Path.Combine(artist.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath)) if (_diskProvider.FileExists(fullPath))
{ {
_logger.Debug("Season image already exists: {0}", fullPath); _logger.Debug("Album image already exists: {0}", fullPath);
continue; continue;
} }
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeasonImage && var metadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.AlbumImage &&
c.SeasonNumber == season.SeasonNumber && c.AlbumId == album.Id &&
c.RelativePath == image.RelativePath) ?? c.RelativePath == image.RelativePath) ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
SeasonNumber = season.SeasonNumber, AlbumId = album.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.SeasonImage, Type = MetadataType.AlbumImage,
RelativePath = image.RelativePath, RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath) Extension = Path.GetExtension(fullPath)
}; };
DownloadImage(series, image); DownloadImage(artist, image);
result.Add(metadata); result.Add(metadata);
} }
@ -346,26 +401,26 @@ namespace NzbDrone.Core.Extras.Metadata
return result; return result;
} }
private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles) private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Artist artist, TrackFile trackFile, List<MetadataFile> existingMetadataFiles)
{ {
var result = new List<MetadataFile>(); var result = new List<MetadataFile>();
foreach (var image in consumer.EpisodeImages(series, episodeFile)) foreach (var image in consumer.TrackImages(artist, trackFile))
{ {
var fullPath = Path.Combine(series.Path, image.RelativePath); var fullPath = Path.Combine(artist.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath)) if (_diskProvider.FileExists(fullPath))
{ {
_logger.Debug("Episode image already exists: {0}", fullPath); _logger.Debug("Track image already exists: {0}", fullPath);
continue; continue;
} }
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage && var existingMetadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.TrackImage &&
c.EpisodeFileId == episodeFile.Id); c.TrackFileId == trackFile.Id);
if (existingMetadata != null) if (existingMetadata != null)
{ {
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); var existingFullPath = Path.Combine(artist.Path, existingMetadata.RelativePath);
if (fullPath.PathNotEquals(existingFullPath)) if (fullPath.PathNotEquals(existingFullPath))
{ {
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
@ -378,16 +433,16 @@ namespace NzbDrone.Core.Extras.Metadata
var metadata = existingMetadata ?? var metadata = existingMetadata ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
SeasonNumber = episodeFile.SeasonNumber, AlbumId = trackFile.AlbumId,
EpisodeFileId = episodeFile.Id, TrackFileId = trackFile.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeImage, Type = MetadataType.TrackImage,
RelativePath = image.RelativePath, RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath) Extension = Path.GetExtension(fullPath)
}; };
DownloadImage(series, image); DownloadImage(artist, image);
result.Add(metadata); result.Add(metadata);
} }
@ -395,9 +450,9 @@ namespace NzbDrone.Core.Extras.Metadata
return result; return result;
} }
private void DownloadImage(Series series, ImageFileResult image) private void DownloadImage(Artist artist, ImageFileResult image)
{ {
var fullPath = Path.Combine(series.Path, image.RelativePath); var fullPath = Path.Combine(artist.Path, image.RelativePath);
try try
{ {
@ -413,11 +468,11 @@ namespace NzbDrone.Core.Extras.Metadata
} }
catch (WebException ex) catch (WebException ex)
{ {
_logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message); _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, artist, ex.Message);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message); _logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, artist, ex.Message);
} }
} }
@ -427,7 +482,7 @@ namespace NzbDrone.Core.Extras.Metadata
_mediaFileAttributeService.SetFilePermissions(path); _mediaFileAttributeService.SetFilePermissions(path);
} }
private MetadataFile GetMetadataFile(Series series, List<MetadataFile> existingMetadataFiles, Func<MetadataFile, bool> predicate) private MetadataFile GetMetadataFile(Artist artist, List<MetadataFile> existingMetadataFiles, Func<MetadataFile, bool> predicate)
{ {
var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList(); var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList();
@ -439,7 +494,7 @@ namespace NzbDrone.Core.Extras.Metadata
//Remove duplicate metadata files from DB and disk //Remove duplicate metadata files from DB and disk
foreach (var file in matchingMetadataFiles.Skip(1)) foreach (var file in matchingMetadataFiles.Skip(1))
{ {
var path = Path.Combine(series.Path, file.RelativePath); var path = Path.Combine(artist.Path, file.RelativePath);
_logger.Debug("Removing duplicate Metadata file: {0}", path); _logger.Debug("Removing duplicate Metadata file: {0}", path);

@ -1,12 +1,13 @@
namespace NzbDrone.Core.Extras.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
public enum MetadataType public enum MetadataType
{ {
Unknown = 0, Unknown = 0,
SeriesMetadata = 1, ArtistMetadata = 1,
EpisodeMetadata = 2, TrackMetadata = 2,
SeriesImage = 3, ArtistImage = 3,
SeasonImage = 4, AlbumImage = 4,
EpisodeImage = 5 TrackImage = 5,
AlbumMetadata = 6
} }
} }

@ -1,11 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Others namespace NzbDrone.Core.Extras.Others
{ {
@ -27,12 +27,12 @@ namespace NzbDrone.Core.Extras.Others
public override int Order => 2; public override int Order => 2;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) public override IEnumerable<ExtraFile> ProcessFiles(Artist artist, List<string> filesOnDisk, List<string> importedFiles)
{ {
_logger.Debug("Looking for existing extra files in {0}", series.Path); _logger.Debug("Looking for existing extra files in {0}", artist.Path);
var extraFiles = new List<OtherExtraFile>(); var extraFiles = new List<OtherExtraFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles);
foreach (var possibleExtraFile in filterResult.FilesOnDisk) foreach (var possibleExtraFile in filterResult.FilesOnDisk)
{ {
@ -44,21 +44,21 @@ namespace NzbDrone.Core.Extras.Others
continue; continue;
} }
var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series); var localTrack = _parsingService.GetLocalTrack(possibleExtraFile, artist);
if (localEpisode == null) if (localTrack == null)
{ {
_logger.Debug("Unable to parse extra file: {0}", possibleExtraFile); _logger.Debug("Unable to parse extra file: {0}", possibleExtraFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) if (localTrack.Tracks.Empty())
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile); _logger.Debug("Cannot find related tracks for: {0}", possibleExtraFile);
continue; continue;
} }
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1)
{ {
_logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile); _logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile);
continue; continue;
@ -66,10 +66,10 @@ namespace NzbDrone.Core.Extras.Others
var extraFile = new OtherExtraFile var extraFile = new OtherExtraFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
SeasonNumber = localEpisode.SeasonNumber, AlbumId = localTrack.Album.Id,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, TrackFileId = localTrack.Tracks.First().TrackFileId,
RelativePath = series.Path.GetRelativePath(possibleExtraFile), RelativePath = artist.Path.GetRelativePath(possibleExtraFile),
Extension = extension Extension = extension
}; };

@ -1,8 +1,8 @@
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Others namespace NzbDrone.Core.Extras.Others
{ {
@ -12,8 +12,8 @@ namespace NzbDrone.Core.Extras.Others
public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService
{ {
public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger) : base(repository, artistService, diskProvider, recycleBinProvider, logger)
{ {
} }
} }

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -7,7 +7,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Others namespace NzbDrone.Core.Extras.Others
{ {
@ -27,33 +27,33 @@ namespace NzbDrone.Core.Extras.Others
public override int Order => 2; public override int Order => 2;
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> CreateAfterArtistScan(Artist artist, List<Album> albums, List<TrackFile> trackFiles)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) public override IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, TrackFile trackFile)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Artist artist, List<TrackFile> episodeFiles)
{ {
var extraFiles = _otherExtraFileService.GetFilesBySeries(series.Id); var extraFiles = _otherExtraFileService.GetFilesByArtist(artist.Id);
var movedFiles = new List<OtherExtraFile>(); var movedFiles = new List<OtherExtraFile>();
foreach (var episodeFile in episodeFiles) foreach (var episodeFile in episodeFiles)
{ {
var extraFilesForEpisodeFile = extraFiles.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); var extraFilesForEpisodeFile = extraFiles.Where(m => m.TrackFileId == episodeFile.Id).ToList();
foreach (var extraFile in extraFilesForEpisodeFile) foreach (var extraFile in extraFilesForEpisodeFile)
{ {
movedFiles.AddIfNotNull(MoveFile(series, episodeFile, extraFile)); movedFiles.AddIfNotNull(MoveFile(artist, episodeFile, extraFile));
} }
} }
@ -62,7 +62,7 @@ namespace NzbDrone.Core.Extras.Others
return movedFiles; return movedFiles;
} }
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly)
{ {
// If the extension is .nfo we need to change it to .nfo-orig // If the extension is .nfo we need to change it to .nfo-orig
if (Path.GetExtension(path).Equals(".nfo")) if (Path.GetExtension(path).Equals(".nfo"))
@ -70,7 +70,7 @@ namespace NzbDrone.Core.Extras.Others
extension += "-orig"; extension += "-orig";
} }
var extraFile = ImportFile(series, episodeFile, path, readOnly, extension, null); var extraFile = ImportFile(artist, trackFile, path, readOnly, extension, null);
_otherExtraFileService.Upsert(extraFile); _otherExtraFileService.Upsert(extraFile);

@ -1,11 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Subtitles namespace NzbDrone.Core.Extras.Subtitles
{ {
@ -27,12 +27,12 @@ namespace NzbDrone.Core.Extras.Subtitles
public override int Order => 1; public override int Order => 1;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) public override IEnumerable<ExtraFile> ProcessFiles(Artist artist, List<string> filesOnDisk, List<string> importedFiles)
{ {
_logger.Debug("Looking for existing subtitle files in {0}", series.Path); _logger.Debug("Looking for existing subtitle files in {0}", artist.Path);
var subtitleFiles = new List<SubtitleFile>(); var subtitleFiles = new List<SubtitleFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles);
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
{ {
@ -40,21 +40,21 @@ namespace NzbDrone.Core.Extras.Subtitles
if (SubtitleFileExtensions.Extensions.Contains(extension)) if (SubtitleFileExtensions.Extensions.Contains(extension))
{ {
var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series); var localTrack = _parsingService.GetLocalTrack(possibleSubtitleFile, artist);
if (localEpisode == null) if (localTrack == null)
{ {
_logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile); _logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) if (localTrack.Tracks.Empty())
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleSubtitleFile); _logger.Debug("Cannot find related tracks for: {0}", possibleSubtitleFile);
continue; continue;
} }
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1)
{ {
_logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile); _logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile);
continue; continue;
@ -62,10 +62,10 @@ namespace NzbDrone.Core.Extras.Subtitles
var subtitleFile = new SubtitleFile var subtitleFile = new SubtitleFile
{ {
SeriesId = series.Id, ArtistId = artist.Id,
SeasonNumber = localEpisode.SeasonNumber, AlbumId = localTrack.Album.Id,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, TrackFileId = localTrack.Tracks.First().TrackFileId,
RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), RelativePath = artist.Path.GetRelativePath(possibleSubtitleFile),
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile),
Extension = extension Extension = extension
}; };

@ -1,8 +1,8 @@
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Extras.Subtitles namespace NzbDrone.Core.Extras.Subtitles
{ {
@ -12,8 +12,8 @@ namespace NzbDrone.Core.Extras.Subtitles
public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService
{ {
public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger) : base(repository, artistService, diskProvider, recycleBinProvider, logger)
{ {
} }
} }

@ -9,7 +9,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Music;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Extras.Subtitles namespace NzbDrone.Core.Extras.Subtitles
@ -32,46 +32,46 @@ namespace NzbDrone.Core.Extras.Subtitles
public override int Order => 1; public override int Order => 1;
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> CreateAfterArtistScan(Artist artist, List<Album> albums, List<TrackFile> trackFiles)
{ {
return Enumerable.Empty<SubtitleFile>(); return Enumerable.Empty<SubtitleFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) public override IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, TrackFile trackFile)
{ {
return Enumerable.Empty<SubtitleFile>(); return Enumerable.Empty<SubtitleFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder)
{ {
return Enumerable.Empty<SubtitleFile>(); return Enumerable.Empty<SubtitleFile>();
} }
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Artist artist, List<TrackFile> trackFiles)
{ {
var subtitleFiles = _subtitleFileService.GetFilesBySeries(series.Id); var subtitleFiles = _subtitleFileService.GetFilesByArtist(artist.Id);
var movedFiles = new List<SubtitleFile>(); var movedFiles = new List<SubtitleFile>();
foreach (var episodeFile in episodeFiles) foreach (var trackFile in trackFiles)
{ {
var groupedExtraFilesForEpisodeFile = subtitleFiles.Where(m => m.EpisodeFileId == episodeFile.Id) var groupedExtraFilesForTrackFile = subtitleFiles.Where(m => m.TrackFileId == trackFile.Id)
.GroupBy(s => s.Language + s.Extension).ToList(); .GroupBy(s => s.Language + s.Extension).ToList();
foreach (var group in groupedExtraFilesForEpisodeFile) foreach (var group in groupedExtraFilesForTrackFile)
{ {
var groupCount = group.Count(); var groupCount = group.Count();
var copy = 1; var copy = 1;
if (groupCount > 1) if (groupCount > 1)
{ {
_logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath)); _logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(artist.Path, trackFile.RelativePath));
} }
foreach (var subtitleFile in group) foreach (var subtitleFile in group)
{ {
var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1); var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1);
movedFiles.AddIfNotNull(MoveFile(series, episodeFile, subtitleFile, suffix)); movedFiles.AddIfNotNull(MoveFile(artist, trackFile, subtitleFile, suffix));
copy++; copy++;
} }
@ -83,13 +83,13 @@ namespace NzbDrone.Core.Extras.Subtitles
return movedFiles; return movedFiles;
} }
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly)
{ {
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path))) if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
{ {
var language = LanguageParser.ParseSubtitleLanguage(path); var language = LanguageParser.ParseSubtitleLanguage(path);
var suffix = GetSuffix(language, 1, false); var suffix = GetSuffix(language, 1, false);
var subtitleFile = ImportFile(series, episodeFile, path, readOnly, extension, suffix); var subtitleFile = ImportFile(artist, trackFile, path, readOnly, extension, suffix);
subtitleFile.Language = language; subtitleFile.Language = language;
_subtitleFileService.Upsert(subtitleFile); _subtitleFileService.Upsert(subtitleFile);

@ -1,4 +1,4 @@
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
@ -13,12 +13,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean() public void Clean()
{ {
DeleteDuplicateSeriesMetadata(); DeleteDuplicateArtistMetadata();
DeleteDuplicateEpisodeMetadata(); DeleteDuplicateTrackMetadata();
DeleteDuplicateEpisodeImages(); DeleteDuplicateTrackImages();
} }
private void DeleteDuplicateSeriesMetadata() private void DeleteDuplicateArtistMetadata()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
@ -26,12 +26,25 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type = 1 WHERE Type = 1
GROUP BY SeriesId, Consumer GROUP BY ArtistId, Consumer
HAVING COUNT(SeriesId) > 1 HAVING COUNT(ArtistId) > 1
)"); )");
} }
private void DeleteDuplicateEpisodeMetadata() private void DeleteDuplicateAlbumMetadata()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type = 6
GROUP BY AlbumId, Consumer
HAVING COUNT(AlbumId) > 1
)");
}
private void DeleteDuplicateTrackMetadata()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
@ -39,12 +52,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type = 2 WHERE Type = 2
GROUP BY EpisodeFileId, Consumer GROUP BY TrackFileId, Consumer
HAVING COUNT(EpisodeFileId) > 1 HAVING COUNT(TrackFileId) > 1
)"); )");
} }
private void DeleteDuplicateEpisodeImages() private void DeleteDuplicateTrackImages()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
@ -52,8 +65,8 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type = 5 WHERE Type = 5
GROUP BY EpisodeFileId, Consumer GROUP BY TrackFileId, Consumer
HAVING COUNT(EpisodeFileId) > 1 HAVING COUNT(TrackFileId) > 1
)"); )");
} }
} }

@ -1,4 +1,4 @@
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
@ -13,37 +13,51 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean() public void Clean()
{ {
DeleteOrphanedBySeries(); DeleteOrphanedByArtist();
DeleteOrphanedByEpisodeFile(); DeleteOrphanedByAlbum();
DeleteWhereEpisodeFileIsZero(); DeleteOrphanedByTrackFile();
DeleteWhereTrackFileIsZero();
} }
private void DeleteOrphanedBySeries() private void DeleteOrphanedByArtist()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN ( WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN Series LEFT OUTER JOIN Artists
ON MetadataFiles.SeriesId = Series.Id ON MetadataFiles.ArtistId = Artists.Id
WHERE Series.Id IS NULL)"); WHERE Artists.Id IS NULL)");
} }
private void DeleteOrphanedByEpisodeFile() private void DeleteOrphanedByAlbum()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN ( WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN EpisodeFiles LEFT OUTER JOIN Albums
ON MetadataFiles.EpisodeFileId = EpisodeFiles.Id ON MetadataFiles.AlbumId = Albums.Id
WHERE MetadataFiles.EpisodeFileId > 0 WHERE MetadataFiles.AlbumId > 0
AND EpisodeFiles.Id IS NULL)"); AND Albums.Id IS NULL)");
} }
private void DeleteWhereEpisodeFileIsZero() private void DeleteOrphanedByTrackFile()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN TrackFiles
ON MetadataFiles.TrackFileId = TrackFiles.Id
WHERE MetadataFiles.TrackFileId > 0
AND TrackFiles.Id IS NULL)");
}
private void DeleteWhereTrackFileIsZero()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
@ -51,7 +65,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type IN (2, 5) WHERE Type IN (2, 5)
AND EpisodeFileId = 0)"); AND TrackFileId = 0)");
} }
} }
} }

@ -38,7 +38,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
foreach (var artist in artists) foreach (var artist in artists)
{ {
var images = _metaFileService.GetFilesBySeries(artist.Id) var images = _metaFileService.GetFilesByArtist(artist.Id)
.Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)); .Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase));
foreach (var image in images) foreach (var image in images)

@ -252,6 +252,7 @@
<Compile Include="Datastore\Migration\068_add_release_restrictions.cs" /> <Compile Include="Datastore\Migration\068_add_release_restrictions.cs" />
<Compile Include="Datastore\Migration\069_quality_proper.cs" /> <Compile Include="Datastore\Migration\069_quality_proper.cs" />
<Compile Include="Datastore\Migration\070_delay_profile.cs" /> <Compile Include="Datastore\Migration\070_delay_profile.cs" />
<Compile Include="Datastore\Migration\123_music_extras.cs" />
<Compile Include="Datastore\Migration\121_update_types_existing_artist.cs" /> <Compile Include="Datastore\Migration\121_update_types_existing_artist.cs" />
<Compile Include="Datastore\Migration\102_add_language_to_episodeFiles_history_and_blacklist.cs" /> <Compile Include="Datastore\Migration\102_add_language_to_episodeFiles_history_and_blacklist.cs" />
<Compile Include="Datastore\Migration\110_fix_extra_files_config.cs" /> <Compile Include="Datastore\Migration\110_fix_extra_files_config.cs" />

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Organizer namespace NzbDrone.Core.Organizer
@ -10,8 +9,6 @@ namespace NzbDrone.Core.Organizer
public string FileName { get; set; } public string FileName { get; set; }
public Artist Artist { get; set; } public Artist Artist { get; set; }
public Album Album { get; set; } public Album Album { get; set; }
public List<Episode> Episodes { get; set; }
public EpisodeFile EpisodeFile { get; set; }
public List<Track> Tracks { get; set; } public List<Track> Tracks { get; set; }
public TrackFile TrackFile { get; set; } public TrackFile TrackFile { get; set; }
} }

@ -4,7 +4,6 @@ using NLog;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
namespace NzbDrone.Core.Profiles.Qualities namespace NzbDrone.Core.Profiles.Qualities

Loading…
Cancel
Save