mirror of
https://github.com/Lidarr/Lidarr
synced 2026-01-06 23:56:11 +01:00
Merge pull request #4 from mattman86/feature/database
Implemented ability to search iTunes for a series and add to database
This commit is contained in:
commit
d47909b1f4
62 changed files with 2820 additions and 346 deletions
62
src/NzbDrone.Api/Music/AlbumResource.cs
Normal file
62
src/NzbDrone.Api/Music/AlbumResource.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using NzbDrone.Core.Music;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Api.Music
|
||||
{
|
||||
public class AlbumResource
|
||||
{
|
||||
public int AlbumId { get; set; }
|
||||
public string AlbumName { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public int Year { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public string ArtworkUrl { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public static class AlbumResourceMapper
|
||||
{
|
||||
public static AlbumResource ToResource(this Album model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new AlbumResource
|
||||
{
|
||||
AlbumId = model.AlbumId,
|
||||
Monitored = model.Monitored,
|
||||
Year = model.Year,
|
||||
AlbumName = model.Title,
|
||||
Genres = model.Genres,
|
||||
ArtworkUrl = model.ArtworkUrl
|
||||
};
|
||||
}
|
||||
|
||||
public static Album ToModel(this AlbumResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new Album
|
||||
{
|
||||
AlbumId = resource.AlbumId,
|
||||
Monitored = resource.Monitored,
|
||||
Year = resource.Year,
|
||||
Title = resource.AlbumName,
|
||||
Genres = resource.Genres,
|
||||
ArtworkUrl = resource.ArtworkUrl
|
||||
};
|
||||
}
|
||||
|
||||
public static List<AlbumResource> ToResource(this IEnumerable<Album> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<Album> ToModel(this IEnumerable<AlbumResource> resources)
|
||||
{
|
||||
return resources?.Select(ToModel).ToList() ?? new List<Album>();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/NzbDrone.Api/Music/ArtistLookupModule.cs
Normal file
46
src/NzbDrone.Api/Music/ArtistLookupModule.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Nancy;
|
||||
using NzbDrone.Api.Extensions;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Api.Music
|
||||
{
|
||||
public class ArtistLookupModule : NzbDroneRestModule<ArtistResource>
|
||||
{
|
||||
private readonly ISearchForNewSeries _searchProxy; //TODO: Switch out for Music varriant
|
||||
|
||||
public ArtistLookupModule(ISearchForNewSeries searchProxy)
|
||||
: base("/artist/lookup")
|
||||
{
|
||||
_searchProxy = searchProxy;
|
||||
Get["/"] = x => Search();
|
||||
}
|
||||
|
||||
|
||||
private Response Search()
|
||||
{
|
||||
var iTunesResults = _searchProxy.SearchForNewArtist((string)Request.Query.term);
|
||||
return MapToResource(iTunesResults).AsResponse();
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<ArtistResource> MapToResource(IEnumerable<Core.Music.Artist> artists)
|
||||
{
|
||||
foreach (var currentArtist in artists)
|
||||
{
|
||||
var resource = currentArtist.ToResource();
|
||||
var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
|
||||
if (poster != null)
|
||||
{
|
||||
resource.RemotePoster = poster.Url;
|
||||
}
|
||||
|
||||
yield return resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/NzbDrone.Api/Music/ArtistModule.cs
Normal file
194
src/NzbDrone.Api/Music/ArtistModule.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
using NzbDrone.Core.SeriesStats;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.SignalR;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Api.Music
|
||||
{
|
||||
public class ArtistModule : NzbDroneRestModuleWithSignalR<ArtistResource, Core.Music.Artist>,
|
||||
IHandle<TrackImportedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>,
|
||||
IHandle<ArtistUpdatedEvent>,
|
||||
IHandle<ArtistEditedEvent>,
|
||||
IHandle<ArtistDeletedEvent>,
|
||||
IHandle<ArtistRenamedEvent>
|
||||
//IHandle<MediaCoversUpdatedEvent>
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAddArtistService _addSeriesService;
|
||||
private readonly ISeriesStatisticsService _seriesStatisticsService;
|
||||
private readonly IMapCoversToLocal _coverMapper;
|
||||
|
||||
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IArtistService artistService,
|
||||
IAddArtistService addSeriesService,
|
||||
ISeriesStatisticsService seriesStatisticsService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
RootFolderValidator rootFolderValidator,
|
||||
ArtistPathValidator seriesPathValidator,
|
||||
ArtistExistsValidator artistExistsValidator,
|
||||
DroneFactoryValidator droneFactoryValidator,
|
||||
SeriesAncestorValidator seriesAncestorValidator,
|
||||
ProfileExistsValidator profileExistsValidator
|
||||
)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_addSeriesService = addSeriesService;
|
||||
_seriesStatisticsService = seriesStatisticsService;
|
||||
|
||||
_coverMapper = coverMapper;
|
||||
|
||||
GetResourceAll = AllArtist;
|
||||
GetResourceById = GetArtist;
|
||||
CreateResource = AddArtist;
|
||||
UpdateResource = UpdatArtist;
|
||||
DeleteResource = DeleteArtist;
|
||||
|
||||
Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId));
|
||||
|
||||
SharedValidator.RuleFor(s => s.Path)
|
||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(seriesPathValidator)
|
||||
.SetValidator(droneFactoryValidator)
|
||||
.SetValidator(seriesAncestorValidator)
|
||||
.When(s => !s.Path.IsNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
|
||||
|
||||
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.ItunesId).GreaterThan(0).SetValidator(artistExistsValidator);
|
||||
|
||||
PutValidator.RuleFor(s => s.Path).IsValidPath();
|
||||
}
|
||||
|
||||
private ArtistResource GetArtist(int id)
|
||||
{
|
||||
var artist = _artistService.GetArtist(id);
|
||||
return MapToResource(artist);
|
||||
}
|
||||
|
||||
private ArtistResource MapToResource(Artist artist)
|
||||
{
|
||||
if (artist == null) return null;
|
||||
|
||||
var resource = artist.ToResource();
|
||||
MapCoversToLocal(resource);
|
||||
//FetchAndLinkSeriesStatistics(resource);
|
||||
//PopulateAlternateTitles(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private List<ArtistResource> AllArtist()
|
||||
{
|
||||
//var seriesStats = _seriesStatisticsService.SeriesStatistics();
|
||||
var artistResources = _artistService.GetAllArtists().ToResource();
|
||||
|
||||
MapCoversToLocal(artistResources.ToArray());
|
||||
//LinkSeriesStatistics(seriesResources, seriesStats);
|
||||
//PopulateAlternateTitles(seriesResources);
|
||||
|
||||
return artistResources;
|
||||
}
|
||||
|
||||
private int AddArtist(ArtistResource seriesResource)
|
||||
{
|
||||
var model = seriesResource.ToModel();
|
||||
|
||||
return _addSeriesService.AddArtist(model).Id;
|
||||
}
|
||||
|
||||
private void UpdatArtist(ArtistResource artistResource)
|
||||
{
|
||||
var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id));
|
||||
|
||||
_artistService.UpdateArtist(model);
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, artistResource.Id);
|
||||
}
|
||||
|
||||
private void DeleteArtist(int id)
|
||||
{
|
||||
var deleteFiles = false;
|
||||
var deleteFilesQuery = Request.Query.deleteFiles;
|
||||
|
||||
if (deleteFilesQuery.HasValue)
|
||||
{
|
||||
deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value);
|
||||
}
|
||||
|
||||
_artistService.DeleteArtist(id, deleteFiles);
|
||||
}
|
||||
|
||||
private void MapCoversToLocal(params ArtistResource[] artists)
|
||||
{
|
||||
foreach (var artistResource in artists)
|
||||
{
|
||||
_coverMapper.ConvertToLocalUrls(artistResource.Id, artistResource.Images);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(TrackImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.ItunesTrackId);
|
||||
}
|
||||
|
||||
public void Handle(TrackFileDeletedEvent message)
|
||||
{
|
||||
if (message.Reason == DeleteMediaFileReason.Upgrade) return;
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ItunesTrackId);
|
||||
}
|
||||
|
||||
public void Handle(ArtistUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
|
||||
}
|
||||
|
||||
public void Handle(ArtistEditedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
|
||||
}
|
||||
|
||||
public void Handle(ArtistDeletedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource());
|
||||
}
|
||||
|
||||
public void Handle(ArtistRenamedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
|
||||
}
|
||||
|
||||
//public void Handle(ArtistDeletedEvent message)
|
||||
//{
|
||||
// BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource());
|
||||
//}
|
||||
|
||||
//public void Handle(ArtistRenamedEvent message)
|
||||
//{
|
||||
// BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
|
||||
//}
|
||||
|
||||
//public void Handle(MediaCoversUpdatedEvent message)
|
||||
//{
|
||||
// BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
188
src/NzbDrone.Api/Music/ArtistResource.cs
Normal file
188
src/NzbDrone.Api/Music/ArtistResource.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using NzbDrone.Api.REST;
|
||||
using NzbDrone.Api.Series;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Tv;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Api.Music
|
||||
{
|
||||
public class ArtistResource : RestResource
|
||||
{
|
||||
public ArtistResource()
|
||||
{
|
||||
Monitored = true;
|
||||
}
|
||||
|
||||
|
||||
//View Only
|
||||
public string ArtistName { get; set; }
|
||||
public int ItunesId { get; set; }
|
||||
//public List<AlternateTitleResource> AlternateTitles { get; set; }
|
||||
//public string SortTitle { get; set; }
|
||||
|
||||
public int AlbumCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Albums == null) return 0;
|
||||
|
||||
return Albums.Where(s => s.AlbumId > 0).Count(); // TODO: CHeck this condition
|
||||
}
|
||||
}
|
||||
|
||||
public int? TotalTrackCount { get; set; }
|
||||
public int? TrackCount { get; set; }
|
||||
public int? TrackFileCount { get; set; }
|
||||
public long? SizeOnDisk { get; set; }
|
||||
//public SeriesStatusType Status { get; set; }
|
||||
|
||||
public List<MediaCover> Images { get; set; }
|
||||
|
||||
public string RemotePoster { get; set; }
|
||||
public List<AlbumResource> Albums { get; set; }
|
||||
|
||||
|
||||
//View & Edit
|
||||
public string Path { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
|
||||
//Editing Only
|
||||
public bool ArtistFolder { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
|
||||
public string RootFolderPath { get; set; }
|
||||
public string Certification { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public HashSet<int> 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 static class ArtistResourceMapper
|
||||
{
|
||||
public static ArtistResource ToResource(this Core.Music.Artist model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new ArtistResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
ArtistName = model.ArtistName,
|
||||
//AlternateTitles
|
||||
//SortTitle = resource.SortTitle,
|
||||
|
||||
//TotalEpisodeCount
|
||||
//EpisodeCount
|
||||
//EpisodeFileCount
|
||||
//SizeOnDisk
|
||||
//Status = resource.Status,
|
||||
//Overview = resource.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
//Network = resource.Network,
|
||||
//AirTime = resource.AirTime,
|
||||
Images = model.Images,
|
||||
|
||||
Albums = model.Albums.ToResource(),
|
||||
//Year = resource.Year,
|
||||
|
||||
Path = model.Path,
|
||||
ProfileId = model.ProfileId,
|
||||
|
||||
ArtistFolder = model.ArtistFolder,
|
||||
Monitored = model.Monitored,
|
||||
|
||||
//UseSceneNumbering = resource.UseSceneNumbering,
|
||||
//Runtime = resource.Runtime,
|
||||
//TvdbId = resource.TvdbId,
|
||||
//TvRageId = resource.TvRageId,
|
||||
//TvMazeId = resource.TvMazeId,
|
||||
//FirstAired = resource.FirstAired,
|
||||
//LastInfoSync = resource.LastInfoSync,
|
||||
//SeriesType = resource.SeriesType,
|
||||
ItunesId = model.ItunesId,
|
||||
ArtistSlug = model.ArtistSlug,
|
||||
|
||||
RootFolderPath = model.RootFolderPath,
|
||||
Genres = model.Genres,
|
||||
Tags = model.Tags,
|
||||
Added = model.Added,
|
||||
AddOptions = model.AddOptions,
|
||||
//Ratings = resource.Ratings
|
||||
};
|
||||
}
|
||||
|
||||
public static Core.Music.Artist ToModel(this ArtistResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new Core.Music.Artist
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
ArtistName = resource.ArtistName,
|
||||
//AlternateTitles
|
||||
//SortTitle = resource.SortTitle,
|
||||
|
||||
//TotalEpisodeCount
|
||||
//EpisodeCount
|
||||
//EpisodeFileCount
|
||||
//SizeOnDisk
|
||||
//Status = resource.Status,
|
||||
//Overview = resource.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
//Network = resource.Network,
|
||||
//AirTime = resource.AirTime,
|
||||
Images = resource.Images,
|
||||
|
||||
Albums = resource.Albums.ToModel(),
|
||||
//Year = resource.Year,
|
||||
|
||||
Path = resource.Path,
|
||||
ProfileId = resource.ProfileId,
|
||||
|
||||
ArtistFolder = resource.ArtistFolder,
|
||||
Monitored = resource.Monitored,
|
||||
|
||||
//UseSceneNumbering = resource.UseSceneNumbering,
|
||||
//Runtime = resource.Runtime,
|
||||
//TvdbId = resource.TvdbId,
|
||||
//TvRageId = resource.TvRageId,
|
||||
//TvMazeId = resource.TvMazeId,
|
||||
//FirstAired = resource.FirstAired,
|
||||
//LastInfoSync = resource.LastInfoSync,
|
||||
//SeriesType = resource.SeriesType,
|
||||
ItunesId = resource.ItunesId,
|
||||
ArtistSlug = resource.ArtistSlug,
|
||||
|
||||
RootFolderPath = resource.RootFolderPath,
|
||||
Genres = resource.Genres,
|
||||
Tags = resource.Tags,
|
||||
Added = resource.Added,
|
||||
AddOptions = resource.AddOptions,
|
||||
//Ratings = resource.Ratings
|
||||
};
|
||||
}
|
||||
|
||||
public static Core.Music.Artist ToModel(this ArtistResource resource, Core.Music.Artist artist)
|
||||
{
|
||||
var updatedArtist = resource.ToModel();
|
||||
|
||||
artist.ApplyChanges(updatedArtist);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
public static List<ArtistResource> ToResource(this IEnumerable<Core.Music.Artist> artist)
|
||||
{
|
||||
return artist.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +111,10 @@
|
|||
<Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" />
|
||||
<Compile Include="Indexers\ReleaseModuleBase.cs" />
|
||||
<Compile Include="Indexers\ReleasePushModule.cs" />
|
||||
<Compile Include="Music\AlbumResource.cs" />
|
||||
<Compile Include="Music\ArtistLookupModule.cs" />
|
||||
<Compile Include="Music\ArtistModule.cs" />
|
||||
<Compile Include="Music\ArtistResource.cs" />
|
||||
<Compile Include="Parse\ParseModule.cs" />
|
||||
<Compile Include="Parse\ParseResource.cs" />
|
||||
<Compile Include="ManualImport\ManualImportModule.cs" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace NzbDrone.Common.Cloud
|
|||
public interface ILidarrCloudRequestBuilder
|
||||
{
|
||||
IHttpRequestBuilderFactory Services { get; }
|
||||
IHttpRequestBuilderFactory Search { get; }
|
||||
IHttpRequestBuilderFactory SkyHookTvdb { get; }
|
||||
}
|
||||
|
||||
|
|
@ -12,16 +13,21 @@ public class LidarrCloudRequestBuilder : ILidarrCloudRequestBuilder
|
|||
{
|
||||
public LidarrCloudRequestBuilder()
|
||||
{
|
||||
Services = new HttpRequestBuilder("http://services.lidarr.audio/v1/")
|
||||
Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/")
|
||||
.CreateFactory();
|
||||
|
||||
SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.audio/v1/tvdb/{route}/{language}/")
|
||||
Search = new HttpRequestBuilder("https://itunes.apple.com/{route}/")
|
||||
.CreateFactory();
|
||||
|
||||
SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.tv/v1/tvdb/{route}/{language}/")
|
||||
.SetSegment("language", "en")
|
||||
.CreateFactory();
|
||||
}
|
||||
|
||||
public IHttpRequestBuilderFactory Services { get; }
|
||||
|
||||
public IHttpRequestBuilderFactory Search { get; }
|
||||
|
||||
public IHttpRequestBuilderFactory SkyHookTvdb { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
using NzbDrone.Test.Common;
|
||||
using FluentAssertions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.DataAugmentation;
|
||||
|
||||
namespace NzbDrone.Core.Test.DataAugmentation.Scene
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,243 +11,243 @@
|
|||
|
||||
namespace NzbDrone.Core.DataAugmentation.Scene
|
||||
{
|
||||
//public interface ISceneMappingService
|
||||
//{
|
||||
// List<string> GetSceneNames(int tvdbId, List<int> seasonNumbers, List<int> sceneSeasonNumbers);
|
||||
// int? FindTvdbId(string title);
|
||||
// List<SceneMapping> FindByTvdbId(int tvdbId);
|
||||
// SceneMapping FindSceneMapping(string title);
|
||||
// int? GetSceneSeasonNumber(string title);
|
||||
// int? GetTvdbSeasonNumber(string title);
|
||||
// int? GetSceneSeasonNumber(int tvdbId, int seasonNumber);
|
||||
//}
|
||||
public interface ISceneMappingService
|
||||
{
|
||||
List<string> GetSceneNames(int tvdbId, List<int> seasonNumbers, List<int> sceneSeasonNumbers);
|
||||
int? FindTvdbId(string title);
|
||||
List<SceneMapping> FindByTvdbId(int tvdbId);
|
||||
SceneMapping FindSceneMapping(string title);
|
||||
int? GetSceneSeasonNumber(string title);
|
||||
int? GetTvdbSeasonNumber(string title);
|
||||
int? GetSceneSeasonNumber(int tvdbId, int seasonNumber);
|
||||
}
|
||||
|
||||
//public class SceneMappingService : ISceneMappingService,
|
||||
// IHandle<SeriesRefreshStartingEvent>,
|
||||
// IExecute<UpdateSceneMappingCommand>
|
||||
//{
|
||||
// private readonly ISceneMappingRepository _repository;
|
||||
// private readonly IEnumerable<ISceneMappingProvider> _sceneMappingProviders;
|
||||
// private readonly IEventAggregator _eventAggregator;
|
||||
// private readonly Logger _logger;
|
||||
// private readonly ICachedDictionary<List<SceneMapping>> _getTvdbIdCache;
|
||||
// private readonly ICachedDictionary<List<SceneMapping>> _findByTvdbIdCache;
|
||||
public class SceneMappingService : ISceneMappingService,
|
||||
IHandle<SeriesRefreshStartingEvent>,
|
||||
IExecute<UpdateSceneMappingCommand>
|
||||
{
|
||||
private readonly ISceneMappingRepository _repository;
|
||||
private readonly IEnumerable<ISceneMappingProvider> _sceneMappingProviders;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
private readonly ICachedDictionary<List<SceneMapping>> _getTvdbIdCache;
|
||||
private readonly ICachedDictionary<List<SceneMapping>> _findByTvdbIdCache;
|
||||
|
||||
//public SceneMappingService(ISceneMappingRepository repository,
|
||||
// ICacheManager cacheManager,
|
||||
// IEnumerable<ISceneMappingProvider> sceneMappingProviders,
|
||||
// IEventAggregator eventAggregator,
|
||||
// Logger logger)
|
||||
//{
|
||||
// _repository = repository;
|
||||
// _sceneMappingProviders = sceneMappingProviders;
|
||||
// _eventAggregator = eventAggregator;
|
||||
// _logger = logger;
|
||||
public SceneMappingService(ISceneMappingRepository repository,
|
||||
ICacheManager cacheManager,
|
||||
IEnumerable<ISceneMappingProvider> sceneMappingProviders,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_sceneMappingProviders = sceneMappingProviders;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
|
||||
// _getTvdbIdCache = cacheManager.GetCacheDictionary<List<SceneMapping>>(GetType(), "tvdb_id");
|
||||
// _findByTvdbIdCache = cacheManager.GetCacheDictionary<List<SceneMapping>>(GetType(), "find_tvdb_id");
|
||||
//}
|
||||
_getTvdbIdCache = cacheManager.GetCacheDictionary<List<SceneMapping>>(GetType(), "tvdb_id");
|
||||
_findByTvdbIdCache = cacheManager.GetCacheDictionary<List<SceneMapping>>(GetType(), "find_tvdb_id");
|
||||
}
|
||||
|
||||
// public List<string> GetSceneNames(int tvdbId, List<int> seasonNumbers, List<int> sceneSeasonNumbers)
|
||||
// {
|
||||
// var mappings = FindByTvdbId(tvdbId);
|
||||
public List<string> GetSceneNames(int tvdbId, List<int> seasonNumbers, List<int> sceneSeasonNumbers)
|
||||
{
|
||||
var mappings = FindByTvdbId(tvdbId);
|
||||
|
||||
// if (mappings == null)
|
||||
// {
|
||||
// return new List<string>();
|
||||
// }
|
||||
if (mappings == null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) ||
|
||||
// n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) ||
|
||||
// (n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1)
|
||||
// .Select(n => n.SearchTerm).Distinct().ToList();
|
||||
var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) ||
|
||||
n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) ||
|
||||
(n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1)
|
||||
.Select(n => n.SearchTerm).Distinct().ToList();
|
||||
|
||||
// return FilterNonEnglish(names);
|
||||
// }
|
||||
return FilterNonEnglish(names);
|
||||
}
|
||||
|
||||
// public int? FindTvdbId(string title)
|
||||
// {
|
||||
// var mapping = FindMapping(title);
|
||||
public int? FindTvdbId(string title)
|
||||
{
|
||||
var mapping = FindMapping(title);
|
||||
|
||||
// if (mapping == null)
|
||||
// return null;
|
||||
if (mapping == null)
|
||||
return null;
|
||||
|
||||
// return mapping.TvdbId;
|
||||
// }
|
||||
return mapping.TvdbId;
|
||||
}
|
||||
|
||||
// public List<SceneMapping> FindByTvdbId(int tvdbId)
|
||||
// {
|
||||
// if (_findByTvdbIdCache.Count == 0)
|
||||
// {
|
||||
// RefreshCache();
|
||||
// }
|
||||
public List<SceneMapping> FindByTvdbId(int tvdbId)
|
||||
{
|
||||
if (_findByTvdbIdCache.Count == 0)
|
||||
{
|
||||
RefreshCache();
|
||||
}
|
||||
|
||||
// var mappings = _findByTvdbIdCache.Find(tvdbId.ToString());
|
||||
var mappings = _findByTvdbIdCache.Find(tvdbId.ToString());
|
||||
|
||||
// if (mappings == null)
|
||||
// {
|
||||
// return new List<SceneMapping>();
|
||||
// }
|
||||
if (mappings == null)
|
||||
{
|
||||
return new List<SceneMapping>();
|
||||
}
|
||||
|
||||
// return mappings;
|
||||
// }
|
||||
return mappings;
|
||||
}
|
||||
|
||||
// public SceneMapping FindSceneMapping(string title)
|
||||
// {
|
||||
// return FindMapping(title);
|
||||
// }
|
||||
public SceneMapping FindSceneMapping(string title)
|
||||
{
|
||||
return FindMapping(title);
|
||||
}
|
||||
|
||||
// public int? GetSceneSeasonNumber(string title)
|
||||
// {
|
||||
// var mapping = FindMapping(title);
|
||||
public int? GetSceneSeasonNumber(string title)
|
||||
{
|
||||
var mapping = FindMapping(title);
|
||||
|
||||
// if (mapping == null)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
if (mapping == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// return mapping.SceneSeasonNumber;
|
||||
// }
|
||||
return mapping.SceneSeasonNumber;
|
||||
}
|
||||
|
||||
// public int? GetTvdbSeasonNumber(string title)
|
||||
// {
|
||||
// var mapping = FindMapping(title);
|
||||
public int? GetTvdbSeasonNumber(string title)
|
||||
{
|
||||
var mapping = FindMapping(title);
|
||||
|
||||
// if (mapping == null)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
if (mapping == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// return mapping.SeasonNumber;
|
||||
// }
|
||||
return mapping.SeasonNumber;
|
||||
}
|
||||
|
||||
// public int? GetSceneSeasonNumber(int tvdbId, int seasonNumber)
|
||||
// {
|
||||
// var mappings = FindByTvdbId(tvdbId);
|
||||
public int? GetSceneSeasonNumber(int tvdbId, int seasonNumber)
|
||||
{
|
||||
var mappings = FindByTvdbId(tvdbId);
|
||||
|
||||
// if (mappings == null)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
if (mappings == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// var mapping = mappings.FirstOrDefault(e => e.SeasonNumber == seasonNumber && e.SceneSeasonNumber.HasValue);
|
||||
var mapping = mappings.FirstOrDefault(e => e.SeasonNumber == seasonNumber && e.SceneSeasonNumber.HasValue);
|
||||
|
||||
// if (mapping == null)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
if (mapping == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// return mapping.SceneSeasonNumber;
|
||||
// }
|
||||
return mapping.SceneSeasonNumber;
|
||||
}
|
||||
|
||||
// private void UpdateMappings()
|
||||
// {
|
||||
// _logger.Info("Updating Scene mappings");
|
||||
private void UpdateMappings()
|
||||
{
|
||||
_logger.Info("Updating Scene mappings");
|
||||
|
||||
// foreach (var sceneMappingProvider in _sceneMappingProviders)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var mappings = sceneMappingProvider.GetSceneMappings();
|
||||
foreach (var sceneMappingProvider in _sceneMappingProviders)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mappings = sceneMappingProvider.GetSceneMappings();
|
||||
|
||||
// if (mappings.Any())
|
||||
// {
|
||||
// _repository.Clear(sceneMappingProvider.GetType().Name);
|
||||
if (mappings.Any())
|
||||
{
|
||||
_repository.Clear(sceneMappingProvider.GetType().Name);
|
||||
|
||||
// mappings.RemoveAll(sceneMapping =>
|
||||
// {
|
||||
// if (sceneMapping.Title.IsNullOrWhiteSpace() ||
|
||||
// sceneMapping.SearchTerm.IsNullOrWhiteSpace())
|
||||
// {
|
||||
// _logger.Warn("Invalid scene mapping found for: {0}, skipping", sceneMapping.TvdbId);
|
||||
// return true;
|
||||
// }
|
||||
mappings.RemoveAll(sceneMapping =>
|
||||
{
|
||||
if (sceneMapping.Title.IsNullOrWhiteSpace() ||
|
||||
sceneMapping.SearchTerm.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Warn("Invalid scene mapping found for: {0}, skipping", sceneMapping.TvdbId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// return false;
|
||||
// });
|
||||
return false;
|
||||
});
|
||||
|
||||
// foreach (var sceneMapping in mappings)
|
||||
// {
|
||||
// sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle();
|
||||
// sceneMapping.Type = sceneMappingProvider.GetType().Name;
|
||||
// }
|
||||
foreach (var sceneMapping in mappings)
|
||||
{
|
||||
sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle();
|
||||
sceneMapping.Type = sceneMappingProvider.GetType().Name;
|
||||
}
|
||||
|
||||
// _repository.InsertMany(mappings.ToList());
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// _logger.Warn("Received empty list of mapping. will not update.");
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.Error(ex, "Failed to Update Scene Mappings.");
|
||||
// }
|
||||
// }
|
||||
|
||||
// RefreshCache();
|
||||
_repository.InsertMany(mappings.ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Received empty list of mapping. will not update.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to Update Scene Mappings.");
|
||||
}
|
||||
}
|
||||
|
||||
// _eventAggregator.PublishEvent(new SceneMappingsUpdatedEvent());
|
||||
// }
|
||||
RefreshCache();
|
||||
|
||||
// private SceneMapping FindMapping(string title)
|
||||
// {
|
||||
// if (_getTvdbIdCache.Count == 0)
|
||||
// {
|
||||
// RefreshCache();
|
||||
// }
|
||||
_eventAggregator.PublishEvent(new SceneMappingsUpdatedEvent());
|
||||
}
|
||||
|
||||
// var candidates = _getTvdbIdCache.Find(title.CleanSeriesTitle());
|
||||
private SceneMapping FindMapping(string title)
|
||||
{
|
||||
if (_getTvdbIdCache.Count == 0)
|
||||
{
|
||||
RefreshCache();
|
||||
}
|
||||
|
||||
// if (candidates == null)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
var candidates = _getTvdbIdCache.Find(title.CleanSeriesTitle());
|
||||
|
||||
// if (candidates.Count == 1)
|
||||
// {
|
||||
// return candidates.First();
|
||||
// }
|
||||
if (candidates == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// var exactMatch = candidates.OrderByDescending(v => v.SeasonNumber)
|
||||
// .FirstOrDefault(v => v.Title == title);
|
||||
if (candidates.Count == 1)
|
||||
{
|
||||
return candidates.First();
|
||||
}
|
||||
|
||||
// if (exactMatch != null)
|
||||
// {
|
||||
// return exactMatch;
|
||||
// }
|
||||
var exactMatch = candidates.OrderByDescending(v => v.SeasonNumber)
|
||||
.FirstOrDefault(v => v.Title == title);
|
||||
|
||||
// var closestMatch = candidates.OrderBy(v => title.LevenshteinDistance(v.Title, 10, 1, 10))
|
||||
// .ThenByDescending(v => v.SeasonNumber)
|
||||
// .First();
|
||||
if (exactMatch != null)
|
||||
{
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
// return closestMatch;
|
||||
// }
|
||||
var closestMatch = candidates.OrderBy(v => title.LevenshteinDistance(v.Title, 10, 1, 10))
|
||||
.ThenByDescending(v => v.SeasonNumber)
|
||||
.First();
|
||||
|
||||
// private void RefreshCache()
|
||||
// {
|
||||
// var mappings = _repository.All().ToList();
|
||||
return closestMatch;
|
||||
}
|
||||
|
||||
// _getTvdbIdCache.Update(mappings.GroupBy(v => v.ParseTerm).ToDictionary(v => v.Key, v => v.ToList()));
|
||||
// _findByTvdbIdCache.Update(mappings.GroupBy(v => v.TvdbId).ToDictionary(v => v.Key.ToString(), v => v.ToList()));
|
||||
// }
|
||||
private void RefreshCache()
|
||||
{
|
||||
var mappings = _repository.All().ToList();
|
||||
|
||||
// private List<string> FilterNonEnglish(List<string> titles)
|
||||
// {
|
||||
// return titles.Where(title => title.All(c => c <= 255)).ToList();
|
||||
// }
|
||||
_getTvdbIdCache.Update(mappings.GroupBy(v => v.ParseTerm).ToDictionary(v => v.Key, v => v.ToList()));
|
||||
_findByTvdbIdCache.Update(mappings.GroupBy(v => v.TvdbId).ToDictionary(v => v.Key.ToString(), v => v.ToList()));
|
||||
}
|
||||
|
||||
// public void Handle(SeriesRefreshStartingEvent message)
|
||||
// {
|
||||
// if (message.ManualTrigger && _findByTvdbIdCache.IsExpired(TimeSpan.FromMinutes(1)))
|
||||
// {
|
||||
// UpdateMappings();
|
||||
// }
|
||||
// }
|
||||
private List<string> FilterNonEnglish(List<string> titles)
|
||||
{
|
||||
return titles.Where(title => title.All(c => c <= 255)).ToList();
|
||||
}
|
||||
|
||||
// public void Execute(UpdateSceneMappingCommand message)
|
||||
// {
|
||||
// UpdateMappings();
|
||||
// }
|
||||
//}
|
||||
public void Handle(SeriesRefreshStartingEvent message)
|
||||
{
|
||||
if (message.ManualTrigger && _findByTvdbIdCache.IsExpired(TimeSpan.FromMinutes(1)))
|
||||
{
|
||||
UpdateMappings();
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(UpdateSceneMappingCommand message)
|
||||
{
|
||||
UpdateMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,58 +33,58 @@ private void PerformUpdate(Series series)
|
|||
{
|
||||
_logger.Debug("Updating scene numbering mapping for: {0}", series);
|
||||
|
||||
try
|
||||
{
|
||||
var mappings = _xemProxy.GetSceneTvdbMappings(series.TvdbId);
|
||||
//try
|
||||
//{
|
||||
// var mappings = _xemProxy.GetSceneTvdbMappings(series.TvdbId);
|
||||
|
||||
if (!mappings.Any() && !series.UseSceneNumbering)
|
||||
{
|
||||
_logger.Debug("Mappings for: {0} are empty, skipping", series);
|
||||
return;
|
||||
}
|
||||
// if (!mappings.Any() && !series.UseSceneNumbering)
|
||||
// {
|
||||
// _logger.Debug("Mappings for: {0} are empty, skipping", series);
|
||||
// return;
|
||||
// }
|
||||
|
||||
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
// var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
episode.SceneAbsoluteEpisodeNumber = null;
|
||||
episode.SceneSeasonNumber = null;
|
||||
episode.SceneEpisodeNumber = null;
|
||||
episode.UnverifiedSceneNumbering = false;
|
||||
}
|
||||
// foreach (var episode in episodes)
|
||||
// {
|
||||
// episode.SceneAbsoluteEpisodeNumber = null;
|
||||
// episode.SceneSeasonNumber = null;
|
||||
// episode.SceneEpisodeNumber = null;
|
||||
// episode.UnverifiedSceneNumbering = false;
|
||||
// }
|
||||
|
||||
foreach (var mapping in mappings)
|
||||
{
|
||||
_logger.Debug("Setting scene numbering mappings for {0} S{1:00}E{2:00}", series, mapping.Tvdb.Season, mapping.Tvdb.Episode);
|
||||
// foreach (var mapping in mappings)
|
||||
// {
|
||||
// _logger.Debug("Setting scene numbering mappings for {0} S{1:00}E{2:00}", series, mapping.Tvdb.Season, mapping.Tvdb.Episode);
|
||||
|
||||
var episode = episodes.SingleOrDefault(e => e.SeasonNumber == mapping.Tvdb.Season && e.EpisodeNumber == mapping.Tvdb.Episode);
|
||||
// var episode = episodes.SingleOrDefault(e => e.SeasonNumber == mapping.Tvdb.Season && e.EpisodeNumber == mapping.Tvdb.Episode);
|
||||
|
||||
if (episode == null)
|
||||
{
|
||||
_logger.Debug("Information hasn't been added to TheTVDB yet, skipping.");
|
||||
continue;
|
||||
}
|
||||
// if (episode == null)
|
||||
// {
|
||||
// _logger.Debug("Information hasn't been added to TheTVDB yet, skipping.");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
episode.SceneAbsoluteEpisodeNumber = mapping.Scene.Absolute;
|
||||
episode.SceneSeasonNumber = mapping.Scene.Season;
|
||||
episode.SceneEpisodeNumber = mapping.Scene.Episode;
|
||||
}
|
||||
// episode.SceneAbsoluteEpisodeNumber = mapping.Scene.Absolute;
|
||||
// episode.SceneSeasonNumber = mapping.Scene.Season;
|
||||
// episode.SceneEpisodeNumber = mapping.Scene.Episode;
|
||||
// }
|
||||
|
||||
if (episodes.Any(v => v.SceneEpisodeNumber.HasValue && v.SceneSeasonNumber != 0))
|
||||
{
|
||||
ExtrapolateMappings(series, episodes, mappings);
|
||||
}
|
||||
// if (episodes.Any(v => v.SceneEpisodeNumber.HasValue && v.SceneSeasonNumber != 0))
|
||||
// {
|
||||
// ExtrapolateMappings(series, episodes, mappings);
|
||||
// }
|
||||
|
||||
_episodeService.UpdateEpisodes(episodes);
|
||||
series.UseSceneNumbering = mappings.Any();
|
||||
_seriesService.UpdateSeries(series);
|
||||
// _episodeService.UpdateEpisodes(episodes);
|
||||
// series.UseSceneNumbering = mappings.Any();
|
||||
// _seriesService.UpdateSeries(series);
|
||||
|
||||
_logger.Debug("XEM mapping updated for {0}", series);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error updating scene numbering mappings for {0}", series);
|
||||
}
|
||||
// _logger.Debug("XEM mapping updated for {0}", series);
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// _logger.Error(ex, "Error updating scene numbering mappings for {0}", series);
|
||||
//}
|
||||
}
|
||||
|
||||
private void ExtrapolateMappings(Series series, List<Episode> episodes, List<Model.XemSceneTvdbMapping> mappings)
|
||||
|
|
@ -212,32 +212,32 @@ public List<SceneMapping> GetSceneMappings()
|
|||
|
||||
public void Handle(SeriesUpdatedEvent message)
|
||||
{
|
||||
if (_cache.IsExpired(TimeSpan.FromHours(3)))
|
||||
{
|
||||
UpdateXemSeriesIds();
|
||||
}
|
||||
//if (_cache.IsExpired(TimeSpan.FromHours(3)))
|
||||
//{
|
||||
// UpdateXemSeriesIds();
|
||||
//}
|
||||
|
||||
if (_cache.Count == 0)
|
||||
{
|
||||
_logger.Debug("Scene numbering is not available");
|
||||
return;
|
||||
}
|
||||
//if (_cache.Count == 0)
|
||||
//{
|
||||
// _logger.Debug("Scene numbering is not available");
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (!_cache.Find(message.Series.TvdbId.ToString()) && !message.Series.UseSceneNumbering)
|
||||
{
|
||||
_logger.Debug("Scene numbering is not available for {0} [{1}]", message.Series.Title, message.Series.TvdbId);
|
||||
return;
|
||||
}
|
||||
//if (!_cache.Find(message.Series.TvdbId.ToString()) && !message.Series.UseSceneNumbering)
|
||||
//{
|
||||
// _logger.Debug("Scene numbering is not available for {0} [{1}]", message.Series.Title, message.Series.TvdbId);
|
||||
// return;
|
||||
//}
|
||||
|
||||
PerformUpdate(message.Series);
|
||||
//PerformUpdate(message.Series);
|
||||
}
|
||||
|
||||
public void Handle(SeriesRefreshStartingEvent message)
|
||||
{
|
||||
if (message.ManualTrigger && _cache.IsExpired(TimeSpan.FromMinutes(1)))
|
||||
{
|
||||
UpdateXemSeriesIds();
|
||||
}
|
||||
//if (message.ManualTrigger && _cache.IsExpired(TimeSpan.FromMinutes(1)))
|
||||
//{
|
||||
// UpdateXemSeriesIds();
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs
Normal file
74
src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(111)]
|
||||
public class setup_music : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("Artist")
|
||||
.WithColumn("ItunesId").AsInt32().Unique()
|
||||
.WithColumn("ArtistName").AsString().Unique()
|
||||
.WithColumn("ArtistSlug").AsString().Nullable() //.Unique()
|
||||
.WithColumn("CleanTitle").AsString().Nullable() // Do we need this?
|
||||
.WithColumn("Monitored").AsBoolean()
|
||||
.WithColumn("AlbumFolder").AsBoolean().Nullable()
|
||||
.WithColumn("ArtistFolder").AsBoolean().Nullable()
|
||||
.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("Genres").AsString().Nullable()
|
||||
.WithColumn("Albums").AsString().Nullable()
|
||||
.WithColumn("Tags").AsString().Nullable()
|
||||
.WithColumn("AddOptions").AsString().Nullable()
|
||||
;
|
||||
|
||||
Create.TableForModel("Albums")
|
||||
.WithColumn("AlbumId").AsInt32()
|
||||
.WithColumn("ArtistId").AsInt32()
|
||||
.WithColumn("Title").AsString()
|
||||
.WithColumn("Year").AsInt32()
|
||||
.WithColumn("Image").AsInt32()
|
||||
.WithColumn("TrackCount").AsInt32()
|
||||
.WithColumn("DiscCount").AsInt32()
|
||||
.WithColumn("Monitored").AsBoolean()
|
||||
.WithColumn("Overview").AsString();
|
||||
|
||||
Create.TableForModel("Tracks")
|
||||
.WithColumn("ItunesTrackId").AsInt32().Unique()
|
||||
.WithColumn("AlbumId").AsInt32()
|
||||
.WithColumn("ArtistsId").AsString().Nullable()
|
||||
.WithColumn("TrackNumber").AsInt32()
|
||||
.WithColumn("Title").AsString().Nullable()
|
||||
.WithColumn("Ignored").AsBoolean().Nullable()
|
||||
.WithColumn("Explict").AsBoolean()
|
||||
.WithColumn("TrackExplicitName").AsString().Nullable()
|
||||
.WithColumn("TrackCensoredName").AsString().Nullable()
|
||||
.WithColumn("TrackFileId").AsInt32().Nullable()
|
||||
.WithColumn("ReleaseDate").AsDateTime().Nullable();
|
||||
|
||||
|
||||
Create.TableForModel("TrackFiles")
|
||||
.WithColumn("ArtistId").AsInt32()
|
||||
.WithColumn("Path").AsString().Unique()
|
||||
.WithColumn("Quality").AsString()
|
||||
.WithColumn("Size").AsInt64()
|
||||
.WithColumn("DateAdded").AsDateTime()
|
||||
.WithColumn("AlbumId").AsInt32(); // How does this impact stand alone tracks?
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
using NzbDrone.Core.Extras.Others;
|
||||
using NzbDrone.Core.Extras.Subtitles;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
|
|
@ -91,6 +92,26 @@ public static void Map()
|
|||
.Relationship()
|
||||
.HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId);
|
||||
|
||||
Mapper.Entity<Artist>().RegisterModel("Artist")
|
||||
.Ignore(s => s.RootFolderPath)
|
||||
.Relationship()
|
||||
.HasOne(a => a.Profile, a => a.ProfileId);
|
||||
|
||||
Mapper.Entity<TrackFile>().RegisterModel("TrackFiles")
|
||||
.Ignore(f => f.Path)
|
||||
.Relationships.AutoMapICollectionOrComplexProperties()
|
||||
.For("Tracks")
|
||||
.LazyLoad(condition: parent => parent.Id > 0,
|
||||
query: (db, parent) => db.Query<Track>().Where(c => c.ItunesTrackId == parent.Id).ToList())
|
||||
.HasOne(file => file.Artist, file => file.AlbumId);
|
||||
|
||||
Mapper.Entity<Track>().RegisterModel("Tracks")
|
||||
//.Ignore(e => e.SeriesTitle)
|
||||
.Ignore(e => e.Album)
|
||||
.Ignore(e => e.HasFile)
|
||||
.Relationship()
|
||||
.HasOne(track => track.TrackFile, track => track.TrackFileId); // TODO: Check lazy load for artists
|
||||
|
||||
Mapper.Entity<QualityDefinition>().RegisterModel("QualityDefinitions")
|
||||
.Ignore(d => d.Weight);
|
||||
|
||||
|
|
|
|||
31
src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs
Normal file
31
src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using NzbDrone.Common.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
public class ArtistNotFoundException : NzbDroneException
|
||||
{
|
||||
public int ItunesId { get; set; }
|
||||
|
||||
public ArtistNotFoundException(int itunesId)
|
||||
: base(string.Format("Series with iTunesId {0} was not found, it may have been removed from iTunes.", itunesId))
|
||||
{
|
||||
ItunesId = itunesId;
|
||||
}
|
||||
|
||||
public ArtistNotFoundException(int itunesId, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
ItunesId = itunesId;
|
||||
}
|
||||
|
||||
public ArtistNotFoundException(int itunesId, string message)
|
||||
: base(message)
|
||||
{
|
||||
ItunesId = itunesId;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs
Normal file
19
src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs
Normal file
|
|
@ -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 ArtistRenamedEvent : IEvent
|
||||
{
|
||||
public Artist Artist { get; private set; }
|
||||
|
||||
public ArtistRenamedEvent(Artist artist)
|
||||
{
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs
Normal file
20
src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using NzbDrone.Common.Messaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.Events
|
||||
{
|
||||
public class TrackFileDeletedEvent : IEvent
|
||||
{
|
||||
public TrackFile TrackFile { get; private set; }
|
||||
public DeleteMediaFileReason Reason { get; private set; }
|
||||
|
||||
public TrackFileDeletedEvent(TrackFile trackFile, DeleteMediaFileReason reason)
|
||||
{
|
||||
TrackFile = trackFile;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs
Normal file
36
src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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 TrackImportedEvent : IEvent
|
||||
{
|
||||
public LocalTrack TrackInfo { get; private set; }
|
||||
public TrackFile ImportedTrack { get; private set; }
|
||||
public bool NewDownload { get; private set; }
|
||||
public string DownloadClient { get; private set; }
|
||||
public string DownloadId { get; private set; }
|
||||
public bool IsReadOnly { get; set; }
|
||||
|
||||
public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, bool newDownload)
|
||||
{
|
||||
TrackInfo = trackInfo;
|
||||
ImportedTrack = importedTrack;
|
||||
NewDownload = newDownload;
|
||||
}
|
||||
|
||||
public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, bool newDownload, string downloadClient, string downloadId, bool isReadOnly)
|
||||
{
|
||||
TrackInfo = trackInfo;
|
||||
ImportedTrack = importedTrack;
|
||||
NewDownload = newDownload;
|
||||
DownloadClient = downloadClient;
|
||||
DownloadId = downloadId;
|
||||
IsReadOnly = isReadOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/NzbDrone.Core/MediaFiles/TrackFile.cs
Normal file
34
src/NzbDrone.Core/MediaFiles/TrackFile.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using Marr.Data;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public class TrackFile : ModelBase
|
||||
{
|
||||
public int ItunesTrackId { 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 string ReleaseGroup { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public LazyLoaded<List<Track>> Episodes { get; set; }
|
||||
public LazyLoaded<Artist> Artist { get; set; }
|
||||
public LazyLoaded<List<Track>> Tracks { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] {1}", Id, RelativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs
Normal file
11
src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using NzbDrone.Core.Music;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public interface IProvideArtistInfo
|
||||
{
|
||||
Tuple<Artist, List<Track>> GetArtistInfo(int itunesId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource
|
||||
{
|
||||
public interface ISearchForNewSeries
|
||||
{
|
||||
List<Series> SearchForNewSeries(string title);
|
||||
List<Artist> SearchForNewArtist(string title);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
||||
{
|
||||
public class AlbumResource
|
||||
{
|
||||
public AlbumResource()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public string ArtistName { get; set; }
|
||||
public int ArtistId { get; set; }
|
||||
public string CollectionName { get; set; }
|
||||
public int CollectionId { get; set; }
|
||||
public string PrimaryGenreName { get; set; }
|
||||
public string ArtworkUrl100 { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string CollectionExplicitness { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public string Copyright { get; set; }
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class ArtistResource
|
||||
{
|
||||
public ArtistResource()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public int ResultCount { get; set; }
|
||||
public List<AlbumResource> Results { get; set; }
|
||||
//public string ArtistName { get; set; }
|
||||
//public List<AlbumResource> Albums { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,13 @@
|
|||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
|
||||
using NzbDrone.Core.Tv;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Core.Music;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries
|
||||
public class SkyHookProxy : IProvideSeriesInfo, IProvideArtistInfo, ISearchForNewSeries
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
|
@ -23,12 +26,13 @@ public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries
|
|||
public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_requestBuilder = requestBuilder.SkyHookTvdb;
|
||||
_requestBuilder = requestBuilder.Search;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId)
|
||||
{
|
||||
Console.WriteLine("[GetSeriesInfo] id:" + tvdbSeriesId);
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", "shows")
|
||||
.Resource(tvdbSeriesId.ToString())
|
||||
|
|
@ -62,35 +66,51 @@ public List<Series> SearchForNewSeries(string title)
|
|||
try
|
||||
{
|
||||
var lowerTitle = title.ToLowerInvariant();
|
||||
Console.WriteLine("Searching for " + lowerTitle);
|
||||
|
||||
if (lowerTitle.StartsWith("tvdb:") || lowerTitle.StartsWith("tvdbid:"))
|
||||
{
|
||||
var slug = lowerTitle.Split(':')[1].Trim();
|
||||
//if (lowerTitle.StartsWith("tvdb:") || lowerTitle.StartsWith("tvdbid:"))
|
||||
//{
|
||||
// var slug = lowerTitle.Split(':')[1].Trim();
|
||||
|
||||
int tvdbId;
|
||||
// int tvdbId;
|
||||
|
||||
if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out tvdbId) || tvdbId <= 0)
|
||||
{
|
||||
return new List<Series>();
|
||||
}
|
||||
// if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out tvdbId) || tvdbId <= 0)
|
||||
// {
|
||||
// return new List<Series>();
|
||||
// }
|
||||
|
||||
try
|
||||
{
|
||||
return new List<Series> { GetSeriesInfo(tvdbId).Item1 };
|
||||
}
|
||||
catch (SeriesNotFoundException)
|
||||
{
|
||||
return new List<Series>();
|
||||
}
|
||||
}
|
||||
// try
|
||||
// {
|
||||
// return new List<Series> { GetSeriesInfo(tvdbId).Item1 };
|
||||
// }
|
||||
// catch (SeriesNotFoundException)
|
||||
// {
|
||||
// return new List<Series>();
|
||||
// }
|
||||
//}
|
||||
|
||||
// Majora: Temporarily, use iTunes to test.
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", "search")
|
||||
.AddQueryParam("term", title.ToLower().Trim())
|
||||
.Build();
|
||||
.AddQueryParam("entity", "album")
|
||||
.AddQueryParam("term", title.ToLower().Trim())
|
||||
.Build();
|
||||
|
||||
|
||||
|
||||
Console.WriteLine("httpRequest: ", httpRequest);
|
||||
|
||||
var httpResponse = _httpClient.Get<List<ShowResource>>(httpRequest);
|
||||
|
||||
//Console.WriteLine("Response: ", httpResponse.GetType());
|
||||
//_logger.Info("Response: ", httpResponse.Resource.ResultCount);
|
||||
|
||||
//_logger.Info("HTTP Response: ", httpResponse.Resource.ResultCount);
|
||||
var tempList = new List<Series>();
|
||||
var tempSeries = new Series();
|
||||
tempSeries.Title = "AFI";
|
||||
tempList.Add(tempSeries);
|
||||
return tempList;
|
||||
|
||||
return httpResponse.Resource.SelectList(MapSeries);
|
||||
}
|
||||
catch (HttpException)
|
||||
|
|
@ -104,6 +124,130 @@ public List<Series> SearchForNewSeries(string title)
|
|||
}
|
||||
}
|
||||
|
||||
public Tuple<Artist, List<Track>> GetArtistInfo(int itunesId)
|
||||
{
|
||||
Console.WriteLine("[GetArtistInfo] id:" + itunesId);
|
||||
//https://itunes.apple.com/lookup?id=909253
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", "lookup")
|
||||
.AddQueryParam("id", itunesId.ToString())
|
||||
.Build();
|
||||
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
var httpResponse = _httpClient.Get<ArtistResource>(httpRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new ArtistNotFoundException(itunesId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(httpRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("GetArtistInfo, GetArtistInfo");
|
||||
//var tracks = httpResponse.Resource.Episodes.Select(MapEpisode);
|
||||
//var artist = MapArtist(httpResponse.Resource);
|
||||
// I don't know how we are getting tracks from iTunes yet.
|
||||
return new Tuple<Artist, List<Track>>(MapArtists(httpResponse.Resource)[0], new List<Track>());
|
||||
//return new Tuple<Artist, List<Track>>(artist, tracks.ToList());
|
||||
}
|
||||
public List<Artist> SearchForNewArtist(string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lowerTitle = title.ToLowerInvariant();
|
||||
Console.WriteLine("Searching for " + lowerTitle);
|
||||
|
||||
if (lowerTitle.StartsWith("itunes:") || lowerTitle.StartsWith("itunesid:"))
|
||||
{
|
||||
var slug = lowerTitle.Split(':')[1].Trim();
|
||||
|
||||
int itunesId;
|
||||
|
||||
if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out itunesId) || itunesId <= 0)
|
||||
{
|
||||
return new List<Artist>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new List<Artist> { GetArtistInfo(itunesId).Item1 };
|
||||
}
|
||||
catch (ArtistNotFoundException)
|
||||
{
|
||||
return new List<Artist>();
|
||||
}
|
||||
}
|
||||
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", "search")
|
||||
.AddQueryParam("entity", "album")
|
||||
.AddQueryParam("term", title.ToLower().Trim())
|
||||
.Build();
|
||||
|
||||
|
||||
|
||||
var httpResponse = _httpClient.Get<ArtistResource>(httpRequest);
|
||||
|
||||
return MapArtists(httpResponse.Resource);
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", title);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, ex.Message);
|
||||
throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", title);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Artist> MapArtists(ArtistResource resource)
|
||||
{
|
||||
Album tempAlbum;
|
||||
List<Artist> artists = new List<Artist>();
|
||||
foreach (var album in resource.Results)
|
||||
{
|
||||
int index = artists.FindIndex(a => a.ItunesId == album.ArtistId);
|
||||
tempAlbum = MapAlbum(album);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
artists[index].Albums.Add(tempAlbum);
|
||||
}
|
||||
else
|
||||
{
|
||||
Artist tempArtist = new Artist();
|
||||
tempArtist.ItunesId = album.ArtistId;
|
||||
tempArtist.ArtistName = album.ArtistName;
|
||||
tempArtist.Genres.Add(album.PrimaryGenreName);
|
||||
tempArtist.Albums.Add(tempAlbum);
|
||||
artists.Add(tempArtist);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
private Album MapAlbum(AlbumResource albumQuery)
|
||||
{
|
||||
Album album = new Album();
|
||||
|
||||
album.AlbumId = albumQuery.CollectionId;
|
||||
album.Title = albumQuery.CollectionName;
|
||||
album.Year = albumQuery.ReleaseDate.Year;
|
||||
album.ArtworkUrl = albumQuery.ArtworkUrl100;
|
||||
album.Explicitness = albumQuery.CollectionExplicitness;
|
||||
return album;
|
||||
}
|
||||
|
||||
private static Series MapSeries(ShowResource show)
|
||||
{
|
||||
var series = new Series();
|
||||
|
|
|
|||
13
src/NzbDrone.Core/Music/AddArtistOptions.cs
Normal file
13
src/NzbDrone.Core/Music/AddArtistOptions.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using NzbDrone.Core.Tv;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class AddArtistOptions : MonitoringOptions
|
||||
{
|
||||
public bool SearchForMissingTracks { get; set; }
|
||||
}
|
||||
}
|
||||
101
src/NzbDrone.Core/Music/AddArtistService.cs
Normal file
101
src/NzbDrone.Core/Music/AddArtistService.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.MetadataSource.SkyHook;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public interface IAddArtistService
|
||||
{
|
||||
Artist AddArtist(Artist newArtist);
|
||||
}
|
||||
|
||||
public class AddSeriesService : IAddArtistService
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IProvideArtistInfo _artistInfo;
|
||||
private readonly IBuildFileNames _fileNameBuilder;
|
||||
private readonly IAddArtistValidator _addArtistValidator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AddSeriesService(IArtistService artistService,
|
||||
IProvideArtistInfo artistInfo,
|
||||
IBuildFileNames fileNameBuilder,
|
||||
IAddArtistValidator addArtistValidator,
|
||||
Logger logger)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_artistInfo = artistInfo;
|
||||
_fileNameBuilder = fileNameBuilder;
|
||||
_addArtistValidator = addArtistValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Artist AddArtist(Artist newArtist)
|
||||
{
|
||||
Ensure.That(newArtist, () => newArtist).IsNotNull();
|
||||
|
||||
newArtist = AddSkyhookData(newArtist);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newArtist.Path))
|
||||
{
|
||||
var folderName = newArtist.ArtistName;// _fileNameBuilder.GetArtistFolder(newArtist);
|
||||
newArtist.Path = Path.Combine(newArtist.RootFolderPath, folderName);
|
||||
}
|
||||
|
||||
newArtist.CleanTitle = newArtist.ArtistName.CleanSeriesTitle();
|
||||
//newArtist.SortTitle = ArtistNameNormalizer.Normalize(newArtist.ArtistName, newArtist.ItunesId); // There is no Sort Title
|
||||
newArtist.Added = DateTime.UtcNow;
|
||||
|
||||
var validationResult = _addArtistValidator.Validate(newArtist);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
_logger.Info("Adding Series {0} Path: [{1}]", newArtist, newArtist.Path);
|
||||
_artistService.AddArtist(newArtist);
|
||||
|
||||
return newArtist;
|
||||
}
|
||||
|
||||
private Artist AddSkyhookData(Artist newArtist)
|
||||
{
|
||||
Tuple<Artist, List<Track>> tuple;
|
||||
|
||||
try
|
||||
{
|
||||
tuple = _artistInfo.GetArtistInfo(newArtist.ItunesId);
|
||||
}
|
||||
catch (SeriesNotFoundException)
|
||||
{
|
||||
_logger.Error("tvdbid {1} was not found, it may have been removed from TheTVDB.", newArtist.ItunesId);
|
||||
|
||||
throw new ValidationException(new List<ValidationFailure>
|
||||
{
|
||||
new ValidationFailure("TvdbId", "A series with this ID was not found", newArtist.ItunesId)
|
||||
});
|
||||
}
|
||||
|
||||
var artist = tuple.Item1;
|
||||
|
||||
// If seasons were passed in on the new series use them, otherwise use the seasons from Skyhook
|
||||
// TODO: Refactor for albums
|
||||
newArtist.Albums = newArtist.Albums != null && newArtist.Albums.Any() ? newArtist.Albums : artist.Albums;
|
||||
|
||||
artist.ApplyChanges(newArtist);
|
||||
|
||||
return artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/NzbDrone.Core/Music/AddArtistValidator.cs
Normal file
34
src/NzbDrone.Core/Music/AddArtistValidator.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public interface IAddArtistValidator
|
||||
{
|
||||
ValidationResult Validate(Artist instance);
|
||||
}
|
||||
|
||||
public class AddArtistValidator : AbstractValidator<Artist>, IAddArtistValidator
|
||||
{
|
||||
public AddArtistValidator(RootFolderValidator rootFolderValidator,
|
||||
SeriesPathValidator seriesPathValidator,
|
||||
DroneFactoryValidator droneFactoryValidator,
|
||||
SeriesAncestorValidator seriesAncestorValidator,
|
||||
ArtistSlugValidator seriesTitleSlugValidator)
|
||||
{
|
||||
RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(seriesPathValidator)
|
||||
.SetValidator(droneFactoryValidator)
|
||||
.SetValidator(seriesAncestorValidator);
|
||||
|
||||
RuleFor(c => c.ArtistSlug).SetValidator(seriesTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/NzbDrone.Core/Music/Album.cs
Normal file
29
src/NzbDrone.Core/Music/Album.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Tv;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class Album : IEmbeddedDocument
|
||||
{
|
||||
public Album()
|
||||
{
|
||||
Images = new List<MediaCover.MediaCover>();
|
||||
}
|
||||
|
||||
public int AlbumId { 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 int DiscCount { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
public List<Actor> Actors { get; set; } // These are band members. TODO: Refactor
|
||||
public List<string> Genres { get; set; }
|
||||
public string ArtworkUrl { get; set; }
|
||||
public string Explicitness { get; set; }
|
||||
}
|
||||
}
|
||||
108
src/NzbDrone.Core/Music/Artist.cs
Normal file
108
src/NzbDrone.Core/Music/Artist.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using Marr.Data;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class Artist : ModelBase
|
||||
{
|
||||
public Artist()
|
||||
{
|
||||
Images = new List<MediaCover.MediaCover>();
|
||||
Genres = new List<string>();
|
||||
//Members = new List<Person>(); // Artist Band Member? (NOTE: This should be per album)
|
||||
Albums = new List<Album>();
|
||||
Tags = new HashSet<int>();
|
||||
|
||||
}
|
||||
|
||||
public int ItunesId { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
public string ArtistSlug { get; set; }
|
||||
public string CleanTitle { 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<MediaCover.MediaCover> Images { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public int QualityProfileId { get; set; }
|
||||
|
||||
public string RootFolderPath { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public LazyLoaded<Profile> Profile { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
public List<Album> Albums { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public AddSeriesOptions AddOptions { get; set; }
|
||||
|
||||
//public string SortTitle { get; set; }
|
||||
//public SeriesStatusType Status { get; set; }
|
||||
//public int Runtime { get; set; }
|
||||
//public SeriesTypes SeriesType { get; set; }
|
||||
//public string Network { get; set; }
|
||||
//public bool UseSceneNumbering { get; set; }
|
||||
//public string TitleSlug { get; set; }
|
||||
//public int Year { get; set; }
|
||||
//public Ratings Ratings { get; set; }
|
||||
//public List<Actor> Actors { get; set; } // MOve to album?
|
||||
//public string Certification { get; set; }
|
||||
//public DateTime? FirstAired { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}][{1}]", ItunesId, ArtistName.NullSafe());
|
||||
}
|
||||
|
||||
public void ApplyChanges(Artist otherArtist)
|
||||
{
|
||||
|
||||
ItunesId = otherArtist.ItunesId;
|
||||
ArtistName = otherArtist.ArtistName;
|
||||
ArtistSlug = otherArtist.ArtistSlug;
|
||||
CleanTitle = otherArtist.CleanTitle;
|
||||
Monitored = otherArtist.Monitored;
|
||||
AlbumFolder = otherArtist.AlbumFolder;
|
||||
LastInfoSync = otherArtist.LastInfoSync;
|
||||
Images = otherArtist.Images;
|
||||
Path = otherArtist.Path;
|
||||
Genres = otherArtist.Genres;
|
||||
RootFolderPath = otherArtist.RootFolderPath;
|
||||
Added = otherArtist.Added;
|
||||
Profile = otherArtist.Profile;
|
||||
ProfileId = otherArtist.ProfileId;
|
||||
Albums = otherArtist.Albums;
|
||||
Tags = otherArtist.Tags;
|
||||
ArtistFolder = otherArtist.ArtistFolder;
|
||||
AddOptions = otherArtist.AddOptions;
|
||||
|
||||
|
||||
//TODO: Implement
|
||||
ItunesId = otherArtist.ItunesId;
|
||||
|
||||
Albums = otherArtist.Albums;
|
||||
Path = otherArtist.Path;
|
||||
ProfileId = otherArtist.ProfileId;
|
||||
|
||||
AlbumFolder = otherArtist.AlbumFolder;
|
||||
Monitored = otherArtist.Monitored;
|
||||
|
||||
//SeriesType = otherArtist.SeriesType;
|
||||
RootFolderPath = otherArtist.RootFolderPath;
|
||||
Tags = otherArtist.Tags;
|
||||
AddOptions = otherArtist.AddOptions;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/NzbDrone.Core/Music/ArtistNameNormalizer.cs
Normal file
28
src/NzbDrone.Core/Music/ArtistNameNormalizer.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
|
||||
public static class ArtistNameNormalizer
|
||||
{
|
||||
private readonly static Dictionary<int, string> PreComputedTitles = new Dictionary<int, string>
|
||||
{
|
||||
{ 281588, "a to z" },
|
||||
{ 266757, "ad trials triumph early church" },
|
||||
{ 289260, "ad bible continues"}
|
||||
};
|
||||
|
||||
public static string Normalize(string title, int iTunesId)
|
||||
{
|
||||
if (PreComputedTitles.ContainsKey(iTunesId))
|
||||
{
|
||||
return PreComputedTitles[iTunesId];
|
||||
}
|
||||
|
||||
return Parser.Parser.NormalizeTitle(title).ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/NzbDrone.Core/Music/ArtistRepository.cs
Normal file
40
src/NzbDrone.Core/Music/ArtistRepository.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public interface IArtistRepository : IBasicRepository<Artist>
|
||||
{
|
||||
bool ArtistPathExists(string path);
|
||||
Artist FindByName(string cleanTitle);
|
||||
Artist FindByItunesId(int iTunesId);
|
||||
}
|
||||
|
||||
public class ArtistRepository : BasicRepository<Artist>, IArtistRepository
|
||||
{
|
||||
public ArtistRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public bool ArtistPathExists(string path)
|
||||
{
|
||||
return Query.Where(c => c.Path == path).Any();
|
||||
}
|
||||
|
||||
public Artist FindByItunesId(int iTunesId)
|
||||
{
|
||||
return Query.Where(s => s.ItunesId == iTunesId).SingleOrDefault();
|
||||
}
|
||||
|
||||
public Artist FindByName(string cleanName)
|
||||
{
|
||||
cleanName = cleanName.ToLowerInvariant();
|
||||
|
||||
return Query.Where(s => s.CleanTitle == cleanName)
|
||||
.SingleOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
155
src/NzbDrone.Core/Music/ArtistService.cs
Normal file
155
src/NzbDrone.Core/Music/ArtistService.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
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 IArtistService
|
||||
{
|
||||
Artist GetArtist(int artistId);
|
||||
List<Artist> GetArtists(IEnumerable<int> artistIds);
|
||||
Artist AddArtist(Artist newArtist);
|
||||
Artist FindByItunesId(int itunesId);
|
||||
Artist FindByName(string title);
|
||||
Artist FindByTitleInexact(string title);
|
||||
void DeleteArtist(int artistId, bool deleteFiles);
|
||||
List<Artist> GetAllArtists();
|
||||
Artist UpdateArtist(Artist artist);
|
||||
List<Artist> UpdateArtists(List<Artist> artist);
|
||||
bool ArtistPathExists(string folder);
|
||||
void RemoveAddOptions(Artist artist);
|
||||
}
|
||||
|
||||
public class ArtistService : IArtistService
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ITrackService _trackService;
|
||||
private readonly IBuildFileNames _fileNameBuilder;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ArtistService(IArtistRepository artistRepository,
|
||||
IEventAggregator eventAggregator,
|
||||
ITrackService trackService,
|
||||
IBuildFileNames fileNameBuilder,
|
||||
Logger logger)
|
||||
{
|
||||
_artistRepository = artistRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_trackService = trackService;
|
||||
_fileNameBuilder = fileNameBuilder;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Artist AddArtist(Artist newArtist)
|
||||
{
|
||||
_artistRepository.Insert(newArtist);
|
||||
_eventAggregator.PublishEvent(new ArtistAddedEvent(GetArtist(newArtist.Id)));
|
||||
|
||||
return newArtist;
|
||||
}
|
||||
|
||||
public bool ArtistPathExists(string folder)
|
||||
{
|
||||
return _artistRepository.ArtistPathExists(folder);
|
||||
}
|
||||
|
||||
public void DeleteArtist(int artistId, bool deleteFiles)
|
||||
{
|
||||
var artist = _artistRepository.Get(artistId);
|
||||
_artistRepository.Delete(artistId);
|
||||
_eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles));
|
||||
}
|
||||
|
||||
public Artist FindByItunesId(int itunesId)
|
||||
{
|
||||
return _artistRepository.FindByItunesId(itunesId);
|
||||
}
|
||||
|
||||
public Artist FindByName(string title)
|
||||
{
|
||||
return _artistRepository.FindByName(title.CleanArtistTitle());
|
||||
}
|
||||
|
||||
public Artist FindByTitleInexact(string title)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Artist> GetAllArtists()
|
||||
{
|
||||
_logger.Debug("Count of repository: " + _artistRepository.Count());
|
||||
// TEMP: Return empty list while we debug the DB error
|
||||
return new List<Artist>();
|
||||
//return _artistRepository.All().ToList();
|
||||
}
|
||||
|
||||
public Artist GetArtist(int artistId)
|
||||
{
|
||||
return _artistRepository.Get(artistId);
|
||||
}
|
||||
|
||||
public List<Artist> GetArtists(IEnumerable<int> artistIds)
|
||||
{
|
||||
return _artistRepository.Get(artistIds).ToList();
|
||||
}
|
||||
|
||||
public void RemoveAddOptions(Artist artist)
|
||||
{
|
||||
_artistRepository.SetFields(artist, s => s.AddOptions);
|
||||
}
|
||||
|
||||
public Artist UpdateArtist(Artist artist)
|
||||
{
|
||||
var storedArtist = GetArtist(artist.Id); // Is it Id or iTunesId?
|
||||
|
||||
foreach (var album in artist.Albums)
|
||||
{
|
||||
var storedAlbum = storedArtist.Albums.SingleOrDefault(s => s.AlbumId == album.AlbumId);
|
||||
|
||||
if (storedAlbum != null && album.Monitored != storedAlbum.Monitored)
|
||||
{
|
||||
_trackService.SetTrackMonitoredByAlbum(artist.Id, album.AlbumId, album.Monitored);
|
||||
}
|
||||
}
|
||||
|
||||
var updatedArtist = _artistRepository.Update(artist);
|
||||
_eventAggregator.PublishEvent(new ArtistEditedEvent(updatedArtist, storedArtist));
|
||||
|
||||
return updatedArtist;
|
||||
}
|
||||
|
||||
public List<Artist> UpdateArtists(List<Artist> artist)
|
||||
{
|
||||
_logger.Debug("Updating {0} artist", artist.Count);
|
||||
foreach (var s in artist)
|
||||
{
|
||||
_logger.Trace("Updating: {0}", s.ArtistName);
|
||||
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);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_logger.Trace("Not changing path for: {0}", s.ArtistName);
|
||||
}
|
||||
}
|
||||
|
||||
_artistRepository.UpdateMany(artist);
|
||||
_logger.Debug("{0} artists updated", artist.Count);
|
||||
|
||||
return artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/NzbDrone.Core/Music/ArtistSlugValidator.cs
Normal file
29
src/NzbDrone.Core/Music/ArtistSlugValidator.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using FluentValidation.Validators;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class ArtistSlugValidator : PropertyValidator
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
|
||||
public ArtistSlugValidator(IArtistService artistService)
|
||||
: base("Title slug is in use by another artist with a similar name")
|
||||
{
|
||||
_artistService = artistService;
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue == null) return true;
|
||||
|
||||
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||
var instanceId = (int)instance.Id;
|
||||
|
||||
return !_artistService.GetAllArtists().Exists(s => s.ArtistSlug.Equals(context.PropertyValue.ToString()) && s.Id != instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs
Normal file
18
src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs
Normal file
|
|
@ -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 ArtistAddedEvent : IEvent
|
||||
{
|
||||
public Artist Artist { get; private set; }
|
||||
|
||||
public ArtistAddedEvent(Artist artist)
|
||||
{
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs
Normal file
20
src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs
Normal file
|
|
@ -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 ArtistDeletedEvent : IEvent
|
||||
{
|
||||
public Artist Artist { get; private set; }
|
||||
public bool DeleteFiles { get; private set; }
|
||||
|
||||
public ArtistDeletedEvent(Artist artist, bool deleteFiles)
|
||||
{
|
||||
Artist = artist;
|
||||
DeleteFiles = deleteFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs
Normal file
20
src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs
Normal file
|
|
@ -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 ArtistEditedEvent : IEvent
|
||||
{
|
||||
public Artist Artist { get; private set; }
|
||||
public Artist OldArtist { get; private set; }
|
||||
|
||||
public ArtistEditedEvent(Artist artist, Artist oldArtist)
|
||||
{
|
||||
Artist = artist;
|
||||
OldArtist = oldArtist;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs
Normal file
14
src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Music.Events
|
||||
{
|
||||
public class ArtistUpdatedEvent : IEvent
|
||||
{
|
||||
public Artist Artist { get; private set; }
|
||||
|
||||
public ArtistUpdatedEvent(Artist artist)
|
||||
{
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/NzbDrone.Core/Music/Track.cs
Normal file
52
src/NzbDrone.Core/Music/Track.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using Marr.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class Track : ModelBase
|
||||
{
|
||||
public Track()
|
||||
{
|
||||
}
|
||||
|
||||
public const string RELEASE_DATE_FORMAT = "yyyy-MM-dd";
|
||||
|
||||
public int ItunesTrackId { get; set; }
|
||||
public int AlbumId { get; set; }
|
||||
public LazyLoaded<Artist> ArtistsId { get; set; }
|
||||
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 string TrackExplicitName { get; set; }
|
||||
public string TrackCensoredName { get; set; }
|
||||
public string Monitored { get; set; }
|
||||
public int TrackFileId { get; set; } // JVM: Is this needed with TrackFile reference?
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
/*public int? SceneEpisodeNumber { get; set; }
|
||||
public bool UnverifiedSceneNumbering { get; set; }
|
||||
public Ratings Ratings { get; set; } // This might be aplicable as can be pulled from IDv3 tags
|
||||
public List<MediaCover.MediaCover> Images { get; set; }*/
|
||||
|
||||
//public string SeriesTitle { get; private set; }
|
||||
|
||||
public LazyLoaded<TrackFile> TrackFile { get; set; }
|
||||
|
||||
public Album Album { get; set; }
|
||||
|
||||
public bool HasFile => TrackFileId > 0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}]{1}", ItunesTrackId, Title.NullSafe());
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/NzbDrone.Core/Music/TrackService.cs
Normal file
117
src/NzbDrone.Core/Music/TrackService.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public interface ITrackService
|
||||
{
|
||||
Track GetTrack(int id);
|
||||
List<Track> GetTracks(IEnumerable<int> ids);
|
||||
Track FindTrack(int artistId, int albumId, int trackNumber);
|
||||
Track FindTrackByTitle(int artistId, int albumId, string releaseTitle);
|
||||
List<Track> GetTrackByArtist(int artistId);
|
||||
List<Track> GetTracksByAblum(int artistId, int albumId);
|
||||
List<Track> GetTracksByAblumTitle(int artistId, string albumTitle);
|
||||
List<Track> TracksWithFiles(int artistId);
|
||||
PagingSpec<Track> TracksWithoutFiles(PagingSpec<Track> pagingSpec);
|
||||
List<Track> GeTracksByFileId(int trackFileId);
|
||||
void UpdateTrack(Track track);
|
||||
void SetTrackMonitored(int trackId, bool monitored);
|
||||
void UpdateTracks(List<Track> tracks);
|
||||
void InsertMany(List<Track> tracks);
|
||||
void UpdateMany(List<Track> tracks);
|
||||
void DeleteMany(List<Track> tracks);
|
||||
void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored);
|
||||
}
|
||||
|
||||
public class TrackService : ITrackService
|
||||
{
|
||||
public void DeleteMany(List<Track> tracks)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Track FindTrack(int artistId, int albumId, int trackNumber)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Track> GeTracksByFileId(int trackFileId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Track GetTrack(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Track> GetTrackByArtist(int artistId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Track> GetTracks(IEnumerable<int> ids)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Track> GetTracksByAblum(int artistId, int albumId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Track> GetTracksByAblumTitle(int artistId, string albumTitle)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InsertMany(List<Track> tracks)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetTrackMonitored(int trackId, bool monitored)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<Track> TracksWithFiles(int artistId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public PagingSpec<Track> TracksWithoutFiles(PagingSpec<Track> pagingSpec)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void UpdateMany(List<Track> tracks)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void UpdateTrack(Track track)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void UpdateTracks(List<Track> tracks)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -287,6 +287,7 @@
|
|||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Datastore\Migration\105_rename_torrent_downloadstation.cs" />
|
||||
<Compile Include="Datastore\Migration\111_setup_music.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
|
||||
|
|
@ -517,6 +518,7 @@
|
|||
<Compile Include="Download\ProcessDownloadDecisions.cs" />
|
||||
<Compile Include="Download\ProcessedDecisions.cs" />
|
||||
<Compile Include="Download\RedownloadFailedDownloadService.cs" />
|
||||
<Compile Include="Exceptions\ArtistNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\BadRequestException.cs" />
|
||||
<Compile Include="Exceptions\DownstreamException.cs" />
|
||||
<Compile Include="Exceptions\NzbDroneClientException.cs" />
|
||||
|
|
@ -757,6 +759,7 @@
|
|||
<Compile Include="MediaFiles\EpisodeImport\Specifications\SameEpisodesImportSpecification.cs" />
|
||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\UnverifiedSceneNumberingSpecification.cs" />
|
||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecification.cs" />
|
||||
<Compile Include="MediaFiles\Events\ArtistRenamedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\EpisodeDownloadedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\EpisodeFileAddedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" />
|
||||
|
|
@ -765,6 +768,8 @@
|
|||
<Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\SeriesScanSkippedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\TrackFileDeletedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\TrackImportedEvent.cs" />
|
||||
<Compile Include="MediaFiles\FileDateType.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileAttributeService.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileExtensions.cs" />
|
||||
|
|
@ -781,6 +786,7 @@
|
|||
<Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" />
|
||||
<Compile Include="MediaFiles\RenameEpisodeFileService.cs" />
|
||||
<Compile Include="MediaFiles\SameFilenameException.cs" />
|
||||
<Compile Include="MediaFiles\TrackFile.cs" />
|
||||
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
|
||||
<Compile Include="MediaFiles\UpgradeMediaFileService.cs" />
|
||||
<Compile Include="Messaging\Commands\BackendCommandAttribute.cs" />
|
||||
|
|
@ -806,7 +812,9 @@
|
|||
<Compile Include="Messaging\Events\IEventAggregator.cs" />
|
||||
<Compile Include="Messaging\Events\IHandle.cs" />
|
||||
<Compile Include="Messaging\IProcessMessage.cs" />
|
||||
<Compile Include="MetadataSource\IProvideArtistInfo.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ActorResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ArtistResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\EpisodeResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\RatingResource.cs" />
|
||||
|
|
@ -837,6 +845,21 @@
|
|||
<Compile Include="Extras\Metadata\MetadataType.cs" />
|
||||
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
|
||||
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
|
||||
<Compile Include="Music\AddArtistOptions.cs" />
|
||||
<Compile Include="Music\AddArtistService.cs" />
|
||||
<Compile Include="Music\AddArtistValidator.cs" />
|
||||
<Compile Include="Music\Album.cs" />
|
||||
<Compile Include="Music\Artist.cs" />
|
||||
<Compile Include="Music\ArtistNameNormalizer.cs" />
|
||||
<Compile Include="Music\ArtistSlugValidator.cs" />
|
||||
<Compile Include="Music\ArtistRepository.cs" />
|
||||
<Compile Include="Music\ArtistService.cs" />
|
||||
<Compile Include="Music\Events\ArtistAddedEvent.cs" />
|
||||
<Compile Include="Music\Events\ArtistDeletedEvent.cs" />
|
||||
<Compile Include="Music\Events\ArtistEditedEvent.cs" />
|
||||
<Compile Include="Music\Events\ArtistUpdatedEvent.cs" />
|
||||
<Compile Include="Music\Track.cs" />
|
||||
<Compile Include="Music\TrackService.cs" />
|
||||
<Compile Include="Notifications\Join\JoinAuthException.cs" />
|
||||
<Compile Include="Notifications\Join\JoinInvalidDeviceException.cs" />
|
||||
<Compile Include="Notifications\Join\JoinResponseModel.cs" />
|
||||
|
|
@ -892,6 +915,8 @@
|
|||
<Compile Include="Parser\IsoLanguage.cs" />
|
||||
<Compile Include="Parser\IsoLanguages.cs" />
|
||||
<Compile Include="Parser\LanguageParser.cs" />
|
||||
<Compile Include="Parser\Model\LocalTrack.cs" />
|
||||
<Compile Include="Parser\Model\ParsedTrackInfo.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfile.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileService.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />
|
||||
|
|
@ -1130,6 +1155,8 @@
|
|||
<Compile Include="Validation\NzbDroneValidationFailure.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationResult.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationState.cs" />
|
||||
<Compile Include="Validation\Paths\ArtistExistsValidator.cs" />
|
||||
<Compile Include="Validation\Paths\ArtistPathValidator.cs" />
|
||||
<Compile Include="Validation\Paths\MappedNetworkDriveValidator.cs" />
|
||||
<Compile Include="Validation\Paths\DroneFactoryValidator.cs" />
|
||||
<Compile Include="Validation\Paths\FolderWritableValidator.cs" />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
|
|
@ -22,6 +23,9 @@ public interface IBuildFileNames
|
|||
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
|
||||
string GetSeriesFolder(Series series, NamingConfig namingConfig = null);
|
||||
string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null);
|
||||
|
||||
// TODO: Implement Music functions
|
||||
//string GetArtistFolder(Artist artist, NamingConfig namingConfig = null);
|
||||
}
|
||||
|
||||
public class FileNameBuilder : IBuildFileNames
|
||||
|
|
@ -278,6 +282,12 @@ private void AddSeriesTokens(Dictionary<string, Func<TokenMatch, string>> tokenH
|
|||
tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title);
|
||||
}
|
||||
|
||||
private void AddArtistTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Artist artist)
|
||||
{
|
||||
tokenHandlers["{Artist Name}"] = m => artist.ArtistName;
|
||||
tokenHandlers["{Artist CleanTitle}"] = m => CleanTitle(artist.ArtistName);
|
||||
}
|
||||
|
||||
private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary<string, Func<TokenMatch, string>> tokenHandlers, List<Episode> episodes, NamingConfig namingConfig)
|
||||
{
|
||||
var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList();
|
||||
|
|
@ -768,6 +778,36 @@ private string GetOriginalFileName(EpisodeFile episodeFile)
|
|||
|
||||
return Path.GetFileNameWithoutExtension(episodeFile.RelativePath);
|
||||
}
|
||||
|
||||
//public string GetArtistFolder(Artist artist, NamingConfig namingConfig = null)
|
||||
//{
|
||||
// if (namingConfig == null)
|
||||
// {
|
||||
// namingConfig = _namingConfigService.GetConfig();
|
||||
// }
|
||||
|
||||
// var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(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<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
|
||||
|
||||
// //AddSeriesTokens(tokenHandlers, artist);
|
||||
// //AddSeasonTokens(tokenHandlers, seasonNumber);
|
||||
|
||||
// //return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig));
|
||||
//}
|
||||
}
|
||||
|
||||
internal sealed class TokenMatch
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ public class NamingConfig : ModelBase
|
|||
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}"
|
||||
SeasonFolderFormat = "Season {season}",
|
||||
ArtistFolderFormat = "{Artist Name}",
|
||||
AlbumFolderFormat = "{Album Name} ({Year})"
|
||||
};
|
||||
|
||||
public bool RenameEpisodes { get; set; }
|
||||
|
|
@ -24,5 +26,7 @@ public class NamingConfig : ModelBase
|
|||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
src/NzbDrone.Core/Parser/Model/LocalTrack.cs
Normal file
42
src/NzbDrone.Core/Parser/Model/LocalTrack.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
{
|
||||
public class LocalTrack
|
||||
{
|
||||
public LocalTrack()
|
||||
{
|
||||
Tracks = new List<Track>();
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public long Size { get; set; }
|
||||
public ParsedTrackInfo ParsedTrackInfo { get; set; }
|
||||
public Artist Artist { get; set; }
|
||||
public List<Track> Tracks { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public bool ExistingFile { get; set; }
|
||||
|
||||
public int Album
|
||||
{
|
||||
get
|
||||
{
|
||||
return Tracks.Select(c => c.AlbumId).Distinct().Single();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSpecial => Album == 0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Path;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs
Normal file
97
src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
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 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 ReleaseGroup { get; set; }
|
||||
public string ReleaseHash { get; set; }
|
||||
|
||||
public ParsedTrackInfo()
|
||||
{
|
||||
EpisodeNumbers = new int[0];
|
||||
AbsoluteEpisodeNumbers = 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]";
|
||||
|
||||
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())
|
||||
{
|
||||
episodeString = string.Format("{0}", string.Join("-", AbsoluteEpisodeNumbers.Select(c => c.ToString("000"))));
|
||||
}
|
||||
|
||||
return string.Format("{0} - {1} {2}", ArtistTitle, episodeString, Quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -277,6 +277,27 @@ public static class Parser
|
|||
|
||||
private static readonly string[] Numbers = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
|
||||
|
||||
public static ParsedTrackInfo ParseMusicPath(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
||||
var result = ParseMusicTitle(fileInfo.Name);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Logger.Debug("Attempting to parse track info using directory and file names. {0}", fileInfo.Directory.Name);
|
||||
result = ParseMusicTitle(fileInfo.Directory.Name + " " + fileInfo.Name);
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Logger.Debug("Attempting to parse track info using directory name. {0}", fileInfo.Directory.Name);
|
||||
result = ParseMusicTitle(fileInfo.Directory.Name + fileInfo.Extension);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ParsedEpisodeInfo ParsePath(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
|
@ -298,6 +319,116 @@ public static ParsedEpisodeInfo ParsePath(string path)
|
|||
return result;
|
||||
}
|
||||
|
||||
public static ParsedTrackInfo ParseMusicTitle(string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ValidateBeforeParsing(title)) return null;
|
||||
|
||||
Logger.Debug("Parsing string '{0}'", title);
|
||||
|
||||
if (ReversedTitleRegex.IsMatch(title))
|
||||
{
|
||||
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray();
|
||||
Array.Reverse(titleWithoutExtension);
|
||||
|
||||
title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length);
|
||||
|
||||
Logger.Debug("Reversed name detected. Converted to '{0}'", title);
|
||||
}
|
||||
|
||||
var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty);
|
||||
|
||||
simpleTitle = RemoveFileExtension(simpleTitle);
|
||||
|
||||
// TODO: Quick fix stripping [url] - prefixes.
|
||||
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty);
|
||||
|
||||
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty);
|
||||
|
||||
var airDateMatch = AirDateRegex.Match(simpleTitle);
|
||||
if (airDateMatch.Success)
|
||||
{
|
||||
simpleTitle = airDateMatch.Groups[1].Value + airDateMatch.Groups["airyear"].Value + "." + airDateMatch.Groups["airmonth"].Value + "." + airDateMatch.Groups["airday"].Value;
|
||||
}
|
||||
|
||||
var sixDigitAirDateMatch = SixDigitAirDateRegex.Match(simpleTitle);
|
||||
if (sixDigitAirDateMatch.Success)
|
||||
{
|
||||
var airYear = sixDigitAirDateMatch.Groups["airyear"].Value;
|
||||
var airMonth = sixDigitAirDateMatch.Groups["airmonth"].Value;
|
||||
var airDay = sixDigitAirDateMatch.Groups["airday"].Value;
|
||||
|
||||
if (airMonth != "00" || airDay != "00")
|
||||
{
|
||||
var fixedDate = string.Format("20{0}.{1}.{2}", airYear, airMonth, airDay);
|
||||
|
||||
simpleTitle = simpleTitle.Replace(sixDigitAirDateMatch.Groups["airdate"].Value, fixedDate);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var regex in ReportTitleRegex)
|
||||
{
|
||||
var match = regex.Matches(simpleTitle);
|
||||
|
||||
if (match.Count != 0)
|
||||
{
|
||||
Logger.Trace(regex);
|
||||
try
|
||||
{
|
||||
var result = ParseMatchMusicCollection(match);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
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.Quality = QualityParser.ParseQuality(title);
|
||||
Logger.Debug("Quality parsed: {0}", result.Quality);
|
||||
|
||||
result.ReleaseGroup = ParseReleaseGroup(title);
|
||||
|
||||
var subGroup = GetSubGroup(match);
|
||||
if (!subGroup.IsNullOrWhiteSpace())
|
||||
{
|
||||
result.ReleaseGroup = subGroup;
|
||||
}
|
||||
|
||||
Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup);
|
||||
|
||||
result.ReleaseHash = GetReleaseHash(match);
|
||||
if (!result.ReleaseHash.IsNullOrWhiteSpace())
|
||||
{
|
||||
Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (InvalidDateException ex)
|
||||
{
|
||||
Logger.Debug(ex, ex.Message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc"))
|
||||
Logger.Error(e, "An error has occurred while trying to parse {0}", title);
|
||||
}
|
||||
|
||||
Logger.Debug("Unable to parse {0}", title);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ParsedEpisodeInfo ParseTitle(string title)
|
||||
{
|
||||
try
|
||||
|
|
@ -433,6 +564,17 @@ public static string CleanSeriesTitle(this string title)
|
|||
return NormalizeRegex.Replace(title, string.Empty).ToLower().RemoveAccent();
|
||||
}
|
||||
|
||||
public static string CleanArtistTitle(this string title)
|
||||
{
|
||||
long number = 0;
|
||||
|
||||
//If Title only contains numbers return it as is.
|
||||
if (long.TryParse(title, out number))
|
||||
return title;
|
||||
|
||||
return NormalizeRegex.Replace(title, string.Empty).ToLower().RemoveAccent();
|
||||
}
|
||||
|
||||
public static string NormalizeEpisodeTitle(string title)
|
||||
{
|
||||
title = SpecialEpisodeWordRegex.Replace(title, string.Empty);
|
||||
|
|
@ -522,6 +664,10 @@ private static SeriesTitleInfo GetSeriesTitleInfo(string title)
|
|||
return seriesTitleInfo;
|
||||
}
|
||||
|
||||
private static ParsedTrackInfo ParseMatchMusicCollection(MatchCollection matchCollection)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection)
|
||||
{
|
||||
var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' ');
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Music;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Parser
|
||||
{
|
||||
|
|
@ -20,13 +22,20 @@ public interface IParsingService
|
|||
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds);
|
||||
List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null);
|
||||
ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
|
||||
|
||||
// Music stuff here
|
||||
LocalTrack GetLocalTrack(string filename, Artist artist);
|
||||
LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource);
|
||||
|
||||
}
|
||||
|
||||
public class ParsingService : IParsingService
|
||||
{
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
// private readonly ISceneMappingService _sceneMappingService;
|
||||
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly ITrackService _trackService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ParsingService(IEpisodeService episodeService,
|
||||
|
|
@ -474,5 +483,89 @@ private List<Episode> GetStandardEpisodes(Series series, ParsedEpisodeInfo parse
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
public LocalTrack GetLocalTrack(string filename, Artist artist)
|
||||
{
|
||||
return GetLocalTrack(filename, artist, null, false);
|
||||
}
|
||||
|
||||
public LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource)
|
||||
{
|
||||
ParsedTrackInfo parsedTrackInfo;
|
||||
|
||||
if (folderInfo != null)
|
||||
{
|
||||
parsedTrackInfo = folderInfo.JsonClone();
|
||||
parsedTrackInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
parsedTrackInfo = Parser.ParseMusicPath(filename);
|
||||
}
|
||||
|
||||
if (parsedTrackInfo == null || parsedTrackInfo.IsPossibleSpecialEpisode)
|
||||
{
|
||||
var title = Path.GetFileNameWithoutExtension(filename);
|
||||
//var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series);
|
||||
|
||||
//if (specialEpisodeInfo != null)
|
||||
//{
|
||||
// parsedTrackInfo = specialEpisodeInfo;
|
||||
//}
|
||||
}
|
||||
|
||||
if (parsedTrackInfo == null)
|
||||
{
|
||||
if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename)))
|
||||
{
|
||||
_logger.Warn("Unable to parse track info from path {0}", filename);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var tracks = GetTracks(parsedTrackInfo, artist, sceneSource);
|
||||
|
||||
return new LocalTrack
|
||||
{
|
||||
Artist = artist,
|
||||
Quality = parsedTrackInfo.Quality,
|
||||
Tracks = tracks,
|
||||
Path = filename,
|
||||
ParsedTrackInfo = parsedTrackInfo,
|
||||
ExistingFile = artist.Path.IsParentPath(filename)
|
||||
};
|
||||
}
|
||||
|
||||
private List<Track> GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist, bool sceneSource)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
/*if (parsedTrackInfo.FullSeason) // IF Album
|
||||
{
|
||||
return _trackService.GetTracksByAlbumTitle(artist.Id, parsedTrackInfo.AlbumTitle);
|
||||
}
|
||||
|
||||
if (parsedTrackInfo.IsDaily)
|
||||
{
|
||||
if (artist.SeriesType == SeriesTypes.Standard)
|
||||
{
|
||||
_logger.Warn("Found daily-style episode for non-daily series: {0}.", series);
|
||||
return new List<Episode>();
|
||||
}
|
||||
|
||||
var episodeInfo = GetDailyEpisode(artist, parsedTrackInfo.AirDate, searchCriteria);
|
||||
|
||||
if (episodeInfo != null)
|
||||
{
|
||||
return new List<Episode> { episodeInfo };
|
||||
}
|
||||
|
||||
return new List<Track>();
|
||||
}
|
||||
|
||||
return GetStandardEpisodes(artist, parsedTrackInfo, sceneSource, searchCriteria);*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,5 +3,6 @@
|
|||
public class AddSeriesOptions : MonitoringOptions
|
||||
{
|
||||
public bool SearchForMissingEpisodes { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ public class MonitoringOptions : IEmbeddedDocument
|
|||
{
|
||||
public bool IgnoreEpisodesWithFiles { get; set; }
|
||||
public bool IgnoreEpisodesWithoutFiles { get; set; }
|
||||
|
||||
public bool IgnoreTracksWithFiles { get; set; }
|
||||
public bool IgnoreTracksWithoutFiles { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs
Normal file
29
src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using FluentValidation.Validators;
|
||||
using NzbDrone.Core.Music;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Validation.Paths
|
||||
{
|
||||
public class ArtistExistsValidator : PropertyValidator
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
|
||||
public ArtistExistsValidator(IArtistService artistService)
|
||||
: base("This artist has already been added")
|
||||
{
|
||||
_artistService = artistService;
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue == null) return true;
|
||||
|
||||
var itunesId = Convert.ToInt32(context.PropertyValue.ToString());
|
||||
|
||||
return (!_artistService.GetAllArtists().Exists(s => s.ItunesId == itunesId));
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/NzbDrone.Core/Validation/Paths/ArtistPathValidator.cs
Normal file
31
src/NzbDrone.Core/Validation/Paths/ArtistPathValidator.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Music;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Validation.Paths
|
||||
{
|
||||
public class ArtistPathValidator : PropertyValidator
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
|
||||
public ArtistPathValidator(IArtistService artistService)
|
||||
: base("Path is already configured for another artist")
|
||||
{
|
||||
_artistService = artistService;
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue == null) return true;
|
||||
|
||||
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||
var instanceId = (int)instance.Id;
|
||||
|
||||
return (!_artistService.GetAllArtists().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != instanceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
var Backbone = require('backbone');
|
||||
var SeriesModel = require('../Series/SeriesModel');
|
||||
var ArtistModel = require('../Artist/ArtistModel');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/series/lookup',
|
||||
model : SeriesModel,
|
||||
url : window.NzbDrone.ApiRoot + '/artist/lookup',
|
||||
model : ArtistModel,
|
||||
|
||||
parse : function(response) {
|
||||
var self = this;
|
||||
|
|
@ -16,6 +16,7 @@ module.exports = Backbone.Collection.extend({
|
|||
model.path = self.unmappedFolderModel.get('folder').path;
|
||||
}
|
||||
});
|
||||
console.log('response: ', response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
<div class="btn-group add-series-btn-group btn-group-lg btn-block">
|
||||
<button type="button" class="btn btn-default col-md-10 col-xs-8 add-series-import-btn x-import">
|
||||
<i class="icon-sonarr-hdd"/>
|
||||
Import existing series on disk
|
||||
Import existing artists on disk
|
||||
</button>
|
||||
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Series</button>
|
||||
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Artist</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ module.exports = Marionette.Layout.extend({
|
|||
initialize : function(options) {
|
||||
this.isExisting = options.isExisting;
|
||||
this.collection = new AddSeriesCollection();
|
||||
console.log('this.collection:', this.collection);
|
||||
|
||||
if (this.isExisting) {
|
||||
this.collection.unmappedFolderModel = this.model;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
{{#if folder}}
|
||||
<input type="text" class="form-control x-series-search" value="{{folder.name}}">
|
||||
{{else}}
|
||||
<input type="text" class="form-control x-series-search" placeholder="Start typing the name of series you want to add ...">
|
||||
<input type="text" class="form-control x-series-search" placeholder="Start typing the name of music you want to add ...">
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<div class="text-center hint col-md-12">
|
||||
<span>You can also search by tvdbid using the tvdb: prefixes.</span>
|
||||
<span>You can also search by iTunes using the itunes: prefixes.</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
There was an error searching for '{{term}}'.
|
||||
</h3>
|
||||
|
||||
If the series title contains non-alphanumeric characters try removing them, otherwise try your search again later.
|
||||
If the artist name contains non-alphanumeric characters try removing them, otherwise try your search again later.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="x-existing-folders">
|
||||
<div class="loading-folders x-loading-folders">
|
||||
Loading search results from TheTVDB for your series, this may take a few minutes.
|
||||
Loading search results from iTunes for your artists, this may take a few minutes.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -6,7 +6,7 @@ var Marionette = require('marionette');
|
|||
var Profiles = require('../Profile/ProfileCollection');
|
||||
var RootFolders = require('./RootFolders/RootFolderCollection');
|
||||
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
|
||||
var SeriesCollection = require('../Series/SeriesCollection');
|
||||
var ArtistCollection = require('../Artist/ArtistCollection');
|
||||
var Config = require('../Config');
|
||||
var Messenger = require('../Shared/Messenger');
|
||||
var AsValidatedView = require('../Mixins/AsValidatedView');
|
||||
|
|
@ -18,25 +18,29 @@ var view = Marionette.ItemView.extend({
|
|||
template : 'AddSeries/SearchResultViewTemplate',
|
||||
|
||||
ui : {
|
||||
profile : '.x-profile',
|
||||
rootFolder : '.x-root-folder',
|
||||
seasonFolder : '.x-season-folder',
|
||||
seriesType : '.x-series-type',
|
||||
monitor : '.x-monitor',
|
||||
monitorTooltip : '.x-monitor-tooltip',
|
||||
addButton : '.x-add',
|
||||
addSearchButton : '.x-add-search',
|
||||
overview : '.x-overview'
|
||||
profile : '.x-profile',
|
||||
rootFolder : '.x-root-folder',
|
||||
seasonFolder : '.x-season-folder',
|
||||
seriesType : '.x-series-type',
|
||||
monitor : '.x-monitor',
|
||||
monitorTooltip : '.x-monitor-tooltip',
|
||||
addButton : '.x-add',
|
||||
addAlbumButton : '.x-add-album',
|
||||
addSearchButton : '.x-add-search',
|
||||
addAlbumSearchButton : '.x-add-album-search',
|
||||
overview : '.x-overview'
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-add' : '_addWithoutSearch',
|
||||
'click .x-add-search' : '_addAndSearch',
|
||||
'change .x-profile' : '_profileChanged',
|
||||
'change .x-root-folder' : '_rootFolderChanged',
|
||||
'change .x-season-folder' : '_seasonFolderChanged',
|
||||
'change .x-series-type' : '_seriesTypeChanged',
|
||||
'change .x-monitor' : '_monitorChanged'
|
||||
'click .x-add' : '_addWithoutSearch',
|
||||
'click .x-add-album' : '_addWithoutSearch',
|
||||
'click .x-add-search' : '_addAndSearch',
|
||||
'click .x-add-album-search' : '_addAndSearch',
|
||||
'change .x-profile' : '_profileChanged',
|
||||
'change .x-root-folder' : '_rootFolderChanged',
|
||||
'change .x-season-folder' : '_seasonFolderChanged',
|
||||
'change .x-series-type' : '_seriesTypeChanged',
|
||||
'change .x-monitor' : '_monitorChanged'
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
|
|
@ -93,7 +97,7 @@ var view = Marionette.ItemView.extend({
|
|||
},
|
||||
|
||||
_configureTemplateHelpers : function() {
|
||||
var existingSeries = SeriesCollection.where({ tvdbId : this.model.get('tvdbId') });
|
||||
var existingSeries = ArtistCollection.where({ iTunesId : this.model.get('itunesId') });
|
||||
|
||||
if (existingSeries.length > 0) {
|
||||
this.templateHelpers.existing = existingSeries[0].toJSON();
|
||||
|
|
@ -161,7 +165,8 @@ var view = Marionette.ItemView.extend({
|
|||
this._rootFolderChanged();
|
||||
},
|
||||
|
||||
_addWithoutSearch : function() {
|
||||
_addWithoutSearch : function(evt) {
|
||||
console.log(evt);
|
||||
this._addSeries(false);
|
||||
},
|
||||
|
||||
|
|
@ -169,20 +174,22 @@ var view = Marionette.ItemView.extend({
|
|||
this._addSeries(true);
|
||||
},
|
||||
|
||||
_addSeries : function(searchForMissingEpisodes) {
|
||||
_addSeries : function(searchForMissing) {
|
||||
// TODO: Refactor to handle multiple add buttons/albums
|
||||
var addButton = this.ui.addButton;
|
||||
var addSearchButton = this.ui.addSearchButton;
|
||||
console.log('_addSeries, searchForMissing=', searchForMissing);
|
||||
|
||||
addButton.addClass('disabled');
|
||||
addSearchButton.addClass('disabled');
|
||||
|
||||
var profile = this.ui.profile.val();
|
||||
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
|
||||
var seriesType = this.ui.seriesType.val();
|
||||
var seriesType = this.ui.seriesType.val(); // Perhaps make this a differnitator between artist or Album?
|
||||
var seasonFolder = this.ui.seasonFolder.prop('checked');
|
||||
|
||||
var options = this._getAddSeriesOptions();
|
||||
options.searchForMissingEpisodes = searchForMissingEpisodes;
|
||||
options.searchForMissing = searchForMissing;
|
||||
|
||||
this.model.set({
|
||||
profileId : profile,
|
||||
|
|
@ -196,7 +203,7 @@ var view = Marionette.ItemView.extend({
|
|||
var self = this;
|
||||
var promise = this.model.save();
|
||||
|
||||
if (searchForMissingEpisodes) {
|
||||
if (searchForMissing) {
|
||||
this.ui.addSearchButton.spinForPromise(promise);
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +217,8 @@ var view = Marionette.ItemView.extend({
|
|||
});
|
||||
|
||||
promise.done(function() {
|
||||
SeriesCollection.add(self.model);
|
||||
console.log('[SearchResultView] _addSeries promise resolve:', self.model);
|
||||
ArtistCollection.add(self.model);
|
||||
|
||||
self.close();
|
||||
|
||||
|
|
@ -218,9 +226,9 @@ var view = Marionette.ItemView.extend({
|
|||
message : 'Added: ' + self.model.get('title'),
|
||||
actions : {
|
||||
goToSeries : {
|
||||
label : 'Go to Series',
|
||||
label : 'Go to Artist',
|
||||
action : function() {
|
||||
Backbone.history.navigate('/series/' + self.model.get('titleSlug'), { trigger : true });
|
||||
Backbone.history.navigate('/artist/' + self.model.get('titleSlug'), { trigger : true });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -239,10 +247,11 @@ var view = Marionette.ItemView.extend({
|
|||
|
||||
_getAddSeriesOptions : function() {
|
||||
var monitor = this.ui.monitor.val();
|
||||
//[TODO]: Refactor for albums
|
||||
var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber');
|
||||
var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber');
|
||||
|
||||
this.model.setSeasonPass(firstSeason.seasonNumber);
|
||||
//this.model.setSeasonPass(firstSeason.seasonNumber); // TODO
|
||||
|
||||
var options = {
|
||||
ignoreEpisodesWithFiles : false,
|
||||
|
|
@ -258,14 +267,14 @@ var view = Marionette.ItemView.extend({
|
|||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'latest') {
|
||||
/*else if (monitor === 'latest') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber);
|
||||
}
|
||||
|
||||
else if (monitor === 'first') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
|
||||
this.model.setSeasonMonitored(firstSeason.seasonNumber);
|
||||
}
|
||||
}*/
|
||||
|
||||
else if (monitor === 'missing') {
|
||||
options.ignoreEpisodesWithFiles = true;
|
||||
|
|
@ -275,9 +284,9 @@ var view = Marionette.ItemView.extend({
|
|||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'none') {
|
||||
/*else if (monitor === 'none') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
|
||||
}
|
||||
}*/
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<a href="{{tvdbUrl}}" target="_blank">
|
||||
{{poster}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 class="series-title">
|
||||
{{titleWithYear}}
|
||||
<!--{{titleWithYear}}-->
|
||||
{{artistName}}
|
||||
|
||||
<span class="labels">
|
||||
<!--<span class="labels">
|
||||
<span class="label label-default">{{network}}</span>
|
||||
{{#unless_eq status compare="continuing"}}
|
||||
<span class="label label-danger">Ended</span>
|
||||
{{/unless_eq}}
|
||||
</span>
|
||||
</span>-->
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row new-series-overview x-overview">
|
||||
<!-- <div class="row new-series-overview x-overview">
|
||||
<div class="col-md-12 overview-internal">
|
||||
{{overview}}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="row">
|
||||
{{#unless existing}}
|
||||
{{#unless path}}
|
||||
|
|
@ -41,8 +37,6 @@
|
|||
<option value="future">Future</option>
|
||||
<option value="missing">Missing</option>
|
||||
<option value="existing">Existing</option>
|
||||
<option value="first">First Season</option>
|
||||
<option value="latest">Latest Season</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -52,13 +46,12 @@
|
|||
{{> ProfileSelectionPartial profiles}}
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label>Series Type</label>
|
||||
{{> SeriesTypeSelectionPartial}}
|
||||
</div>
|
||||
<!--<div class="form-group col-md-2">
|
||||
|
||||
</div>-->
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label>Season Folders</label>
|
||||
<label>Album Folders</label>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="checkbox toggle well">
|
||||
|
|
@ -75,22 +68,22 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
{{#unless existing}}
|
||||
{{#if title}}
|
||||
{{#if artistName}}
|
||||
<div class="form-group col-md-2 col-md-offset-10">
|
||||
<!--Uncomment if we need to add even more controls to add series-->
|
||||
<!--<label style="visibility: hidden">Add</label>-->
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success add x-add" title="Add">
|
||||
<button class="btn btn-success add x-add" title="Add" data-artist="{{artistName}}">
|
||||
<i class="icon-sonarr-add"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success add x-add-search" title="Add and Search for missing episodes">
|
||||
<button class="btn btn-success add x-add-search" title="Add and Search for missing tracks" data-artist="{{artistName}}">
|
||||
<i class="icon-sonarr-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="col-md-2 col-md-offset-10" title="Series requires an English title">
|
||||
<div class="col-md-2 col-md-offset-10">
|
||||
<button class="btn add-series disabled">
|
||||
Add
|
||||
</button>
|
||||
|
|
@ -106,4 +99,48 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{{#each albums}}
|
||||
<div class="col-md-12" style="border:1px dashed black;">
|
||||
<div class="col-md-2">
|
||||
<a href="{{artworkUrl}}" target="_blank">
|
||||
<!-- {{poster}} -->
|
||||
<img class="album-poster" src="{{artworkUrl}}">
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<h2>{{albumName}} ({{year}})</h2>
|
||||
{{#unless existing}}
|
||||
{{#if albumName}}
|
||||
<div class="form-group col-md-offset-10">
|
||||
<!--Uncomment if we need to add even more controls to add series-->
|
||||
<!--<label style="visibility: hidden">Add</label>-->
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success add x-add-album" title="Add" data-album="{{albumName}}">
|
||||
<i class="icon-sonarr-add"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success add x-add-album-search" title="Add and Search for missing tracks" data-album="{{albumName}}">
|
||||
<i class="icon-sonarr-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="col-md-2 col-md-offset-10">
|
||||
<button class="btn add-series disabled">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="col-md-2 col-md-offset-10">
|
||||
<a class="btn btn-default" href="{{route}}">
|
||||
Already Exists
|
||||
</a>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@
|
|||
margin : 10px;
|
||||
}
|
||||
|
||||
.album-poster {
|
||||
min-width : 100px;
|
||||
min-height : 100px;
|
||||
max-width : 138px;
|
||||
max-height : 203px;
|
||||
margin : 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color : #343434;
|
||||
}
|
||||
|
|
|
|||
124
src/UI/Artist/ArtistCollection.js
Normal file
124
src/UI/Artist/ArtistCollection.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var PageableCollection = require('backbone.pageable');
|
||||
var ArtistModel = require('./ArtistModel');
|
||||
var ApiData = require('../Shared/ApiData');
|
||||
var AsFilteredCollection = require('../Mixins/AsFilteredCollection');
|
||||
var AsSortedCollection = require('../Mixins/AsSortedCollection');
|
||||
var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection');
|
||||
var moment = require('moment');
|
||||
require('../Mixins/backbone.signalr.mixin');
|
||||
|
||||
var Collection = PageableCollection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/artist',
|
||||
model : ArtistModel,
|
||||
tableName : 'artist',
|
||||
|
||||
state : {
|
||||
sortKey : 'sortTitle',
|
||||
order : -1,
|
||||
pageSize : 100000,
|
||||
secondarySortKey : 'sortTitle',
|
||||
secondarySortOrder : -1
|
||||
},
|
||||
|
||||
mode : 'client',
|
||||
|
||||
save : function() {
|
||||
var self = this;
|
||||
|
||||
var proxy = _.extend(new Backbone.Model(), {
|
||||
id : '',
|
||||
|
||||
url : self.url + '/editor',
|
||||
|
||||
toJSON : function() {
|
||||
return self.filter(function(model) {
|
||||
return model.edited;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.listenTo(proxy, 'sync', function(proxyModel, models) {
|
||||
this.add(models, { merge : true });
|
||||
this.trigger('save', this);
|
||||
});
|
||||
|
||||
return proxy.save();
|
||||
},
|
||||
|
||||
filterModes : {
|
||||
'all' : [
|
||||
null,
|
||||
null
|
||||
],
|
||||
'continuing' : [
|
||||
'status',
|
||||
'continuing'
|
||||
],
|
||||
'ended' : [
|
||||
'status',
|
||||
'ended'
|
||||
],
|
||||
'monitored' : [
|
||||
'monitored',
|
||||
true
|
||||
],
|
||||
'missing' : [
|
||||
null,
|
||||
null,
|
||||
function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); }
|
||||
]
|
||||
},
|
||||
|
||||
sortMappings : {
|
||||
title : {
|
||||
sortKey : 'sortTitle'
|
||||
},
|
||||
|
||||
artistName: {
|
||||
sortKey : 'artistName'
|
||||
},
|
||||
|
||||
nextAiring : {
|
||||
sortValue : function(model, attr, order) {
|
||||
var nextAiring = model.get(attr);
|
||||
|
||||
if (nextAiring) {
|
||||
return moment(nextAiring).unix();
|
||||
}
|
||||
|
||||
if (order === 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
},
|
||||
|
||||
percentOfEpisodes : {
|
||||
sortValue : function(model, attr) {
|
||||
var percentOfEpisodes = model.get(attr);
|
||||
var episodeCount = model.get('episodeCount');
|
||||
|
||||
return percentOfEpisodes + episodeCount / 1000000;
|
||||
}
|
||||
},
|
||||
|
||||
path : {
|
||||
sortValue : function(model) {
|
||||
var path = model.get('path');
|
||||
|
||||
return path.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Collection = AsFilteredCollection.call(Collection);
|
||||
Collection = AsSortedCollection.call(Collection);
|
||||
Collection = AsPersistedStateCollection.call(Collection);
|
||||
|
||||
var data = ApiData.get('series'); // TOOD: Build backend for artist
|
||||
|
||||
module.exports = new Collection(data, { full : true }).bindSignalR();
|
||||
37
src/UI/Artist/ArtistController.js
Normal file
37
src/UI/Artist/ArtistController.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
var NzbDroneController = require('../Shared/NzbDroneController');
|
||||
var AppLayout = require('../AppLayout');
|
||||
var ArtistCollection = require('./ArtistCollection');
|
||||
var SeriesIndexLayout = require('./Index/SeriesIndexLayout');
|
||||
var SeriesDetailsLayout = require('../Series/Details/SeriesDetailsLayout');
|
||||
|
||||
module.exports = NzbDroneController.extend({
|
||||
_originalInit : NzbDroneController.prototype.initialize,
|
||||
|
||||
initialize : function() {
|
||||
this.route('', this.series);
|
||||
this.route('artist', this.series);
|
||||
this.route('artist/:query', this.seriesDetails);
|
||||
|
||||
this._originalInit.apply(this, arguments);
|
||||
},
|
||||
|
||||
artist : function() {
|
||||
this.setTitle('Lidarr');
|
||||
this.setArtistName('Lidarr');
|
||||
this.showMainRegion(new SeriesIndexLayout());
|
||||
},
|
||||
|
||||
seriesDetails : function(query) {
|
||||
var artists = ArtistCollection.where({ artistNameSlug : query });
|
||||
console.log('seriesDetails, artists: ', artists);
|
||||
if (artists.length !== 0) {
|
||||
var targetSeries = artists[0];
|
||||
console.log("[ArtistController] targetSeries: ", targetSeries);
|
||||
this.setTitle(targetSeries.get('title'));
|
||||
this.setArtistName(targetSeries.get('artistName'));
|
||||
this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries }));
|
||||
} else {
|
||||
this.showNotFound();
|
||||
}
|
||||
}
|
||||
});
|
||||
31
src/UI/Artist/ArtistModel.js
Normal file
31
src/UI/Artist/ArtistModel.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
urlRoot : window.NzbDrone.ApiRoot + '/artist',
|
||||
|
||||
defaults : {
|
||||
episodeFileCount : 0,
|
||||
episodeCount : 0,
|
||||
isExisting : false,
|
||||
status : 0
|
||||
},
|
||||
|
||||
setAlbumsMonitored : function(albumName) {
|
||||
_.each(this.get('albums'), function(album) {
|
||||
if (season.albumName === albumName) {
|
||||
album.monitored = !album.monitored;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setAlbumPass : function(seasonNumber) {
|
||||
_.each(this.get('albums'), function(album) {
|
||||
if (album.seasonNumber >= seasonNumber) {
|
||||
album.monitored = true;
|
||||
} else {
|
||||
album.monitored = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -71,7 +71,7 @@ Handlebars.registerHelper('seasonCountHelper', function() {
|
|||
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount));
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('titleWithYear', function() {
|
||||
/*Handlebars.registerHelper('titleWithYear', function() {
|
||||
if (this.title.endsWith(' ({0})'.format(this.year))) {
|
||||
return this.title;
|
||||
}
|
||||
|
|
@ -81,4 +81,4 @@ Handlebars.registerHelper('titleWithYear', function() {
|
|||
}
|
||||
|
||||
return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year));
|
||||
});
|
||||
});*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ var vent = require('vent');
|
|||
var reqres = require('../../reqres');
|
||||
var Marionette = require('marionette');
|
||||
var Backbone = require('backbone');
|
||||
var SeriesCollection = require('../SeriesCollection');
|
||||
var ArtistCollection = require('../../Artist/ArtistCollection');
|
||||
var EpisodeCollection = require('../EpisodeCollection');
|
||||
var EpisodeFileCollection = require('../EpisodeFileCollection');
|
||||
var SeasonCollection = require('../SeasonCollection');
|
||||
|
|
@ -45,7 +45,7 @@ module.exports = Marionette.Layout.extend({
|
|||
},
|
||||
|
||||
initialize : function() {
|
||||
this.seriesCollection = SeriesCollection.clone();
|
||||
this.seriesCollection = ArtistCollection.clone();
|
||||
this.seriesCollection.shadowCollection.bindSignalR();
|
||||
|
||||
this.listenTo(this.model, 'change:monitored', this._setMonitoredState);
|
||||
|
|
@ -155,6 +155,7 @@ module.exports = Marionette.Layout.extend({
|
|||
},
|
||||
|
||||
_seriesSearch : function() {
|
||||
console.log('_seriesSearch:', this.model);
|
||||
CommandController.Execute('seriesSearch', {
|
||||
name : 'seriesSearch',
|
||||
seriesId : this.model.id
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
<div class="row">
|
||||
<div class="well col-md-12">
|
||||
<i class="icon-sonarr-comment"/>
|
||||
You must be new around here, You should add some series.
|
||||
You must be new around here, You should add some music.
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<a href="/addseries" class='btn btn-lg btn-block btn-success x-add-series'>
|
||||
<i class='icon-sonarr-add'></i>
|
||||
Add Series
|
||||
Add Music
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue