Refactored most code for track parsing.

This commit is contained in:
Joseph Milazzo 2017-05-11 13:43:05 -05:00
commit 76db95947c
18 changed files with 430 additions and 177 deletions

View file

@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.IndexerSearch.Definitions namespace NzbDrone.Core.IndexerSearch.Definitions
{ {
@ -19,6 +20,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public virtual bool MonitoredEpisodesOnly { get; set; } public virtual bool MonitoredEpisodesOnly { get; set; }
public virtual bool UserInvokedSearch { get; set; } public virtual bool UserInvokedSearch { get; set; }
public Artist Artist { get; set; }
public List<Track> Tracks { get; set; }
public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList(); public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList();
public static string GetQueryTitle(string title) public static string GetQueryTitle(string title)

View file

@ -0,0 +1,26 @@
using NzbDrone.Core.Messaging.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.MediaFiles.Events
{
public class RescanArtistCommand : Command
{
public string ArtistId { get; set; }
public override bool SendUpdatesToClient => true;
public RescanArtistCommand()
{
ArtistId = "";
}
public RescanArtistCommand(string artistId)
{
ArtistId = artistId;
}
}
}

View file

@ -16,12 +16,15 @@ using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
public interface IDiskScanService public interface IDiskScanService
{ {
void Scan(Series series); void Scan(Series series);
void Scan(Artist artist);
string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetVideoFiles(string path, bool allDirectories = true);
string[] GetNonVideoFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true);
List<string> FilterFiles(Series series, IEnumerable<string> files); List<string> FilterFiles(Series series, IEnumerable<string> files);
@ -30,13 +33,16 @@ namespace NzbDrone.Core.MediaFiles
public class DiskScanService : public class DiskScanService :
IDiskScanService, IDiskScanService,
IHandle<SeriesUpdatedEvent>, IHandle<SeriesUpdatedEvent>,
IExecute<RescanSeriesCommand> IExecute<RescanSeriesCommand>,
IHandle<ArtistUpdatedEvent>,
IExecute<RescanArtistCommand>
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IMakeImportDecision _importDecisionMaker; private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes; private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
private readonly IArtistService _artistService;
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
@ -46,6 +52,7 @@ namespace NzbDrone.Core.MediaFiles
IImportApprovedEpisodes importApprovedEpisodes, IImportApprovedEpisodes importApprovedEpisodes,
IConfigService configService, IConfigService configService,
ISeriesService seriesService, ISeriesService seriesService,
IArtistService artistService,
IMediaFileTableCleanupService mediaFileTableCleanupService, IMediaFileTableCleanupService mediaFileTableCleanupService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
@ -55,6 +62,7 @@ namespace NzbDrone.Core.MediaFiles
_importApprovedEpisodes = importApprovedEpisodes; _importApprovedEpisodes = importApprovedEpisodes;
_configService = configService; _configService = configService;
_seriesService = seriesService; _seriesService = seriesService;
_artistService = artistService;
_mediaFileTableCleanupService = mediaFileTableCleanupService; _mediaFileTableCleanupService = mediaFileTableCleanupService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
@ -63,6 +71,58 @@ namespace NzbDrone.Core.MediaFiles
private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(extras|@eadir|extrafanart|plex\sversions|\..+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(extras|@eadir|extrafanart|plex\sversions|\..+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|Thumbs\.db", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|Thumbs\.db", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public void Scan(Artist artist)
{
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
if (!_diskProvider.FolderExists(rootFolder))
{
_logger.Warn("Artist' root folder ({0}) doesn't exist.", rootFolder);
_eventAggregator.PublishEvent(new ArtistScanSkippedEvent(artist, ArtistScanSkippedReason.RootFolderDoesNotExist));
return;
}
if (_diskProvider.GetDirectories(rootFolder).Empty())
{
_logger.Warn("Artist' root folder ({0}) is empty.", rootFolder);
_eventAggregator.PublishEvent(new ArtistScanSkippedEvent(artist, ArtistScanSkippedReason.RootFolderIsEmpty));
return;
}
_logger.ProgressInfo("Scanning disk for {0}", artist.ArtistName);
if (!_diskProvider.FolderExists(artist.Path))
{
if (_configService.CreateEmptySeriesFolders)
{
_logger.Debug("Creating missing artist folder: {0}", artist.Path);
_diskProvider.CreateFolder(artist.Path);
SetPermissions(artist.Path);
}
else
{
_logger.Debug("Artist folder doesn't exist: {0}", artist.Path);
}
CleanMediaFiles(artist, new List<string>());
CompletedScanning(artist);
return;
}
var musicFilesStopwatch = Stopwatch.StartNew();
var mediaFileList = FilterFiles(artist, GetMusicFiles(artist.Path)).ToList();
musicFilesStopwatch.Stop();
_logger.Trace("Finished getting track files for: {0} [{1}]", artist, musicFilesStopwatch.Elapsed);
CleanMediaFiles(artist, mediaFileList);
var decisionsStopwatch = Stopwatch.StartNew();
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist);
decisionsStopwatch.Stop();
_logger.Trace("Import decisions complete for: {0} [{1}]", artist, decisionsStopwatch.Elapsed);
_importApprovedTracks.Import(decisions, false);
CompletedScanning(artist);
}
public void Scan(Series series) public void Scan(Series series)
{ {
var rootFolder = _diskProvider.GetParentFolder(series.Path); var rootFolder = _diskProvider.GetParentFolder(series.Path);
@ -122,12 +182,24 @@ namespace NzbDrone.Core.MediaFiles
_mediaFileTableCleanupService.Clean(series, mediaFileList); _mediaFileTableCleanupService.Clean(series, mediaFileList);
} }
private void CleanMediaFiles(Artist artist, List<string> mediaFileList)
{
_logger.Debug("{0} Cleaning up media files in DB", artist);
_mediaFileTableCleanupService.Clean(artist, mediaFileList);
}
private void CompletedScanning(Series series) private void CompletedScanning(Series series)
{ {
_logger.Info("Completed scanning disk for {0}", series.Title); _logger.Info("Completed scanning disk for {0}", series.Title);
_eventAggregator.PublishEvent(new SeriesScannedEvent(series)); _eventAggregator.PublishEvent(new SeriesScannedEvent(series));
} }
private void CompletedScanning(Artist artist)
{
_logger.Info("Completed scanning disk for {0}", artist.ArtistName);
_eventAggregator.PublishEvent(new ArtistScannedEvent(artist));
}
public string[] GetVideoFiles(string path, bool allDirectories = true) public string[] GetVideoFiles(string path, bool allDirectories = true)
{ {
_logger.Debug("Scanning '{0}' for video files", path); _logger.Debug("Scanning '{0}' for video files", path);
@ -143,9 +215,24 @@ namespace NzbDrone.Core.MediaFiles
return mediaFileList.ToArray(); return mediaFileList.ToArray();
} }
public string[] GetMusicFiles(string path, bool allDirectories = true)
{
_logger.Debug("Scanning '{0}' for music files", path);
var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList();
var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file).ToLower()))
.ToList();
_logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path);
_logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path);
return mediaFileList.ToArray();
}
public string[] GetNonVideoFiles(string path, bool allDirectories = true) public string[] GetNonVideoFiles(string path, bool allDirectories = true)
{ {
_logger.Debug("Scanning '{0}' for non-video files", path); _logger.Debug("Scanning '{0}' for non-music files", path);
var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList();
@ -154,7 +241,7 @@ namespace NzbDrone.Core.MediaFiles
.ToList(); .ToList();
_logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path);
_logger.Debug("{0} non-video files were found in {1}", mediaFileList.Count, path); _logger.Debug("{0} non-music files were found in {1}", mediaFileList.Count, path);
return mediaFileList.ToArray(); return mediaFileList.ToArray();
} }
@ -165,6 +252,13 @@ namespace NzbDrone.Core.MediaFiles
.ToList(); .ToList();
} }
public List<string> FilterFiles(Artist artist, IEnumerable<string> files)
{
return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(artist.Path.GetRelativePath(file)))
.Where(file => !ExcludedFilesRegex.IsMatch(Path.GetFileName(file)))
.ToList();
}
private void SetPermissions(string path) private void SetPermissions(string path)
{ {
if (!_configService.SetPermissionsLinux) if (!_configService.SetPermissionsLinux)
@ -191,6 +285,11 @@ namespace NzbDrone.Core.MediaFiles
Scan(message.Series); Scan(message.Series);
} }
public void Handle(ArtistUpdatedEvent message)
{
Scan(message.Artist);
}
public void Execute(RescanSeriesCommand message) public void Execute(RescanSeriesCommand message)
{ {
if (message.SeriesId.HasValue) if (message.SeriesId.HasValue)
@ -209,5 +308,24 @@ namespace NzbDrone.Core.MediaFiles
} }
} }
} }
public void Execute(RescanArtistCommand message)
{
if (message.ArtistId.IsNotNullOrWhiteSpace())
{
var artist = _artistService.FindById(message.ArtistId);
Scan(artist);
}
else
{
var allArtists = _artistService.GetAllArtists();
foreach (var artist in allArtists)
{
Scan(artist);
}
}
}
} }
} }

