From 916db8d35614a484f3c42886306c387f26f1201e Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 20 Jun 2017 23:06:28 -0400 Subject: [PATCH] Add Track and TrackFile API Resources Add Track and TrackFile API Resources, Add Rename Track Resource, Add GetFilesByAlbum function to Media File Service, Add Links to Artist Detail Page, Misc other UI work --- src/NzbDrone.Api/Music/ArtistResource.cs | 16 +- src/NzbDrone.Api/NzbDrone.Api.csproj | 7 + .../TrackFiles/TrackFileModule.cs | 101 ++++++++++ .../TrackFiles/TrackFileResource.cs | 64 ++++++ src/NzbDrone.Api/Tracks/RenameTrackModule.cs | 37 ++++ .../Tracks/RenameTrackResource.cs | 39 ++++ src/NzbDrone.Api/Tracks/TrackModule.cs | 40 ++++ .../Tracks/TrackModuleWithSignalR.cs | 126 ++++++++++++ src/NzbDrone.Api/Tracks/TrackResource.cs | 78 ++++++++ .../Commands/RenameArtistCommand.cs | 16 ++ .../MediaFiles/MediaFileService.cs | 6 + .../MediaFiles/RenameTrackFilePreview.cs | 14 ++ .../MediaFiles/RenameTrackFileService.cs | 185 ++++++++++++++++++ src/NzbDrone.Core/MediaFiles/TrackFile.cs | 1 - src/NzbDrone.Core/Music/Artist.cs | 8 + src/NzbDrone.Core/Music/Track.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 + .../Organizer/FileNameBuilder.cs | 29 +++ src/UI/AddArtist/SearchResultViewTemplate.hbs | 4 +- src/UI/Artist/Details/InfoViewTemplate.hbs | 16 +- src/UI/Artist/Index/ArtistIndexLayout.js | 15 +- src/UI/Artist/Index/FooterViewTemplate.hbs | 7 +- src/UI/Artist/Index/TrackProgressPartial.hbs | 6 +- src/UI/Artist/TrackCollection.js | 30 +-- src/UI/Artist/TrackFileCollection.js | 4 +- src/UI/Artist/TrackModel.js | 4 +- src/UI/Controller.js | 5 - src/UI/Handlebars/Helpers/Artist.js | 35 ++-- src/UI/Router.js | 1 - 29 files changed, 834 insertions(+), 65 deletions(-) create mode 100644 src/NzbDrone.Api/TrackFiles/TrackFileModule.cs create mode 100644 src/NzbDrone.Api/TrackFiles/TrackFileResource.cs create mode 100644 src/NzbDrone.Api/Tracks/RenameTrackModule.cs create mode 100644 src/NzbDrone.Api/Tracks/RenameTrackResource.cs create mode 100644 src/NzbDrone.Api/Tracks/TrackModule.cs create mode 100644 src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs create mode 100644 src/NzbDrone.Api/Tracks/TrackResource.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs create mode 100644 src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs create mode 100644 src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs index d75fbebdb..4405c0d38 100644 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -20,6 +20,10 @@ namespace NzbDrone.Api.Music //View Only 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,7 +57,7 @@ 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; } @@ -71,7 +75,10 @@ namespace NzbDrone.Api.Music return new ArtistResource { Id = model.Id, - + MBId = model.MBId, + TADBId = model.TADBId, + DiscogsId = model.DiscogsId, + AMId = model.AMId, Name = model.Name, //AlternateTitles //SortTitle = resource.SortTitle, @@ -127,7 +134,10 @@ namespace NzbDrone.Api.Music Name = resource.Name, //AlternateTitles //SortTitle = resource.SortTitle, - + MBId = resource.MBId, + TADBId = resource.TADBId, + DiscogsId = resource.DiscogsId, + AMId = resource.AMId, //TotalEpisodeCount //EpisodeCount //EpisodeFileCount 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.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/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index eb20ae076..ff426b021 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles 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); @@ -97,5 +98,10 @@ namespace NzbDrone.Core.MediaFiles { 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/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 e20b3b5dc..cade66616 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -12,7 +12,6 @@ namespace NzbDrone.Core.MediaFiles { public class TrackFile : ModelBase { - public string SpotifyTrackId { get; set; } public int AlbumId { get; set; } public int ArtistId { get; set; } public string RelativePath { get; set; } diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index cdce64860..0ed7ff34e 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -23,6 +23,10 @@ namespace NzbDrone.Core.Music } 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; } @@ -52,6 +56,10 @@ namespace NzbDrone.Core.Music { ForeignArtistId = otherArtist.ForeignArtistId; + MBId = otherArtist.MBId; + TADBId = otherArtist.TADBId; + DiscogsId = otherArtist.DiscogsId; + AMId = otherArtist.AMId; Name = otherArtist.Name; NameSlug = otherArtist.NameSlug; CleanName = otherArtist.CleanName; diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 8e281260a..f0f25ae96 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Music public string ForeignTrackId { get; set; } public int AlbumId { get; set; } - public LazyLoaded Artist { 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; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 0818a8ce7..963338f76 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -724,7 +724,10 @@ + + + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f5d3c3c24..dd0bf3497 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -20,7 +20,9 @@ namespace NzbDrone.Core.Organizer 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); @@ -202,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; @@ -225,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(); diff --git a/src/UI/AddArtist/SearchResultViewTemplate.hbs b/src/UI/AddArtist/SearchResultViewTemplate.hbs index 3c27e1b97..0a3bbe016 100644 --- a/src/UI/AddArtist/SearchResultViewTemplate.hbs +++ b/src/UI/AddArtist/SearchResultViewTemplate.hbs @@ -16,11 +16,11 @@ -
+
{{#unless existing}} {{#unless path}} diff --git a/src/UI/Artist/Details/InfoViewTemplate.hbs b/src/UI/Artist/Details/InfoViewTemplate.hbs index c6e689630..f305c976b 100644 --- a/src/UI/Artist/Details/InfoViewTemplate.hbs +++ b/src/UI/Artist/Details/InfoViewTemplate.hbs @@ -29,20 +29,18 @@
- 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/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/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/Controller.js b/src/UI/Controller.js index 5125bbc30..04dcce602 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -19,11 +19,6 @@ module.exports = NzbDroneController.extend({ this.showMainRegion(new AddArtistLayout({ action : action })); }, - artistDetails: function(query) { - this.setTitle('Artist Detail'); - this.showMainRegion(new SeriesDetailsLayout()); - }, - calendar : function() { this.setTitle('Calendar'); this.showMainRegion(new CalendarLayout()); diff --git a/src/UI/Handlebars/Helpers/Artist.js b/src/UI/Handlebars/Helpers/Artist.js index 13990530c..36c8983ba 100644 --- a/src/UI/Handlebars/Helpers/Artist.js +++ b/src/UI/Handlebars/Helpers/Artist.js @@ -19,24 +19,20 @@ 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() { @@ -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/Router.js b/src/UI/Router.js index ccc4b964d..ce8db831c 100644 --- a/src/UI/Router.js +++ b/src/UI/Router.js @@ -18,7 +18,6 @@ module.exports = Marionette.AppRouter.extend({ 'rss' : 'rss', 'system' : 'system', 'system/:action' : 'system', - 'artist/:query' : 'artistDetails', 'seasonpass' : 'seasonPass', 'artisteditor' : 'artistEditor', ':whatever' : 'showNotFound'