diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index 0b72e0b0c..d2f05539a 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -35,10 +35,13 @@ namespace NzbDrone.Api.Config SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5); SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); + SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); + SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -74,6 +77,7 @@ namespace NzbDrone.Api.Config var sampleResource = new NamingSampleResource(); var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); @@ -83,6 +87,10 @@ namespace NzbDrone.Api.Config ? "Invalid format" : singleEpisodeSampleResult.FileName; + sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null + ? "Invalid format" + : singleTrackSampleResult.FileName; + sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null ? "Invalid format" : multiEpisodeSampleResult.FileName; @@ -107,18 +115,28 @@ namespace NzbDrone.Api.Config ? "Invalid format" : _filenameSampleService.GetSeasonFolderSample(nameSpec); + sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace() + ? "Invalid format" + : _filenameSampleService.GetArtistFolderSample(nameSpec); + + sampleResource.AlbumFolderExample = nameSpec.AlbumFolderFormat.IsNullOrWhiteSpace() + ? "Invalid format" + : _filenameSampleService.GetAlbumFolderSample(nameSpec); + return sampleResource.AsResponse(); } private void ValidateFormatResult(NamingConfig nameSpec) { var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); var singleEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult); + var singleTrackValidationResult = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult); var multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult); var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult); var animeEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult); @@ -127,6 +145,7 @@ namespace NzbDrone.Api.Config var validationFailures = new List(); validationFailures.AddIfNotNull(singleEpisodeValidationResult); + validationFailures.AddIfNotNull(singleTrackValidationResult); validationFailures.AddIfNotNull(multiEpisodeValidationResult); validationFailures.AddIfNotNull(dailyEpisodeValidationResult); validationFailures.AddIfNotNull(animeEpisodeValidationResult); diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index 39147b993..d49c39936 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -6,13 +6,17 @@ namespace NzbDrone.Api.Config public class NamingConfigResource : RestResource { public bool RenameEpisodes { get; set; } + public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } + public string StandardTrackFormat { get; set; } public string DailyEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; } public string SeriesFolderFormat { get; set; } public string SeasonFolderFormat { get; set; } + public string ArtistFolderFormat { get; set; } + public string AlbumFolderFormat { get; set; } public bool IncludeSeriesTitle { get; set; } public bool IncludeEpisodeTitle { get; set; } public bool IncludeQuality { get; set; } @@ -30,13 +34,17 @@ namespace NzbDrone.Api.Config Id = model.Id, RenameEpisodes = model.RenameEpisodes, + RenameTracks = model.RenameTracks, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, MultiEpisodeStyle = model.MultiEpisodeStyle, StandardEpisodeFormat = model.StandardEpisodeFormat, + StandardTrackFormat = model.StandardTrackFormat, DailyEpisodeFormat = model.DailyEpisodeFormat, AnimeEpisodeFormat = model.AnimeEpisodeFormat, SeriesFolderFormat = model.SeriesFolderFormat, - SeasonFolderFormat = model.SeasonFolderFormat + SeasonFolderFormat = model.SeasonFolderFormat, + ArtistFolderFormat = model.ArtistFolderFormat, + AlbumFolderFormat = model.AlbumFolderFormat //IncludeSeriesTitle //IncludeEpisodeTitle //IncludeQuality @@ -63,13 +71,17 @@ namespace NzbDrone.Api.Config Id = resource.Id, RenameEpisodes = resource.RenameEpisodes, + RenameTracks = resource.RenameTracks, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, MultiEpisodeStyle = resource.MultiEpisodeStyle, StandardEpisodeFormat = resource.StandardEpisodeFormat, + StandardTrackFormat = resource.StandardTrackFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat, SeriesFolderFormat = resource.SeriesFolderFormat, - SeasonFolderFormat = resource.SeasonFolderFormat + SeasonFolderFormat = resource.SeasonFolderFormat, + ArtistFolderFormat = resource.ArtistFolderFormat, + AlbumFolderFormat = resource.AlbumFolderFormat }; } } diff --git a/src/NzbDrone.Api/Config/NamingSampleResource.cs b/src/NzbDrone.Api/Config/NamingSampleResource.cs index 1f9c7f066..f6d6d15b3 100644 --- a/src/NzbDrone.Api/Config/NamingSampleResource.cs +++ b/src/NzbDrone.Api/Config/NamingSampleResource.cs @@ -3,11 +3,14 @@ public class NamingSampleResource { public string SingleEpisodeExample { get; set; } + public string SingleTrackExample { get; set; } public string MultiEpisodeExample { get; set; } public string DailyEpisodeExample { get; set; } public string AnimeEpisodeExample { get; set; } public string AnimeMultiEpisodeExample { get; set; } public string SeriesFolderExample { get; set; } public string SeasonFolderExample { get; set; } + public string ArtistFolderExample { get; set; } + public string AlbumFolderExample { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs index c2044bda3..d89a0068c 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; using NzbDrone.SignalR; +using System; namespace NzbDrone.Api.EpisodeFiles { @@ -47,24 +48,26 @@ namespace NzbDrone.Api.EpisodeFiles private EpisodeFileResource GetEpisodeFile(int id) { - var episodeFile = _mediaFileService.Get(id); - var series = _seriesService.GetSeries(episodeFile.SeriesId); + throw new NotImplementedException(); + //var episodeFile = _mediaFileService.Get(id); + //var series = _seriesService.GetSeries(episodeFile.SeriesId); - return episodeFile.ToResource(series, _qualityUpgradableSpecification); + //return episodeFile.ToResource(series, _qualityUpgradableSpecification); } private List GetEpisodeFiles() { - if (!Request.Query.SeriesId.HasValue) - { - throw new BadRequestException("seriesId is missing"); - } + throw new NotImplementedException(); + //if (!Request.Query.SeriesId.HasValue) + //{ + // throw new BadRequestException("seriesId is missing"); + //} - var seriesId = (int)Request.Query.SeriesId; + //var seriesId = (int)Request.Query.SeriesId; - var series = _seriesService.GetSeries(seriesId); + //var series = _seriesService.GetSeries(seriesId); - return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); + //return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); } private void SetQuality(EpisodeFileResource episodeFileResource) @@ -76,14 +79,15 @@ namespace NzbDrone.Api.EpisodeFiles private void DeleteEpisodeFile(int id) { - var episodeFile = _mediaFileService.Get(id); - var series = _seriesService.GetSeries(episodeFile.SeriesId); - var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); - var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); + throw new NotImplementedException(); + //var episodeFile = _mediaFileService.Get(id); + //var series = _seriesService.GetSeries(episodeFile.SeriesId); + //var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + //var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); - _logger.Info("Deleting episode file: {0}", fullPath); - _recycleBinProvider.DeleteFile(fullPath, subfolder); - _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); + //_logger.Info("Deleting episode file: {0}", fullPath); + //_recycleBinProvider.DeleteFile(fullPath, subfolder); + //_mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); } public void Handle(EpisodeFileAddedEvent message) diff --git a/src/NzbDrone.Api/Music/AlbumResource.cs b/src/NzbDrone.Api/Music/AlbumResource.cs index d3e243c66..5ae4b2efa 100644 --- a/src/NzbDrone.Api/Music/AlbumResource.cs +++ b/src/NzbDrone.Api/Music/AlbumResource.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Api.Music public string AlbumId { get; set; } public string AlbumName { get; set; } public bool Monitored { get; set; } - public int Year { get; set; } + public DateTime ReleaseDate { get; set; } public List Genres { get; set; } public string ArtworkUrl { get; set; } @@ -25,12 +25,12 @@ namespace NzbDrone.Api.Music return new AlbumResource { - AlbumId = model.AlbumId, + AlbumId = model.ForeignAlbumId, Monitored = model.Monitored, - Year = model.Year, + ReleaseDate = model.ReleaseDate, AlbumName = model.Title, Genres = model.Genres, - ArtworkUrl = model.ArtworkUrl + //ArtworkUrl = model.ArtworkUrl }; } @@ -40,12 +40,12 @@ namespace NzbDrone.Api.Music return new Album { - AlbumId = resource.AlbumId, + ForeignAlbumId = resource.AlbumId, Monitored = resource.Monitored, - Year = resource.Year, + ReleaseDate = resource.ReleaseDate, Title = resource.AlbumName, Genres = resource.Genres, - ArtworkUrl = resource.ArtworkUrl + //ArtworkUrl = resource.ArtworkUrl }; } diff --git a/src/NzbDrone.Api/Music/ArtistModule.cs b/src/NzbDrone.Api/Music/ArtistModule.cs index d7ef5fed3..4ee6898b9 100644 --- a/src/NzbDrone.Api/Music/ArtistModule.cs +++ b/src/NzbDrone.Api/Music/ArtistModule.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Api.Music PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.SpotifyId).NotEqual("").SetValidator(artistExistsValidator); + PostValidator.RuleFor(s => s.ForeignArtistId).NotEqual("").SetValidator(artistExistsValidator); PutValidator.RuleFor(s => s.Path).IsValidPath(); } @@ -144,14 +144,14 @@ namespace NzbDrone.Api.Music public void Handle(TrackImportedEvent message) { - BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.ItunesTrackId); + BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.Id); // TODO: Ensure we can pass DB ID instead of Metadata ID (SpotifyID) } public void Handle(TrackFileDeletedEvent message) { if (message.Reason == DeleteMediaFileReason.Upgrade) return; - BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ItunesTrackId); + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); // TODO: Ensure we can pass DB ID instead of Metadata ID (SpotifyID) } public void Handle(ArtistUpdatedEvent message) diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs index 71cc14b85..4405c0d38 100644 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -18,8 +18,12 @@ namespace NzbDrone.Api.Music //View Only - public string ArtistName { get; set; } - public string SpotifyId { get; set; } + public string Name { get; set; } + public string ForeignArtistId { get; set; } + public string MBId { get; set; } + public int TADBId { get; set; } + public int DiscogsId { get; set; } + public string AMId { get; set; } public string Overview { get; set; } public int AlbumCount @@ -53,13 +57,13 @@ namespace NzbDrone.Api.Music public bool Monitored { get; set; } public string RootFolderPath { get; set; } - public string Certification { get; set; } + //public string Certification { get; set; } public List Genres { get; set; } public HashSet Tags { get; set; } public DateTime Added { get; set; } public AddSeriesOptions AddOptions { get; set; } public Ratings Ratings { get; set; } - public string ArtistSlug { get; internal set; } + public string NameSlug { get; set; } } public static class ArtistResourceMapper @@ -71,8 +75,11 @@ namespace NzbDrone.Api.Music return new ArtistResource { Id = model.Id, - - ArtistName = model.ArtistName, + MBId = model.MBId, + TADBId = model.TADBId, + DiscogsId = model.DiscogsId, + AMId = model.AMId, + Name = model.Name, //AlternateTitles //SortTitle = resource.SortTitle, @@ -94,7 +101,6 @@ namespace NzbDrone.Api.Music Path = model.Path, ProfileId = model.ProfileId, - ArtistFolder = model.ArtistFolder, Monitored = model.Monitored, //UseSceneNumbering = resource.UseSceneNumbering, @@ -105,8 +111,8 @@ namespace NzbDrone.Api.Music //FirstAired = resource.FirstAired, //LastInfoSync = resource.LastInfoSync, //SeriesType = resource.SeriesType, - SpotifyId = model.SpotifyId, - ArtistSlug = model.ArtistSlug, + ForeignArtistId = model.ForeignArtistId, + NameSlug = model.NameSlug, RootFolderPath = model.RootFolderPath, Genres = model.Genres, @@ -125,10 +131,13 @@ namespace NzbDrone.Api.Music { Id = resource.Id, - ArtistName = resource.ArtistName, + Name = resource.Name, //AlternateTitles //SortTitle = resource.SortTitle, - + MBId = resource.MBId, + TADBId = resource.TADBId, + DiscogsId = resource.DiscogsId, + AMId = resource.AMId, //TotalEpisodeCount //EpisodeCount //EpisodeFileCount @@ -147,11 +156,10 @@ namespace NzbDrone.Api.Music Path = resource.Path, ProfileId = resource.ProfileId, - ArtistFolder = resource.ArtistFolder, Monitored = resource.Monitored, //LastInfoSync = resource.LastInfoSync, - SpotifyId = resource.SpotifyId, - ArtistSlug = resource.ArtistSlug, + ForeignArtistId = resource.ForeignArtistId, + NameSlug = resource.NameSlug, RootFolderPath = resource.RootFolderPath, Genres = resource.Genres, diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 80572e068..877563c2a 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -104,6 +104,13 @@ + + + + + + + diff --git a/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs b/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs new file mode 100644 index 000000000..1f679ac5d --- /dev/null +++ b/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Api.REST; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.SignalR; +using System; + +namespace NzbDrone.Api.TrackFiles +{ + public class TrackFileModule : NzbDroneRestModuleWithSignalR, + IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IDiskProvider _diskProvider; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly ISeriesService _seriesService; + private readonly IArtistService _artistService; + private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly Logger _logger; + + public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IDiskProvider diskProvider, + IRecycleBinProvider recycleBinProvider, + ISeriesService seriesService, + IArtistService artistService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + Logger logger) + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _diskProvider = diskProvider; + _recycleBinProvider = recycleBinProvider; + _seriesService = seriesService; + _artistService = artistService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + _logger = logger; + GetResourceById = GetTrackFile; + GetResourceAll = GetTrackFiles; + UpdateResource = SetQuality; + DeleteResource = DeleteTrackFile; + } + + private TrackFileResource GetTrackFile(int id) + { + throw new NotImplementedException(); + //var episodeFile = _mediaFileService.Get(id); + //var series = _seriesService.GetSeries(episodeFile.SeriesId); + + //return episodeFile.ToResource(series, _qualityUpgradableSpecification); + } + + private List GetTrackFiles() + { + if (!Request.Query.ArtistId.HasValue) + { + throw new BadRequestException("artistId is missing"); + } + + var artistId = (int)Request.Query.ArtistId; + + var artist = _artistService.GetArtist(artistId); + + return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _qualityUpgradableSpecification)); + } + + private void SetQuality(TrackFileResource trackFileResource) + { + var trackFile = _mediaFileService.Get(trackFileResource.Id); + trackFile.Quality = trackFileResource.Quality; + _mediaFileService.Update(trackFile); + } + + private void DeleteTrackFile(int id) + { + throw new NotImplementedException(); + //var episodeFile = _mediaFileService.Get(id); + //var series = _seriesService.GetSeries(episodeFile.SeriesId); + //var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + //var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); + + //_logger.Info("Deleting episode file: {0}", fullPath); + //_recycleBinProvider.DeleteFile(fullPath, subfolder); + //_mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); + } + + public void Handle(TrackFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs b/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs new file mode 100644 index 000000000..4c75dc429 --- /dev/null +++ b/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.TrackFiles +{ + public class TrackFileResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + //public string SceneName { get; set; } + public QualityModel Quality { get; set; } + + public bool QualityCutoffNotMet { get; set; } + } + + public static class TrackFileResourceMapper + { + private static TrackFileResource ToResource(this Core.MediaFiles.TrackFile model) + { + if (model == null) return null; + + return new TrackFileResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + RelativePath = model.RelativePath, + //Path + Size = model.Size, + DateAdded = model.DateAdded, + //SceneName = model.SceneName, + Quality = model.Quality, + //QualityCutoffNotMet + }; + } + + public static TrackFileResource ToResource(this Core.MediaFiles.TrackFile model, Core.Music.Artist artist, Core.DecisionEngine.IQualityUpgradableSpecification qualityUpgradableSpecification) + { + if (model == null) return null; + + return new TrackFileResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + RelativePath = model.RelativePath, + Path = Path.Combine(artist.Path, model.RelativePath), + Size = model.Size, + DateAdded = model.DateAdded, + //SceneName = model.SceneName, + Quality = model.Quality, + QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(artist.Profile.Value, model.Quality) + }; + } + } +} diff --git a/src/NzbDrone.Api/Tracks/RenameTrackModule.cs b/src/NzbDrone.Api/Tracks/RenameTrackModule.cs new file mode 100644 index 000000000..9467ec43e --- /dev/null +++ b/src/NzbDrone.Api/Tracks/RenameTrackModule.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Api.Tracks +{ + public class RenameTrackModule : NzbDroneRestModule + { + private readonly IRenameTrackFileService _renameTrackFileService; + + public RenameTrackModule(IRenameTrackFileService renameTrackFileService) + : base("rename") + { + _renameTrackFileService = renameTrackFileService; + + GetResourceAll = GetTracks; + } + + private List GetTracks() + { + if (!Request.Query.ArtistId.HasValue) + { + throw new BadRequestException("artistId is missing"); + } + + var artistId = (int)Request.Query.ArtistId; + + if (Request.Query.AlbumId.HasValue) + { + var albumId = (int)Request.Query.AlbumId; + return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource(); + } + + return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); + } + } +} diff --git a/src/NzbDrone.Api/Tracks/RenameTrackResource.cs b/src/NzbDrone.Api/Tracks/RenameTrackResource.cs new file mode 100644 index 000000000..12f67bb60 --- /dev/null +++ b/src/NzbDrone.Api/Tracks/RenameTrackResource.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Tracks +{ + public class RenameTrackResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public List TrackNumbers { get; set; } + public int TrackFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } + + public static class RenameTrackResourceMapper + { + public static RenameTrackResource ToResource(this Core.MediaFiles.RenameTrackFilePreview model) + { + if (model == null) return null; + + return new RenameTrackResource + { + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + TrackNumbers = model.TrackNumbers.ToList(), + TrackFileId = model.TrackFileId, + ExistingPath = model.ExistingPath, + NewPath = model.NewPath + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/Tracks/TrackModule.cs b/src/NzbDrone.Api/Tracks/TrackModule.cs new file mode 100644 index 000000000..fa3222c51 --- /dev/null +++ b/src/NzbDrone.Api/Tracks/TrackModule.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using NzbDrone.Api.REST; +using NzbDrone.Core.Music; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.SignalR; + +namespace NzbDrone.Api.Tracks +{ + public class TrackModule : TrackModuleWithSignalR + { + public TrackModule(IArtistService artistService, + ITrackService trackService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(trackService, artistService, qualityUpgradableSpecification, signalRBroadcaster) + { + GetResourceAll = GetTracks; + UpdateResource = SetMonitored; + } + + private List GetTracks() + { + if (!Request.Query.ArtistId.HasValue) + { + throw new BadRequestException("artistId is missing"); + } + + var artistId = (int)Request.Query.ArtistId; + + var resources = MapToResource(_trackService.GetTracksByArtist(artistId), false, true); + + return resources; + } + + private void SetMonitored(TrackResource trackResource) + { + _trackService.SetTrackMonitored(trackResource.Id, trackResource.Monitored); + } + } +} diff --git a/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs b/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs new file mode 100644 index 000000000..f5926768e --- /dev/null +++ b/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Api.TrackFiles; +using NzbDrone.Api.Music; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; + +namespace NzbDrone.Api.Tracks +{ + public abstract class TrackModuleWithSignalR : NzbDroneRestModuleWithSignalR, + IHandle + { + protected readonly ITrackService _trackService; + protected readonly IArtistService _artistService; + protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + + protected TrackModuleWithSignalR(ITrackService trackService, + IArtistService artistService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) + { + _trackService = trackService; + _artistService = artistService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetTrack; + } + + protected TrackModuleWithSignalR(ITrackService trackService, + IArtistService artistService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster, + string resource) + : base(signalRBroadcaster, resource) + { + _trackService = trackService; + _artistService = artistService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetTrack; + } + + protected TrackResource GetTrack(int id) + { + var track = _trackService.GetTrack(id); + var resource = MapToResource(track, true, true); + return resource; + } + + protected TrackResource MapToResource(Track track, bool includeArtist, bool includeTrackFile) + { + var resource = track.ToResource(); + + if (includeArtist || includeTrackFile) + { + var artist = track.Artist ?? _artistService.GetArtist(track.ArtistId); + + if (includeArtist) + { + resource.Artist = artist.ToResource(); + } + if (includeTrackFile && track.TrackFileId != 0) + { + resource.TrackFile = track.TrackFile.Value.ToResource(artist, _qualityUpgradableSpecification); + } + } + + return resource; + } + + protected List MapToResource(List tracks, bool includeArtist, bool includeTrackFile) + { + var result = tracks.ToResource(); + + if (includeArtist || includeTrackFile) + { + var artistDict = new Dictionary(); + for (var i = 0; i < tracks.Count; i++) + { + var track = tracks[i]; + var resource = result[i]; + + var artist = track.Artist ?? artistDict.GetValueOrDefault(tracks[i].ArtistId) ?? _artistService.GetArtist(tracks[i].ArtistId); + artistDict[artist.Id] = artist; + + if (includeArtist) + { + resource.Artist = artist.ToResource(); + } + if (includeTrackFile && tracks[i].TrackFileId != 0) + { + resource.TrackFile = tracks[i].TrackFile.Value.ToResource(artist, _qualityUpgradableSpecification); + } + } + } + + return result; + } + + //public void Handle(TrackGrabbedEvent message) + //{ + // foreach (var track in message.Track.Tracks) + // { + // var resource = track.ToResource(); + // resource.Grabbed = true; + + // BroadcastResourceChange(ModelAction.Updated, resource); + // } + //} + + public void Handle(TrackDownloadedEvent message) + { + foreach (var track in message.Track.Tracks) + { + BroadcastResourceChange(ModelAction.Updated, track.Id); + } + } + } +} diff --git a/src/NzbDrone.Api/Tracks/TrackResource.cs b/src/NzbDrone.Api/Tracks/TrackResource.cs new file mode 100644 index 000000000..f12a9948c --- /dev/null +++ b/src/NzbDrone.Api/Tracks/TrackResource.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Api.TrackFiles; +using NzbDrone.Api.REST; +using NzbDrone.Api.Music; +using NzbDrone.Core.Music; + +namespace NzbDrone.Api.Tracks +{ + public class TrackResource : RestResource + { + public int ArtistId { get; set; } + public int TrackFileId { get; set; } + public int AlbumId { get; set; } + //public int EpisodeNumber { get; set; } + public string Title { get; set; } + //public string AirDate { get; set; } + //public DateTime? AirDateUtc { get; set; } + //public string Overview { get; set; } + public TrackFileResource TrackFile { get; set; } + + public bool HasFile { get; set; } + public bool Monitored { get; set; } + //public int? AbsoluteEpisodeNumber { get; set; } + //public int? SceneAbsoluteEpisodeNumber { get; set; } + //public int? SceneEpisodeNumber { get; set; } + //public int? SceneSeasonNumber { get; set; } + //public bool UnverifiedSceneNumbering { get; set; } + //public string SeriesTitle { get; set; } + public ArtistResource Artist { get; set; } + + //Hiding this so people don't think its usable (only used to set the initial state) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Grabbed { get; set; } + } + + public static class TrackResourceMapper + { + public static TrackResource ToResource(this Track model) + { + if (model == null) return null; + + return new TrackResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + TrackFileId = model.TrackFileId, + AlbumId = model.AlbumId, + //EpisodeNumber = model.EpisodeNumber, + Title = model.Title, + //AirDate = model.AirDate, + //AirDateUtc = model.AirDateUtc, + //Overview = model.Overview, + //EpisodeFile + + HasFile = model.HasFile, + Monitored = model.Monitored, + //AbsoluteEpisodeNumber = model.AbsoluteEpisodeNumber, + //SceneAbsoluteEpisodeNumber = model.SceneAbsoluteEpisodeNumber, + //SceneEpisodeNumber = model.SceneEpisodeNumber, + //SceneSeasonNumber = model.SceneSeasonNumber, + //UnverifiedSceneNumbering = model.UnverifiedSceneNumbering, + //SeriesTitle = model.SeriesTitle, + //Series = model.Series.MapToResource(), + }; + } + + public static List ToResource(this IEnumerable models) + { + if (models == null) return null; + + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs index ca8385242..c53cb4f39 100644 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs @@ -17,14 +17,9 @@ namespace NzbDrone.Common.Cloud Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/") .CreateFactory(); - //Search = new HttpRequestBuilder("https://api.spotify.com/{version}/{route}/") // TODO: maybe use {version} - // .SetSegment("version", "v1") - // .CreateFactory(); - Search = new HttpRequestBuilder("http://localhost:5000/{route}/") // TODO: maybe use {version} + Search = new HttpRequestBuilder("http://localhost:3000/{route}/") // TODO: Add {version} once LidarrAPI.Metadata is released. .CreateFactory(); - InternalSearch = new HttpRequestBuilder("https://itunes.apple.com/WebObjects/MZStore.woa/wa/{route}") //viewArtist or search - .CreateFactory(); SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.tv/v1/tvdb/{route}/{language}/") .SetSegment("language", "en") diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 12b0c2d0b..117412062 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -12,64 +12,94 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Create.TableForModel("Artist") - .WithColumn("SpotifyId").AsString().Nullable().Unique() - .WithColumn("ArtistName").AsString().Unique() - .WithColumn("ArtistSlug").AsString().Nullable() //.Unique() - .WithColumn("CleanTitle").AsString().Nullable() // Do we need this? - .WithColumn("Monitored").AsBoolean() + Create.TableForModel("Artists") + .WithColumn("ForeignArtistId").AsString().Unique() + .WithColumn("MBId").AsString().Nullable() + .WithColumn("AMId").AsString().Nullable() + .WithColumn("TADBId").AsInt32().Nullable() + .WithColumn("DiscogsId").AsInt32().Nullable() + .WithColumn("Name").AsString() + .WithColumn("NameSlug").AsString().Nullable().Unique() + .WithColumn("CleanName").AsString().Indexed() + .WithColumn("Status").AsInt32() .WithColumn("Overview").AsString().Nullable() - .WithColumn("AlbumFolder").AsBoolean().Nullable() - .WithColumn("ArtistFolder").AsBoolean().Nullable() + .WithColumn("Images").AsString() + .WithColumn("Path").AsString().Indexed() + .WithColumn("Monitored").AsBoolean() + .WithColumn("AlbumFolder").AsBoolean() .WithColumn("LastInfoSync").AsDateTime().Nullable() .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("Status").AsInt32().Nullable() - .WithColumn("Path").AsString() - .WithColumn("Images").AsString().Nullable() - .WithColumn("QualityProfileId").AsInt32().Nullable() - .WithColumn("RootFolderPath").AsString().Nullable() - .WithColumn("Added").AsDateTime().Nullable() - .WithColumn("ProfileId").AsInt32().Nullable() // This is either ProfileId or Profile + .WithColumn("DateFormed").AsDateTime().Nullable() + .WithColumn("Members").AsString().Nullable() + .WithColumn("Ratings").AsString().Nullable() .WithColumn("Genres").AsString().Nullable() - .WithColumn("Albums").AsString().Nullable() + .WithColumn("SortName").AsString().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() .WithColumn("Tags").AsString().Nullable() - .WithColumn("AddOptions").AsString().Nullable() - ; + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("AddOptions").AsString().Nullable(); Create.TableForModel("Albums") - .WithColumn("AlbumId").AsString().Unique() - .WithColumn("ArtistId").AsInt32() // Should this be artistId (string) + .WithColumn("ForeignAlbumId").AsString().Unique() + .WithColumn("ArtistId").AsInt32() + .WithColumn("MBId").AsString().Nullable().Indexed() + .WithColumn("AMId").AsString().Nullable() + .WithColumn("TADBId").AsInt32().Nullable().Indexed() + .WithColumn("DiscogsId").AsInt32().Nullable() .WithColumn("Title").AsString() - .WithColumn("Year").AsInt32() - .WithColumn("Image").AsInt32() - .WithColumn("TrackCount").AsInt32() - .WithColumn("DiscCount").AsInt32() + .WithColumn("TitleSlug").AsString().Nullable().Unique() + .WithColumn("CleanTitle").AsString().Indexed() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Images").AsString() + .WithColumn("Path").AsString().Indexed() .WithColumn("Monitored").AsBoolean() - .WithColumn("Overview").AsString(); + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Label").AsString().Nullable() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("AlbumType").AsString() + .WithColumn("AddOptions").AsString().Nullable(); Create.TableForModel("Tracks") - .WithColumn("SpotifyTrackId").AsString().Nullable() // This shouldn't be nullable, but TrackRepository won't behave. Someone please fix this. - .WithColumn("AlbumId").AsString() - .WithColumn("ArtistId").AsString() // This may be a list of Ids in future for compilations - .WithColumn("ArtistSpotifyId").AsString() - .WithColumn("Compilation").AsBoolean() + .WithColumn("ForeignTrackId").AsString().Unique() + .WithColumn("ArtistId").AsInt32().Indexed() + .WithColumn("AlbumId").AsInt32() + .WithColumn("MBId").AsString().Nullable().Indexed() .WithColumn("TrackNumber").AsInt32() .WithColumn("Title").AsString().Nullable() - .WithColumn("Ignored").AsBoolean().Nullable() - .WithColumn("Explict").AsBoolean() + .WithColumn("Explicit").AsBoolean() + .WithColumn("Compilation").AsBoolean() + .WithColumn("DiscNumber").AsInt32().Nullable() + .WithColumn("TrackFileId").AsInt32().Nullable().Indexed() .WithColumn("Monitored").AsBoolean() - .WithColumn("TrackFileId").AsInt32().Nullable() - .WithColumn("ReleaseDate").AsDateTime().Nullable(); + .WithColumn("Ratings").AsString().Nullable(); + Create.Index().OnTable("Tracks").OnColumn("ArtistId").Ascending() + .OnColumn("AlbumId").Ascending() + .OnColumn("TrackNumber").Ascending(); Create.TableForModel("TrackFiles") - .WithColumn("ArtistId").AsInt32() - .WithColumn("Path").AsString().Unique() + .WithColumn("ArtistId").AsInt32().Indexed() + .WithColumn("AlbumId").AsInt32().Indexed() .WithColumn("Quality").AsString() .WithColumn("Size").AsInt64() .WithColumn("DateAdded").AsDateTime() - .WithColumn("AlbumId").AsInt32(); // How does this impact stand alone tracks? + .WithColumn("SceneName").AsString().Nullable() + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("MediaInfo").AsString().Nullable() + .WithColumn("RelativePath").AsString().Nullable(); + Alter.Table("NamingConfig") + .AddColumn("ArtistFolderFormat").AsString().Nullable() + .AddColumn("RenameTracks").AsBoolean().Nullable() + .AddColumn("StandardTrackFormat").AsString().Nullable() + .AddColumn("AlbumFolderFormat").AsString().Nullable(); } } diff --git a/src/NzbDrone.Core/Datastore/Migration/112_Add_music_fields_to_NamingConfig.cs b/src/NzbDrone.Core/Datastore/Migration/112_Add_music_fields_to_NamingConfig.cs deleted file mode 100644 index b855e1e28..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/112_Add_music_fields_to_NamingConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(112)] - public class add_music_fields_to_namingconfig : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("NamingConfig").AddColumn("ArtistFolderFormat").AsAnsiString().Nullable(); - Alter.Table("NamingConfig").AddColumn("AlbumFolderFormat").AsAnsiString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 9b9905adc..c5945047b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -92,11 +92,13 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId); - Mapper.Entity().RegisterModel("Artist") + Mapper.Entity().RegisterModel("Artists") .Ignore(s => s.RootFolderPath) .Relationship() .HasOne(a => a.Profile, a => a.ProfileId); + Mapper.Entity().RegisterModel("Albums"); + Mapper.Entity().RegisterModel("TrackFiles") .Ignore(f => f.Path) .Relationships.AutoMapICollectionOrComplexProperties() diff --git a/src/NzbDrone.Core/Download/AlbumGrabbedEvent.cs b/src/NzbDrone.Core/Download/AlbumGrabbedEvent.cs new file mode 100644 index 000000000..5b7972802 --- /dev/null +++ b/src/NzbDrone.Core/Download/AlbumGrabbedEvent.cs @@ -0,0 +1,17 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public class AlbumGrabbedEvent : IEvent + { + public RemoteAlbum Album { get; private set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } + + public AlbumGrabbedEvent(RemoteAlbum album) + { + Album = album; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 4e46b3729..d9a48965c 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.Download { @@ -130,7 +131,7 @@ namespace NzbDrone.Core.Download { var statusMessages = importResults .Where(v => v.Result != ImportResultType.Imported) - .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)) + .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalTrack.Path), v.Errors)) .ToArray(); trackedDownload.Warn(statusMessages); diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 811d3ebea..7447e162e 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -16,6 +16,7 @@ using NzbDrone.Core.Tv; 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 { void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); @@ -136,16 +137,17 @@ namespace NzbDrone.Core.Extras private List GetEpisodeFiles(int seriesId) { - var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); - var episodes = _episodeService.GetEpisodeBySeries(seriesId); + //var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); + //var episodes = _episodeService.GetEpisodeBySeries(seriesId); - foreach (var episodeFile in episodeFiles) - { - var localEpisodeFile = episodeFile; - episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); - } + //foreach (var episodeFile in episodeFiles) + //{ + // var localEpisodeFile = episodeFile; + // episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); + //} - return episodeFiles; + //return episodeFiles; + return new List(); } } } diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index c5e602e59..630f5edc1 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.IndexerSearch.Definitions { @@ -19,6 +20,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions public virtual bool MonitoredEpisodesOnly { get; set; } public virtual bool UserInvokedSearch { get; set; } + public Artist Artist { get; set; } + public List Tracks { get; set; } + public List QueryTitles => SceneTitles.Select(GetQueryTitle).ToList(); public static string GetQueryTitle(string title) diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs index ab2f80480..70befbb18 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.MediaFiles.Commands diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs new file mode 100644 index 000000000..af86d061b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RenameArtistCommand : Command + { + public List ArtistIds { get; set; } + + public override bool SendUpdatesToClient => true; + + public RenameArtistCommand() + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs new file mode 100644 index 000000000..904350d5a --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class RescanArtistCommand : Command + { + + public string ArtistId { get; set; } + + public override bool SendUpdatesToClient => true; + + public RescanArtistCommand() + { + ArtistId = ""; + } + + public RescanArtistCommand(string artistId) + { + ArtistId = artistId; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 2f6ba4b25..db2f389f4 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -16,12 +16,15 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles { public interface IDiskScanService { - void Scan(Series series); + void Scan(Artist artist); string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true); List FilterFiles(Series series, IEnumerable files); @@ -29,32 +32,35 @@ namespace NzbDrone.Core.MediaFiles public class DiskScanService : IDiskScanService, - IHandle, - IExecute + IHandle, + IExecute { private readonly IDiskProvider _diskProvider; private readonly IMakeImportDecision _importDecisionMaker; - private readonly IImportApprovedEpisodes _importApprovedEpisodes; + private readonly IImportApprovedTracks _importApprovedTracks; private readonly IConfigService _configService; private readonly ISeriesService _seriesService; + private readonly IArtistService _artistService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public DiskScanService(IDiskProvider diskProvider, IMakeImportDecision importDecisionMaker, - IImportApprovedEpisodes importApprovedEpisodes, + IImportApprovedTracks importApprovedTracks, IConfigService configService, ISeriesService seriesService, + IArtistService artistService, IMediaFileTableCleanupService mediaFileTableCleanupService, IEventAggregator eventAggregator, Logger logger) { _diskProvider = diskProvider; _importDecisionMaker = importDecisionMaker; - _importApprovedEpisodes = importApprovedEpisodes; + _importApprovedTracks = importApprovedTracks; _configService = configService; _seriesService = seriesService; + _artistService = artistService; _mediaFileTableCleanupService = mediaFileTableCleanupService; _eventAggregator = eventAggregator; _logger = logger; @@ -63,69 +69,75 @@ namespace NzbDrone.Core.MediaFiles private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(extras|@eadir|extrafanart|plex\sversions|\..+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|Thumbs\.db", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public void Scan(Series series) + public void Scan(Artist artist) { - var rootFolder = _diskProvider.GetParentFolder(series.Path); + var rootFolder = _diskProvider.GetParentFolder(artist.Path); if (!_diskProvider.FolderExists(rootFolder)) { - _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); - _eventAggregator.PublishEvent(new SeriesScanSkippedEvent(series, SeriesScanSkippedReason.RootFolderDoesNotExist)); + _logger.Warn("Artist' root folder ({0}) doesn't exist.", rootFolder); + _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(artist, ArtistScanSkippedReason.RootFolderDoesNotExist)); return; } if (_diskProvider.GetDirectories(rootFolder).Empty()) { - _logger.Warn("Series' root folder ({0}) is empty.", rootFolder); - _eventAggregator.PublishEvent(new SeriesScanSkippedEvent(series, SeriesScanSkippedReason.RootFolderIsEmpty)); + _logger.Warn("Artist' root folder ({0}) is empty.", rootFolder); + _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(artist, ArtistScanSkippedReason.RootFolderIsEmpty)); return; } - _logger.ProgressInfo("Scanning disk for {0}", series.Title); + _logger.ProgressInfo("Scanning disk for {0}", artist.Name); - if (!_diskProvider.FolderExists(series.Path)) + if (!_diskProvider.FolderExists(artist.Path)) { if (_configService.CreateEmptySeriesFolders) { - _logger.Debug("Creating missing series folder: {0}", series.Path); - _diskProvider.CreateFolder(series.Path); - SetPermissions(series.Path); + _logger.Debug("Creating missing artist folder: {0}", artist.Path); + _diskProvider.CreateFolder(artist.Path); + SetPermissions(artist.Path); } else { - _logger.Debug("Series folder doesn't exist: {0}", series.Path); + _logger.Debug("Artist folder doesn't exist: {0}", artist.Path); } - CleanMediaFiles(series, new List()); - CompletedScanning(series); + CleanMediaFiles(artist, new List()); + CompletedScanning(artist); return; } - var videoFilesStopwatch = Stopwatch.StartNew(); - var mediaFileList = FilterFiles(series, GetVideoFiles(series.Path)).ToList(); - videoFilesStopwatch.Stop(); - _logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed); + var musicFilesStopwatch = Stopwatch.StartNew(); + var mediaFileList = FilterFiles(artist, GetMusicFiles(artist.Path)).ToList(); + musicFilesStopwatch.Stop(); + _logger.Trace("Finished getting track files for: {0} [{1}]", artist, musicFilesStopwatch.Elapsed); - CleanMediaFiles(series, mediaFileList); + CleanMediaFiles(artist, mediaFileList); var decisionsStopwatch = Stopwatch.StartNew(); - var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series); + var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist); decisionsStopwatch.Stop(); - _logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed); - _importApprovedEpisodes.Import(decisions, false); + _logger.Trace("Import decisions complete for: {0} [{1}]", artist, decisionsStopwatch.Elapsed); + _importApprovedTracks.Import(decisions, false); - CompletedScanning(series); + CompletedScanning(artist); } - - private void CleanMediaFiles(Series series, List mediaFileList) + + private void CleanMediaFiles(Artist artist, List mediaFileList) { - _logger.Debug("{0} Cleaning up media files in DB", series); - _mediaFileTableCleanupService.Clean(series, mediaFileList); + _logger.Debug("{0} Cleaning up media files in DB", artist); + _mediaFileTableCleanupService.Clean(artist, mediaFileList); } - private void CompletedScanning(Series series) + //private void CompletedScanning(Series series) + //{ + // _logger.Info("Completed scanning disk for {0}", series.Title); + // _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); + //} + + private void CompletedScanning(Artist artist) { - _logger.Info("Completed scanning disk for {0}", series.Title); - _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); + _logger.Info("Completed scanning disk for {0}", artist.Name); + _eventAggregator.PublishEvent(new ArtistScannedEvent(artist)); } public string[] GetVideoFiles(string path, bool allDirectories = true) @@ -143,9 +155,24 @@ namespace NzbDrone.Core.MediaFiles return mediaFileList.ToArray(); } + public string[] GetMusicFiles(string path, bool allDirectories = true) + { + _logger.Debug("Scanning '{0}' for music files", path); + + var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); + + var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file).ToLower())) + .ToList(); + + _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); + _logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path); + return mediaFileList.ToArray(); + } + public string[] GetNonVideoFiles(string path, bool allDirectories = true) { - _logger.Debug("Scanning '{0}' for non-video files", path); + _logger.Debug("Scanning '{0}' for non-music files", path); var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); @@ -154,7 +181,7 @@ namespace NzbDrone.Core.MediaFiles .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); - _logger.Debug("{0} non-video files were found in {1}", mediaFileList.Count, path); + _logger.Debug("{0} non-music files were found in {1}", mediaFileList.Count, path); return mediaFileList.ToArray(); } @@ -165,6 +192,13 @@ namespace NzbDrone.Core.MediaFiles .ToList(); } + public List FilterFiles(Artist artist, IEnumerable files) + { + return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(artist.Path.GetRelativePath(file))) + .Where(file => !ExcludedFilesRegex.IsMatch(Path.GetFileName(file))) + .ToList(); + } + private void SetPermissions(string path) { if (!_configService.SetPermissionsLinux) @@ -186,26 +220,26 @@ namespace NzbDrone.Core.MediaFiles } } - public void Handle(SeriesUpdatedEvent message) + public void Handle(ArtistUpdatedEvent message) { - Scan(message.Series); + Scan(message.Artist); } - public void Execute(RescanSeriesCommand message) + public void Execute(RescanArtistCommand message) { - if (message.SeriesId.HasValue) + if (message.ArtistId.IsNotNullOrWhiteSpace()) { - var series = _seriesService.GetSeries(message.SeriesId.Value); - Scan(series); + var artist = _artistService.FindById(message.ArtistId); + Scan(artist); } else { - var allSeries = _seriesService.GetAllSeries(); + var allArtists = _artistService.GetAllArtists(); - foreach (var series in allSeries) + foreach (var artist in allArtists) { - Scan(series); + Scan(artist); } } } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesCommandService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesCommandService.cs index 2f5e19a41..f3ded84b2 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesCommandService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesCommandService.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles { diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index febc267a8..d5caf8fd2 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles { @@ -59,7 +60,7 @@ namespace NzbDrone.Core.MediaFiles results.AddRange(folderResults); } - foreach (var videoFile in _diskScanService.GetVideoFiles(directoryInfo.FullName, false)) + foreach (var videoFile in _diskScanService.GetNonVideoFiles(directoryInfo.FullName, false)) { var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null); results.AddRange(fileResults); @@ -100,7 +101,7 @@ namespace NzbDrone.Core.MediaFiles public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series) { - var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + var videoFiles = _diskScanService.GetNonVideoFiles(directoryInfo.FullName); var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); foreach (var videoFile in videoFiles) @@ -152,48 +153,50 @@ namespace NzbDrone.Core.MediaFiles private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Series series, DownloadClientItem downloadClientItem) { - if (_seriesService.SeriesPathExists(directoryInfo.FullName)) - { - _logger.Warn("Unable to process folder that is mapped to an existing show"); - return new List(); - } - - var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); - var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); - - if (folderInfo != null) - { - _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); - } - - var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); - - if (downloadClientItem == null) - { - foreach (var videoFile in videoFiles) - { - if (_diskProvider.IsFileLocked(videoFile)) - { - return new List - { - FileIsLockedResult(videoFile) - }; - } - } - } - - var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, folderInfo, true); - var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); - - if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && - importResults.Any(i => i.Result == ImportResultType.Imported) && - ShouldDeleteFolder(directoryInfo, series)) - { - _logger.Debug("Deleting folder after importing valid files"); - _diskProvider.DeleteFolder(directoryInfo.FullName, true); - } - - return importResults; + throw new System.NotImplementedException("Will be removed"); + + //if (_seriesService.SeriesPathExists(directoryInfo.FullName)) + //{ + // _logger.Warn("Unable to process folder that is mapped to an existing show"); + // return new List(); + //} + + //var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + //var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); + + //if (folderInfo != null) + //{ + // _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); + //} + + //var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + + //if (downloadClientItem == null) + //{ + // foreach (var videoFile in videoFiles) + // { + // if (_diskProvider.IsFileLocked(videoFile)) + // { + // return new List + // { + // FileIsLockedResult(videoFile) + // }; + // } + // } + //} + + //var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, folderInfo, true); + //var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); + + //if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && + // importResults.Any(i => i.Result == ImportResultType.Imported) && + // ShouldDeleteFolder(directoryInfo, series)) + //{ + // _logger.Debug("Deleting folder after importing valid files"); + // _diskProvider.DeleteFolder(directoryInfo.FullName, true); + //} + + //return importResults; } private List ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) @@ -215,30 +218,31 @@ namespace NzbDrone.Core.MediaFiles private List ProcessFile(FileInfo fileInfo, ImportMode importMode, Series series, DownloadClientItem downloadClientItem) { - if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) - { - _logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName); - - return new List - { - new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, new Rejection("Invalid video file, filename starts with '._'")), "Invalid video file, filename starts with '._'") - }; - } - - if (downloadClientItem == null) - { - if (_diskProvider.IsFileLocked(fileInfo.FullName)) - { - return new List - { - FileIsLockedResult(fileInfo.FullName) - }; - } - } - - var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, null, true); - - return _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); + throw new System.NotImplementedException("Will be removed"); + //if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) + //{ + // _logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName); + + // return new List + // { + // new ImportResult(new ImportDecision(new LocalTrack { Path = fileInfo.FullName }, new Rejection("Invalid music file, filename starts with '._'")), "Invalid music file, filename starts with '._'") + // }; + //} + + //if (downloadClientItem == null) + //{ + // if (_diskProvider.IsFileLocked(fileInfo.FullName)) + // { + // return new List + // { + // FileIsLockedResult(fileInfo.FullName) + // }; + // } + //} + + //var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, null, true); + + //return _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); } private string GetCleanedUpFolderName(string folder) @@ -251,15 +255,17 @@ namespace NzbDrone.Core.MediaFiles private ImportResult FileIsLockedResult(string videoFile) { - _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); - return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); + throw new System.NotImplementedException("Will be removed"); + //_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); + //return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); } private ImportResult UnknownSeriesResult(string message, string videoFile = null) { - var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; + throw new System.NotImplementedException("Will be removed"); + //var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; - return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Series")), message); + //return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Series")), message); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs index 86abb87b7..50f491ca6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs @@ -6,5 +6,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IImportDecisionEngineSpecification { Decision IsSatisfiedBy(LocalEpisode localEpisode); + Decision IsSatisfiedBy(LocalTrack localTrack); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index cdfd289db..f3cc3404a 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -12,7 +12,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Download; using NzbDrone.Core.Extras; - +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles.EpisodeImport { @@ -47,105 +47,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto) { - var qualifiedImports = decisions.Where(c => c.Approved) - .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s - .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.Profile)) - .ThenByDescending(c => c.LocalEpisode.Size)) - .SelectMany(c => c) - .ToList(); - - var importResults = new List(); - - foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalEpisode.Episodes.Select(episode => episode.EpisodeNumber).MinOrDefault()) - .ThenByDescending(e => e.LocalEpisode.Size)) - { - var localEpisode = importDecision.LocalEpisode; - var oldFiles = new List(); - - try - { - //check if already imported - if (importResults.SelectMany(r => r.ImportDecision.LocalEpisode.Episodes) - .Select(e => e.Id) - .Intersect(localEpisode.Episodes.Select(e => e.Id)) - .Any()) - { - importResults.Add(new ImportResult(importDecision, "Episode has already been imported")); - continue; - } - - var episodeFile = new EpisodeFile(); - episodeFile.DateAdded = DateTime.UtcNow; - episodeFile.SeriesId = localEpisode.Series.Id; - episodeFile.Path = localEpisode.Path.CleanFilePath(); - episodeFile.Size = _diskProvider.GetFileSize(localEpisode.Path); - episodeFile.Quality = localEpisode.Quality; - episodeFile.MediaInfo = localEpisode.MediaInfo; - episodeFile.SeasonNumber = localEpisode.SeasonNumber; - episodeFile.Episodes = localEpisode.Episodes; - episodeFile.ReleaseGroup = localEpisode.ParsedEpisodeInfo.ReleaseGroup; - - bool copyOnly; - switch (importMode) - { - default: - case ImportMode.Auto: - copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly; - break; - case ImportMode.Move: - copyOnly = false; - break; - case ImportMode.Copy: - copyOnly = true; - break; - } - - if (newDownload) - { - episodeFile.SceneName = GetSceneName(downloadClientItem, localEpisode); - - var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly); - oldFiles = moveResult.OldFiles; - } - else - { - episodeFile.RelativePath = localEpisode.Series.Path.GetRelativePath(episodeFile.Path); - } - - _mediaFileService.Add(episodeFile); - importResults.Add(new ImportResult(importDecision)); - - if (newDownload) - { - _extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly); - } - - if (downloadClientItem != null) - { - _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); - } - else - { - _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload)); - } - - if (newDownload) - { - _eventAggregator.PublishEvent(new EpisodeDownloadedEvent(localEpisode, episodeFile, oldFiles)); - } - } - catch (Exception e) - { - _logger.Warn(e, "Couldn't import episode " + localEpisode); - importResults.Add(new ImportResult(importDecision, "Failed to import episode")); - } - } - - //Adding all the rejected decisions - importResults.AddRange(decisions.Where(c => !c.Approved) - .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray()))); - - return importResults; + throw new NotImplementedException("This will be removed"); } private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs deleted file mode 100644 index 5e4e2ede2..000000000 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.EpisodeImport -{ - public class ImportDecision - { - public LocalEpisode LocalEpisode { get; private set; } - public IEnumerable Rejections { get; private set; } - - public bool Approved => Rejections.Empty(); - - public ImportDecision(LocalEpisode localEpisode, params Rejection[] rejections) - { - LocalEpisode = localEpisode; - Rejections = rejections.ToList(); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 764e1b88f..4209e98c7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -11,14 +11,17 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.MediaFiles.MediaInfo; - +using NzbDrone.Core.Music; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles.EpisodeImport { public interface IMakeImportDecision { - List GetImportDecisions(List videoFiles, Series series); - List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); + //List GetImportDecisions(List videoFiles, Series series); + List GetImportDecisions(List musicFiles, Artist artist); + //List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); + List GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo); } public class ImportDecisionMaker : IMakeImportDecision @@ -48,84 +51,124 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _logger = logger; } - public List GetImportDecisions(List videoFiles, Series series) + //public List GetImportDecisions(List videoFiles, Series series) + //{ + // return GetImportDecisions(videoFiles, series, null, false); + //} + + //public List GetImportDecisions(List videoFiles, Artist series, ParsedEpisodeInfo folderInfo, bool sceneSource) + //{ + // var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); + + // _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); + + // var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo); + // var decisions = new List(); + + // foreach (var file in newFiles) + // { + // decisions.AddIfNotNull(GetDecision(file, series, folderInfo, sceneSource, shouldUseFolderName)); + // } + + // return decisions; + //} + + public List GetImportDecisions(List musicFiles, Artist artist) { - return GetImportDecisions(videoFiles, series, null, false); + return GetImportDecisions(musicFiles, artist, null); } - public List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) + public List GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo) { - var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); + var newFiles = _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist); - _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); + _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, musicFiles.Count()); - var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo); + var shouldUseFolderName = ShouldUseFolderName(musicFiles, artist, folderInfo); var decisions = new List(); foreach (var file in newFiles) { - decisions.AddIfNotNull(GetDecision(file, series, folderInfo, sceneSource, shouldUseFolderName)); + decisions.AddIfNotNull(GetDecision(file, artist, folderInfo, shouldUseFolderName)); } return decisions; } - private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) + private ImportDecision GetDecision(string file, Artist artist, ParsedTrackInfo folderInfo, bool shouldUseFolderName) { ImportDecision decision = null; try { - var localEpisode = _parsingService.GetLocalEpisode(file, series, shouldUseFolderName ? folderInfo : null, sceneSource); + var localTrack = _parsingService.GetLocalTrack(file, artist, shouldUseFolderName ? folderInfo : null); - if (localEpisode != null) + if (localTrack != null) { - localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, series); - localEpisode.Size = _diskProvider.GetFileSize(file); + localTrack.Quality = GetQuality(folderInfo, localTrack.Quality, artist); + localTrack.Size = _diskProvider.GetFileSize(file); - _logger.Debug("Size: {0}", localEpisode.Size); + _logger.Debug("Size: {0}", localTrack.Size); - //TODO: make it so media info doesn't ruin the import process of a new series - if (sceneSource) - { - localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); - } + //TODO: make it so media info doesn't ruin the import process of a new artist - if (localEpisode.Episodes.Empty()) + if (localTrack.Tracks.Empty()) { - decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); + decision = new ImportDecision(localTrack, new Rejection("Invalid album or track")); } else { - decision = GetDecision(localEpisode); + decision = GetDecision(localTrack); } } else { - localEpisode = new LocalEpisode(); - localEpisode.Path = file; + localTrack = new LocalTrack(); + localTrack.Path = file; - decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); + decision = new ImportDecision(localTrack, new Rejection("Unable to parse file")); } } catch (Exception e) { _logger.Error(e, "Couldn't import file. {0}", file); - var localEpisode = new LocalEpisode { Path = file }; - decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); + var localTrack = new LocalTrack { Path = file }; + decision = new ImportDecision(localTrack, new Rejection("Unexpected error processing file")); } return decision; } - private ImportDecision GetDecision(LocalEpisode localEpisode) + private ImportDecision GetDecision(LocalTrack localTrack) { - var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode)) + var reasons = _specifications.Select(c => EvaluateSpec(c, localTrack)) .Where(c => c != null); - return new ImportDecision(localEpisode, reasons.ToArray()); + return new ImportDecision(localTrack, reasons.ToArray()); + } + + private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalTrack localTrack) + { + try + { + var result = spec.IsSatisfiedBy(localTrack); + + if (!result.Accepted) + { + return new Rejection(result.Reason); + } + } + catch (Exception e) + { + //e.Data.Add("report", remoteEpisode.Report.ToJson()); + //e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); + _logger.Error(e, "Couldn't evaluate decision on {0}", localTrack.Path); + return new Rejection($"{spec.GetType().Name}: {e.Message}"); + } + + return null; } private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode) @@ -150,28 +193,28 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return null; } - private bool ShouldUseFolderName(List videoFiles, Series series, ParsedEpisodeInfo folderInfo) + private bool ShouldUseFolderName(List musicFiles, Artist artist, ParsedTrackInfo folderInfo) { if (folderInfo == null) { return false; } - if (folderInfo.FullSeason) - { - return false; - } + //if (folderInfo.FullSeason) + //{ + // return false; + //} - return videoFiles.Count(file => + return musicFiles.Count(file => { var size = _diskProvider.GetFileSize(file); var fileQuality = QualityParser.ParseQuality(file); - var sample = _detectSample.IsSample(series, GetQuality(folderInfo, fileQuality, series), file, size, folderInfo.IsPossibleSpecialEpisode); + //var sample = _detectSample.IsSample(artist, GetQuality(folderInfo, fileQuality, artist), file, size, folderInfo.IsPossibleSpecialEpisode); - if (sample) - { - return false; - } + //if (sample) + //{ + // return false; + //} if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) { @@ -182,9 +225,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport }) == 1; } - private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) + private QualityModel GetQuality(ParsedTrackInfo folderInfo, QualityModel fileQuality, Artist artist) { - if (UseFolderQuality(folderInfo, fileQuality, series)) + if (UseFolderQuality(folderInfo, fileQuality, artist)) { _logger.Debug("Using quality from folder: {0}", folderInfo.Quality); return folderInfo.Quality; @@ -193,7 +236,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return fileQuality; } - private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) + private bool UseFolderQuality(ParsedTrackInfo folderInfo, QualityModel fileQuality, Artist artist) { if (folderInfo == null) { @@ -210,7 +253,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return true; } - if (new QualityModelComparer(series.Profile).Compare(folderInfo.Quality, fileQuality) > 0) + if (new QualityModelComparer(artist.Profile).Compare(folderInfo.Quality, fileQuality) > 0) { return true; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs index 38ed485b7..28168385f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index d85a2e119..a12c841ea 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -15,6 +15,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { @@ -94,65 +95,67 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual private List ProcessFolder(string folder, string downloadId) { - var directoryInfo = new DirectoryInfo(folder); - var series = _parsingService.GetSeries(directoryInfo.Name); + throw new System.NotImplementedException("TODO: This will be rewritten for Music"); + //var directoryInfo = new DirectoryInfo(folder); + //var series = _parsingService.GetSeries(directoryInfo.Name); - if (series == null && downloadId.IsNotNullOrWhiteSpace()) - { - var trackedDownload = _trackedDownloadService.Find(downloadId); - series = trackedDownload.RemoteEpisode.Series; - } + //if (series == null && downloadId.IsNotNullOrWhiteSpace()) + //{ + // var trackedDownload = _trackedDownloadService.Find(downloadId); + // series = trackedDownload.RemoteEpisode.Series; + //} - if (series == null) - { - var files = _diskScanService.GetVideoFiles(folder); + //if (series == null) + //{ + // var files = _diskScanService.GetVideoFiles(folder); - return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList(); - } + // return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList(); + //} - var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); - var seriesFiles = _diskScanService.GetVideoFiles(folder).ToList(); - var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, folderInfo, SceneSource(series, folder)); + //var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); + //var seriesFiles = _diskScanService.GetVideoFiles(folder).ToList(); + //var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, folderInfo, SceneSource(series, folder)); - return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList(); + //return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList(); } private ManualImportItem ProcessFile(string file, string downloadId, string folder = null) { - if (folder.IsNullOrWhiteSpace()) - { - folder = new FileInfo(file).Directory.FullName; - } + throw new System.NotImplementedException("TODO: This will be rewritten for Music"); + //if (folder.IsNullOrWhiteSpace()) + //{ + // folder = new FileInfo(file).Directory.FullName; + //} - var relativeFile = folder.GetRelativePath(file); + //var relativeFile = folder.GetRelativePath(file); - var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); + //var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); - if (series == null) - { - series = _parsingService.GetSeries(relativeFile); - } + //if (series == null) + //{ + // series = _parsingService.GetSeries(relativeFile); + //} - if (series == null && downloadId.IsNotNullOrWhiteSpace()) - { - var trackedDownload = _trackedDownloadService.Find(downloadId); - series = trackedDownload.RemoteEpisode.Series; - } + //if (series == null && downloadId.IsNotNullOrWhiteSpace()) + //{ + // var trackedDownload = _trackedDownloadService.Find(downloadId); + // series = trackedDownload.RemoteEpisode.Series; + //} - if (series == null) - { - var localEpisode = new LocalEpisode(); - localEpisode.Path = file; - localEpisode.Quality = QualityParser.ParseQuality(file); - localEpisode.Size = _diskProvider.GetFileSize(file); + //if (series == null) + //{ + // var localEpisode = new LocalEpisode(); + // localEpisode.Path = file; + // localEpisode.Quality = QualityParser.ParseQuality(file); + // localEpisode.Size = _diskProvider.GetFileSize(file); - return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId); - } + // return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId); + //} - var importDecisions = _importDecisionMaker.GetImportDecisions(new List {file}, - series, null, SceneSource(series, folder)); + //var importDecisions = _importDecisionMaker.GetImportDecisions(new List {file}, + // series, null, SceneSource(series, folder)); - return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null; + //return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null; } private bool SceneSource(Series series, string folder) @@ -162,107 +165,109 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId) { - var item = new ManualImportItem(); - - item.Path = decision.LocalEpisode.Path; - item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path); - item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); - item.DownloadId = downloadId; - - if (decision.LocalEpisode.Series != null) - { - item.Series = decision.LocalEpisode.Series; - } - - if (decision.LocalEpisode.Episodes.Any()) - { - item.SeasonNumber = decision.LocalEpisode.SeasonNumber; - item.Episodes = decision.LocalEpisode.Episodes; - } - - item.Quality = decision.LocalEpisode.Quality; - item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); - item.Rejections = decision.Rejections; - - return item; + throw new System.NotImplementedException("TODO: This will be rewritten for Music"); + //var item = new ManualImportItem(); + + //item.Path = decision.LocalEpisode.Path; + //item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path); + //item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); + //item.DownloadId = downloadId; + + //if (decision.LocalEpisode.Series != null) + //{ + // item.Series = decision.LocalEpisode.Series; + //} + + //if (decision.LocalEpisode.Episodes.Any()) + //{ + // item.SeasonNumber = decision.LocalEpisode.SeasonNumber; + // item.Episodes = decision.LocalEpisode.Episodes; + //} + + //item.Quality = decision.LocalEpisode.Quality; + //item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); + //item.Rejections = decision.Rejections; + + //return item; } public void Execute(ManualImportCommand message) { _logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode); - - var imported = new List(); - var importedTrackedDownload = new List(); - - for (int i = 0; i < message.Files.Count; i++) - { - _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count); - - var file = message.Files[i]; - var series = _seriesService.GetSeries(file.SeriesId); - var episodes = _episodeService.GetEpisodes(file.EpisodeIds); - var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); - var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); - var existingFile = series.Path.IsParentPath(file.Path); - - var localEpisode = new LocalEpisode - { - ExistingFile = false, - Episodes = episodes, - MediaInfo = mediaInfo, - ParsedEpisodeInfo = parsedEpisodeInfo, - Path = file.Path, - Quality = file.Quality, - Series = series, - Size = 0 - }; - - //TODO: Cleanup non-tracked downloads - - var importDecision = new ImportDecision(localEpisode); - - if (file.DownloadId.IsNullOrWhiteSpace()) - { - imported.AddRange(_importApprovedEpisodes.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); - } - - else - { - var trackedDownload = _trackedDownloadService.Find(file.DownloadId); - var importResult = _importApprovedEpisodes.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); - - imported.Add(importResult); - - importedTrackedDownload.Add(new ManuallyImportedFile - { - TrackedDownload = trackedDownload, - ImportResult = importResult - }); - } - } - - _logger.ProgressTrace("Manually imported {0} files", imported.Count); - - foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) - { - var trackedDownload = groupedTrackedDownload.First().TrackedDownload; - - if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) - { - if (_downloadedEpisodesImportService.ShouldDeleteFolder( - new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), - trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly) - { - _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); - } - } - - if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) - { - trackedDownload.State = TrackedDownloadStage.Imported; - _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); - } - } + throw new System.NotImplementedException("TODO: This will be rewritten for Music"); + + //var imported = new List(); + //var importedTrackedDownload = new List(); + + //for (int i = 0; i < message.Files.Count; i++) + //{ + // _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count); + + // var file = message.Files[i]; + // var series = _seriesService.GetSeries(file.SeriesId); + // var episodes = _episodeService.GetEpisodes(file.EpisodeIds); + // var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); + // var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); + // var existingFile = series.Path.IsParentPath(file.Path); + + // var localEpisode = new LocalEpisode + // { + // ExistingFile = false, + // Episodes = episodes, + // MediaInfo = mediaInfo, + // ParsedEpisodeInfo = parsedEpisodeInfo, + // Path = file.Path, + // Quality = file.Quality, + // Series = series, + // Size = 0 + // }; + + // //TODO: Cleanup non-tracked downloads + + // var importDecision = new ImportDecision(localEpisode); + + // if (file.DownloadId.IsNullOrWhiteSpace()) + // { + // imported.AddRange(_importApprovedEpisodes.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); + // } + + // else + // { + // var trackedDownload = _trackedDownloadService.Find(file.DownloadId); + // var importResult = _importApprovedEpisodes.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); + + // imported.Add(importResult); + + // importedTrackedDownload.Add(new ManuallyImportedFile + // { + // TrackedDownload = trackedDownload, + // ImportResult = importResult + // }); + // } + //} + + //_logger.ProgressTrace("Manually imported {0} files", imported.Count); + + //foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) + //{ + // var trackedDownload = groupedTrackedDownload.First().TrackedDownload; + + // if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) + // { + // if (_downloadedEpisodesImportService.ShouldDeleteFolder( + // new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), + // trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly) + // { + // _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); + // } + // } + + // if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + // { + // trackedDownload.State = TrackedDownloadStage.Imported; + // _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + // } + //} } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs index 32f904e4d..4780240e2 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index 490bdb941..6e9695d34 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -63,5 +63,48 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + if (_configService.SkipFreeSpaceCheckWhenImporting) + { + _logger.Debug("Skipping free space check when importing"); + return Decision.Accept(); + } + + try + { + if (localTrack.ExistingFile) + { + _logger.Debug("Skipping free space check for existing episode"); + return Decision.Accept(); + } + + var path = Directory.GetParent(localTrack.Artist.Path); + var freeSpace = _diskProvider.GetAvailableSpace(path.FullName); + + if (!freeSpace.HasValue) + { + _logger.Debug("Free space check returned an invalid result for: {0}", path); + return Decision.Accept(); + } + + if (freeSpace < localTrack.Size + 100.Megabytes()) + { + _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localTrack, localTrack.Size); + return Decision.Reject("Not enough free space"); + } + } + catch (DirectoryNotFoundException ex) + { + _logger.Error(ex, "Unable to check free disk space while importing."); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to check free disk space while importing. {0}", localTrack.Path); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 7397c13e7..4357c07bc 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -13,6 +14,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + throw new NotImplementedException("Interface will be removed"); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ParsedEpisodeInfo.FullSeason) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs index 79ef96f88..b97c42f35 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; @@ -14,6 +15,53 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { _logger = logger; } + + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + if (localTrack.ExistingFile) + { + return Decision.Accept(); + } + + var dirInfo = new FileInfo(localTrack.Path).Directory; + + if (dirInfo == null) + { + return Decision.Accept(); + } + + throw new System.NotImplementedException("Needs to be implemented"); + + //var folderInfo = Parser.Parser.ParseTitle(dirInfo.Name); + + //if (folderInfo == null) + //{ + // return Decision.Accept(); + //} + + //if (!folderInfo.TrackNumbers.Any()) + //{ + // return Decision.Accept(); + //} + + + //var unexpected = localTrack.ParsedTrackInfo.TrackNumbers.Where(f => !folderInfo.TrackNumbers.Contains(f)).ToList(); + //// TODO: Implement MatchesFolderSpecification + //if (unexpected.Any()) + //{ + // _logger.Debug("Unexpected track number(s) in file: {0}", string.Join(", ", unexpected)); + + // if (unexpected.Count == 1) + // { + // return Decision.Reject("Track Number {0} was unexpected considering the {1} folder name", unexpected.First(), dirInfo.Name); + // } + + // return Decision.Reject("Episode Numbers {0} were unexpected considering the {1} folder name", string.Join(", ", unexpected), dirInfo.Name); + //} + + return Decision.Accept(); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index c7b61d802..e8eb664ec 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -16,6 +17,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index 2260ed71a..7fe4bef26 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -56,5 +56,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs index ee6c02c53..8adb7416b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -27,5 +28,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.Debug("Episode file on disk contains more episodes than this file contains"); return Decision.Reject("Episode file on disk contains more episodes than this file contains"); } + + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs index ce65eb304..95c39b1d6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -13,6 +14,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + throw new NotImplementedException("This is not needed, Interface will be removed"); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 3d07306af..c0eaa6a92 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -15,6 +16,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + var qualityComparer = new QualityModelComparer(localTrack.Artist.Profile); + if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) > 0)) + { + _logger.Debug("This file isn't an upgrade for all tracks. Skipping {0}", localTrack.Path); + return Decision.Reject("Not an upgrade for existing track file(s)"); + } + + return Decision.Accept(); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { var qualityComparer = new QualityModelComparer(localEpisode.Series.Profile); diff --git a/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs new file mode 100644 index 000000000..5188ffd07 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs @@ -0,0 +1,27 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class ArtistScanSkippedEvent : IEvent + { + public Artist Artist { get; private set; } + public ArtistScanSkippedReason Reason { get; private set; } + + public ArtistScanSkippedEvent(Artist artist, ArtistScanSkippedReason reason) + { + Artist = artist; + Reason = reason; + } + } + + public enum ArtistScanSkippedReason + { + RootFolderDoesNotExist, + RootFolderIsEmpty + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs new file mode 100644 index 000000000..63f5f4656 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs @@ -0,0 +1,19 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class ArtistScannedEvent : IEvent + { + public Artist Artist { get; private set; } + + public ArtistScannedEvent(Artist artist) + { + Artist = artist; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackDownloadedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackDownloadedEvent.cs new file mode 100644 index 000000000..82ea9f788 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackDownloadedEvent.cs @@ -0,0 +1,23 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class TrackDownloadedEvent : IEvent + { + public LocalTrack Track { get; private set; } + public TrackFile TrackFile { get; private set; } + public List OldFiles { get; private set; } + + public TrackDownloadedEvent(LocalTrack track, TrackFile trackFile, List oldFiles) + { + Track = track; + TrackFile = trackFile; + OldFiles = oldFiles; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 206942356..4b93d8591 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -5,36 +6,28 @@ using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.MediaFiles { - public interface IMediaFileRepository : IBasicRepository + public interface IMediaFileRepository : IBasicRepository { - List GetFilesBySeries(int seriesId); - List GetFilesBySeason(int seriesId, int seasonNumber); - List GetFilesWithoutMediaInfo(); + List GetFilesByArtist(int artistId); + List GetFilesWithoutMediaInfo(); } - public class MediaFileRepository : BasicRepository, IMediaFileRepository + public class MediaFileRepository : BasicRepository, IMediaFileRepository { public MediaFileRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { } - public List GetFilesBySeries(int seriesId) + public List GetFilesWithoutMediaInfo() { - return Query.Where(c => c.SeriesId == seriesId).ToList(); - } - - public List GetFilesBySeason(int seriesId, int seasonNumber) - { - return Query.Where(c => c.SeriesId == seriesId) - .AndWhere(c => c.SeasonNumber == seasonNumber) - .ToList(); + return Query.Where(c => c.MediaInfo == null).ToList(); } - public List GetFilesWithoutMediaInfo() + public List GetFilesByArtist(int artistId) { - return Query.Where(c => c.MediaInfo == null).ToList(); + return Query.Where(c => c.ArtistId == artistId).ToList(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index ca3f68ce2..5cd1b65f0 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -7,24 +7,27 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; using NzbDrone.Common; +using NzbDrone.Core.Music; +using System; +using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.MediaFiles { public interface IMediaFileService { - EpisodeFile Add(EpisodeFile episodeFile); - void Update(EpisodeFile episodeFile); - void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); - List GetFilesBySeries(int seriesId); - List GetFilesBySeason(int seriesId, int seasonNumber); - List GetFilesWithoutMediaInfo(); - List FilterExistingFiles(List files, Series series); - EpisodeFile Get(int id); - List Get(IEnumerable ids); + TrackFile Add(TrackFile trackFile); + void Update(TrackFile trackFile); + void Delete(TrackFile trackFile, DeleteMediaFileReason reason); + List GetFilesByArtist(int artistId); + List GetFilesByAlbum(int artistId, int albumId); + List GetFilesWithoutMediaInfo(); + List FilterExistingFiles(List files, Artist artist); + TrackFile Get(int id); + List Get(IEnumerable ids); } - public class MediaFileService : IMediaFileService, IHandleAsync + public class MediaFileService : IMediaFileService, IHandleAsync { private readonly IEventAggregator _eventAggregator; private readonly IMediaFileRepository _mediaFileRepository; @@ -37,66 +40,69 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public EpisodeFile Add(EpisodeFile episodeFile) + public TrackFile Add(TrackFile trackFile) { - var addedFile = _mediaFileRepository.Insert(episodeFile); - _eventAggregator.PublishEvent(new EpisodeFileAddedEvent(addedFile)); + var addedFile = _mediaFileRepository.Insert(trackFile); + _eventAggregator.PublishEvent(new TrackFileAddedEvent(addedFile)); return addedFile; } - public void Update(EpisodeFile episodeFile) + public void Update(TrackFile trackFile) { - _mediaFileRepository.Update(episodeFile); + _mediaFileRepository.Update(trackFile); } - public void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason) + public void Delete(TrackFile trackFile, DeleteMediaFileReason reason) { - //Little hack so we have the episodes and series attached for the event consumers - episodeFile.Episodes.LazyLoad(); - episodeFile.Path = Path.Combine(episodeFile.Series.Value.Path, episodeFile.RelativePath); + //Little hack so we have the tracks and artist attached for the event consumers + trackFile.Episodes.LazyLoad(); + trackFile.Path = Path.Combine(trackFile.Artist.Value.Path, trackFile.RelativePath); - _mediaFileRepository.Delete(episodeFile); - _eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason)); + _mediaFileRepository.Delete(trackFile); + _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); } - public List GetFilesBySeries(int seriesId) - { - return _mediaFileRepository.GetFilesBySeries(seriesId); - } - - public List GetFilesBySeason(int seriesId, int seasonNumber) - { - return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); - } - public List GetFilesWithoutMediaInfo() + public List GetFilesWithoutMediaInfo() { return _mediaFileRepository.GetFilesWithoutMediaInfo(); } - public List FilterExistingFiles(List files, Series series) + public List FilterExistingFiles(List files, Artist artist) { - var seriesFiles = GetFilesBySeries(series.Id).Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(); + //var artistFiles = GetFilesByArtist(artist.ForeignArtistId).Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList(); + var artistFiles = GetFilesByArtist(artist.Id).Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList(); - if (!seriesFiles.Any()) return files; + if (!artistFiles.Any()) return files; - return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); + return files.Except(artistFiles, PathEqualityComparer.Instance).ToList(); } - public EpisodeFile Get(int id) + public TrackFile Get(int id) { + // TODO: Should this be spotifyID or DB Id? return _mediaFileRepository.Get(id); } - public List Get(IEnumerable ids) + public List Get(IEnumerable ids) { return _mediaFileRepository.Get(ids).ToList(); } - public void HandleAsync(SeriesDeletedEvent message) + public void HandleAsync(ArtistDeletedEvent message) { - var files = GetFilesBySeries(message.Series.Id); + var files = GetFilesByArtist(message.Artist.Id); _mediaFileRepository.DeleteMany(files); } + + public List GetFilesByArtist(int artistId) + { + return _mediaFileRepository.GetFilesByArtist(artistId); + } + + public List GetFilesByAlbum(int artistId, int albumId) + { + return _mediaFileRepository.GetFilesByArtist(artistId); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index 0b7b2cba3..70cc1fdde 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -1,85 +1,78 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using NLog; using NzbDrone.Common; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles { public interface IMediaFileTableCleanupService { - void Clean(Series series, List filesOnDisk); + void Clean(Artist artist, List filesOnDisk); } public class MediaFileTableCleanupService : IMediaFileTableCleanupService { private readonly IMediaFileService _mediaFileService; - private readonly IEpisodeService _episodeService; + private readonly ITrackService _trackService; private readonly Logger _logger; public MediaFileTableCleanupService(IMediaFileService mediaFileService, - IEpisodeService episodeService, + ITrackService trackService, Logger logger) { _mediaFileService = mediaFileService; - _episodeService = episodeService; + _trackService = trackService; _logger = logger; } - public void Clean(Series series, List filesOnDisk) + public void Clean(Artist artist, List filesOnDisk) { - var seriesFiles = _mediaFileService.GetFilesBySeries(series.Id); - var episodes = _episodeService.GetEpisodeBySeries(series.Id); + var artistFiles = _mediaFileService.GetFilesByArtist(artist.Id); + var tracks = _trackService.GetTracksByArtist(artist.Id); + var filesOnDiskKeys = new HashSet(filesOnDisk, PathEqualityComparer.Instance); - foreach (var seriesFile in seriesFiles) + foreach (var artistFile in artistFiles) { - var episodeFile = seriesFile; - var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); + var trackFile = artistFile; + var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); try { - if (!filesOnDiskKeys.Contains(episodeFilePath)) + if (!filesOnDiskKeys.Contains(trackFilePath)) { - _logger.Debug("File [{0}] no longer exists on disk, removing from db", episodeFilePath); - _mediaFileService.Delete(seriesFile, DeleteMediaFileReason.MissingFromDisk); + _logger.Debug("File [{0}] no longer exists on disk, removing from db", trackFilePath); + _mediaFileService.Delete(artistFile, DeleteMediaFileReason.MissingFromDisk); continue; } - if (episodes.None(e => e.EpisodeFileId == episodeFile.Id)) + if (tracks.None(e => e.TrackFileId == trackFile.Id)) { - _logger.Debug("File [{0}] is not assigned to any episodes, removing from db", episodeFilePath); - _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.NoLinkedEpisodes); + _logger.Debug("File [{0}] is not assigned to any artist, removing from db", trackFilePath); + _mediaFileService.Delete(trackFile, DeleteMediaFileReason.NoLinkedEpisodes); continue; } - -// var localEpsiode = _parsingService.GetLocalEpisode(episodeFile.Path, series); -// -// if (localEpsiode == null || episodes.Count != localEpsiode.Episodes.Count) -// { -// _logger.Debug("File [{0}] parsed episodes has changed, removing from db", episodeFile.Path); -// _mediaFileService.Delete(episodeFile); -// continue; -// } } catch (Exception ex) { - _logger.Error(ex, "Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id); + _logger.Error(ex, "Unable to cleanup EpisodeFile in DB: {0}", trackFile.Id); } } - foreach (var e in episodes) + foreach (var t in tracks) { - var episode = e; + var track = t; - if (episode.EpisodeFileId > 0 && seriesFiles.None(f => f.Id == episode.EpisodeFileId)) + if (track.TrackFileId > 0 && artistFiles.None(f => f.Id == track.TrackFileId)) { - episode.EpisodeFileId = 0; - _episodeService.UpdateEpisode(episode); + track.TrackFileId = 0; + _trackService.UpdateTrack(track); } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index fb232f2f9..593cdcee6 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -7,10 +7,11 @@ using NzbDrone.Core.Tv; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles.MediaInfo { - public class UpdateMediaInfoService : IHandle + public class UpdateMediaInfoService : IHandle { private readonly IDiskProvider _diskProvider; private readonly IMediaFileService _mediaFileService; @@ -33,11 +34,11 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo _logger = logger; } - private void UpdateMediaInfo(Series series, List mediaFiles) + private void UpdateMediaInfo(Artist artist, List mediaFiles) { foreach (var mediaFile in mediaFiles) { - var path = Path.Combine(series.Path, mediaFile.RelativePath); + var path = Path.Combine(artist.Path, mediaFile.RelativePath); if (!_diskProvider.FileExists(path)) { @@ -56,7 +57,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo } } - public void Handle(SeriesScannedEvent message) + public void Handle(ArtistScannedEvent message) { if (!_configService.EnableMediaInfo) { @@ -64,10 +65,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return; } - var allMediaFiles = _mediaFileService.GetFilesBySeries(message.Series.Id); + var allMediaFiles = _mediaFileService.GetFilesByArtist(message.Artist.Id); var filteredMediaFiles = allMediaFiles.Where(c => c.MediaInfo == null || c.MediaInfo.SchemaRevision < CURRENT_MEDIA_INFO_SCHEMA_REVISION).ToList(); - UpdateMediaInfo(message.Series, filteredMediaFiles); + UpdateMediaInfo(message.Artist, filteredMediaFiles); } } } diff --git a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs index 4cfe84b37..d279b7289 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs @@ -55,24 +55,28 @@ namespace NzbDrone.Core.MediaFiles public List GetRenamePreviews(int seriesId) { - var series = _seriesService.GetSeries(seriesId); - var episodes = _episodeService.GetEpisodeBySeries(seriesId); - var files = _mediaFileService.GetFilesBySeries(seriesId); - - return GetPreviews(series, episodes, files) - .OrderByDescending(e => e.SeasonNumber) - .ThenByDescending(e => e.EpisodeNumbers.First()) - .ToList(); + // TODO + throw new NotImplementedException(); + //var series = _seriesService.GetSeries(seriesId); + //var episodes = _episodeService.GetEpisodeBySeries(seriesId); + //var files = _mediaFileService.GetFilesBySeries(seriesId); + + //return GetPreviews(series, episodes, files) + // .OrderByDescending(e => e.SeasonNumber) + // .ThenByDescending(e => e.EpisodeNumbers.First()) + // .ToList(); } public List GetRenamePreviews(int seriesId, int seasonNumber) { - var series = _seriesService.GetSeries(seriesId); - var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber); - var files = _mediaFileService.GetFilesBySeason(seriesId, seasonNumber); - - return GetPreviews(series, episodes, files) - .OrderByDescending(e => e.EpisodeNumbers.First()).ToList(); + // TODO + throw new NotImplementedException(); + //var series = _seriesService.GetSeries(seriesId); + //var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber); + //var files = _mediaFileService.GetFilesBySeason(seriesId, seasonNumber); + + //return GetPreviews(series, episodes, files) + // .OrderByDescending(e => e.EpisodeNumbers.First()).ToList(); } private IEnumerable GetPreviews(Series series, List episodes, List files) @@ -110,62 +114,68 @@ namespace NzbDrone.Core.MediaFiles private void RenameFiles(List episodeFiles, Series series) { - var renamed = new List(); - - foreach (var episodeFile in episodeFiles) - { - var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); - - try - { - _logger.Debug("Renaming episode file: {0}", episodeFile); - _episodeFileMover.MoveEpisodeFile(episodeFile, series); - - _mediaFileService.Update(episodeFile); - renamed.Add(episodeFile); - - _logger.Debug("Renamed episode file: {0}", episodeFile); - } - catch (SameFilenameException ex) - { - _logger.Debug("File not renamed, source and destination are the same: {0}", ex.Filename); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to rename file {0}", episodeFilePath); - } - } - - if (renamed.Any()) - { - _diskProvider.RemoveEmptySubfolders(series.Path); - - _eventAggregator.PublishEvent(new SeriesRenamedEvent(series)); - } + // TODO + throw new NotImplementedException(); + //var renamed = new List(); + + //foreach (var episodeFile in episodeFiles) + //{ + // var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); + + // try + // { + // _logger.Debug("Renaming episode file: {0}", episodeFile); + // _episodeFileMover.MoveEpisodeFile(episodeFile, series); + + // _mediaFileService.Update(episodeFile); + // renamed.Add(episodeFile); + + // _logger.Debug("Renamed episode file: {0}", episodeFile); + // } + // catch (SameFilenameException ex) + // { + // _logger.Debug("File not renamed, source and destination are the same: {0}", ex.Filename); + // } + // catch (Exception ex) + // { + // _logger.Error(ex, "Failed to rename file {0}", episodeFilePath); + // } + //} + + //if (renamed.Any()) + //{ + // _diskProvider.RemoveEmptySubfolders(series.Path); + + // _eventAggregator.PublishEvent(new SeriesRenamedEvent(series)); + //} } public void Execute(RenameFilesCommand message) { - var series = _seriesService.GetSeries(message.SeriesId); - var episodeFiles = _mediaFileService.Get(message.Files); - - _logger.ProgressInfo("Renaming {0} files for {1}", episodeFiles.Count, series.Title); - RenameFiles(episodeFiles, series); - _logger.ProgressInfo("Selected episode files renamed for {0}", series.Title); + // TODO + throw new NotImplementedException(); + //var series = _seriesService.GetSeries(message.SeriesId); + //var episodeFiles = _mediaFileService.Get(message.Files); + + //_logger.ProgressInfo("Renaming {0} files for {1}", episodeFiles.Count, series.Title); + //RenameFiles(episodeFiles, series); + //_logger.ProgressInfo("Selected episode files renamed for {0}", series.Title); } public void Execute(RenameSeriesCommand message) { - _logger.Debug("Renaming all files for selected series"); - var seriesToRename = _seriesService.GetSeries(message.SeriesIds); - - foreach (var series in seriesToRename) - { - var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); - _logger.ProgressInfo("Renaming all files in series: {0}", series.Title); - RenameFiles(episodeFiles, series); - _logger.ProgressInfo("All episode files renamed for {0}", series.Title); - } + // TODO + throw new NotImplementedException(); + //_logger.Debug("Renaming all files for selected series"); + //var seriesToRename = _seriesService.GetSeries(message.SeriesIds); + + //foreach (var series in seriesToRename) + //{ + // var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); + // _logger.ProgressInfo("Renaming all files in series: {0}", series.Title); + // RenameFiles(episodeFiles, series); + // _logger.ProgressInfo("All episode files renamed for {0}", series.Title); + //} } } } diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs new file mode 100644 index 000000000..25bfe75f7 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MediaFiles +{ + public class RenameTrackFilePreview + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public List TrackNumbers { get; set; } + public int TrackFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs new file mode 100644 index 000000000..001774de5 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IRenameTrackFileService + { + List GetRenamePreviews(int artistId); + List GetRenamePreviews(int artistId, int albumId); + } + + public class RenameTrackFileService : IRenameTrackFileService, + IExecute, + IExecute + { + private readonly IArtistService _artistService; + private readonly IAlbumService _albumService; + private readonly IMediaFileService _mediaFileService; + private readonly IMoveTrackFiles _trackFileMover; + private readonly IEventAggregator _eventAggregator; + private readonly ITrackService _trackService; + private readonly IBuildFileNames _filenameBuilder; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public RenameTrackFileService(IArtistService artistService, + IAlbumService albumService, + IMediaFileService mediaFileService, + IMoveTrackFiles trackFileMover, + IEventAggregator eventAggregator, + ITrackService trackService, + IBuildFileNames filenameBuilder, + IDiskProvider diskProvider, + Logger logger) + { + _artistService = artistService; + _albumService = albumService; + _mediaFileService = mediaFileService; + _trackFileMover = trackFileMover; + _eventAggregator = eventAggregator; + _trackService = trackService; + _filenameBuilder = filenameBuilder; + _diskProvider = diskProvider; + _logger = logger; + } + + public List GetRenamePreviews(int artistId) + { + // TODO + throw new NotImplementedException(); + //var artist = _artistService.GetArtist(artistId); + //var tracks = _trackService.GetTracksByArtist(artistId); + //var files = _mediaFileService.GetFilesByArtist(artistId); + + //return GetPreviews(artist, tracks, files) + // .OrderByDescending(e => e.SeasonNumber) + // .ThenByDescending(e => e.TrackNumbers.First()) + // .ToList(); + } + + public List GetRenamePreviews(int artistId, int albumId) + { + // TODO + //throw new NotImplementedException(); + var artist = _artistService.GetArtist(artistId); + var album = _albumService.GetAlbum(albumId); + var tracks = _trackService.GetTracksByAlbum(artistId, albumId); + var files = _mediaFileService.GetFilesByAlbum(artistId, albumId); + + return GetPreviews(artist, album, tracks, files) + .OrderByDescending(e => e.TrackNumbers.First()).ToList(); + } + + private IEnumerable GetPreviews(Artist artist, Album album, List tracks, List files) + { + foreach (var f in files) + { + var file = f; + var tracksInFile = tracks.Where(e => e.TrackFileId == file.Id).ToList(); + var trackFilePath = Path.Combine(artist.Path, file.RelativePath); + + if (!tracksInFile.Any()) + { + _logger.Warn("File ({0}) is not linked to any tracks", trackFilePath); + continue; + } + + var albumId = tracksInFile.First().AlbumId; + var newName = _filenameBuilder.BuildTrackFileName(tracksInFile, artist, album, file); + var newPath = _filenameBuilder.BuildTrackFilePath(artist, album, newName, Path.GetExtension(trackFilePath)); + + if (!trackFilePath.PathEquals(newPath, StringComparison.Ordinal)) + { + yield return new RenameTrackFilePreview + { + ArtistId = artist.Id, + AlbumId = albumId, + TrackNumbers = tracksInFile.Select(e => e.TrackNumber).ToList(), + TrackFileId = file.Id, + ExistingPath = file.RelativePath, + NewPath = artist.Path.GetRelativePath(newPath) + }; + } + } + } + + private void RenameFiles(List trackFiles, Artist artist) + { + // TODO + throw new NotImplementedException(); + //var renamed = new List(); + + //foreach (var trackFile in trackFiles) + //{ + // var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + + // try + // { + // _logger.Debug("Renaming track file: {0}", trackFile); + // _trackFileMover.MoveTrackFile(trackFile, artist); + + // _mediaFileService.Update(trackFile); + // renamed.Add(trackFile); + + // _logger.Debug("Renamed track file: {0}", trackFile); + // } + // catch (SameFilenameException ex) + // { + // _logger.Debug("File not renamed, source and destination are the same: {0}", ex.Filename); + // } + // catch (Exception ex) + // { + // _logger.Error(ex, "Failed to rename file {0}", trackFilePath); + // } + //} + + //if (renamed.Any()) + //{ + // _diskProvider.RemoveEmptySubfolders(artist.Path); + + // _eventAggregator.PublishEvent(new ArtistRenamedEvent(artist)); + //} + } + + public void Execute(RenameFilesCommand message) + { + // TODO + throw new NotImplementedException(); + //var artist = _artistService.GetArtist(message.ArtistId); + //var trackFiles = _mediaFileService.Get(message.Files); + + //_logger.ProgressInfo("Renaming {0} files for {1}", trackFiles.Count, artist.Title); + //RenameFiles(trackFiles, artist); + //_logger.ProgressInfo("Selected track files renamed for {0}", artist.Title); + } + + public void Execute(RenameArtistCommand message) + { + // TODO + throw new NotImplementedException(); + //_logger.Debug("Renaming all files for selected artist"); + //var artistToRename = _artistService.GetArtist(message.ArtistIds); + + //foreach (var artist in artistToRename) + //{ + // var trackFiles = _mediaFileService.GetFilesByArtist(artist.Id); + // _logger.ProgressInfo("Renaming all files in artist: {0}", artist.Title); + // RenameFiles(trackFiles, artist); + // _logger.ProgressInfo("All track files renamed for {0}", artist.Title); + //} + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs index 9c2df9875..9aab9a655 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -12,8 +12,10 @@ namespace NzbDrone.Core.MediaFiles { public class TrackFile : ModelBase { - public int ItunesTrackId { get; set; } + //public string ForeignTrackId { get; set; } + //public string ForeignArtistId { get; set; } public int AlbumId { get; set; } + public int ArtistId { get; set; } public string RelativePath { get; set; } public string Path { get; set; } public long Size { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs new file mode 100644 index 000000000..8b4b3d854 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles +{ + public class TrackFileMoveResult + { + public TrackFileMoveResult() + { + OldFiles = new List(); + } + + public TrackFile TrackFile { get; set; } + public List OldFiles { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs new file mode 100644 index 000000000..5500f6007 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -0,0 +1,226 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IMoveTrackFiles + { + TrackFile MoveTrackFile(TrackFile trackFile, Artist artist); + TrackFile MoveTrackFile(TrackFile trackFile, LocalTrack localTrack); + TrackFile CopyTrackFile(TrackFile trackFile, LocalTrack localTrack); + } + + public class TrackFileMovingService : IMoveTrackFiles + { + private readonly ITrackService _trackService; + //private readonly IUpdateTrackFileService _updateTrackFileService; + private readonly IBuildFileNames _buildFileNames; + private readonly IDiskTransferService _diskTransferService; + private readonly IDiskProvider _diskProvider; + private readonly IMediaFileAttributeService _mediaFileAttributeService; + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public TrackFileMovingService(ITrackService episodeService, + //IUpdateEpisodeFileService updateEpisodeFileService, + IBuildFileNames buildFileNames, + IDiskTransferService diskTransferService, + IDiskProvider diskProvider, + IMediaFileAttributeService mediaFileAttributeService, + IEventAggregator eventAggregator, + IConfigService configService, + Logger logger) + { + _trackService = episodeService; + //_updateTrackFileService = updateEpisodeFileService; + _buildFileNames = buildFileNames; + _diskTransferService = diskTransferService; + _diskProvider = diskProvider; + _mediaFileAttributeService = mediaFileAttributeService; + _eventAggregator = eventAggregator; + _configService = configService; + _logger = logger; + } + + public TrackFile MoveTrackFile(TrackFile trackFile, Artist artist) + { + throw new System.NotImplementedException(); + // TODO + //var tracks = _trackService.GetTracksByFileId(trackFile.Id); + //var newFileName = _buildFileNames.BuildFileName(tracks, artist, trackFile); + //var filePath = _buildFileNames.BuildFilePath(artist, tracks.First(), trackFile.AlbumId, newFileName, Path.GetExtension(trackFile.RelativePath)); + + //EnsureAlbumFolder(trackFile, artist, tracks.Select(v => v.Album).First(), filePath); + + //_logger.Debug("Renaming track file: {0} to {1}", trackFile, filePath); + + //return TransferFile(trackFile, artist, tracks, filePath, TransferMode.Move); + } + + public TrackFile MoveTrackFile(TrackFile trackFile, LocalTrack localTrack) + { + // TODO + throw new System.NotImplementedException(); + //var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile); + //var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(localEpisode.Path)); + + //EnsureEpisodeFolder(episodeFile, localEpisode, filePath); + + //_logger.Debug("Moving episode file: {0} to {1}", episodeFile.Path, filePath); + + //return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.Move); + } + + public TrackFile CopyTrackFile(TrackFile trackFile, LocalTrack localTrack) + { + // TODO + throw new System.NotImplementedException(); + //var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile); + //var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(localEpisode.Path)); + + //EnsureEpisodeFolder(episodeFile, localEpisode, filePath); + + //if (_configService.CopyUsingHardlinks) + //{ + // _logger.Debug("Hardlinking episode file: {0} to {1}", episodeFile.Path, filePath); + // return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.HardLinkOrCopy); + //} + + //_logger.Debug("Copying episode file: {0} to {1}", episodeFile.Path, filePath); + //return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.Copy); + } + + private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List episodes, string destinationFilePath, TransferMode mode) + { + // TODO + throw new System.NotImplementedException(); + + //Ensure.That(episodeFile, () => episodeFile).IsNotNull(); + //Ensure.That(series, () => series).IsNotNull(); + //Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath(); + + //var episodeFilePath = episodeFile.Path ?? Path.Combine(series.Path, episodeFile.RelativePath); + + //if (!_diskProvider.FileExists(episodeFilePath)) + //{ + // throw new FileNotFoundException("Episode file path does not exist", episodeFilePath); + //} + + //if (episodeFilePath == destinationFilePath) + //{ + // throw new SameFilenameException("File not moved, source and destination are the same", episodeFilePath); + //} + + //_diskTransferService.TransferFile(episodeFilePath, destinationFilePath, mode); + + //episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath); + + //_updateTrackFileService.ChangeFileDateForFile(episodeFile, series, episodes); + + //try + //{ + // _mediaFileAttributeService.SetFolderLastWriteTime(series.Path, episodeFile.DateAdded); + + // if (series.SeasonFolder) + // { + // var seasonFolder = Path.GetDirectoryName(destinationFilePath); + + // _mediaFileAttributeService.SetFolderLastWriteTime(seasonFolder, episodeFile.DateAdded); + // } + //} + + //catch (Exception ex) + //{ + // _logger.Warn(ex, "Unable to set last write time"); + //} + + //_mediaFileAttributeService.SetFilePermissions(destinationFilePath); + + //return episodeFile; + } + + private void EnsureEpisodeFolder(EpisodeFile episodeFile, LocalEpisode localEpisode, string filePath) + { + EnsureEpisodeFolder(episodeFile, localEpisode.Series, localEpisode.SeasonNumber, filePath); + } + + private void EnsureEpisodeFolder(EpisodeFile episodeFile, Series series, int seasonNumber, string filePath) + { + var episodeFolder = Path.GetDirectoryName(filePath); + var seasonFolder = _buildFileNames.BuildSeasonPath(series, seasonNumber); + var seriesFolder = series.Path; + var rootFolder = new OsPath(seriesFolder).Directory.FullPath; + + if (!_diskProvider.FolderExists(rootFolder)) + { + throw new DirectoryNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + } + + var changed = false; + var newEvent = new EpisodeFolderCreatedEvent(series, episodeFile); + + if (!_diskProvider.FolderExists(seriesFolder)) + { + CreateFolder(seriesFolder); + newEvent.SeriesFolder = seriesFolder; + changed = true; + } + + if (seriesFolder != seasonFolder && !_diskProvider.FolderExists(seasonFolder)) + { + CreateFolder(seasonFolder); + newEvent.SeasonFolder = seasonFolder; + changed = true; + } + + if (seasonFolder != episodeFolder && !_diskProvider.FolderExists(episodeFolder)) + { + CreateFolder(episodeFolder); + newEvent.EpisodeFolder = episodeFolder; + changed = true; + } + + if (changed) + { + _eventAggregator.PublishEvent(newEvent); + } + } + + private void CreateFolder(string directoryName) + { + Ensure.That(directoryName, () => directoryName).IsNotNullOrWhiteSpace(); + + var parentFolder = new OsPath(directoryName).Directory.FullPath; + if (!_diskProvider.FolderExists(parentFolder)) + { + CreateFolder(parentFolder); + } + + try + { + _diskProvider.CreateFolder(directoryName); + } + catch (IOException ex) + { + _logger.Error(ex, "Unable to create directory: {0}", directoryName); + } + + _mediaFileAttributeService.SetFolderPermissions(directoryName); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs new file mode 100644 index 000000000..e38c27ca8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -0,0 +1,172 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.TrackImport +{ + public interface IImportApprovedTracks + { + List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto); + } + + public class ImportApprovedTracks : IImportApprovedTracks + { + private readonly IUpgradeMediaFiles _trackFileUpgrader; + private readonly IMediaFileService _mediaFileService; + //private readonly IExtraService _extraService; + private readonly IDiskProvider _diskProvider; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public ImportApprovedTracks(IUpgradeMediaFiles episodeFileUpgrader, + IMediaFileService mediaFileService, + //IExtraService extraService, + IDiskProvider diskProvider, + IEventAggregator eventAggregator, + Logger logger) + { + _trackFileUpgrader = episodeFileUpgrader; + _mediaFileService = mediaFileService; + // _extraService = extraService; + _diskProvider = diskProvider; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto) + { + var qualifiedImports = decisions.Where(c => c.Approved) + .GroupBy(c => c.LocalTrack.Artist.Id, (i, s) => s + .OrderByDescending(c => c.LocalTrack.Quality, new QualityModelComparer(s.First().LocalTrack.Artist.Profile)) + .ThenByDescending(c => c.LocalTrack.Size)) + .SelectMany(c => c) + .ToList(); + + var importResults = new List(); + + foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalTrack.Tracks.Select(track => track.TrackNumber).MinOrDefault()) + .ThenByDescending(e => e.LocalTrack.Size)) + { + var localTrack = importDecision.LocalTrack; + var oldFiles = new List(); + + try + { + //check if already imported + if (importResults.SelectMany(r => r.ImportDecision.LocalTrack.Tracks) + .Select(e => e.Id) + .Intersect(localTrack.Tracks.Select(e => e.Id)) + .Any()) + { + importResults.Add(new ImportResult(importDecision, "Track has already been imported")); + continue; + } + + var trackFile = new TrackFile(); + trackFile.DateAdded = DateTime.UtcNow; + trackFile.ArtistId = localTrack.Artist.Id; + trackFile.Path = localTrack.Path.CleanFilePath(); + trackFile.Size = _diskProvider.GetFileSize(localTrack.Path); + trackFile.Quality = localTrack.Quality; + trackFile.MediaInfo = localTrack.MediaInfo; + trackFile.AlbumId = localTrack.Album.Id; + trackFile.Tracks = localTrack.Tracks; + trackFile.ReleaseGroup = localTrack.ParsedTrackInfo.ReleaseGroup; + + bool copyOnly; + switch (importMode) + { + default: + case ImportMode.Auto: + copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly; + break; + case ImportMode.Move: + copyOnly = false; + break; + case ImportMode.Copy: + copyOnly = true; + break; + } + + if (newDownload) + { + //trackFile.SceneName = GetSceneName(downloadClientItem, localTrack); + + var moveResult = _trackFileUpgrader.UpgradeTrackFile(trackFile, localTrack, copyOnly); + oldFiles = moveResult.OldFiles; + } + else + { + trackFile.RelativePath = localTrack.Artist.Path.GetRelativePath(trackFile.Path); + } + + _mediaFileService.Add(trackFile); + importResults.Add(new ImportResult(importDecision)); + + //if (newDownload) + //{ + // _extraService.ImportExtraFiles(localTrack, trackFile, copyOnly); // TODO: Import Music Extras + //} + + if (downloadClientItem != null) + { + _eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); + } + else + { + _eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, newDownload)); + } + + if (newDownload) + { + _eventAggregator.PublishEvent(new TrackDownloadedEvent(localTrack, trackFile, oldFiles)); + } + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't import track " + localTrack); + importResults.Add(new ImportResult(importDecision, "Failed to import episode")); + } + } + + //Adding all the rejected decisions + importResults.AddRange(decisions.Where(c => !c.Approved) + .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray()))); + + return importResults; + } + + //private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode) + //{ + // if (downloadClientItem != null) + // { + // var title = Parser.Parser.RemoveFileExtension(downloadClientItem.Title); + + // var parsedTitle = Parser.Parser.ParseTitle(title); + + // if (parsedTitle != null && !parsedTitle.FullSeason) + // { + // return title; + // } + // } + + // var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath()); + + // if (SceneChecker.IsSceneTitle(fileName)) + // { + // return fileName; + // } + + // return null; + //} + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecision.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecision.cs new file mode 100644 index 000000000..885ec60b0 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecision.cs @@ -0,0 +1,26 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.TrackImport +{ + public class ImportDecision + { + public LocalTrack LocalTrack { get; private set; } + public IEnumerable Rejections { get; private set; } + + public bool Approved => Rejections.Empty(); + + public object LocalEpisode { get; internal set; } + + public ImportDecision(LocalTrack localTrack, params Rejection[] rejections) + { + LocalTrack = localTrack; + Rejections = rejections.ToList(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportMode.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportMode.cs similarity index 66% rename from src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportMode.cs rename to src/NzbDrone.Core/MediaFiles/TrackImport/ImportMode.cs index ffdf7eed8..43236bc86 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportMode.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportMode.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.MediaFiles.EpisodeImport +namespace NzbDrone.Core.MediaFiles.TrackImport { public enum ImportMode { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportResult.cs similarity index 85% rename from src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs rename to src/NzbDrone.Core/MediaFiles/TrackImport/ImportResult.cs index a0d989335..64f60869b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportResult.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using NzbDrone.Common.EnsureThat; +using System; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Common.EnsureThat; +using System.Text; -namespace NzbDrone.Core.MediaFiles.EpisodeImport +namespace NzbDrone.Core.MediaFiles.TrackImport { public class ImportResult { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResultType.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportResultType.cs similarity index 66% rename from src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResultType.cs rename to src/NzbDrone.Core/MediaFiles/TrackImport/ImportResultType.cs index 7c43332de..e4e3eaef8 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResultType.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportResultType.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.MediaFiles.EpisodeImport +namespace NzbDrone.Core.MediaFiles.TrackImport { public enum ImportResultType { diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index d6c270d2c..0e952a676 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -9,7 +9,8 @@ namespace NzbDrone.Core.MediaFiles { public interface IUpgradeMediaFiles { - EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); + //EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); + TrackFileMoveResult UpgradeTrackFile(TrackFile trackFile, LocalTrack localTrack, bool copyOnly = false); } public class UpgradeMediaFileService : IUpgradeMediaFiles @@ -17,35 +18,36 @@ namespace NzbDrone.Core.MediaFiles private readonly IRecycleBinProvider _recycleBinProvider; private readonly IMediaFileService _mediaFileService; private readonly IMoveEpisodeFiles _episodeFileMover; + private readonly IMoveTrackFiles _trackFileMover; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider, IMediaFileService mediaFileService, - IMoveEpisodeFiles episodeFileMover, + IMoveTrackFiles trackFileMover, IDiskProvider diskProvider, Logger logger) { _recycleBinProvider = recycleBinProvider; _mediaFileService = mediaFileService; - _episodeFileMover = episodeFileMover; + _trackFileMover = trackFileMover; _diskProvider = diskProvider; _logger = logger; } - public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false) + public TrackFileMoveResult UpgradeTrackFile(TrackFile trackFile, LocalTrack localTrack, bool copyOnly = false) { - var moveFileResult = new EpisodeFileMoveResult(); - var existingFiles = localEpisode.Episodes - .Where(e => e.EpisodeFileId > 0) - .Select(e => e.EpisodeFile.Value) + var moveFileResult = new TrackFileMoveResult(); + var existingFiles = localTrack.Tracks + .Where(e => e.TrackFileId > 0) + .Select(e => e.TrackFile.Value) .GroupBy(e => e.Id); foreach (var existingFile in existingFiles) { var file = existingFile.First(); - var episodeFilePath = Path.Combine(localEpisode.Series.Path, file.RelativePath); - var subfolder = _diskProvider.GetParentFolder(localEpisode.Series.Path).GetRelativePath(_diskProvider.GetParentFolder(episodeFilePath)); + var episodeFilePath = Path.Combine(localTrack.Artist.Path, file.RelativePath); + var subfolder = _diskProvider.GetParentFolder(localTrack.Artist.Path).GetRelativePath(_diskProvider.GetParentFolder(episodeFilePath)); if (_diskProvider.FileExists(episodeFilePath)) { @@ -59,11 +61,11 @@ namespace NzbDrone.Core.MediaFiles if (copyOnly) { - moveFileResult.EpisodeFile = _episodeFileMover.CopyEpisodeFile(episodeFile, localEpisode); + moveFileResult.TrackFile = _trackFileMover.CopyTrackFile(trackFile, localTrack); } else { - moveFileResult.EpisodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, localEpisode); + moveFileResult.TrackFile = _trackFileMover.MoveTrackFile(trackFile, localTrack); } return moveFileResult; diff --git a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs index 76810655a..139d70a69 100644 --- a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs +++ b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs @@ -6,6 +6,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook { public interface IProvideArtistInfo { - Tuple> GetArtistInfo(string spotifyId); + Tuple> GetArtistInfo(string lidarrId); } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumInfoResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs similarity index 59% rename from src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumInfoResource.cs rename to src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs index 7f478ea3c..5e5a1f13d 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumInfoResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs @@ -5,22 +5,24 @@ using System.Text; namespace NzbDrone.Core.MetadataSource.SkyHook.Resource { - public class AlbumInfoResource + public class AlbumResource { - public AlbumInfoResource() + public AlbumResource() { - + Tracks = new List(); } //public string AlbumType { get; set; } // Might need to make this a separate class - public List Artists { get; set; } // Will always be length of 1 unless a compilation + public List Artists { get; set; } // Will always be length of 1 unless a compilation public string Url { get; set; } // Link to the endpoint api to give full info for this object public string Id { get; set; } // This is a unique Album ID. Needed for all future API calls - public int Year { get; set; } + public DateTime ReleaseDate { get; set; } public List Images { get; set; } - public string AlbumName { get; set; } // In case of a takedown, this may be empty + public string Title { get; set; } // In case of a takedown, this may be empty public string Overview { get; set; } public List Genres { get; set; } public string Label { get; set; } + public string Type { get; set; } + public List Tracks { get; set; } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs index d85d675a4..f2ab72f94 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -5,47 +5,19 @@ using System.Text; namespace NzbDrone.Core.MetadataSource.SkyHook.Resource { - - public class AristResultResource - { - public AristResultResource() - { - - } - - public List Items { get; set; } - public int Count { get; set; } - } - - public class AlbumResultResource - { - public AlbumResultResource() - { - - } - - public List Items { get; set; } - public int Count { get; set; } - } - - public class TrackResultResource - { - public TrackResultResource() - { - - } - - public List Items { get; set; } - public int Count { get; set; } - } public class ArtistResource { - public ArtistResource() - { - + public ArtistResource() { + Albums = new List(); } - public AristResultResource Artists { get; set; } - public AristResultResource Albums { get; set; } + public List Genres { get; set; } + public string AristUrl { get; set; } + public string Overview { get; set; } + public string Id { get; set; } + public List Images { get; set; } + public string ArtistName { get; set; } + public List Albums { get; set; } + } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackInfoResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs similarity index 78% rename from src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackInfoResource.cs rename to src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs index 3bdf71977..eeb86ba48 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackInfoResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs @@ -5,9 +5,9 @@ using System.Text; namespace NzbDrone.Core.MetadataSource.SkyHook.Resource { - public class TrackInfoResource + public class TrackResource { - public TrackInfoResource() + public TrackResource() { } @@ -19,7 +19,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public string TrackName { get; set; } public int TrackNumber { get; set; } public bool Explicit { get; set; } - public List Artists { get; set; } + public List Artists { get; set; } } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 093fb88e5..dec8c8c9b 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -74,14 +74,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } - public Tuple> GetArtistInfo(string spotifyId) + public Tuple> GetArtistInfo(string foreignArtistId) { - _logger.Debug("Getting Artist with SpotifyId of {0}", spotifyId); + _logger.Debug("Getting Artist with LidarrAPI.MetadataID of {0}", foreignArtistId); // We need to perform a direct lookup of the artist var httpRequest = _requestBuilder.Create() - .SetSegment("route", "artists/" + spotifyId) + .SetSegment("route", "artist/" + foreignArtistId) //.SetSegment("route", "search") //.AddQueryParam("type", "artist,album") //.AddQueryParam("q", spotifyId.ToString()) @@ -99,7 +99,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook { if (httpResponse.StatusCode == HttpStatusCode.NotFound) { - throw new ArtistNotFoundException(spotifyId); + throw new ArtistNotFoundException(foreignArtistId); } else { @@ -108,92 +108,95 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } // It is safe to assume an id will only return one Artist back - Artist artist = new Artist(); - artist.ArtistName = httpResponse.Resource.Artists.Items[0].ArtistName; - artist.SpotifyId = httpResponse.Resource.Artists.Items[0].Id; - artist.Genres = httpResponse.Resource.Artists.Items[0].Genres; + var albums = httpResponse.Resource.Albums.Select(MapAlbum); + var artist = MapArtist(httpResponse.Resource); - var albumRet = MapAlbums(artist); - artist = albumRet.Item1; + //artist.Name = httpResponse.Resource.Artists.Items[0].ArtistName; + //artist.ForeignArtistId = httpResponse.Resource.Artists.Items[0].Id; + //artist.Genres = httpResponse.Resource.Artists.Items[0].Genres; + //var albumRet = MapAlbums(artist); + //artist = albumRet.Item1; - return new Tuple>(albumRet.Item1, albumRet.Item2); + return new Tuple>(artist, albums.ToList()); } - private Tuple> MapAlbums(Artist artist) - { - - // Find all albums for the artist and all tracks for said album - ///v1/artists/{id}/albums - var httpRequest = _requestBuilder.Create() - .SetSegment("route", "artists/" + artist.SpotifyId + "/albums") - .Build(); - httpRequest.AllowAutoRedirect = true; - httpRequest.SuppressHttpError = true; - - var httpResponse = _httpClient.Get(httpRequest); - - if (httpResponse.HasHttpError) - { - throw new HttpException(httpRequest, httpResponse); - } - - List masterTracks = new List(); - List albums = new List(); - foreach(var albumResource in httpResponse.Resource.Items) - { - Album album = new Album(); - album.AlbumId = albumResource.Id; - album.Title = albumResource.AlbumName; - album.ArtworkUrl = albumResource.Images.Count > 0 ? albumResource.Images[0].Url : ""; - album.Tracks = MapTracksToAlbum(album); - masterTracks.InsertRange(masterTracks.Count, album.Tracks); - albums.Add(album); - } - - // TODO: We now need to get all tracks for each album - - artist.Albums = albums; - return new Tuple>(artist, masterTracks); - } - - private List MapTracksToAlbum(Album album) - { - var httpRequest = _requestBuilder.Create() - .SetSegment("route", "albums/" + album.AlbumId + "/tracks") - .Build(); - - httpRequest.AllowAutoRedirect = true; - httpRequest.SuppressHttpError = true; - var httpResponse = _httpClient.Get(httpRequest); - if (httpResponse.HasHttpError) - { - throw new HttpException(httpRequest, httpResponse); - } + //private Tuple> MapAlbums(Artist artist) + //{ - List tracks = new List(); - foreach(var trackResource in httpResponse.Resource.Items) - { - Track track = new Track(); - track.AlbumId = album.AlbumId; - //track.Album = album; // This will cause infinite loop when trying to serialize. - // TODO: Implement more track mapping - //track.Artist = trackResource.Artists - //track.ArtistId = album. - track.SpotifyTrackId = trackResource.Id; - track.ArtistSpotifyId = trackResource.Artists.Count > 0 ? trackResource.Artists[0].Id : null; - track.Explict = trackResource.Explicit; - track.Compilation = trackResource.Artists.Count > 1; - track.TrackNumber = trackResource.TrackNumber; - track.Title = trackResource.TrackName; - tracks.Add(track); - } + // // Find all albums for the artist and all tracks for said album + // ///v1/artists/{id}/albums + // var httpRequest = _requestBuilder.Create() + // .SetSegment("route", "artists/" + artist.ForeignArtistId + "/albums") + // .Build(); + // httpRequest.AllowAutoRedirect = true; + // httpRequest.SuppressHttpError = true; + + // var httpResponse = _httpClient.Get(httpRequest); + + // if (httpResponse.HasHttpError) + // { + // throw new HttpException(httpRequest, httpResponse); + // } + + // List masterTracks = new List(); + // List albums = new List(); + // foreach(var albumResource in httpResponse.Resource.Items) + // { + // Album album = new Album(); + // album.AlbumId = albumResource.Id; + // album.Title = albumResource.AlbumName; + // album.ArtworkUrl = albumResource.Images.Count > 0 ? albumResource.Images[0].Url : ""; + // album.Tracks = MapTracksToAlbum(album); + // masterTracks.InsertRange(masterTracks.Count, album.Tracks); + // albums.Add(album); + // } + + // // TODO: We now need to get all tracks for each album + + // artist.Albums = albums; + // return new Tuple>(artist, masterTracks); + //} - return tracks; - } + //private List MapTracksToAlbum(Album album) + //{ + // var httpRequest = _requestBuilder.Create() + // .SetSegment("route", "albums/" + album.AlbumId + "/tracks") + // .Build(); + + // httpRequest.AllowAutoRedirect = true; + // httpRequest.SuppressHttpError = true; + + // var httpResponse = _httpClient.Get(httpRequest); + + // if (httpResponse.HasHttpError) + // { + // throw new HttpException(httpRequest, httpResponse); + // } + + // List tracks = new List(); + // foreach (var trackResource in httpResponse.Resource.Items) + // { + // Track track = new Track(); + // track.AlbumId = album.AlbumId; + // //track.Album = album; // This will cause infinite loop when trying to serialize. + // // TODO: Implement more track mapping + // //track.Artist = trackResource.Artists + // //track.ArtistId = album. + // track.SpotifyTrackId = trackResource.Id; + // track.ArtistSpotifyId = trackResource.Artists.Count > 0 ? trackResource.Artists[0].Id : null; + // track.Explict = trackResource.Explicit; + // track.Compilation = trackResource.Artists.Count > 1; + // track.TrackNumber = trackResource.TrackNumber; + // track.Title = trackResource.TrackName; + // tracks.Add(track); + // } + + // return tracks; + //} public List SearchForNewArtist(string title) @@ -203,7 +206,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var lowerTitle = title.ToLowerInvariant(); Console.WriteLine("Searching for " + lowerTitle); - if (lowerTitle.StartsWith("itunes:") || lowerTitle.StartsWith("itunesid:")) + if (lowerTitle.StartsWith("spotify:") || lowerTitle.StartsWith("spotifyid:")) { var slug = lowerTitle.Split(':')[1].Trim(); @@ -224,18 +227,32 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var httpRequest = _requestBuilder.Create() .SetSegment("route", "search") - .AddQueryParam("type", "artist") // TODO: LidarrAPI.Metadata is getting , encoded. Needs to be raw , + .AddQueryParam("type", "artist") .AddQueryParam("query", title.ToLower().Trim()) .Build(); - var httpResponse = _httpClient.Get(httpRequest); + var httpResponse = _httpClient.Get>(httpRequest); + + return httpResponse.Resource.SelectList(MapArtist); + //List artists = MapArtists(httpResponse.Resource); + //List artists = new List(); + //foreach (var artistResource in httpResponse.Resource.Artists.Items) + //{ + // Artist artist = new Artist(); + // artist.Name = artistResource.ArtistName; + // artist.ForeignArtistId = artistResource.Id; // TODO: Rename spotifyId to LidarrId + // artist.Genres = artistResource.Genres; + // artist.NameSlug = Parser.Parser.CleanArtistTitle(artist.Name); + // artist.CleanName = Parser.Parser.CleanArtistTitle(artist.Name); + //artist.Images = artistResource.Images; + // artists.Add(artist); + //} - List artists = MapArtists(httpResponse.Resource); - return artists; + //return artists; } catch (HttpException) { @@ -248,27 +265,50 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - private List MapArtists(ArtistResource resource) + private static Album MapAlbum(AlbumResource resource) { + Album album = new Album(); + album.Title = resource.Title; + album.ForeignAlbumId = resource.Id; + album.ReleaseDate = resource.ReleaseDate; + album.CleanTitle = Parser.Parser.CleanArtistTitle(album.Title); + album.AlbumType = resource.Type; + + var tracks = resource.Tracks.Select(MapTrack); + album.Tracks = tracks.ToList(); - List artists = new List(); - foreach(var artistResource in resource.Artists.Items) - { - Artist artist = new Artist(); - artist.ArtistName = artistResource.ArtistName; - artist.SpotifyId = artistResource.Id; // TODO: Rename spotifyId to LidarrId - artist.Genres = artistResource.Genres; - artist.ArtistSlug = Parser.Parser.CleanArtistTitle(artist.ArtistName); - //artist.Images = artistResource.Images; - - artists.Add(artist); - } + return album; + } + + private static Track MapTrack(TrackResource resource) + { + Track track = new Track(); + track.Title = resource.TrackName; + track.ForeignTrackId = resource.Id; + track.TrackNumber = resource.TrackNumber; + return track; + } + + private static Artist MapArtist(ArtistResource resource) + { + + Artist artist = new Artist(); + + artist.Name = resource.ArtistName; + artist.ForeignArtistId = resource.Id; + artist.Genres = resource.Genres; + artist.Overview = resource.Overview; + artist.NameSlug = Parser.Parser.CleanArtistTitle(artist.Name); + artist.CleanName = Parser.Parser.CleanArtistTitle(artist.Name); + //artist.Images = resource.Artists.Items[0].Images; + + // Maybe? Get all the albums for said artist - return artists; + return artist; } //private Album MapAlbum(AlbumResource albumQuery) diff --git a/src/NzbDrone.Core/Music/AddArtistService.cs b/src/NzbDrone.Core/Music/AddArtistService.cs index 2a1358b5b..3811764f0 100644 --- a/src/NzbDrone.Core/Music/AddArtistService.cs +++ b/src/NzbDrone.Core/Music/AddArtistService.cs @@ -48,11 +48,11 @@ namespace NzbDrone.Core.Music if (string.IsNullOrWhiteSpace(newArtist.Path)) { - var folderName = newArtist.ArtistName;// TODO: _fileNameBuilder.GetArtistFolder(newArtist); + var folderName = _fileNameBuilder.GetArtistFolder(newArtist); newArtist.Path = Path.Combine(newArtist.RootFolderPath, folderName); } - newArtist.CleanTitle = newArtist.ArtistName.CleanSeriesTitle(); + newArtist.CleanName = newArtist.Name.CleanArtistTitle(); //newArtist.SortTitle = ArtistNameNormalizer.Normalize(newArtist.ArtistName, newArtist.ItunesId); // There is no Sort Title newArtist.Added = DateTime.UtcNow; @@ -71,19 +71,19 @@ namespace NzbDrone.Core.Music private Artist AddSkyhookData(Artist newArtist) { - Tuple> tuple; + Tuple> tuple; try { - tuple = _artistInfo.GetArtistInfo(newArtist.SpotifyId); + tuple = _artistInfo.GetArtistInfo(newArtist.ForeignArtistId); } catch (ArtistNotFoundException) { - _logger.Error("SpotifyId {1} was not found, it may have been removed from Spotify.", newArtist.SpotifyId); + _logger.Error("LidarrId {1} was not found, it may have been removed from Lidarr.", newArtist.ForeignArtistId); throw new ValidationException(new List { - new ValidationFailure("SpotifyId", "An artist with this ID was not found", newArtist.SpotifyId) + new ValidationFailure("SpotifyId", "An artist with this ID was not found", newArtist.ForeignArtistId) }); } diff --git a/src/NzbDrone.Core/Music/AddArtistValidator.cs b/src/NzbDrone.Core/Music/AddArtistValidator.cs index ab789c2fc..bc860f09e 100644 --- a/src/NzbDrone.Core/Music/AddArtistValidator.cs +++ b/src/NzbDrone.Core/Music/AddArtistValidator.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Music .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator); - RuleFor(c => c.ArtistSlug).SetValidator(artistTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName + RuleFor(c => c.NameSlug).SetValidator(artistTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName } } } diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index 6d18a766b..ee88b2e02 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Tv; using System; using System.Collections.Generic; @@ -7,24 +8,34 @@ using System.Text; namespace NzbDrone.Core.Music { - public class Album : IEmbeddedDocument + public class Album : ModelBase { public Album() { Images = new List(); } - public string AlbumId { get; set; } + public string ForeignAlbumId { get; set; } + public int ArtistId { get; set; } public string Title { get; set; } // NOTE: This should be CollectionName in API - public int Year { get; set; } - public int TrackCount { get; set; } + public string CleanTitle { get; set; } + public DateTime ReleaseDate { get; set; } + //public int TrackCount { get; set; } + public string Path { get; set; } public List Tracks { get; set; } - public int DiscCount { get; set; } + //public int DiscCount { get; set; } public bool Monitored { get; set; } public List Images { get; set; } - public List Actors { get; set; } // These are band members. TODO: Refactor + //public List Actors { get; set; } // These are band members. TODO: Refactor public List Genres { get; set; } - public string ArtworkUrl { get; set; } - public string Explicitness { get; set; } + public String AlbumType { get; set; } //Turn this into a type similar to Series Type in TV + //public string ArtworkUrl { get; set; } + //public string Explicitness { get; set; } + public AddSeriesOptions AddOptions { get; set; } + + public override string ToString() + { + return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe()); + } } } diff --git a/src/NzbDrone.Core/Music/AlbumRepository.cs b/src/NzbDrone.Core/Music/AlbumRepository.cs new file mode 100644 index 000000000..1b072e19a --- /dev/null +++ b/src/NzbDrone.Core/Music/AlbumRepository.cs @@ -0,0 +1,46 @@ +using System.Linq; +using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Music +{ + public interface IAlbumRepository : IBasicRepository + { + bool AlbumPathExists(string path); + List GetAlbums(int artistId); + Album FindByName(string cleanTitle); + Album FindById(string spotifyId); + } + + public class AlbumRepository : BasicRepository, IAlbumRepository + { + public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + + public bool AlbumPathExists(string path) + { + return Query.Where(c => c.Path == path).Any(); + } + public List GetAlbums(int artistId) + { + return Query.Where(s => s.ArtistId == artistId).ToList(); + } + + public Album FindById(string foreignAlbumId) + { + return Query.Where(s => s.ForeignAlbumId == foreignAlbumId).SingleOrDefault(); + } + + public Album FindByName(string cleanTitle) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanTitle) + .SingleOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/Music/AlbumService.cs b/src/NzbDrone.Core/Music/AlbumService.cs new file mode 100644 index 000000000..8d470830c --- /dev/null +++ b/src/NzbDrone.Core/Music/AlbumService.cs @@ -0,0 +1,147 @@ +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Organizer; +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Parser; +using System.Text; +using System.IO; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Music +{ + public interface IAlbumService + { + Album GetAlbum(int albumid); + List GetAlbums(IEnumerable albumIds); + List GetAlbumsByArtist(int artistId); + Album AddAlbum(Album newAlbum); + Album FindById(string spotifyId); + Album FindByTitleInexact(string title); + void DeleteAlbum(int albumId, bool deleteFiles); + List GetAllAlbums(); + Album UpdateAlbum(Album album); + List UpdateAlbums(List album); + void InsertMany(List albums); + void UpdateMany(List albums); + void DeleteMany(List albums); + bool AlbumPathExists(string folder); + void RemoveAddOptions(Album album); + } + + public class AlbumService : IAlbumService + { + private readonly IAlbumRepository _albumRepository; + private readonly IEventAggregator _eventAggregator; + private readonly ITrackService _trackService; + private readonly IBuildFileNames _fileNameBuilder; + private readonly Logger _logger; + + public AlbumService(IAlbumRepository albumRepository, + IEventAggregator eventAggregator, + ITrackService trackService, + IBuildFileNames fileNameBuilder, + Logger logger) + { + _albumRepository = albumRepository; + _eventAggregator = eventAggregator; + _trackService = trackService; + _fileNameBuilder = fileNameBuilder; + _logger = logger; + } + + public Album AddAlbum(Album newAlbum) + { + _albumRepository.Insert(newAlbum); + _eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id))); + + return newAlbum; + } + + public bool AlbumPathExists(string folder) + { + return _albumRepository.AlbumPathExists(folder); + } + + public void DeleteAlbum(int albumId, bool deleteFiles) + { + var album = _albumRepository.Get(albumId); + _albumRepository.Delete(albumId); + _eventAggregator.PublishEvent(new AlbumDeletedEvent(album, deleteFiles)); + } + + public Album FindById(string spotifyId) + { + return _albumRepository.FindById(spotifyId); + } + + + + public Album FindByTitleInexact(string title) + { + throw new NotImplementedException(); + } + + public List GetAllAlbums() + { + return _albumRepository.All().ToList(); + } + + public Album GetAlbum(int albumId) + { + return _albumRepository.Get(albumId); + } + + public List GetAlbums(IEnumerable albumIds) + { + return _albumRepository.Get(albumIds).ToList(); + } + + public List GetAlbumsByArtist(int artistId) + { + return _albumRepository.GetAlbums(artistId).ToList(); + } + + public void RemoveAddOptions(Album album) + { + _albumRepository.SetFields(album, s => s.AddOptions); + } + + public void InsertMany(List albums) + { + _albumRepository.InsertMany(albums); + } + + public void UpdateMany(List albums) + { + _albumRepository.UpdateMany(albums); + } + + public void DeleteMany(List albums) + { + _albumRepository.DeleteMany(albums); + } + + public Album UpdateAlbum(Album album) + { + var storedAlbum = GetAlbum(album.Id); // Is it Id or iTunesId? + + var updatedAlbum = _albumRepository.Update(album); + _eventAggregator.PublishEvent(new AlbumEditedEvent(updatedAlbum, storedAlbum)); + + return updatedAlbum; + } + + public List UpdateAlbums(List album) + { + _logger.Debug("Updating {0} album", album.Count); + + _albumRepository.UpdateMany(album); + _logger.Debug("{0} albums updated", album.Count); + + return album; + } + } +} diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 000aaf928..0ed7ff34e 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -22,21 +22,23 @@ namespace NzbDrone.Core.Music } - public string SpotifyId { get; set; } - public string ArtistName { get; set; } - public string ArtistSlug { get; set; } - public string CleanTitle { get; set; } + public string ForeignArtistId { get; set; } + public string MBId { get; set; } + public int TADBId { get; set; } + public int DiscogsId { get; set; } + public string AMId { get; set; } + public string Name { get; set; } + public string NameSlug { get; set; } + public string CleanName { get; set; } public string Overview { get; set; } public bool Monitored { get; set; } public bool AlbumFolder { get; set; } - public bool ArtistFolder { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime? LastDiskSync { get; set; } public int Status { get; set; } // TODO: Figure out what this is, do we need it? public string Path { get; set; } public List Images { get; set; } public List Genres { get; set; } - public int QualityProfileId { get; set; } public string RootFolderPath { get; set; } public DateTime Added { get; set; } public LazyLoaded Profile { get; set; } @@ -47,16 +49,20 @@ namespace NzbDrone.Core.Music public override string ToString() { - return string.Format("[{0}][{1}]", SpotifyId, ArtistName.NullSafe()); + return string.Format("[{0}][{1}]", ForeignArtistId, Name.NullSafe()); } public void ApplyChanges(Artist otherArtist) { - SpotifyId = otherArtist.SpotifyId; - ArtistName = otherArtist.ArtistName; - ArtistSlug = otherArtist.ArtistSlug; - CleanTitle = otherArtist.CleanTitle; + ForeignArtistId = otherArtist.ForeignArtistId; + MBId = otherArtist.MBId; + TADBId = otherArtist.TADBId; + DiscogsId = otherArtist.DiscogsId; + AMId = otherArtist.AMId; + Name = otherArtist.Name; + NameSlug = otherArtist.NameSlug; + CleanName = otherArtist.CleanName; Monitored = otherArtist.Monitored; AlbumFolder = otherArtist.AlbumFolder; LastInfoSync = otherArtist.LastInfoSync; @@ -69,7 +75,6 @@ namespace NzbDrone.Core.Music ProfileId = otherArtist.ProfileId; Albums = otherArtist.Albums; Tags = otherArtist.Tags; - ArtistFolder = otherArtist.ArtistFolder; AddOptions = otherArtist.AddOptions; Albums = otherArtist.Albums; diff --git a/src/NzbDrone.Core/Music/ArtistRepository.cs b/src/NzbDrone.Core/Music/ArtistRepository.cs index 0da04ad0d..907af1662 100644 --- a/src/NzbDrone.Core/Music/ArtistRepository.cs +++ b/src/NzbDrone.Core/Music/ArtistRepository.cs @@ -24,16 +24,16 @@ namespace NzbDrone.Core.Music return Query.Where(c => c.Path == path).Any(); } - public Artist FindById(string spotifyId) + public Artist FindById(string foreignArtistId) { - return Query.Where(s => s.SpotifyId == spotifyId).SingleOrDefault(); + return Query.Where(s => s.ForeignArtistId == foreignArtistId).SingleOrDefault(); } public Artist FindByName(string cleanName) { cleanName = cleanName.ToLowerInvariant(); - return Query.Where(s => s.CleanTitle == cleanName) + return Query.Where(s => s.CleanName == cleanName) .SingleOrDefault(); } } diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index bedf41f74..1297a42e9 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -89,11 +89,12 @@ namespace NzbDrone.Core.Music return _artistRepository.All().ToList(); } - public Artist GetArtist(int artistId) + public Artist GetArtist(int artistDBId) { - return _artistRepository.Get(artistId); + return _artistRepository.Get(artistDBId); } + public List GetArtists(IEnumerable artistIds) { return _artistRepository.Get(artistIds).ToList(); @@ -110,11 +111,11 @@ namespace NzbDrone.Core.Music foreach (var album in artist.Albums) { - var storedAlbum = storedArtist.Albums.SingleOrDefault(s => s.AlbumId == album.AlbumId); + var storedAlbum = storedArtist.Albums.SingleOrDefault(s => s.ForeignAlbumId == album.ForeignAlbumId); if (storedAlbum != null && album.Monitored != storedAlbum.Monitored) { - _trackService.SetTrackMonitoredByAlbum(artist.SpotifyId, album.AlbumId, album.Monitored); + _trackService.SetTrackMonitoredByAlbum(artist.Id, album.Id, album.Monitored); } } @@ -129,17 +130,17 @@ namespace NzbDrone.Core.Music _logger.Debug("Updating {0} artist", artist.Count); foreach (var s in artist) { - _logger.Trace("Updating: {0}", s.ArtistName); + _logger.Trace("Updating: {0}", s.Name); if (!s.RootFolderPath.IsNullOrWhiteSpace()) { var folderName = new DirectoryInfo(s.Path).Name; s.Path = Path.Combine(s.RootFolderPath, folderName); - _logger.Trace("Changing path for {0} to {1}", s.ArtistName, s.Path); + _logger.Trace("Changing path for {0} to {1}", s.Name, s.Path); } else { - _logger.Trace("Not changing path for: {0}", s.ArtistName); + _logger.Trace("Not changing path for: {0}", s.Name); } } diff --git a/src/NzbDrone.Core/Music/ArtistSlugValidator.cs b/src/NzbDrone.Core/Music/ArtistSlugValidator.cs index 4d5626c89..1d894fee2 100644 --- a/src/NzbDrone.Core/Music/ArtistSlugValidator.cs +++ b/src/NzbDrone.Core/Music/ArtistSlugValidator.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Music dynamic instance = context.ParentContext.InstanceToValidate; var instanceId = (int)instance.Id; - return !_artistService.GetAllArtists().Exists(s => s.ArtistSlug.Equals(context.PropertyValue.ToString()) && s.Id != instanceId); + return !_artistService.GetAllArtists().Exists(s => s.NameSlug.Equals(context.PropertyValue.ToString()) && s.Id != instanceId); } } } diff --git a/src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs b/src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs new file mode 100644 index 000000000..985cc419a --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs @@ -0,0 +1,18 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class AlbumAddedEvent : IEvent + { + public Album Album { get; private set; } + + public AlbumAddedEvent(Album album) + { + Album = album; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/AlbumDeletedEvent.cs b/src/NzbDrone.Core/Music/Events/AlbumDeletedEvent.cs new file mode 100644 index 000000000..28548116c --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/AlbumDeletedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class AlbumDeletedEvent : IEvent + { + public Album Album { get; private set; } + public bool DeleteFiles { get; private set; } + + public AlbumDeletedEvent(Album album, bool deleteFiles) + { + Album = album; + DeleteFiles = deleteFiles; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/AlbumEditedEvent.cs b/src/NzbDrone.Core/Music/Events/AlbumEditedEvent.cs new file mode 100644 index 000000000..aed8f155e --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/AlbumEditedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class AlbumEditedEvent : IEvent + { + public Album Album { get; private set; } + public Album OldAlbum { get; private set; } + + public AlbumEditedEvent(Album album, Album oldAlbum) + { + Album = album; + OldAlbum = oldAlbum; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs b/src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs new file mode 100644 index 000000000..5fdb3f539 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs @@ -0,0 +1,23 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class AlbumInfoRefreshedEvent : IEvent + { + public Artist Artist { get; set; } + public ReadOnlyCollection Added { get; private set; } + public ReadOnlyCollection Updated { get; private set; } + + public AlbumInfoRefreshedEvent(Artist artist, IList added, IList updated) + { + Artist = artist; + Added = new ReadOnlyCollection(added); + Updated = new ReadOnlyCollection(updated); + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs b/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs index 99661c480..ef6eb1a8b 100644 --- a/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs +++ b/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs @@ -9,13 +9,13 @@ namespace NzbDrone.Core.Music.Events { public class TrackInfoRefreshedEvent : IEvent { - public Artist Artist { get; set; } + public Album Album { get; set; } public ReadOnlyCollection Added { get; private set; } public ReadOnlyCollection Updated { get; private set; } - public TrackInfoRefreshedEvent(Artist artist, IList added, IList updated) + public TrackInfoRefreshedEvent(Album album, IList added, IList updated) { - Artist = artist; + Album = album; Added = new ReadOnlyCollection(added); Updated = new ReadOnlyCollection(updated); } diff --git a/src/NzbDrone.Core/Music/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/RefreshAlbumService.cs new file mode 100644 index 000000000..1fbb8f6a8 --- /dev/null +++ b/src/NzbDrone.Core/Music/RefreshAlbumService.cs @@ -0,0 +1,133 @@ +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using NzbDrone.Core.Organizer; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public interface IRefreshAlbumService + { + void RefreshAlbumInfo(Artist artist, IEnumerable remoteAlbums); + } + + public class RefreshAlbumService : IRefreshAlbumService + { + private readonly IAlbumService _albumService; + private readonly IRefreshTrackService _refreshTrackService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public RefreshAlbumService(IAlbumService albumService, IRefreshTrackService refreshTrackService, IEventAggregator eventAggregator, Logger logger) + { + _albumService = albumService; + _refreshTrackService = refreshTrackService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public void RefreshAlbumInfo(Artist artist, IEnumerable remoteAlbums) + { + _logger.Info("Starting album info refresh for: {0}", artist); + var successCount = 0; + var failCount = 0; + + var existingAlbums = _albumService.GetAlbumsByArtist(artist.Id); + var albums = artist.Albums; + + var updateList = new List(); + var newList = new List(); + var dupeFreeRemoteAlbums = remoteAlbums.DistinctBy(m => new { m.ForeignAlbumId, m.ReleaseDate }).ToList(); + + foreach (var album in OrderAlbums(artist, dupeFreeRemoteAlbums)) + { + try + { + var albumToUpdate = GetAlbumToUpdate(artist, album, existingAlbums); + + if (albumToUpdate != null) + { + existingAlbums.Remove(albumToUpdate); + updateList.Add(albumToUpdate); + } + else + { + albumToUpdate = new Album(); + albumToUpdate.Monitored = artist.Monitored; + newList.Add(albumToUpdate); + //var folderName = _fileNameBuilder.GetAlbumFolder(albumToUpdate); //This likely does not belong here, need to create AddAlbumService + //albumToUpdate.Path = Path.Combine(newArtist.RootFolderPath, folderName); + } + + albumToUpdate.ForeignAlbumId = album.ForeignAlbumId; + albumToUpdate.CleanTitle = album.CleanTitle; + //albumToUpdate.TrackNumber = album.TrackNumber; + albumToUpdate.Title = album.Title ?? "Unknown"; + //albumToUpdate.AlbumId = album.AlbumId; + //albumToUpdate.Album = album.Album; + //albumToUpdate.Explicit = album.Explicit; + albumToUpdate.ArtistId = artist.Id; + albumToUpdate.Path = artist.Path + album.Title; + albumToUpdate.AlbumType = album.AlbumType; + //albumToUpdate.Compilation = album.Compilation; + + _refreshTrackService.RefreshTrackInfo(album, album.Tracks); + + + successCount++; + } + catch (Exception e) + { + _logger.Fatal(e, "An error has occurred while updating track info for artist {0}. {1}", artist, album); + failCount++; + } + } + + var allAlbums = new List(); + allAlbums.AddRange(newList); + allAlbums.AddRange(updateList); + + // TODO: See if anything needs to be done here + //AdjustMultiEpisodeAirTime(artist, allTracks); + //AdjustDirectToDvdAirDate(artist, allTracks); + + _albumService.DeleteMany(existingAlbums); + _albumService.UpdateMany(updateList); + _albumService.InsertMany(newList); + + _eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(artist, newList, updateList)); + + if (failCount != 0) + { + _logger.Info("Finished album refresh for artist: {0}. Successful: {1} - Failed: {2} ", + artist.Name, successCount, failCount); + } + else + { + _logger.Info("Finished album refresh for artist: {0}.", artist); + } + } + + private bool GetMonitoredStatus(Album album, IEnumerable artists) + { + var artist = artists.SingleOrDefault(c => c.Id == album.ArtistId); + return album == null || album.Monitored; + } + + + private Album GetAlbumToUpdate(Artist artist, Album album, List existingAlbums) + { + return existingAlbums.FirstOrDefault(e => e.ForeignAlbumId == album.ForeignAlbumId && e.ReleaseDate == album.ReleaseDate); + } + + private IEnumerable OrderAlbums(Artist artist, List albums) + { + return albums.OrderBy(e => e.ForeignAlbumId).ThenBy(e => e.ReleaseDate); + } + } +} + diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs index 314597c63..5b159bc9e 100644 --- a/src/NzbDrone.Core/Music/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Music { private readonly IProvideArtistInfo _artistInfo; private readonly IArtistService _artistService; - private readonly IRefreshTrackService _refreshTrackService; + private readonly IRefreshAlbumService _refreshAlbumService; private readonly IEventAggregator _eventAggregator; private readonly IDiskScanService _diskScanService; private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed; @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Music public RefreshArtistService(IProvideArtistInfo artistInfo, IArtistService artistService, - IRefreshTrackService refreshTrackService, + IRefreshAlbumService refreshAlbumService, IEventAggregator eventAggregator, IDiskScanService diskScanService, ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed, @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Music { _artistInfo = artistInfo; _artistService = artistService; - _refreshTrackService = refreshTrackService; + _refreshAlbumService = refreshAlbumService; _eventAggregator = eventAggregator; _diskScanService = diskScanService; _checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed; @@ -45,33 +45,33 @@ namespace NzbDrone.Core.Music private void RefreshArtistInfo(Artist artist) { - _logger.ProgressInfo("Updating Info for {0}", artist.ArtistName); + _logger.ProgressInfo("Updating Info for {0}", artist.Name); - Tuple> tuple; + Tuple> tuple; try { - tuple = _artistInfo.GetArtistInfo(artist.SpotifyId); + tuple = _artistInfo.GetArtistInfo(artist.ForeignArtistId); } catch (ArtistNotFoundException) { - _logger.Error("Artist '{0}' (SpotifyId {1}) was not found, it may have been removed from Spotify.", artist.ArtistName, artist.SpotifyId); + _logger.Error("Artist '{0}' (SpotifyId {1}) was not found, it may have been removed from Spotify.", artist.Name, artist.ForeignArtistId); return; } var artistInfo = tuple.Item1; - if (artist.SpotifyId != artistInfo.SpotifyId) + if (artist.ForeignArtistId != artistInfo.ForeignArtistId) { - _logger.Warn("Artist '{0}' (SpotifyId {1}) was replaced with '{2}' (SpotifyId {3}), because the original was a duplicate.", artist.ArtistName, artist.SpotifyId, artistInfo.ArtistName, artistInfo.SpotifyId); - artist.SpotifyId = artistInfo.SpotifyId; + _logger.Warn("Artist '{0}' (SpotifyId {1}) was replaced with '{2}' (SpotifyId {3}), because the original was a duplicate.", artist.Name, artist.ForeignArtistId, artistInfo.Name, artistInfo.ForeignArtistId); + artist.ForeignArtistId = artistInfo.ForeignArtistId; } - artist.ArtistName = artistInfo.ArtistName; - artist.ArtistSlug = artistInfo.ArtistSlug; + artist.Name = artistInfo.Name; + artist.NameSlug = artistInfo.NameSlug; artist.Overview = artistInfo.Overview; artist.Status = artistInfo.Status; - artist.CleanTitle = artistInfo.CleanTitle; + artist.CleanName = artistInfo.CleanName; artist.LastInfoSync = DateTime.UtcNow; artist.Images = artistInfo.Images; artist.Genres = artistInfo.Genres; @@ -89,19 +89,20 @@ namespace NzbDrone.Core.Music artist.Albums = UpdateAlbums(artist, artistInfo); _artistService.UpdateArtist(artist); - _refreshTrackService.RefreshTrackInfo(artist, tuple.Item2); + _refreshAlbumService.RefreshAlbumInfo(artist, tuple.Item2); + //_refreshTrackService.RefreshTrackInfo(artist, tuple.Item2); - _logger.Debug("Finished artist refresh for {0}", artist.ArtistName); + _logger.Debug("Finished artist refresh for {0}", artist.Name); _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); } private List UpdateAlbums(Artist artist, Artist artistInfo) { - var albums = artistInfo.Albums.DistinctBy(s => s.AlbumId).ToList(); + var albums = artistInfo.Albums.DistinctBy(s => s.ForeignAlbumId).ToList(); foreach (var album in albums) { - var existingAlbum = artist.Albums.FirstOrDefault(s => s.AlbumId == album.AlbumId); + var existingAlbum = artist.Albums.FirstOrDefault(s => s.ForeignAlbumId == album.ForeignAlbumId); //Todo: Should this should use the previous season's monitored state? if (existingAlbum == null) @@ -112,7 +113,7 @@ namespace NzbDrone.Core.Music // continue; //} - _logger.Debug("New album ({0}) for artist: [{1}] {2}, setting monitored to true", album.Title, artist.SpotifyId, artist.ArtistName); + _logger.Debug("New album ({0}) for artist: [{1}] {2}, setting monitored to true", album.Title, artist.ForeignArtistId, artist.Name); album.Monitored = true; } @@ -136,7 +137,7 @@ namespace NzbDrone.Core.Music } else { - var allArtists = _artistService.GetAllArtists().OrderBy(c => c.ArtistName).ToList(); + var allArtists = _artistService.GetAllArtists().OrderBy(c => c.Name).ToList(); foreach (var artist in allArtists) { @@ -156,8 +157,8 @@ namespace NzbDrone.Core.Music { try { - _logger.Info("Skipping refresh of artist: {0}", artist.ArtistName); - //TODO: _diskScanService.Scan(artist); + _logger.Info("Skipping refresh of artist: {0}", artist.Name); + _diskScanService.Scan(artist); } catch (Exception e) { diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs index 169582e3d..b1bd6900f 100644 --- a/src/NzbDrone.Core/Music/RefreshTrackService.cs +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Music { public interface IRefreshTrackService { - void RefreshTrackInfo(Artist artist, IEnumerable remoteTracks); + void RefreshTrackInfo(Album album, IEnumerable remoteTracks); } public class RefreshTrackService : IRefreshTrackService @@ -27,24 +27,23 @@ namespace NzbDrone.Core.Music _logger = logger; } - public void RefreshTrackInfo(Artist artist, IEnumerable remoteTracks) + public void RefreshTrackInfo(Album album, IEnumerable remoteTracks) { - _logger.Info("Starting track info refresh for: {0}", artist); + _logger.Info("Starting track info refresh for: {0}", album); var successCount = 0; var failCount = 0; - var existingTracks = _trackService.GetTracksByArtist(artist.SpotifyId); - var albums = artist.Albums; + var existingTracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); // TODO: JOE: I believe this should be string, string var updateList = new List(); var newList = new List(); var dupeFreeRemoteTracks = remoteTracks.DistinctBy(m => new { m.AlbumId, m.TrackNumber }).ToList(); - foreach (var track in OrderTracks(artist, dupeFreeRemoteTracks)) + foreach (var track in OrderTracks(album, dupeFreeRemoteTracks)) { try { - var trackToUpdate = GetTrackToUpdate(artist, track, existingTracks); + var trackToUpdate = GetTrackToUpdate(album, track, existingTracks); if (trackToUpdate != null) { @@ -54,24 +53,17 @@ namespace NzbDrone.Core.Music else { trackToUpdate = new Track(); - trackToUpdate.Monitored = GetMonitoredStatus(track, albums); + trackToUpdate.Monitored = album.Monitored; newList.Add(trackToUpdate); } - trackToUpdate.SpotifyTrackId = track.SpotifyTrackId; + trackToUpdate.ForeignTrackId = track.ForeignTrackId; trackToUpdate.TrackNumber = track.TrackNumber; trackToUpdate.Title = track.Title ?? "Unknown"; - trackToUpdate.AlbumId = track.AlbumId; + trackToUpdate.AlbumId = album.Id; trackToUpdate.Album = track.Album; - trackToUpdate.Explict = track.Explict; - if (track.ArtistSpotifyId.IsNullOrWhiteSpace()) - { - trackToUpdate.ArtistSpotifyId = artist.SpotifyId; - } else - { - trackToUpdate.ArtistSpotifyId = track.ArtistSpotifyId; - } - trackToUpdate.ArtistId = track.ArtistId; + trackToUpdate.Explicit = track.Explicit; + trackToUpdate.ArtistId = album.ArtistId; trackToUpdate.Compilation = track.Compilation; // TODO: Implement rest of [RefreshTrackService] fields @@ -82,7 +74,7 @@ namespace NzbDrone.Core.Music } catch (Exception e) { - _logger.Fatal(e, "An error has occurred while updating track info for artist {0}. {1}", artist, track); + _logger.Fatal(e, "An error has occurred while updating track info for album {0}. {1}", album, track); failCount++; } } @@ -99,16 +91,16 @@ namespace NzbDrone.Core.Music _trackService.UpdateMany(updateList); _trackService.InsertMany(newList); - _eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(artist, newList, updateList)); + _eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(album, newList, updateList)); if (failCount != 0) { - _logger.Info("Finished track refresh for artist: {0}. Successful: {1} - Failed: {2} ", - artist.ArtistName, successCount, failCount); + _logger.Info("Finished track refresh for album: {0}. Successful: {1} - Failed: {2} ", + album.Title, successCount, failCount); } else { - _logger.Info("Finished track refresh for artist: {0}.", artist); + _logger.Info("Finished track refresh for album: {0}.", album); } } @@ -119,17 +111,17 @@ namespace NzbDrone.Core.Music return false; } - var album = albums.SingleOrDefault(c => c.AlbumId == track.AlbumId); + var album = albums.SingleOrDefault(c => c.Id == track.AlbumId); return album == null || album.Monitored; } - private Track GetTrackToUpdate(Artist artist, Track track, List existingTracks) + private Track GetTrackToUpdate(Album album, Track track, List existingTracks) { return existingTracks.FirstOrDefault(e => e.AlbumId == track.AlbumId && e.TrackNumber == track.TrackNumber); } - private IEnumerable OrderTracks(Artist artist, List tracks) + private IEnumerable OrderTracks(Album album, List tracks) { return tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber); } diff --git a/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs b/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs index 669a1db3e..f38ccaaf5 100644 --- a/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs +++ b/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs @@ -26,13 +26,13 @@ namespace NzbDrone.Core.Music { if (artist.LastInfoSync < DateTime.UtcNow.AddDays(-30)) { - _logger.Trace("Artist {0} last updated more than 30 days ago, should refresh.", artist.ArtistName); + _logger.Trace("Artist {0} last updated more than 30 days ago, should refresh.", artist.Name); return true; } if (artist.LastInfoSync >= DateTime.UtcNow.AddHours(-6)) { - _logger.Trace("Artist {0} last updated less than 6 hours ago, should not be refreshed.", artist.ArtistName); + _logger.Trace("Artist {0} last updated less than 6 hours ago, should not be refreshed.", artist.Name); return false; } diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 599328c58..f0f25ae96 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -17,20 +17,20 @@ namespace NzbDrone.Core.Music public const string RELEASE_DATE_FORMAT = "yyyy-MM-dd"; - public string SpotifyTrackId { get; set; } - public string AlbumId { get; set; } - public LazyLoaded Artist { get; set; } - public string ArtistSpotifyId { get; set; } - public long ArtistId { get; set; } // This is the DB Id of the Artist, not the SpotifyId + public string ForeignTrackId { get; set; } + public int AlbumId { get; set; } + public Artist Artist { get; set; } + + public int ArtistId { get; set; } // This is the DB Id of the Artist, not the SpotifyId //public int CompilationId { get; set; } public bool Compilation { get; set; } public int TrackNumber { get; set; } public string Title { get; set; } - public bool Ignored { get; set; } - public bool Explict { get; set; } + //public bool Ignored { get; set; } + public bool Explicit { get; set; } public bool Monitored { get; set; } public int TrackFileId { get; set; } - public DateTime? ReleaseDate { get; set; } + //public DateTime? ReleaseDate { get; set; } public LazyLoaded TrackFile { get; set; } @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Music public override string ToString() { - return string.Format("[{0}]{1}", SpotifyTrackId, Title.NullSafe()); + return string.Format("[{0}]{1}", ForeignTrackId, Title.NullSafe()); } } } diff --git a/src/NzbDrone.Core/Music/TrackRepository.cs b/src/NzbDrone.Core/Music/TrackRepository.cs index 354d09afc..f4efe3ec6 100644 --- a/src/NzbDrone.Core/Music/TrackRepository.cs +++ b/src/NzbDrone.Core/Music/TrackRepository.cs @@ -13,15 +13,15 @@ namespace NzbDrone.Core.Music { public interface ITrackRepository : IBasicRepository { - Track Find(string artistId, string albumId, int trackNumber); - List GetTracks(string artistId); - List GetTracks(string artistId, string albumId); + Track Find(int artistId, int albumId, int trackNumber); + List GetTracks(int artistId); + List GetTracks(int artistId, int albumId); List GetTracksByFileId(int fileId); - List TracksWithFiles(string artistId); + List TracksWithFiles(int artistId); PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); PagingSpec TracksWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff); void SetMonitoredFlat(Track episode, bool monitored); - void SetMonitoredByAlbum(string artistId, string albumId, bool monitored); + void SetMonitoredByAlbum(int artistId, int albumId, bool monitored); void SetFileId(int trackId, int fileId); } @@ -37,23 +37,23 @@ namespace NzbDrone.Core.Music _logger = logger; } - public Track Find(string artistId, string albumId, int trackNumber) + public Track Find(int artistId, int albumId, int trackNumber) { - return Query.Where(s => s.ArtistSpotifyId == artistId) + return Query.Where(s => s.ArtistId == artistId) .AndWhere(s => s.AlbumId == albumId) .AndWhere(s => s.TrackNumber == trackNumber) .SingleOrDefault(); } - public List GetTracks(string artistId) + public List GetTracks(int artistId) { - return Query.Where(s => s.ArtistSpotifyId == artistId).ToList(); + return Query.Join(JoinType.Inner, s => s.Artist, (track, artist) => track.ArtistId == artist.Id).ToList(); } - public List GetTracks(string artistId, string albumId) + public List GetTracks(int artistId, int albumId) { - return Query.Where(s => s.ArtistSpotifyId == artistId) + return Query.Where(s => s.ArtistId == artistId) .AndWhere(s => s.AlbumId == albumId) .ToList(); } @@ -63,10 +63,10 @@ namespace NzbDrone.Core.Music return Query.Where(e => e.TrackFileId == fileId).ToList(); } - public List TracksWithFiles(string artistId) + public List TracksWithFiles(int artistId) { return Query.Join(JoinType.Inner, e => e.TrackFile, (e, ef) => e.TrackFileId == ef.Id) - .Where(e => e.ArtistSpotifyId == artistId); + .Where(e => e.ArtistId == artistId); } public PagingSpec TracksWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff) @@ -85,7 +85,7 @@ namespace NzbDrone.Core.Music SetFields(track, p => p.Monitored); } - public void SetMonitoredByAlbum(string artistId, string albumId, bool monitored) + public void SetMonitoredByAlbum(int artistId, int albumId, bool monitored) { var mapper = _database.GetDataMapper(); @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Music private SortBuilder GetMissingEpisodesQuery(PagingSpec pagingSpec, DateTime currentTime) { - return Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistSpotifyId == s.SpotifyId) + return Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistId == s.Id) .Where(pagingSpec.FilterExpression) .AndWhere(e => e.TrackFileId == 0) .AndWhere(BuildAirDateUtcCutoffWhereClause(currentTime)) @@ -130,7 +130,7 @@ namespace NzbDrone.Core.Music private SortBuilder EpisodesWhereCutoffUnmetQuery(PagingSpec pagingSpec, List qualitiesBelowCutoff) { - return Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistSpotifyId == s.SpotifyId) + return Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistId == s.Id) .Join(JoinType.Left, e => e.TrackFile, (e, s) => e.TrackFileId == s.Id) .Where(pagingSpec.FilterExpression) .AndWhere(e => e.TrackFileId != 0) diff --git a/src/NzbDrone.Core/Music/TrackService.cs b/src/NzbDrone.Core/Music/TrackService.cs index cf09139e5..39c065b0e 100644 --- a/src/NzbDrone.Core/Music/TrackService.cs +++ b/src/NzbDrone.Core/Music/TrackService.cs @@ -15,12 +15,12 @@ namespace NzbDrone.Core.Music { Track GetTrack(int id); List GetTracks(IEnumerable ids); - Track FindTrack(string artistId, string albumId, int trackNumber); - Track FindTrackByTitle(string artistId, string albumId, string releaseTitle); - List GetTracksByArtist(string artistId); - //List GetTracksByAlbum(string artistId, string albumId); + Track FindTrack(int artistId, int albumId, int trackNumber); + Track FindTrackByTitle(int artistId, int albumId, string releaseTitle); + List GetTracksByArtist(int artistId); + List GetTracksByAlbum(int artistId, int albumId); //List GetTracksByAlbumTitle(string artistId, string albumTitle); - List TracksWithFiles(string artistId); + List TracksWithFiles(int artistId); //PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); List GetTracksByFileId(int trackFileId); void UpdateTrack(Track track); @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Music void InsertMany(List tracks); void UpdateMany(List tracks); void DeleteMany(List tracks); - void SetTrackMonitoredByAlbum(string artistId, string albumId, bool monitored); + void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored); } public class TrackService : ITrackService @@ -55,22 +55,22 @@ namespace NzbDrone.Core.Music return _trackRepository.Get(ids).ToList(); } - public Track FindTrack(string artistId, string albumId, int episodeNumber) + public Track FindTrack(int artistId, int albumId, int trackNumber) { - return _trackRepository.Find(artistId, albumId, episodeNumber); + return _trackRepository.Find(artistId, albumId, trackNumber); } - public List GetTracksByArtist(string artistId) + public List GetTracksByArtist(int artistId) { return _trackRepository.GetTracks(artistId).ToList(); } - public List GetTracksByAlbum(string artistId, string albumId) + public List GetTracksByAlbum(int artistId, int albumId) { return _trackRepository.GetTracks(artistId, albumId); } - public Track FindTrackByTitle(string artistId, string albumId, string releaseTitle) + public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle) { // TODO: can replace this search mechanism with something smarter/faster/better var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " "); @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Music return null; } - public List TracksWithFiles(string artistId) + public List TracksWithFiles(int artistId) { return _trackRepository.TracksWithFiles(artistId); } @@ -127,12 +127,12 @@ namespace NzbDrone.Core.Music _logger.Debug("Monitored flag for Track:{0} was set to {1}", trackId, monitored); } - public void SetTrackMonitoredByAlbum(string artistId, string albumId, bool monitored) + public void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored) { _trackRepository.SetMonitoredByAlbum(artistId, albumId, monitored); } - public void UpdateEpisodes(List tracks) + public void UpdateTracks(List tracks) { _trackRepository.UpdateMany(tracks); } @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Music public void HandleAsync(ArtistDeletedEvent message) { - var tracks = GetTracksByArtist(message.Artist.SpotifyId); + var tracks = GetTracksByArtist(message.Artist.Id); _trackRepository.DeleteMany(tracks); } @@ -182,10 +182,5 @@ namespace NzbDrone.Core.Music _logger.Debug("Linking [{0}] > [{1}]", message.TrackFile.RelativePath, track); } } - - public void UpdateTracks(List tracks) - { - _trackRepository.UpdateMany(tracks); - } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 65494030b..963338f76 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -288,7 +288,6 @@ - @@ -725,7 +724,13 @@ - + + + + + + + @@ -740,12 +745,10 @@ - - - + @@ -761,11 +764,14 @@ + + + @@ -789,6 +795,9 @@ + + + @@ -816,7 +825,7 @@ - + @@ -825,7 +834,7 @@ - + @@ -857,17 +866,24 @@ + + + + + + + @@ -928,6 +944,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c85e72927..dd0bf3497 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -18,10 +18,15 @@ namespace NzbDrone.Core.Organizer public interface IBuildFileNames { string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildTrackFileName(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig namingConfig = null); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); + string BuildTrackFilePath(Artist artist, Album album, string fileName, string extension); string BuildSeasonPath(Series series, int seasonNumber); + string BuildAlbumPath(Artist artist, Album album); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); + string GetArtistFolder(Artist artist, NamingConfig namingConfig = null); + string GetAlbumFolder(Artist artist, Album album, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); // TODO: Implement Music functions @@ -42,6 +47,9 @@ namespace NzbDrone.Core.Organizer private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex TrackRegex = new Regex(@"(?\{track(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -59,6 +67,12 @@ namespace NzbDrone.Core.Organizer public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static readonly Regex ArtistNameRegex = new Regex(@"(?\{(?:Artist)(?[- ._])(Clean)?Name\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(Clean)?Title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); @@ -140,6 +154,47 @@ namespace NzbDrone.Core.Organizer return fileName; } + public string BuildTrackFileName(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameTracks) + { + return GetOriginalTitle(trackFile); + } + + if (namingConfig.StandardTrackFormat.IsNullOrWhiteSpace()) + { + throw new NamingFormatException("Standard track format cannot be empty"); + } + + var pattern = namingConfig.StandardTrackFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + tracks = tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber).ToList(); + + //pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); + + pattern = FormatTrackNumberTokens(pattern, "", tracks); + //pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + + AddArtistTokens(tokenHandlers, artist); + AddAlbumTokens(tokenHandlers, album); + AddTrackTokens(tokenHandlers, tracks); + AddTrackFileTokens(tokenHandlers, trackFile); + AddQualityTokens(tokenHandlers, artist, trackFile); + //AddMediaInfoTokens(tokenHandlers, trackFile); TODO ReWork MediaInfo for Tracks + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); @@ -149,6 +204,15 @@ namespace NzbDrone.Core.Organizer return Path.Combine(path, fileName + extension); } + public string BuildTrackFilePath(Artist artist, Album album, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = BuildAlbumPath(artist, album); + + return Path.Combine(path, fileName + extension); + } + public string BuildSeasonPath(Series series, int seasonNumber) { var path = series.Path; @@ -172,6 +236,24 @@ namespace NzbDrone.Core.Organizer return path; } + public string BuildAlbumPath(Artist artist, Album album) + { + var path = artist.Path; + + if (artist.AlbumFolder) + { + + var albumFolder = GetAlbumFolder(artist, album); + + albumFolder = CleanFileName(albumFolder); + + path = Path.Combine(path, albumFolder); + + } + + return path; + } + public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) { var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); @@ -232,6 +314,20 @@ namespace NzbDrone.Core.Organizer return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); } + public string GetArtistFolder(Artist artist, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddArtistTokens(tokenHandlers, artist); + + return CleanFolderName(ReplaceTokens(namingConfig.ArtistFolderFormat, tokenHandlers, namingConfig)); + } + public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) { if (namingConfig == null) @@ -247,6 +343,21 @@ namespace NzbDrone.Core.Organizer return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); } + public string GetAlbumFolder(Artist artist, Album album, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddAlbumTokens(tokenHandlers, album); + AddArtistTokens(tokenHandlers, artist); + + return CleanFolderName(ReplaceTokens(namingConfig.AlbumFolderFormat, tokenHandlers, namingConfig)); + } + public static string CleanTitle(string title) { title = title.Replace("&", "and"); @@ -284,8 +395,15 @@ namespace NzbDrone.Core.Organizer private void AddArtistTokens(Dictionary> tokenHandlers, Artist artist) { - tokenHandlers["{Artist Name}"] = m => artist.ArtistName; - tokenHandlers["{Artist CleanTitle}"] = m => CleanTitle(artist.ArtistName); + tokenHandlers["{Artist Name}"] = m => artist.Name; + tokenHandlers["{Artist CleanName}"] = m => CleanTitle(artist.Name); + } + + private void AddAlbumTokens(Dictionary> tokenHandlers, Album album) + { + tokenHandlers["{Album Title}"] = m => album.Title; + tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title); + tokenHandlers["{Release Year}"] = m => album.ReleaseDate.Year.ToString(); } private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) @@ -432,6 +550,12 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); } + private void AddTrackTokens(Dictionary> tokenHandlers, List tracks) + { + tokenHandlers["{Track Title}"] = m => GetTrackTitle(tracks, "+"); + tokenHandlers["{Track CleanTitle}"] = m => CleanTitle(GetTrackTitle(tracks, "and")); + } + private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); @@ -439,6 +563,13 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Lidarr"); } + private void AddTrackFileTokens(Dictionary> tokenHandlers, TrackFile trackFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(trackFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(trackFile); + tokenHandlers["{Release Group}"] = m => trackFile.ReleaseGroup ?? m.DefaultValue("Lidarr"); + } + private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) { var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; @@ -451,6 +582,18 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Quality Real}"] = m => qualityReal; } + private void AddQualityTokens(Dictionary> tokenHandlers, Artist artist, TrackFile trackFile) + { + var qualityTitle = _qualityDefinitionService.Get(trackFile.Quality.Quality).Title; + //var qualityProper = GetQualityProper(artist, trackFile.Quality); + //var qualityReal = GetQualityReal(artist, trackFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0}", qualityTitle); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + //tokenHandlers["{Quality Proper}"] = m => qualityProper; + //tokenHandlers["{Quality Real}"] = m => qualityReal; + } + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) { if (episodeFile.MediaInfo == null) return; @@ -646,6 +789,20 @@ namespace NzbDrone.Core.Organizer return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); } + private string FormatTrackNumberTokens(string basePattern, string formatPattern, List tracks) + { + var pattern = string.Empty; + + for (int i = 0; i < tracks.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += TrackRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["track"].Value, tracks[i].TrackNumber)); + } + + return pattern; + } + private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) { var pattern = string.Empty; @@ -728,6 +885,30 @@ namespace NzbDrone.Core.Organizer return string.Join(separator, titles); } + private string GetTrackTitle(List tracks, string separator) + { + separator = string.Format(" {0} ", separator.Trim()); + + if (tracks.Count == 1) + { + return tracks.First().Title.TrimEnd(EpisodeTitleTrimCharacters); + } + + var titles = tracks.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Select(CleanupEpisodeTitle) + .Distinct() + .ToList(); + + if (titles.All(t => t.IsNullOrWhiteSpace())) + { + titles = tracks.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Distinct() + .ToList(); + } + + return string.Join(separator, titles); + } + private string CleanupEpisodeTitle(string title) { //this will remove (1),(2) from the end of multi part episodes. @@ -769,6 +950,16 @@ namespace NzbDrone.Core.Organizer return episodeFile.SceneName; } + private string GetOriginalTitle(TrackFile trackFile) + { + if (trackFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(trackFile); + } + + return trackFile.SceneName; + } + private string GetOriginalFileName(EpisodeFile episodeFile) { if (episodeFile.RelativePath.IsNullOrWhiteSpace()) @@ -779,35 +970,16 @@ namespace NzbDrone.Core.Organizer return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); } - //public string GetArtistFolder(Artist artist, NamingConfig namingConfig = null) - //{ - // if (namingConfig == null) - // { - // namingConfig = _namingConfigService.GetConfig(); - // } - - // var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - // AddArtistTokens(tokenHandlers, artist); - - // return CleanFolderName(ReplaceTokens("{Artist Name}", tokenHandlers, namingConfig)); //namingConfig.ArtistFolderFormat, - //} - - //public string GetAlbumFolder(Artist artist, string albumName, NamingConfig namingConfig = null) - //{ - // throw new NotImplementedException(); - // //if (namingConfig == null) - // //{ - // // namingConfig = _namingConfigService.GetConfig(); - // //} - - // //var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + private string GetOriginalFileName(TrackFile trackFile) + { + if (trackFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(trackFile.Path); + } - // //AddSeriesTokens(tokenHandlers, artist); - // //AddSeasonTokens(tokenHandlers, seasonNumber); + return Path.GetFileNameWithoutExtension(trackFile.RelativePath); + } - // //return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); - //} } internal sealed class TokenMatch diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 7f92fe180..324e6e524 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -2,6 +2,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.Organizer @@ -9,26 +10,34 @@ namespace NzbDrone.Core.Organizer public interface IFilenameSampleService { SampleResult GetStandardSample(NamingConfig nameSpec); + SampleResult GetStandardTrackSample(NamingConfig nameSpec); SampleResult GetMultiEpisodeSample(NamingConfig nameSpec); SampleResult GetDailySample(NamingConfig nameSpec); SampleResult GetAnimeSample(NamingConfig nameSpec); SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec); string GetSeriesFolderSample(NamingConfig nameSpec); string GetSeasonFolderSample(NamingConfig nameSpec); + string GetArtistFolderSample(NamingConfig nameSpec); + string GetAlbumFolderSample(NamingConfig nameSpec); } public class FileNameSampleService : IFilenameSampleService { private readonly IBuildFileNames _buildFileNames; private static Series _standardSeries; + private static Artist _standardArtist; + private static Album _standardAlbum; + private static Track _track1; private static Series _dailySeries; private static Series _animeSeries; private static Episode _episode1; private static Episode _episode2; private static Episode _episode3; private static List _singleEpisode; + private static List _singleTrack; private static List _multiEpisodes; private static EpisodeFile _singleEpisodeFile; + private static TrackFile _singleTrackFile; private static EpisodeFile _multiEpisodeFile; private static EpisodeFile _dailyEpisodeFile; private static EpisodeFile _animeEpisodeFile; @@ -44,6 +53,17 @@ namespace NzbDrone.Core.Organizer Title = "Series Title (2010)" }; + _standardArtist = new Artist + { + Name = "Artist Name" + }; + + _standardAlbum = new Album + { + Title = "Album Title", + ReleaseDate = System.DateTime.Today + }; + _dailySeries = new Series { SeriesType = SeriesTypes.Daily, @@ -56,6 +76,14 @@ namespace NzbDrone.Core.Organizer Title = "Series Title (2010)" }; + _track1 = new Track + { + TrackNumber = 3, + + Title = "Track Title (1)", + + }; + _episode1 = new Episode { SeasonNumber = 1, @@ -82,6 +110,7 @@ namespace NzbDrone.Core.Organizer }; _singleEpisode = new List { _episode1 }; + _singleTrack = new List { _track1 }; _multiEpisodes = new List { _episode1, _episode2, _episode3 }; var mediaInfo = new MediaInfoModel() @@ -115,6 +144,15 @@ namespace NzbDrone.Core.Organizer MediaInfo = mediaInfo }; + _singleTrackFile = new TrackFile + { + Quality = new QualityModel(Quality.MP3256, new Revision(2)), + RelativePath = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256.mp3", + SceneName = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256", + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfo + }; + _multiEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.MP3256, new Revision(2)), @@ -165,6 +203,20 @@ namespace NzbDrone.Core.Organizer return result; } + public SampleResult GetStandardTrackSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + FileName = BuildTrackSample(_singleTrack, _standardArtist, _standardAlbum, _singleTrackFile, nameSpec), + Artist = _standardArtist, + Album = _standardAlbum, + Tracks = _singleTrack, + TrackFile = _singleTrackFile + }; + + return result; + } + public SampleResult GetMultiEpisodeSample(NamingConfig nameSpec) { var result = new SampleResult @@ -227,6 +279,16 @@ namespace NzbDrone.Core.Organizer return _buildFileNames.GetSeasonFolder(_standardSeries, _episode1.SeasonNumber, nameSpec); } + public string GetArtistFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetArtistFolder(_standardArtist, nameSpec); + } + + public string GetAlbumFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetAlbumFolder(_standardArtist, _standardAlbum, nameSpec); + } + private string BuildSample(List episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) { try @@ -238,5 +300,17 @@ namespace NzbDrone.Core.Organizer return string.Empty; } } + + private string BuildTrackSample(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig nameSpec) + { + try + { + return _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile, nameSpec); + } + catch (NamingFormatException) + { + return string.Empty; + } + } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index 930b8a044..5231f1fe6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -18,6 +18,12 @@ namespace NzbDrone.Core.Organizer return ruleBuilder.SetValidator(new ValidStandardEpisodeFormatValidator()); } + public static IRuleBuilderOptions ValidTrackFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new ValidStandardTrackFormatValidator()); + } + public static IRuleBuilderOptions ValidDailyEpisodeFormat(this IRuleBuilder ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); @@ -41,6 +47,17 @@ namespace NzbDrone.Core.Organizer ruleBuilder.SetValidator(new NotEmptyValidator(null)); return ruleBuilder.SetValidator(new RegularExpressionValidator(SeasonFolderRegex)).WithMessage("Must contain season number"); } + public static IRuleBuilderOptions ValidArtistFolderFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.ArtistNameRegex)).WithMessage("Must contain Artist name"); + } + + public static IRuleBuilderOptions ValidAlbumFolderFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AlbumTitleRegex)).WithMessage("Must contain Album name"); + } } public class ValidStandardEpisodeFormatValidator : PropertyValidator @@ -65,6 +82,21 @@ namespace NzbDrone.Core.Organizer } } + public class ValidStandardTrackFormatValidator : PropertyValidator + { + public ValidStandardTrackFormatValidator() + : base("Must contain Album Title and Track numbers OR Original Title") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + + return true; //TODO Add Logic here + } + } + public class ValidDailyEpisodeFormatValidator : PropertyValidator { public ValidDailyEpisodeFormatValidator() diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index 9367c11d8..a26b619c8 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Organizer public interface IFilenameValidationService { ValidationFailure ValidateStandardFilename(SampleResult sampleResult); + ValidationFailure ValidateTrackFilename(SampleResult sampleResult); ValidationFailure ValidateDailyFilename(SampleResult sampleResult); ValidationFailure ValidateAnimeFilename(SampleResult sampleResult); } @@ -35,6 +36,27 @@ namespace NzbDrone.Core.Organizer return null; } + public ValidationFailure ValidateTrackFilename(SampleResult sampleResult) + { + var validationFailure = new ValidationFailure("StandardTrackFormat", ERROR_MESSAGE); + + //TODO Add Validation for TrackFilename + //var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + + + //if (parsedEpisodeInfo == null) + //{ + // return validationFailure; + //} + + //if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) + //{ + // return validationFailure; + //} + + return null; + } + public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 637cd15cd..b0a4d811a 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Organizer { @@ -7,21 +7,25 @@ namespace NzbDrone.Core.Organizer public static NamingConfig Default => new NamingConfig { RenameEpisodes = false, + RenameTracks = false, ReplaceIllegalCharacters = true, MultiEpisodeStyle = 0, StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", + StandardTrackFormat = "{Artist Name} - {track:00} - {Album Title} - {Track Title}", DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", SeriesFolderFormat = "{Series Title}", SeasonFolderFormat = "Season {season}", ArtistFolderFormat = "{Artist Name}", - AlbumFolderFormat = "{Album Name} ({Year})" + AlbumFolderFormat = "{Album Title} ({Release Year})" }; public bool RenameEpisodes { get; set; } + public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } + public string StandardTrackFormat { get; set; } public string DailyEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; } public string SeriesFolderFormat { get; set; } diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs index 0f3885a1b..3075032ce 100644 --- a/src/NzbDrone.Core/Organizer/SampleResult.cs +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Organizer { @@ -8,7 +9,11 @@ namespace NzbDrone.Core.Organizer { public string FileName { get; set; } public Series Series { get; set; } + public Artist Artist { get; set; } + public Album Album { get; set; } public List Episodes { get; set; } public EpisodeFile EpisodeFile { get; set; } + public List Tracks { get; set; } + public TrackFile TrackFile { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/ArtistTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/ArtistTitleInfo.cs new file mode 100644 index 000000000..0d1938d24 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/ArtistTitleInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Parser.Model +{ + public class ArtistTitleInfo + { + public string Title { get; set; } + public string TitleWithoutYear { get; set; } + public int Year { get; set; } + } +} diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 2f8b35588..359ce6285 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -19,20 +19,12 @@ namespace NzbDrone.Core.Parser.Model public long Size { get; set; } public ParsedTrackInfo ParsedTrackInfo { get; set; } public Artist Artist { get; set; } + public Album Album { get; set; } public List Tracks { get; set; } public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } - public string Album - { - get - { - return Tracks.Select(c => c.AlbumId).Distinct().Single(); - } - } - - public bool IsSpecial => Album != ""; public override string ToString() { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs index 53cb470c9..f05090f9b 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs @@ -9,87 +9,34 @@ namespace NzbDrone.Core.Parser.Model { public class ParsedTrackInfo { - // [TODO]: Properly fill this out public string ArtistTitle { get; set; } public string AlbumTitle { get; set; } - public SeriesTitleInfo SeriesTitleInfo { get; set; } + public ArtistTitleInfo ArtistTitleInfo { get; set; } public QualityModel Quality { get; set; } - public int SeasonNumber { get; set; } - public int[] EpisodeNumbers { get; set; } - public int[] AbsoluteEpisodeNumbers { get; set; } - public string AirDate { get; set; } - public Language Language { get; set; } - public bool FullSeason { get; set; } - public bool Special { get; set; } + public string AlbumId { get; set; } // maybe + public int[] TrackNumbers { get; set; } + //public Language Language { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } public ParsedTrackInfo() { - EpisodeNumbers = new int[0]; - AbsoluteEpisodeNumbers = new int[0]; + TrackNumbers = new int[0]; } - public bool IsDaily - { - get - { - return !string.IsNullOrWhiteSpace(AirDate); - } - //This prevents manually downloading a release from blowing up in mono - //TODO: Is there a better way? - private set { } - } - public bool IsAbsoluteNumbering - { - get - { - return AbsoluteEpisodeNumbers.Any(); - } - - //This prevents manually downloading a release from blowing up in mono - //TODO: Is there a better way? - private set { } - } - - public bool IsPossibleSpecialEpisode - { - get - { - // if we don't have eny episode numbers we are likely a special episode and need to do a search by episode title - return (AirDate.IsNullOrWhiteSpace() && - ArtistTitle.IsNullOrWhiteSpace() && - (EpisodeNumbers.Length == 0 || SeasonNumber == 0) || - !ArtistTitle.IsNullOrWhiteSpace() && Special); - } - - //This prevents manually downloading a release from blowing up in mono - //TODO: Is there a better way? - private set { } - } public override string ToString() { - string episodeString = "[Unknown Episode]"; + string episodeString = "[Unknown Track]"; - if (IsDaily && EpisodeNumbers.Empty()) - { - episodeString = string.Format("{0}", AirDate); - } - else if (FullSeason) - { - episodeString = string.Format("Season {0:00}", SeasonNumber); - } - else if (EpisodeNumbers != null && EpisodeNumbers.Any()) - { - episodeString = string.Format("S{0:00}E{1}", SeasonNumber, string.Join("-", EpisodeNumbers.Select(c => c.ToString("00")))); - } - else if (AbsoluteEpisodeNumbers != null && AbsoluteEpisodeNumbers.Any()) + + if (TrackNumbers != null && TrackNumbers.Any()) { - episodeString = string.Format("{0}", string.Join("-", AbsoluteEpisodeNumbers.Select(c => c.ToString("000")))); + episodeString = string.Format("T{1}", string.Join("-", TrackNumbers.Select(c => c.ToString("00")))); } + return string.Format("{0} - {1} {2}", ArtistTitle, episodeString, Quality); } diff --git a/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs b/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs new file mode 100644 index 000000000..2b1f0ceb0 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Parser.Model +{ + public class RemoteAlbum + { + public ReleaseInfo Release { get; set; } + public ParsedTrackInfo ParsedTrackInfo { get; set; } + public Artist Artist { get; set; } + public List Albums { get; set; } + public bool DownloadAllowed { get; set; } + + public bool IsRecentAlbum() + { + return Albums.Any(e => e.ReleaseDate >= DateTime.UtcNow.Date.AddDays(-14)); + } + + public override string ToString() + { + return Release.Title; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index be6ed1da3..22e0e07be 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -380,33 +380,34 @@ namespace NzbDrone.Core.Parser if (result != null) { - if (result.FullSeason && title.ContainsIgnoreCase("Special")) - { - result.FullSeason = false; - result.Special = true; - } + //if (result.FullSeason && title.ContainsIgnoreCase("Special")) + //{ + // result.FullSeason = false; + // result.Special = true; + //} - result.Language = LanguageParser.ParseLanguage(title); - Logger.Debug("Language parsed: {0}", result.Language); + //result.Language = LanguageParser.ParseLanguage(title); + //Logger.Debug("Language parsed: {0}", result.Language); result.Quality = QualityParser.ParseQuality(title); Logger.Debug("Quality parsed: {0}", result.Quality); - result.ReleaseGroup = ParseReleaseGroup(title); + // Majora: We don't currently need Release Group for Music. + //result.ReleaseGroup = ParseReleaseGroup(title); - var subGroup = GetSubGroup(match); - if (!subGroup.IsNullOrWhiteSpace()) - { - result.ReleaseGroup = subGroup; - } + //var subGroup = GetSubGroup(match); + //if (!subGroup.IsNullOrWhiteSpace()) + //{ + // result.ReleaseGroup = subGroup; + //} - Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); + //Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); - result.ReleaseHash = GetReleaseHash(match); - if (!result.ReleaseHash.IsNullOrWhiteSpace()) - { - Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); - } + //result.ReleaseHash = GetReleaseHash(match); + //if (!result.ReleaseHash.IsNullOrWhiteSpace()) + //{ + // Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); + //} return result; } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 627bd6df1..1c4ff67d0 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Parser // Music stuff here LocalTrack GetLocalTrack(string filename, Artist artist); - LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource); + LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo); } @@ -40,12 +40,14 @@ namespace NzbDrone.Core.Parser public ParsingService(IEpisodeService episodeService, ISeriesService seriesService, + ITrackService trackService, // ISceneMappingService sceneMappingService, Logger logger) { _episodeService = episodeService; _seriesService = seriesService; // _sceneMappingService = sceneMappingService; + _trackService = trackService; _logger = logger; } @@ -407,6 +409,75 @@ namespace NzbDrone.Core.Parser return result; } + private List GetStandardTracks(Artist artist, ParsedTrackInfo parsedTrackInfo, SearchCriteriaBase searchCriteria) + { + var result = new List(); + + if (parsedTrackInfo.TrackNumbers == null) + { + return result; + } + + foreach (var trackNumber in parsedTrackInfo.TrackNumbers) + { + //if (series.UseSceneNumbering && sceneSource) + //{ + // List episodes = new List(); + + // if (searchCriteria != null) + // { + // episodes = searchCriteria.Episodes.Where(e => e.SceneSeasonNumber == parsedTrackInfo.SeasonNumber && + // e.SceneEpisodeNumber == trackNumber).ToList(); + // } + + // if (!episodes.Any()) + // { + // episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, seasonNumber, trackNumber); + // } + + // if (episodes != null && episodes.Any()) + // { + // _logger.Debug("Using Scene to TVDB Mapping for: {0} - Scene: {1}x{2:00} - TVDB: {3}", + // series.Title, + // episodes.First().SceneSeasonNumber, + // episodes.First().SceneEpisodeNumber, + // string.Join(", ", episodes.Select(e => string.Format("{0}x{1:00}", e.SeasonNumber, e.EpisodeNumber)))); + + // result.AddRange(episodes); + // continue; + // } + //} + + Track trackInfo = null; + + if (searchCriteria != null) + { + trackInfo = searchCriteria.Tracks.SingleOrDefault(e => e.TrackNumber == trackNumber); //e => e.SeasonNumber == seasonNumber && e.TrackNumber == trackNumber + } + + if (trackInfo == null) + { + // TODO: [ParsingService]: FindTrack by artistID and trackNumber (or albumID and trackNumber if we change db schema to album as base) + _logger.Debug("TrackInfo is null, we will not add as FindTrack(artistId, trackNumber) is not implemented"); + //trackInfo = _trackService.FindTrack(artist.SpotifyId, trackNumber); + } + + if (trackInfo != null) + { + result.Add(trackInfo); + } + + else + { + _logger.Debug("Unable to find {0}", parsedTrackInfo); + } + } + + + + return result; + } + private List GetStandardEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource, SearchCriteriaBase searchCriteria) { var result = new List(); @@ -484,12 +555,13 @@ namespace NzbDrone.Core.Parser return result; } + public LocalTrack GetLocalTrack(string filename, Artist artist) { - return GetLocalTrack(filename, artist, null, false); + return GetLocalTrack(filename, artist, null); } - public LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource) + public LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo) { ParsedTrackInfo parsedTrackInfo; @@ -504,7 +576,7 @@ namespace NzbDrone.Core.Parser parsedTrackInfo = Parser.ParseMusicPath(filename); } - if (parsedTrackInfo == null || parsedTrackInfo.IsPossibleSpecialEpisode) + if (parsedTrackInfo == null) { var title = Path.GetFileNameWithoutExtension(filename); //var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series); @@ -525,7 +597,7 @@ namespace NzbDrone.Core.Parser return null; } - var tracks = GetTracks(parsedTrackInfo, artist, sceneSource); + var tracks = GetTracks(parsedTrackInfo, artist); return new LocalTrack { @@ -538,10 +610,10 @@ namespace NzbDrone.Core.Parser }; } - private List GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist, bool sceneSource) + private List GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist) { - throw new NotImplementedException(); + // TODO: Ensure GetTracks(parsedTrackInfo, artist) doesn't need any checks /*if (parsedTrackInfo.FullSeason) // IF Album { return _trackService.GetTracksByAlbumTitle(artist.Id, parsedTrackInfo.AlbumTitle); @@ -566,6 +638,60 @@ namespace NzbDrone.Core.Parser } return GetStandardEpisodes(artist, parsedTrackInfo, sceneSource, searchCriteria);*/ + return GetStandardTracks(artist, parsedTrackInfo); + } + + private List GetStandardTracks(Artist artist, ParsedTrackInfo parsedTrackInfo) + { + var result = new List(); + //var seasonNumber = parsedEpisodeInfo.SeasonNumber; + + //if (sceneSource) + //{ + // var sceneMapping = _sceneMappingService.FindSceneMapping(parsedEpisodeInfo.SeriesTitle); + + // if (sceneMapping != null && sceneMapping.SeasonNumber.HasValue && sceneMapping.SeasonNumber.Value >= 0 && + // sceneMapping.SceneSeasonNumber == seasonNumber) + // { + // seasonNumber = sceneMapping.SeasonNumber.Value; + // } + //} + + if (parsedTrackInfo.TrackNumbers == null) + { + return new List(); + } + + foreach (var trackNumber in parsedTrackInfo.TrackNumbers) + { + + + Track trackInfo = null; + + //if (searchCriteria != null) + //{ + // trackInfo = searchCriteria.Episodes.SingleOrDefault(e => e.SeasonNumber == seasonNumber && e.EpisodeNumber == trackNumber); + //} + + if (trackInfo == null) + { + // TODO: [ParsingService]: FindTrack by artistID and trackNumber (or albumID and trackNumber if we change db schema to album as base) + _logger.Debug("TrackInfo is null, we will not add as FindTrack(artistId, trackNumber) is not implemented"); + //trackInfo = _trackService.FindTrack(artist.SpotifyId, trackNumber); //series.Id, seasonNumber, trackNumber + } + + if (trackInfo != null) + { + result.Add(trackInfo); + } + + else + { + _logger.Debug("Unable to find {0}", parsedTrackInfo); + } + } + + return result; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index acb949cc2..4a8b45026 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -103,6 +103,10 @@ namespace NzbDrone.Core.Qualities public static Quality FindById(int id) { if (id == 0) return Unknown; + else if (id > AllLookup.Length) + { + throw new ArgumentException("ID does not match a known quality", nameof(id)); + } var quality = AllLookup[id]; diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index f177b5857..39e32dbe7 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -176,7 +176,7 @@ namespace NzbDrone.Core.Tv try { _logger.Info("Skipping refresh of series: {0}", series.Title); - _diskScanService.Scan(series); + //_diskScanService.Scan(series); } catch (Exception e) { diff --git a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs index 3260895c5..2e59d3a8d 100644 --- a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Validation.Paths { if (context.PropertyValue == null) return true; - return (!_artistService.GetAllArtists().Exists(s => s.SpotifyId == context.PropertyValue.ToString())); + return (!_artistService.GetAllArtists().Exists(s => s.ForeignArtistId == context.PropertyValue.ToString())); } } } diff --git a/src/UI/AddArtist/EmptyViewTemplate.hbs b/src/UI/AddArtist/EmptyViewTemplate.hbs index 26c712271..e2a20efdc 100644 --- a/src/UI/AddArtist/EmptyViewTemplate.hbs +++ b/src/UI/AddArtist/EmptyViewTemplate.hbs @@ -1,3 +1,3 @@
- You can also search by Spotify using the spotify: prefixes. + You can also search by MusicBrianzID using the MBID: prefixes.
diff --git a/src/UI/AddArtist/SearchResultView.js b/src/UI/AddArtist/SearchResultView.js index cc53f6b15..9c0529c60 100644 --- a/src/UI/AddArtist/SearchResultView.js +++ b/src/UI/AddArtist/SearchResultView.js @@ -97,7 +97,7 @@ var view = Marionette.ItemView.extend({ }, _configureTemplateHelpers : function() { - var existingArtist = ArtistCollection.where({ spotifyId : this.model.get('spotifyId') }); + var existingArtist = ArtistCollection.where({ foreignArtistId : this.model.get('foreignArtistId') }); if (existingArtist.length > 0) { this.templateHelpers.existing = existingArtist[0].toJSON(); @@ -223,12 +223,12 @@ var view = Marionette.ItemView.extend({ self.close(); Messenger.show({ - message : 'Added: ' + self.model.get('artistName'), + message : 'Added: ' + self.model.get('name'), actions : { goToArtist : { label : 'Go to Artist', action : function() { - Backbone.history.navigate('/artist/' + self.model.get('artistSlug'), { trigger : true }); + Backbone.history.navigate('/artist/' + self.model.get('nameSlug'), { trigger : true }); } } }, diff --git a/src/UI/AddArtist/SearchResultViewTemplate.hbs b/src/UI/AddArtist/SearchResultViewTemplate.hbs index 0377ccbc6..0a3bbe016 100644 --- a/src/UI/AddArtist/SearchResultViewTemplate.hbs +++ b/src/UI/AddArtist/SearchResultViewTemplate.hbs @@ -5,7 +5,7 @@

- {{artistName}} + {{name}} +

-->
{{#unless existing}} {{#unless path}} @@ -68,16 +68,16 @@
{{#unless existing}} - {{#if artistName}} + {{#if name}}
- -
diff --git a/src/UI/Artist/ArtistCollection.js b/src/UI/Artist/ArtistCollection.js index 4bffc07e2..8e700d7bf 100644 --- a/src/UI/Artist/ArtistCollection.js +++ b/src/UI/Artist/ArtistCollection.js @@ -77,7 +77,7 @@ var Collection = PageableCollection.extend({ }, artistName: { - sortKey : 'artistName' + sortKey : 'name' }, nextAiring : { diff --git a/src/UI/Artist/ArtistController.js b/src/UI/Artist/ArtistController.js index c6e24a771..2f54bc2cf 100644 --- a/src/UI/Artist/ArtistController.js +++ b/src/UI/Artist/ArtistController.js @@ -21,12 +21,12 @@ module.exports = NzbDroneController.extend({ }, artistDetails : function(query) { - var artists = ArtistCollection.where({ artistSlug : query }); + var artists = ArtistCollection.where({ nameSlug : query }); console.log('artistDetails, artists: ', artists); if (artists.length !== 0) { var targetArtist = artists[0]; console.log("[ArtistController] targetArtist: ", targetArtist); - this.setTitle(targetArtist.get('artistName')); // TODO: Update NzbDroneController + this.setTitle(targetArtist.get('name')); // TODO: Update NzbDroneController //this.setArtistName(targetSeries.get('artistName')); this.showMainRegion(new ArtistDetailsLayout({ model : targetArtist })); } else { diff --git a/src/UI/Artist/Details/ArtistDetailsLayout.js b/src/UI/Artist/Details/ArtistDetailsLayout.js index 67d55c306..fc068d291 100644 --- a/src/UI/Artist/Details/ArtistDetailsLayout.js +++ b/src/UI/Artist/Details/ArtistDetailsLayout.js @@ -63,7 +63,7 @@ module.exports = Marionette.Layout.extend({ onShow : function() { this._showBackdrop(); - this._showSeasons(); + this._showAlbums(); this._setMonitoredState(); this._showInfo(); }, diff --git a/src/UI/Artist/Details/ArtistDetailsTemplate.hbs b/src/UI/Artist/Details/ArtistDetailsTemplate.hbs index 605ead424..8d5b1f5b1 100644 --- a/src/UI/Artist/Details/ArtistDetailsTemplate.hbs +++ b/src/UI/Artist/Details/ArtistDetailsTemplate.hbs @@ -1,35 +1,35 @@ -
+
{{poster}}

- - {{title}} -
+ + {{name}} +
- +
- +
- +

-
+
{{overview}}
-
+
diff --git a/src/UI/Artist/Details/InfoView.js b/src/UI/Artist/Details/InfoView.js index c7fab9fc4..74ddb2bf4 100644 --- a/src/UI/Artist/Details/InfoView.js +++ b/src/UI/Artist/Details/InfoView.js @@ -1,18 +1,18 @@ var Marionette = require('marionette'); module.exports = Marionette.ItemView.extend({ - template : 'Series/Details/InfoViewTemplate', + template : 'Artist/Details/InfoViewTemplate', initialize : function(options) { - this.episodeFileCollection = options.episodeFileCollection; + this.trackFileCollection = options.trackFileCollection; this.listenTo(this.model, 'change', this.render); - this.listenTo(this.episodeFileCollection, 'sync', this.render); + this.listenTo(this.trackFileCollection, 'sync', this.render); }, templateHelpers : function() { return { - fileCount : this.episodeFileCollection.length + fileCount : this.trackFileCollection.length }; } }); \ No newline at end of file diff --git a/src/UI/Artist/Details/InfoViewTemplate.hbs b/src/UI/Artist/Details/InfoViewTemplate.hbs index b52130246..f305c976b 100644 --- a/src/UI/Artist/Details/InfoViewTemplate.hbs +++ b/src/UI/Artist/Details/InfoViewTemplate.hbs @@ -28,21 +28,19 @@ {{/if_eq}}
- - Trakt + + MusicBrainz - The TVDB - - {{#if imdbId}} - IMDB + {{#if tadbId}} + The AudioDB {{/if}} - {{#if tvRageId}} - TV Rage + {{#if discogsId}} + Discogs {{/if}} - {{#if tvMazeId}} - TV Maze + {{#if amId}} + AllMusic {{/if}}
diff --git a/src/UI/Artist/Editor/ArtistEditorLayout.js b/src/UI/Artist/Editor/ArtistEditorLayout.js index 51f0ec52e..6d8520154 100644 --- a/src/UI/Artist/Editor/ArtistEditorLayout.js +++ b/src/UI/Artist/Editor/ArtistEditorLayout.js @@ -45,7 +45,7 @@ module.exports = Marionette.Layout.extend({ cell : ArtistStatusCell }, { - name : 'artistName', + name : 'name', label : 'Artist', cell : ArtistTitleCell, cellValue : 'this' diff --git a/src/UI/Artist/Index/ArtistIndexLayout.js b/src/UI/Artist/Index/ArtistIndexLayout.js index b06bd5311..81884d48c 100644 --- a/src/UI/Artist/Index/ArtistIndexLayout.js +++ b/src/UI/Artist/Index/ArtistIndexLayout.js @@ -321,15 +321,17 @@ module.exports = Marionette.Layout.extend({ _showFooter : function() { var footerModel = new FooterModel(); var artist = this.artistCollection.models.length; - var episodes = 0; - var episodeFiles = 0; + var albums = 0; + var tracks = 0; + var trackFiles = 0; var ended = 0; var continuing = 0; var monitored = 0; _.each(this.artistCollection.models, function(model) { - episodes += model.get('episodeCount'); // TODO: Refactor to Seasons and Tracks - episodeFiles += model.get('episodeFileCount'); + albums += model.get('albumCount'); + tracks += model.get('episodeCount'); // TODO: Refactor to Seasons and Tracks + trackFiles += model.get('episodeFileCount'); /*if (model.get('status').toLowerCase() === 'ended') { ended++; @@ -348,8 +350,9 @@ module.exports = Marionette.Layout.extend({ continuing : continuing, monitored : monitored, unmonitored : artist - monitored, - episodes : episodes, - episodeFiles : episodeFiles + albums : albums, + tracks : tracks, + trackFiles : trackFiles }); this.footer.show(new FooterView({ model : footerModel })); diff --git a/src/UI/Artist/Index/FooterViewTemplate.hbs b/src/UI/Artist/Index/FooterViewTemplate.hbs index 58d744386..f61baa356 100644 --- a/src/UI/Artist/Index/FooterViewTemplate.hbs +++ b/src/UI/Artist/Index/FooterViewTemplate.hbs @@ -34,11 +34,14 @@
+
Albums
+
{{albums}}
+
Tracks
-
{{episodes}}
+
{{tracks}}
Files
-
{{episodeFiles}}
+
{{trackFiles}}
diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs b/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs index 222287a2e..de34cc5cd 100644 --- a/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs +++ b/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs @@ -9,7 +9,7 @@
diff --git a/src/UI/Artist/Index/TrackProgressPartial.hbs b/src/UI/Artist/Index/TrackProgressPartial.hbs index db5c49a2b..a9cec28f7 100644 --- a/src/UI/Artist/Index/TrackProgressPartial.hbs +++ b/src/UI/Artist/Index/TrackProgressPartial.hbs @@ -1,4 +1,4 @@ -
- {{episodeFileCount}} / {{episodeCount}} -
{{episodeFileCount}} / {{episodeCount}}
+
+ {{trackFileCount}} / {{trackCount}} +
{{trackFileCount}} / {{trackCount}}
\ No newline at end of file diff --git a/src/UI/Artist/TrackCollection.js b/src/UI/Artist/TrackCollection.js index 33bd4c3a3..302c416ea 100644 --- a/src/UI/Artist/TrackCollection.js +++ b/src/UI/Artist/TrackCollection.js @@ -4,11 +4,11 @@ var TrackModel = require('./TrackModel'); require('./TrackCollection'); module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/episode', + url : window.NzbDrone.ApiRoot + '/track', model : TrackModel, state : { - sortKey : 'episodeNumber', + sortKey : 'trackNumber', order : 1, pageSize : 100000 }, @@ -18,28 +18,28 @@ module.exports = PageableCollection.extend({ originalFetch : Backbone.Collection.prototype.fetch, initialize : function(options) { - this.seriesId = options.seriesId; + this.artistId = options.artistId; }, - bySeason : function(season) { - var filtered = this.filter(function(episode) { - return episode.get('seasonNumber') === season; + bySeason : function(album) { + var filtered = this.filter(function(track) { + return track.get('albumId') === album; }); - var EpisodeCollection = require('./TrackCollection'); + var TrackCollection = require('./TrackCollection'); - return new EpisodeCollection(filtered); + return new TrackCollection(filtered); }, comparator : function(model1, model2) { - var episode1 = model1.get('episodeNumber'); - var episode2 = model2.get('episodeNumber'); + var track1 = model1.get('trackNumber'); + var track2 = model2.get('trackNumber'); - if (episode1 < episode2) { + if (track1 < track2) { return 1; } - if (episode1 > episode2) { + if (track1 > track2) { return -1; } @@ -47,15 +47,15 @@ module.exports = PageableCollection.extend({ }, fetch : function(options) { - if (!this.seriesId) { - throw 'seriesId is required'; + if (!this.artistId) { + throw 'artistId is required'; } if (!options) { options = {}; } - options.data = { seriesId : this.seriesId }; + options.data = { artistId : this.artistId }; return this.originalFetch.call(this, options); } diff --git a/src/UI/Artist/TrackFileCollection.js b/src/UI/Artist/TrackFileCollection.js index 19c58ebee..9b9909ce6 100644 --- a/src/UI/Artist/TrackFileCollection.js +++ b/src/UI/Artist/TrackFileCollection.js @@ -2,7 +2,7 @@ var Backbone = require('backbone'); var TrackFileModel = require('./TrackFileModel'); module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/episodefile', + url : window.NzbDrone.ApiRoot + '/trackfile', model : TrackFileModel, originalFetch : Backbone.Collection.prototype.fetch, @@ -21,7 +21,7 @@ module.exports = Backbone.Collection.extend({ options = {}; } - options.data = { seriesId : this.seriesId }; + options.data = { artistId : this.artistId }; return this.originalFetch.call(this, options); } diff --git a/src/UI/Artist/TrackModel.js b/src/UI/Artist/TrackModel.js index ebb72cf29..30a2702d7 100644 --- a/src/UI/Artist/TrackModel.js +++ b/src/UI/Artist/TrackModel.js @@ -2,12 +2,12 @@ var Backbone = require('backbone'); module.exports = Backbone.Model.extend({ defaults : { - seasonNumber : 0, + albumId : 0, status : 0 }, methodUrls : { - 'update' : window.NzbDrone.ApiRoot + '/episode' + 'update' : window.NzbDrone.ApiRoot + '/track' }, sync : function(method, model, options) { diff --git a/src/UI/Handlebars/Helpers/Artist.js b/src/UI/Handlebars/Helpers/Artist.js index a44f24802..36c8983ba 100644 --- a/src/UI/Handlebars/Helpers/Artist.js +++ b/src/UI/Handlebars/Helpers/Artist.js @@ -19,28 +19,24 @@ Handlebars.registerHelper('poster', function() { return new Handlebars.SafeString(''.format(placeholder)); }); -Handlebars.registerHelper('traktUrl', function() { - return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show'; +Handlebars.registerHelper('MBUrl', function() { + return 'https://musicbrainz.org/artist/' + this.mbId; }); -Handlebars.registerHelper('imdbUrl', function() { - return 'http://imdb.com/title/' + this.imdbId; +Handlebars.registerHelper('TADBUrl', function() { + return 'http://www.theaudiodb.com/artist/' + this.tadbId; }); -Handlebars.registerHelper('tvdbUrl', function() { - return 'http://www.thetvdb.com/?tab=series&id=' + this.tvdbId; +Handlebars.registerHelper('discogsUrl', function() { + return 'https://www.discogs.com/artist/' + this.discogsId; }); -Handlebars.registerHelper('tvRageUrl', function() { - return 'http://www.tvrage.com/shows/id-' + this.tvRageId; -}); - -Handlebars.registerHelper('tvMazeUrl', function() { - return 'http://www.tvmaze.com/shows/' + this.tvMazeId + '/_'; +Handlebars.registerHelper('allMusicUrl', function() { + return 'http://www.allmusic.com/artist/' + this.amId; }); Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/artist/' + this.artistSlug; + return StatusModel.get('urlBase') + '/artist/' + this.nameSlug; }); Handlebars.registerHelper('percentOfEpisodes', function() { @@ -56,6 +52,19 @@ Handlebars.registerHelper('percentOfEpisodes', function() { return percent; }); +Handlebars.registerHelper('percentOfTracks', function() { + var trackCount = this.trackCount; + var trackFileCount = this.trackFileCount; + + var percent = 100; + + if (trackCount > 0) { + percent = trackFileCount / trackCount * 100; + } + + return percent; +}); + Handlebars.registerHelper('seasonCountHelper', function() { var seasonCount = this.seasonCount; var continuing = this.status === 'continuing'; @@ -87,7 +96,7 @@ Handlebars.registerHelper('albumCountHelper', function() { var albumCount = this.albumCount; if (albumCount === 1) { - return new Handlebars.SafeString('{0} Albums'.format(albumCount)); + return new Handlebars.SafeString('{0} Album'.format(albumCount)); } return new Handlebars.SafeString('{0} Albums'.format(albumCount)); diff --git a/src/UI/Series/Details/SeriesDetailsLayout.js b/src/UI/Series/Details/SeriesDetailsLayout.js index b1b004f9b..4b3d34297 100644 --- a/src/UI/Series/Details/SeriesDetailsLayout.js +++ b/src/UI/Series/Details/SeriesDetailsLayout.js @@ -48,9 +48,10 @@ module.exports = Marionette.Layout.extend({ this.seriesCollection = ArtistCollection.clone(); this.seriesCollection.shadowCollection.bindSignalR(); + this.listenTo(this.model, 'change:monitored', this._setMonitoredState); this.listenTo(this.model, 'remove', this._seriesRemoved); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); + //this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); this.listenTo(this.model, 'change', function(model, options) { if (options && options.changeSource === 'signalr') { @@ -59,6 +60,7 @@ module.exports = Marionette.Layout.extend({ }); this.listenTo(this.model, 'change:images', this._updateImages); + }, onShow : function() { @@ -81,15 +83,16 @@ module.exports = Marionette.Layout.extend({ name : 'seriesSearch' } }); + console.log(this.model); - CommandController.bindToCommand({ + /*CommandController.bindToCommand({ element : this.ui.rename, command : { name : 'renameFiles', - seriesId : this.model.id, + seriesId : this.model.spotifyId, seasonNumber : -1 } - }); + });*/ }, onClose : function() { @@ -164,6 +167,7 @@ module.exports = Marionette.Layout.extend({ _showSeasons : function() { var self = this; + return; this.seasons.show(new LoadingView()); diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs index 19da3e335..6fb80ec50 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs @@ -8,7 +8,7 @@
diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 71e4df4f8..55dbb860f 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -10,26 +10,20 @@ module.exports = (function() { template : 'Settings/MediaManagement/Naming/NamingViewTemplate', ui : { namingOptions : '.x-naming-options', - renameEpisodesCheckbox : '.x-rename-episodes', - singleEpisodeExample : '.x-single-episode-example', - multiEpisodeExample : '.x-multi-episode-example', - dailyEpisodeExample : '.x-daily-episode-example', - animeEpisodeExample : '.x-anime-episode-example', - animeMultiEpisodeExample : '.x-anime-multi-episode-example', + renameTracksCheckbox : '.x-rename-tracks', + singleTrackExample : '.x-single-track-example', namingTokenHelper : '.x-naming-token-helper', - multiEpisodeStyle : '.x-multi-episode-style', - seriesFolderExample : '.x-series-folder-example', - seasonFolderExample : '.x-season-folder-example' + artistFolderExample : '.x-artist-folder-example', + albumFolderExample : '.x-album-folder-example' }, events : { - "change .x-rename-episodes" : '_setFailedDownloadOptionsVisibility', + "change .x-rename-tracks" : '_setFailedDownloadOptionsVisibility', "click .x-show-wizard" : '_showWizard', - "click .x-naming-token-helper a" : '_addToken', - "change .x-multi-episode-style" : '_multiEpisodeFomatChanged' + "click .x-naming-token-helper a" : '_addToken' }, regions : { basicNamingRegion : '.x-basic-naming' }, onRender : function() { - if (!this.model.get('renameEpisodes')) { + if (!this.model.get('renameTracks')) { this.ui.namingOptions.hide(); } var basicNamingView = new BasicNamingView({ model : this.model }); @@ -40,7 +34,7 @@ module.exports = (function() { this._updateSamples(); }, _setFailedDownloadOptionsVisibility : function() { - var checked = this.ui.renameEpisodesCheckbox.prop('checked'); + var checked = this.ui.renameTracksCheckbox.prop('checked'); if (checked) { this.ui.namingOptions.slideDown(); } else { @@ -51,13 +45,9 @@ module.exports = (function() { this.namingSampleModel.fetch({ data : this.model.toJSON() }); }, _showSamples : function() { - this.ui.singleEpisodeExample.html(this.namingSampleModel.get('singleEpisodeExample')); - this.ui.multiEpisodeExample.html(this.namingSampleModel.get('multiEpisodeExample')); - this.ui.dailyEpisodeExample.html(this.namingSampleModel.get('dailyEpisodeExample')); - this.ui.animeEpisodeExample.html(this.namingSampleModel.get('animeEpisodeExample')); - this.ui.animeMultiEpisodeExample.html(this.namingSampleModel.get('animeMultiEpisodeExample')); - this.ui.seriesFolderExample.html(this.namingSampleModel.get('seriesFolderExample')); - this.ui.seasonFolderExample.html(this.namingSampleModel.get('seasonFolderExample')); + this.ui.singleTrackExample.html(this.namingSampleModel.get('singleTrackExample')); + this.ui.artistFolderExample.html(this.namingSampleModel.get('artistFolderExample')); + this.ui.albumFolderExample.html(this.namingSampleModel.get('albumFolderExample')); }, _addToken : function(e) { e.preventDefault(); @@ -75,9 +65,6 @@ module.exports = (function() { this.ui.namingTokenHelper.removeClass('open'); input.focus(); }, - multiEpisodeFormatChanged : function() { - this.model.set('multiEpisodeStyle', this.ui.multiEpisodeStyle.val()); - } }); AsModelBoundView.call(view); AsValidatedView.call(view); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs index 39ddcf63f..6b46ace90 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs @@ -1,13 +1,13 @@
- Episode Naming + Track Naming
- +
diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs new file mode 100644 index 000000000..9e3da7a54 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs @@ -0,0 +1,11 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs new file mode 100644 index 000000000..c951bd123 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs @@ -0,0 +1,11 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs new file mode 100644 index 000000000..0a4153d66 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs @@ -0,0 +1 @@ +
  • Release Year
  • \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs new file mode 100644 index 000000000..68ddc8ff5 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs @@ -0,0 +1,7 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs new file mode 100644 index 000000000..591c57098 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs @@ -0,0 +1,11 @@ +