View file

@ -11,14 +11,16 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.MediaFiles.EpisodeImport namespace NzbDrone.Core.MediaFiles.EpisodeImport
{ {
public interface IMakeImportDecision public interface IMakeImportDecision
{ {
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series); //List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist);
//List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo);
} }
public class ImportDecisionMaker : IMakeImportDecision public class ImportDecisionMaker : IMakeImportDecision
@ -48,65 +50,87 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_logger = logger; _logger = logger;
} }
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series) //public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series)
//{
// return GetImportDecisions(videoFiles, series, null, false);
//}
//public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Artist series, ParsedEpisodeInfo folderInfo, bool sceneSource)
//{
// var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series);
// _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count());
// var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo);
// var decisions = new List<ImportDecision>();
// foreach (var file in newFiles)
// {
// decisions.AddIfNotNull(GetDecision(file, series, folderInfo, sceneSource, shouldUseFolderName));
// }
// return decisions;
//}
public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist)
{ {
return GetImportDecisions(videoFiles, series, null, false); return GetImportDecisions(musicFiles, artist, null);
} }
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
{ {
var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); var newFiles = _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist);
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, musicFiles.Count());
var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo); var shouldUseFolderName = ShouldUseFolderName(musicFiles, artist, folderInfo);
var decisions = new List<ImportDecision>(); var decisions = new List<ImportDecision>();
foreach (var file in newFiles) foreach (var file in newFiles)
{ {
decisions.AddIfNotNull(GetDecision(file, series, folderInfo, sceneSource, shouldUseFolderName)); decisions.AddIfNotNull(GetDecision(file, artist, folderInfo, shouldUseFolderName));
} }
return decisions; return decisions;
} }
private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) private ImportDecision GetDecision(string file, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource, bool shouldUseFolderName)
{ {
ImportDecision decision = null; ImportDecision decision = null;
try try
{ {
var localEpisode = _parsingService.GetLocalEpisode(file, series, shouldUseFolderName ? folderInfo : null, sceneSource); var localTrack = _parsingService.GetLocalTrack(file, artist, shouldUseFolderName ? folderInfo : null);
if (localEpisode != null) if (localTrack != null)
{ {
localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, series); localTrack.Quality = GetQuality(folderInfo, localTrack.Quality, artist);
localEpisode.Size = _diskProvider.GetFileSize(file); localTrack.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", localEpisode.Size); _logger.Debug("Size: {0}", localTrack.Size);
//TODO: make it so media info doesn't ruin the import process of a new series //TODO: make it so media info doesn't ruin the import process of a new series
if (sceneSource) if (sceneSource)
{ {
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); localTrack.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
} }
if (localEpisode.Episodes.Empty()) if (localTrack.Tracks.Empty())
{ {
decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); decision = new ImportDecision(localTrack, new Rejection("Invalid album or track"));
} }
else else
{ {
decision = GetDecision(localEpisode); decision = GetDecision(localTrack);
} }
} }
else else
{ {
localEpisode = new LocalEpisode(); localTrack = new LocalTrack();
localEpisode.Path = file; localTrack.Path = file;
decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); decision = new ImportDecision(localTrack, new Rejection("Unable to parse file"));
} }
} }
catch (Exception e) catch (Exception e)
@ -150,28 +174,28 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return null; return null;
} }
private bool ShouldUseFolderName(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo) private bool ShouldUseFolderName(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
{ {
if (folderInfo == null) if (folderInfo == null)
{ {
return false; return false;
} }
if (folderInfo.FullSeason) //if (folderInfo.FullSeason)
{ //{
return false; // return false;
} //}
return videoFiles.Count(file => return musicFiles.Count(file =>
{ {
var size = _diskProvider.GetFileSize(file); var size = _diskProvider.GetFileSize(file);
var fileQuality = QualityParser.ParseQuality(file); var fileQuality = QualityParser.ParseQuality(file);
var sample = _detectSample.IsSample(series, GetQuality(folderInfo, fileQuality, series), file, size, folderInfo.IsPossibleSpecialEpisode); //var sample = _detectSample.IsSample(artist, GetQuality(folderInfo, fileQuality, artist), file, size, folderInfo.IsPossibleSpecialEpisode);
if (sample) //if (sample)
{ //{
return false; // return false;
} //}
if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) if (SceneChecker.IsSceneTitle(Path.GetFileName(file)))
{ {
@ -182,9 +206,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}) == 1; }) == 1;
} }
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) private QualityModel GetQuality(ParsedTrackInfo folderInfo, QualityModel fileQuality, Artist artist)
{ {
if (UseFolderQuality(folderInfo, fileQuality, series)) if (UseFolderQuality(folderInfo, fileQuality, artist))
{ {
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality); _logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
return folderInfo.Quality; return folderInfo.Quality;
@ -193,7 +217,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return fileQuality; return fileQuality;
} }
private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) private bool UseFolderQuality(ParsedTrackInfo folderInfo, QualityModel fileQuality, Artist artist)
{ {
if (folderInfo == null) if (folderInfo == null)
{ {
@ -210,7 +234,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return true; return true;
} }
if (new QualityModelComparer(series.Profile).Compare(folderInfo.Quality, fileQuality) > 0) if (new QualityModelComparer(artist.Profile).Compare(folderInfo.Quality, fileQuality) > 0)
{ {
return true; return true;
} }

View file

@ -0,0 +1,27 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Music;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.MediaFiles.Events
{
public class ArtistScanSkippedEvent : IEvent
{
public Artist Artist { get; private set; }
public ArtistScanSkippedReason Reason { get; private set; }
public ArtistScanSkippedEvent(Artist artist, ArtistScanSkippedReason reason)
{
Artist = artist;
Reason = reason;
}
}
public enum ArtistScanSkippedReason
{
RootFolderDoesNotExist,
RootFolderIsEmpty
}
}

View 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 ArtistScannedEvent : IEvent
{
public Artist Artist { get; private set; }
public ArtistScannedEvent(Artist artist)
{
Artist = artist;
}
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -5,36 +6,28 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
public interface IMediaFileRepository : IBasicRepository<EpisodeFile> public interface IMediaFileRepository : IBasicRepository<TrackFile>
{ {
List<EpisodeFile> GetFilesBySeries(int seriesId); List<TrackFile> GetFilesByArtist(string artistId);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber); List<TrackFile> GetFilesWithoutMediaInfo();
List<EpisodeFile> GetFilesWithoutMediaInfo();
} }
public class MediaFileRepository : BasicRepository<EpisodeFile>, IMediaFileRepository public class MediaFileRepository : BasicRepository<TrackFile>, IMediaFileRepository
{ {
public MediaFileRepository(IMainDatabase database, IEventAggregator eventAggregator) public MediaFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator) : base(database, eventAggregator)
{ {
} }
public List<EpisodeFile> GetFilesBySeries(int seriesId) public List<TrackFile> GetFilesWithoutMediaInfo()
{
return Query.Where(c => c.SeriesId == seriesId).ToList();
}
public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber)
{
return Query.Where(c => c.SeriesId == seriesId)
.AndWhere(c => c.SeasonNumber == seasonNumber)
.ToList();
}
public List<EpisodeFile> GetFilesWithoutMediaInfo()
{ {
return Query.Where(c => c.MediaInfo == null).ToList(); return Query.Where(c => c.MediaInfo == null).ToList();
} }
public List<TrackFile> GetFilesByArtist(string artistId)
{
return Query.Where(c => c.SpotifyTrackId == artistId).ToList();
}
} }
} }

View file

@ -7,24 +7,26 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Core.Music;
using System;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
public interface IMediaFileService public interface IMediaFileService
{ {
EpisodeFile Add(EpisodeFile episodeFile); TrackFile Add(TrackFile trackFile);
void Update(EpisodeFile episodeFile); void Update(TrackFile trackFile);
void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); void Delete(TrackFile trackFile, DeleteMediaFileReason reason);
List<EpisodeFile> GetFilesBySeries(int seriesId); List<TrackFile> GetFilesByArtist(string artistId);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber); List<TrackFile> GetFilesWithoutMediaInfo();
List<EpisodeFile> GetFilesWithoutMediaInfo(); List<string> FilterExistingFiles(List<string> files, Artist artist);
List<string> FilterExistingFiles(List<string> files, Series series); TrackFile Get(int id);
EpisodeFile Get(int id); List<TrackFile> Get(IEnumerable<int> ids);
List<EpisodeFile> Get(IEnumerable<int> ids);
} }
public class MediaFileService : IMediaFileService, IHandleAsync<SeriesDeletedEvent> public class MediaFileService : IMediaFileService, IHandleAsync<ArtistDeletedEvent>
{ {
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IMediaFileRepository _mediaFileRepository; private readonly IMediaFileRepository _mediaFileRepository;
@ -37,66 +39,63 @@ namespace NzbDrone.Core.MediaFiles
_logger = logger; _logger = logger;
} }
public EpisodeFile Add(EpisodeFile episodeFile) public TrackFile Add(TrackFile trackFile)
{ {
var addedFile = _mediaFileRepository.Insert(episodeFile); var addedFile = _mediaFileRepository.Insert(trackFile);
_eventAggregator.PublishEvent(new EpisodeFileAddedEvent(addedFile)); _eventAggregator.PublishEvent(new TrackFileAddedEvent(addedFile));
return addedFile; return addedFile;
} }
public void Update(EpisodeFile episodeFile) public void Update(TrackFile trackFile)
{ {
_mediaFileRepository.Update(episodeFile); _mediaFileRepository.Update(trackFile);
} }
public void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason) public void Delete(TrackFile trackFile, DeleteMediaFileReason reason)
{ {
//Little hack so we have the episodes and series attached for the event consumers //Little hack so we have the tracks and artist attached for the event consumers
episodeFile.Episodes.LazyLoad(); trackFile.Episodes.LazyLoad();
episodeFile.Path = Path.Combine(episodeFile.Series.Value.Path, episodeFile.RelativePath); trackFile.Path = Path.Combine(trackFile.Artist.Value.Path, trackFile.RelativePath);
_mediaFileRepository.Delete(episodeFile); _mediaFileRepository.Delete(trackFile);
_eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason)); _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason));
} }
public List<EpisodeFile> GetFilesBySeries(int seriesId)
{
return _mediaFileRepository.GetFilesBySeries(seriesId);
}
public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber) public List<TrackFile> GetFilesWithoutMediaInfo()
{
return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber);
}
public List<EpisodeFile> GetFilesWithoutMediaInfo()
{ {
return _mediaFileRepository.GetFilesWithoutMediaInfo(); return _mediaFileRepository.GetFilesWithoutMediaInfo();
} }
public List<string> FilterExistingFiles(List<string> files, Series series) public List<string> FilterExistingFiles(List<string> files, Artist artist)
{ {
var seriesFiles = GetFilesBySeries(series.Id).Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(); var artistFiles = GetFilesByArtist(artist.SpotifyId).Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList();
if (!seriesFiles.Any()) return files; if (!artistFiles.Any()) return files;
return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); return files.Except(artistFiles, PathEqualityComparer.Instance).ToList();
} }
public EpisodeFile Get(int id) public TrackFile Get(int id)
{ {
// TODO: Should this be spotifyID or DB Id?
return _mediaFileRepository.Get(id); return _mediaFileRepository.Get(id);
} }
public List<EpisodeFile> Get(IEnumerable<int> ids) public List<TrackFile> Get(IEnumerable<int> ids)
{ {
return _mediaFileRepository.Get(ids).ToList(); return _mediaFileRepository.Get(ids).ToList();
} }
public void HandleAsync(SeriesDeletedEvent message) public void HandleAsync(ArtistDeletedEvent message)
{ {
var files = GetFilesBySeries(message.Series.Id); var files = GetFilesByArtist(message.Artist.SpotifyId);
_mediaFileRepository.DeleteMany(files); _mediaFileRepository.DeleteMany(files);
} }
public List<TrackFile> GetFilesByArtist(string artistId)
{
return _mediaFileRepository.GetFilesByArtist(artistId);
}
} }
} }

View file

@ -12,7 +12,7 @@ namespace NzbDrone.Core.MediaFiles
{ {
public class TrackFile : ModelBase public class TrackFile : ModelBase
{ {
public int ItunesTrackId { get; set; } public string SpotifyTrackId { get; set; }
public int AlbumId { get; set; } public int AlbumId { get; set; }
public string RelativePath { get; set; } public string RelativePath { get; set; }
public string Path { get; set; } public string Path { get; set; }

View file

@ -89,11 +89,12 @@ namespace NzbDrone.Core.Music
return _artistRepository.All().ToList(); return _artistRepository.All().ToList();
} }
public Artist GetArtist(int artistId) public Artist GetArtist(int artistDBId)
{ {
return _artistRepository.Get(artistId); return _artistRepository.Get(artistDBId);
} }
public List<Artist> GetArtists(IEnumerable<int> artistIds) public List<Artist> GetArtists(IEnumerable<int> artistIds)
{ {
return _artistRepository.Get(artistIds).ToList(); return _artistRepository.Get(artistIds).ToList();

View file

@ -157,7 +157,7 @@ namespace NzbDrone.Core.Music
try try
{ {
_logger.Info("Skipping refresh of artist: {0}", artist.ArtistName); _logger.Info("Skipping refresh of artist: {0}", artist.ArtistName);
//TODO: _diskScanService.Scan(artist); _diskScanService.Scan(artist);
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Parser.Model
{
public class ArtistTitleInfo
{
public string Title { get; set; }
public string TitleWithoutYear { get; set; }
public int Year { get; set; }
}
}

View file

@ -9,87 +9,34 @@ namespace NzbDrone.Core.Parser.Model
{ {
public class ParsedTrackInfo public class ParsedTrackInfo
{ {
// [TODO]: Properly fill this out
public string ArtistTitle { get; set; } public string ArtistTitle { get; set; }
public string AlbumTitle { get; set; } public string AlbumTitle { get; set; }
public SeriesTitleInfo SeriesTitleInfo { get; set; } public ArtistTitleInfo ArtistTitleInfo { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public int SeasonNumber { get; set; } public string AlbumId { get; set; } // maybe
public int[] EpisodeNumbers { get; set; } public int[] TrackNumbers { get; set; }
public int[] AbsoluteEpisodeNumbers { get; set; } //public Language Language { 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 ReleaseGroup { get; set; }
public string ReleaseHash { get; set; } public string ReleaseHash { get; set; }
public ParsedTrackInfo() public ParsedTrackInfo()
{ {
EpisodeNumbers = new int[0]; TrackNumbers = 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() public override string ToString()
{ {
string episodeString = "[Unknown Episode]"; string episodeString = "[Unknown Track]";
if (IsDaily && EpisodeNumbers.Empty())
if (TrackNumbers != null && TrackNumbers.Any())
{ {
episodeString = string.Format("{0}", AirDate); episodeString = string.Format("T{1}", string.Join("-", TrackNumbers.Select(c => c.ToString("00"))));
}
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); return string.Format("{0} - {1} {2}", ArtistTitle, episodeString, Quality);
} }

View file

@ -25,7 +25,7 @@ namespace NzbDrone.Core.Parser
// Music stuff here // Music stuff here
LocalTrack GetLocalTrack(string filename, Artist artist); LocalTrack GetLocalTrack(string filename, Artist artist);
LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource); LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo);
} }
@ -40,12 +40,14 @@ namespace NzbDrone.Core.Parser
public ParsingService(IEpisodeService episodeService, public ParsingService(IEpisodeService episodeService,
ISeriesService seriesService, ISeriesService seriesService,
ITrackService trackService,
// ISceneMappingService sceneMappingService, // ISceneMappingService sceneMappingService,
Logger logger) Logger logger)
{ {
_episodeService = episodeService; _episodeService = episodeService;
_seriesService = seriesService; _seriesService = seriesService;
// _sceneMappingService = sceneMappingService; // _sceneMappingService = sceneMappingService;
_trackService = trackService;
_logger = logger; _logger = logger;
} }
@ -407,6 +409,73 @@ namespace NzbDrone.Core.Parser
return result; return result;
} }
private List<Track> GetStandardTracks(Artist artist, ParsedTrackInfo parsedTrackInfo, SearchCriteriaBase searchCriteria)
{
var result = new List<Track>();
if (parsedTrackInfo.TrackNumbers == null)
{
return result;
}
foreach (var trackNumber in parsedTrackInfo.TrackNumbers)
{
//if (series.UseSceneNumbering && sceneSource)
//{
// List<Episode> episodes = new List<Episode>();
// if (searchCriteria != null)
// {
// episodes = searchCriteria.Episodes.Where(e => e.SceneSeasonNumber == parsedTrackInfo.SeasonNumber &&
// e.SceneEpisodeNumber == trackNumber).ToList();
// }
// if (!episodes.Any())
// {
// episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, seasonNumber, trackNumber);
// }
// if (episodes != null && episodes.Any())
// {
// _logger.Debug("Using Scene to TVDB Mapping for: {0} - Scene: {1}x{2:00} - TVDB: {3}",
// series.Title,
// episodes.First().SceneSeasonNumber,
// episodes.First().SceneEpisodeNumber,
// string.Join(", ", episodes.Select(e => string.Format("{0}x{1:00}", e.SeasonNumber, e.EpisodeNumber))));
// result.AddRange(episodes);
// continue;
// }
//}
Track trackInfo = null;
if (searchCriteria != null)
{
trackInfo = searchCriteria.Tracks.SingleOrDefault(e => e.TrackNumber == trackNumber); //e => e.SeasonNumber == seasonNumber && e.TrackNumber == trackNumber
}
if (trackInfo == null)
{
trackInfo = _trackService.FindTrack(artist.SpotifyId, trackNumber);
}
if (trackInfo != null)
{
result.Add(trackInfo);
}
else
{
_logger.Debug("Unable to find {0}", parsedEpisodeInfo);
}
}
return result;
}
private List<Episode> GetStandardEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource, SearchCriteriaBase searchCriteria) private List<Episode> GetStandardEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource, SearchCriteriaBase searchCriteria)
{ {
var result = new List<Episode>(); var result = new List<Episode>();
@ -486,10 +555,10 @@ namespace NzbDrone.Core.Parser
public LocalTrack GetLocalTrack(string filename, Artist artist) public LocalTrack GetLocalTrack(string filename, Artist artist)
{ {
return GetLocalTrack(filename, artist, null, false); return GetLocalTrack(filename, artist, null);
} }
public LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource) public LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo)
{ {
ParsedTrackInfo parsedTrackInfo; ParsedTrackInfo parsedTrackInfo;
@ -504,7 +573,7 @@ namespace NzbDrone.Core.Parser
parsedTrackInfo = Parser.ParseMusicPath(filename); parsedTrackInfo = Parser.ParseMusicPath(filename);
} }
if (parsedTrackInfo == null || parsedTrackInfo.IsPossibleSpecialEpisode) if (parsedTrackInfo == null)
{ {
var title = Path.GetFileNameWithoutExtension(filename); var title = Path.GetFileNameWithoutExtension(filename);
//var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series); //var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series);
@ -525,7 +594,7 @@ namespace NzbDrone.Core.Parser
return null; return null;
} }
var tracks = GetTracks(parsedTrackInfo, artist, sceneSource); var tracks = GetTracks(parsedTrackInfo, artist);
return new LocalTrack return new LocalTrack
{ {
@ -538,10 +607,10 @@ namespace NzbDrone.Core.Parser
}; };
} }
private List<Track> GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist, bool sceneSource) private List<Track> GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist)
{ {
throw new NotImplementedException();
// TODO: Ensure GetTracks(parsedTrackInfo, artist) doesn't need any checks
/*if (parsedTrackInfo.FullSeason) // IF Album /*if (parsedTrackInfo.FullSeason) // IF Album
{ {
return _trackService.GetTracksByAlbumTitle(artist.Id, parsedTrackInfo.AlbumTitle); return _trackService.GetTracksByAlbumTitle(artist.Id, parsedTrackInfo.AlbumTitle);
@ -566,6 +635,7 @@ namespace NzbDrone.Core.Parser
} }
return GetStandardEpisodes(artist, parsedTrackInfo, sceneSource, searchCriteria);*/ return GetStandardEpisodes(artist, parsedTrackInfo, sceneSource, searchCriteria);*/
return GetStandardTracks(artist, parsedTrackInfo, searchCriteria);
} }
} }
} }

View file

@ -10,6 +10,7 @@ var ReleaseLayout = require('./Release/ReleaseLayout');
var SystemLayout = require('./System/SystemLayout'); var SystemLayout = require('./System/SystemLayout');
var SeasonPassLayout = require('./SeasonPass/SeasonPassLayout'); var SeasonPassLayout = require('./SeasonPass/SeasonPassLayout');
var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout'); var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout');
var SeriesDetailsLayout = require('./Series/Details/SeriesDetailsLayout');
module.exports = NzbDroneController.extend({ module.exports = NzbDroneController.extend({
addSeries : function(action) { addSeries : function(action) {
@ -17,6 +18,11 @@ module.exports = NzbDroneController.extend({
this.showMainRegion(new AddSeriesLayout({ action : action })); this.showMainRegion(new AddSeriesLayout({ action : action }));
}, },
artistDetails: function(query) {
this.setTitle('Artist Detail');
this.showMainRegion(new SeriesDetailsLayout());
},
calendar : function() { calendar : function() {
this.setTitle('Calendar'); this.setTitle('Calendar');
this.showMainRegion(new CalendarLayout()); this.showMainRegion(new CalendarLayout());

View file

@ -18,6 +18,7 @@ module.exports = Marionette.AppRouter.extend({
'rss' : 'rss', 'rss' : 'rss',
'system' : 'system', 'system' : 'system',
'system/:action' : 'system', 'system/:action' : 'system',
'artist/:query' : 'artistDetails',
'seasonpass' : 'seasonPass', 'seasonpass' : 'seasonPass',
'serieseditor' : 'seriesEditor', 'serieseditor' : 'seriesEditor',
':whatever' : 'showNotFound' ':whatever' : 'showNotFound'

View file

@ -48,9 +48,10 @@ module.exports = Marionette.Layout.extend({
this.seriesCollection = ArtistCollection.clone(); this.seriesCollection = ArtistCollection.clone();
this.seriesCollection.shadowCollection.bindSignalR(); this.seriesCollection.shadowCollection.bindSignalR();
this.listenTo(this.model, 'change:monitored', this._setMonitoredState); this.listenTo(this.model, 'change:monitored', this._setMonitoredState);
this.listenTo(this.model, 'remove', this._seriesRemoved); this.listenTo(this.model, 'remove', this._seriesRemoved);
this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); //this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete);
this.listenTo(this.model, 'change', function(model, options) { this.listenTo(this.model, 'change', function(model, options) {
if (options && options.changeSource === 'signalr') { if (options && options.changeSource === 'signalr') {
@ -59,6 +60,7 @@ module.exports = Marionette.Layout.extend({
}); });
this.listenTo(this.model, 'change:images', this._updateImages); this.listenTo(this.model, 'change:images', this._updateImages);
}, },
onShow : function() { onShow : function() {
@ -81,15 +83,16 @@ module.exports = Marionette.Layout.extend({
name : 'seriesSearch' name : 'seriesSearch'
} }
}); });
console.log(this.model);
CommandController.bindToCommand({ /*CommandController.bindToCommand({
element : this.ui.rename, element : this.ui.rename,
command : { command : {
name : 'renameFiles', name : 'renameFiles',
seriesId : this.model.id, seriesId : this.model.spotifyId,
seasonNumber : -1 seasonNumber : -1
} }
}); });*/
}, },
onClose : function() { onClose : function() {
@ -164,6 +167,7 @@ module.exports = Marionette.Layout.extend({
_showSeasons : function() { _showSeasons : function() {
var self = this; var self = this;
return;
this.seasons.show(new LoadingView()); this.seasons.show(new LoadingView());

View file

@ -8,7 +8,7 @@
<div class="col-md-10 col-xs-9"> <div class="col-md-10 col-xs-9">
<div class="row"> <div class="row">
<div class="col-md-10 col-xs-10"> <div class="col-md-10 col-xs-10">
<a href="artist/{{artistNameSlug}}" target="_blank"> <a href="artist/{{artistSlug}}" target="_blank">
<h2>{{artistName}}</h2> <h2>{{artistName}}</h2>
</a> </a>
</div> </div>