aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Activity/ActivityManager.cs56
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs1609
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj99
-rw-r--r--Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs834
-rw-r--r--Emby.Server.Implementations/FileOrganization/Extensions.cs33
-rw-r--r--Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs80
-rw-r--r--Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs283
-rw-r--r--Emby.Server.Implementations/FileOrganization/NameUtils.cs81
-rw-r--r--Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs101
-rw-r--r--Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs210
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs148
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs3066
-rw-r--r--Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs102
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs651
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs157
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs45
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs183
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs68
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs173
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs94
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs297
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs56
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs62
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs77
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs541
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs56
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs103
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs42
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs85
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs75
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs62
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs251
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs45
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs275
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs292
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs44
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs84
-rw-r--r--Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs45
-rw-r--r--Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs74
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs42
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs75
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs45
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs75
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs172
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs45
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs74
-rw-r--r--Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs55
-rw-r--r--Emby.Server.Implementations/Logging/PatternsLogger.cs63
-rw-r--r--Emby.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs361
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs96
-rw-r--r--Emby.Server.Implementations/Sorting/AirTimeComparer.cs71
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs160
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs47
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs46
-rw-r--r--Emby.Server.Implementations/Sorting/AlphanumComparator.cs99
-rw-r--r--Emby.Server.Implementations/Sorting/ArtistComparer.cs51
-rw-r--r--Emby.Server.Implementations/Sorting/BudgetComparer.cs39
-rw-r--r--Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs29
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs37
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs33
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs69
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs69
-rw-r--r--Emby.Server.Implementations/Sorting/GameSystemComparer.cs54
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs58
-rw-r--r--Emby.Server.Implementations/Sorting/IsFolderComparer.cs39
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs58
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs58
-rw-r--r--Emby.Server.Implementations/Sorting/MetascoreComparer.cs41
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs33
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs40
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs63
-rw-r--r--Emby.Server.Implementations/Sorting/PlayersComparer.cs46
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs59
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs52
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs33
-rw-r--r--Emby.Server.Implementations/Sorting/RevenueComparer.cs39
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs32
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs37
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs33
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs47
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs30
-rw-r--r--Emby.Server.Implementations/packages.config5
82 files changed, 13347 insertions, 2 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs
new file mode 100644
index 000000000..b6095f082
--- /dev/null
+++ b/Emby.Server.Implementations/Activity/ActivityManager.cs
@@ -0,0 +1,56 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Emby.Server.Implementations.Activity
+{
+ public class ActivityManager : IActivityManager
+ {
+ public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+
+ private readonly IActivityRepository _repo;
+ private readonly ILogger _logger;
+ private readonly IUserManager _userManager;
+
+ public ActivityManager(ILogger logger, IActivityRepository repo, IUserManager userManager)
+ {
+ _logger = logger;
+ _repo = repo;
+ _userManager = userManager;
+ }
+
+ public async Task Create(ActivityLogEntry entry)
+ {
+ entry.Id = Guid.NewGuid().ToString("N");
+ entry.Date = DateTime.UtcNow;
+
+ await _repo.Create(entry).ConfigureAwait(false);
+
+ EventHelper.FireEventIfNotNull(EntryCreated, this, new GenericEventArgs<ActivityLogEntry>(entry), _logger);
+ }
+
+ public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit)
+ {
+ var result = _repo.GetActivityLogEntries(minDate, startIndex, limit);
+
+ foreach (var item in result.Items.Where(i => !string.IsNullOrWhiteSpace(i.UserId)))
+ {
+ var user = _userManager.GetUserById(item.UserId);
+
+ if (user != null)
+ {
+ var dto = _userManager.GetUserDto(user);
+ item.UserPrimaryImageTag = dto.PrimaryImageTag;
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
new file mode 100644
index 000000000..85549439b
--- /dev/null
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -0,0 +1,1609 @@
+using MediaBrowser.Common;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Sync;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Sync;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Extensions;
+
+namespace Emby.Server.Implementations.Dto
+{
+ public class DtoService : IDtoService
+ {
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserDataManager _userDataRepository;
+ private readonly IItemRepository _itemRepo;
+
+ private readonly IImageProcessor _imageProcessor;
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IProviderManager _providerManager;
+
+ private readonly Func<IChannelManager> _channelManagerFactory;
+ private readonly ISyncManager _syncManager;
+ private readonly IApplicationHost _appHost;
+ private readonly Func<IDeviceManager> _deviceManager;
+ private readonly Func<IMediaSourceManager> _mediaSourceManager;
+ private readonly Func<ILiveTvManager> _livetvManager;
+
+ public DtoService(ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager, Func<IChannelManager> channelManagerFactory, ISyncManager syncManager, IApplicationHost appHost, Func<IDeviceManager> deviceManager, Func<IMediaSourceManager> mediaSourceManager, Func<ILiveTvManager> livetvManager)
+ {
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _userDataRepository = userDataRepository;
+ _itemRepo = itemRepo;
+ _imageProcessor = imageProcessor;
+ _config = config;
+ _fileSystem = fileSystem;
+ _providerManager = providerManager;
+ _channelManagerFactory = channelManagerFactory;
+ _syncManager = syncManager;
+ _appHost = appHost;
+ _deviceManager = deviceManager;
+ _mediaSourceManager = mediaSourceManager;
+ _livetvManager = livetvManager;
+ }
+
+ /// <summary>
+ /// Converts a BaseItem to a DTOBaseItem
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="fields">The fields.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="owner">The owner.</param>
+ /// <returns>Task{DtoBaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null)
+ {
+ var options = new DtoOptions
+ {
+ Fields = fields
+ };
+
+ return GetBaseItemDto(item, options, user, owner);
+ }
+
+ public async Task<List<BaseItemDto>> GetBaseItemDtos(IEnumerable<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException("items");
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException("options");
+ }
+
+ var syncDictionary = GetSyncedItemProgress(options);
+
+ var list = new List<BaseItemDto>();
+ var programTuples = new List<Tuple<BaseItem, BaseItemDto>>();
+ var channelTuples = new List<Tuple<BaseItemDto, LiveTvChannel>>();
+
+ foreach (var item in items)
+ {
+ var dto = await GetBaseItemDtoInternal(item, options, user, owner).ConfigureAwait(false);
+
+ var tvChannel = item as LiveTvChannel;
+ if (tvChannel != null)
+ {
+ channelTuples.Add(new Tuple<BaseItemDto, LiveTvChannel>(dto, tvChannel));
+ }
+ else if (item is LiveTvProgram)
+ {
+ programTuples.Add(new Tuple<BaseItem, BaseItemDto>(item, dto));
+ }
+
+ var byName = item as IItemByName;
+
+ if (byName != null)
+ {
+ if (options.Fields.Contains(ItemFields.ItemCounts))
+ {
+ var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user)
+ {
+ Recursive = true
+ });
+
+ SetItemByNameInfo(item, dto, libraryItems.ToList(), user);
+ }
+ }
+
+ FillSyncInfo(dto, item, options, user, syncDictionary);
+
+ list.Add(dto);
+ }
+
+ if (programTuples.Count > 0)
+ {
+ await _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).ConfigureAwait(false);
+ }
+
+ if (channelTuples.Count > 0)
+ {
+ _livetvManager().AddChannelInfo(channelTuples, options, user);
+ }
+
+ return list;
+ }
+
+ public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
+ {
+ var syncDictionary = GetSyncedItemProgress(options);
+
+ var dto = GetBaseItemDtoInternal(item, options, user, owner).Result;
+ var tvChannel = item as LiveTvChannel;
+ if (tvChannel != null)
+ {
+ var list = new List<Tuple<BaseItemDto, LiveTvChannel>> { new Tuple<BaseItemDto, LiveTvChannel>(dto, tvChannel) };
+ _livetvManager().AddChannelInfo(list, options, user);
+ }
+ else if (item is LiveTvProgram)
+ {
+ var list = new List<Tuple<BaseItem, BaseItemDto>> { new Tuple<BaseItem, BaseItemDto>(item, dto) };
+ var task = _livetvManager().AddInfoToProgramDto(list, options.Fields, user);
+ Task.WaitAll(task);
+ }
+
+ var byName = item as IItemByName;
+
+ if (byName != null)
+ {
+ if (options.Fields.Contains(ItemFields.ItemCounts))
+ {
+ SetItemByNameInfo(item, dto, GetTaggedItems(byName, user), user);
+ }
+
+ FillSyncInfo(dto, item, options, user, syncDictionary);
+ return dto;
+ }
+
+ FillSyncInfo(dto, item, options, user, syncDictionary);
+
+ return dto;
+ }
+
+ private List<BaseItem> GetTaggedItems(IItemByName byName, User user)
+ {
+ var items = byName.GetTaggedItems(new InternalItemsQuery(user)
+ {
+ Recursive = true
+
+ }).ToList();
+
+ return items;
+ }
+
+ public Dictionary<string, SyncedItemProgress> GetSyncedItemProgress(DtoOptions options)
+ {
+ if (!options.Fields.Contains(ItemFields.BasicSyncInfo) &&
+ !options.Fields.Contains(ItemFields.SyncInfo))
+ {
+ return new Dictionary<string, SyncedItemProgress>();
+ }
+
+ var deviceId = options.DeviceId;
+ if (string.IsNullOrWhiteSpace(deviceId))
+ {
+ return new Dictionary<string, SyncedItemProgress>();
+ }
+
+ var caps = _deviceManager().GetCapabilities(deviceId);
+ if (caps == null || !caps.SupportsSync)
+ {
+ return new Dictionary<string, SyncedItemProgress>();
+ }
+
+ return _syncManager.GetSyncedItemProgresses(new SyncJobItemQuery
+ {
+ TargetId = deviceId,
+ Statuses = new[]
+ {
+ SyncJobItemStatus.Converting,
+ SyncJobItemStatus.Queued,
+ SyncJobItemStatus.Transferring,
+ SyncJobItemStatus.ReadyToTransfer,
+ SyncJobItemStatus.Synced
+ }
+ });
+ }
+
+ public void FillSyncInfo(IEnumerable<Tuple<BaseItem, BaseItemDto>> tuples, DtoOptions options, User user)
+ {
+ if (options.Fields.Contains(ItemFields.BasicSyncInfo) ||
+ options.Fields.Contains(ItemFields.SyncInfo))
+ {
+ var syncProgress = GetSyncedItemProgress(options);
+
+ foreach (var tuple in tuples)
+ {
+ var item = tuple.Item1;
+
+ FillSyncInfo(tuple.Item2, item, options, user, syncProgress);
+ }
+ }
+ }
+
+ private void FillSyncInfo(IHasSyncInfo dto, BaseItem item, DtoOptions options, User user, Dictionary<string, SyncedItemProgress> syncProgress)
+ {
+ var hasFullSyncInfo = options.Fields.Contains(ItemFields.SyncInfo);
+
+ if (!options.Fields.Contains(ItemFields.BasicSyncInfo) &&
+ !hasFullSyncInfo)
+ {
+ return;
+ }
+
+ if (dto.SupportsSync ?? false)
+ {
+ SyncedItemProgress syncStatus;
+ if (syncProgress.TryGetValue(dto.Id, out syncStatus))
+ {
+ if (syncStatus.Status == SyncJobItemStatus.Synced)
+ {
+ dto.SyncPercent = 100;
+ }
+ else
+ {
+ dto.SyncPercent = syncStatus.Progress;
+ }
+
+ if (hasFullSyncInfo)
+ {
+ dto.HasSyncJob = true;
+ dto.SyncStatus = syncStatus.Status;
+ }
+ }
+ }
+ }
+
+ private async Task<BaseItemDto> GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
+ {
+ var fields = options.Fields;
+
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (fields == null)
+ {
+ throw new ArgumentNullException("fields");
+ }
+
+ var dto = new BaseItemDto
+ {
+ ServerId = _appHost.SystemId
+ };
+
+ if (item.SourceType == SourceType.Channel)
+ {
+ dto.SourceType = item.SourceType.ToString();
+ }
+
+ if (fields.Contains(ItemFields.People))
+ {
+ AttachPeople(dto, item);
+ }
+
+ if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
+ {
+ try
+ {
+ AttachPrimaryImageAspectRatio(dto, item);
+ }
+ catch (Exception ex)
+ {
+ // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
+ _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
+ }
+ }
+
+ if (fields.Contains(ItemFields.DisplayPreferencesId))
+ {
+ dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N");
+ }
+
+ if (user != null)
+ {
+ await AttachUserSpecificInfo(dto, item, user, options).ConfigureAwait(false);
+ }
+
+ var hasMediaSources = item as IHasMediaSources;
+ if (hasMediaSources != null)
+ {
+ if (fields.Contains(ItemFields.MediaSources))
+ {
+ if (user == null)
+ {
+ dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true).ToList();
+ }
+ else
+ {
+ dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true, user).ToList();
+ }
+ }
+ }
+
+ if (fields.Contains(ItemFields.Studios))
+ {
+ AttachStudios(dto, item);
+ }
+
+ AttachBasicFields(dto, item, owner, options);
+
+ var collectionFolder = item as ICollectionFolder;
+ if (collectionFolder != null)
+ {
+ dto.OriginalCollectionType = collectionFolder.CollectionType;
+
+ dto.CollectionType = user == null ?
+ collectionFolder.CollectionType :
+ collectionFolder.GetViewType(user);
+ }
+
+ if (fields.Contains(ItemFields.CanDelete))
+ {
+ dto.CanDelete = user == null
+ ? item.CanDelete()
+ : item.CanDelete(user);
+ }
+
+ if (fields.Contains(ItemFields.CanDownload))
+ {
+ dto.CanDownload = user == null
+ ? item.CanDownload()
+ : item.CanDownload(user);
+ }
+
+ if (fields.Contains(ItemFields.Etag))
+ {
+ dto.Etag = item.GetEtag(user);
+ }
+
+ if (item is ILiveTvRecording)
+ {
+ _livetvManager().AddInfoToRecordingDto(item, dto, user);
+ }
+
+ return dto;
+ }
+
+ public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, Dictionary<string, SyncedItemProgress> syncProgress, User user = null)
+ {
+ var dto = GetBaseItemDtoInternal(item, options, user).Result;
+
+ if (taggedItems != null && options.Fields.Contains(ItemFields.ItemCounts))
+ {
+ SetItemByNameInfo(item, dto, taggedItems, user);
+ }
+
+ FillSyncInfo(dto, item, options, user, syncProgress);
+
+ return dto;
+ }
+
+ private void SetItemByNameInfo(BaseItem item, BaseItemDto dto, List<BaseItem> taggedItems, User user = null)
+ {
+ if (item is MusicArtist)
+ {
+ dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
+ dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
+ dto.SongCount = taggedItems.Count(i => i is Audio);
+ }
+ else if (item is MusicGenre)
+ {
+ dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
+ dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
+ dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
+ dto.SongCount = taggedItems.Count(i => i is Audio);
+ }
+ else if (item is GameGenre)
+ {
+ dto.GameCount = taggedItems.Count(i => i is Game);
+ }
+ else
+ {
+ // This populates them all and covers Genre, Person, Studio, Year
+
+ dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
+ dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
+ dto.EpisodeCount = taggedItems.Count(i => i is Episode);
+ dto.GameCount = taggedItems.Count(i => i is Game);
+ dto.MovieCount = taggedItems.Count(i => i is Movie);
+ dto.TrailerCount = taggedItems.Count(i => i is Trailer);
+ dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
+ dto.SeriesCount = taggedItems.Count(i => i is Series);
+ dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram);
+ dto.SongCount = taggedItems.Count(i => i is Audio);
+ }
+
+ dto.ChildCount = taggedItems.Count;
+ }
+
+ /// <summary>
+ /// Attaches the user specific info.
+ /// </summary>
+ private async Task AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions dtoOptions)
+ {
+ var fields = dtoOptions.Fields;
+
+ if (item.IsFolder)
+ {
+ var folder = (Folder)item;
+
+ if (dtoOptions.EnableUserData)
+ {
+ dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false);
+ }
+
+ if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
+ {
+ dto.ChildCount = GetChildCount(folder, user);
+ }
+
+ if (fields.Contains(ItemFields.CumulativeRunTimeTicks))
+ {
+ dto.CumulativeRunTimeTicks = item.RunTimeTicks;
+ }
+
+ if (fields.Contains(ItemFields.DateLastMediaAdded))
+ {
+ dto.DateLastMediaAdded = folder.DateLastMediaAdded;
+ }
+ }
+
+ else
+ {
+ if (dtoOptions.EnableUserData)
+ {
+ dto.UserData = _userDataRepository.GetUserDataDto(item, user).Result;
+ }
+ }
+
+ dto.PlayAccess = item.GetPlayAccess(user);
+
+ if (fields.Contains(ItemFields.BasicSyncInfo) || fields.Contains(ItemFields.SyncInfo))
+ {
+ var userCanSync = user != null && user.Policy.EnableSync;
+ if (userCanSync && _syncManager.SupportsSync(item))
+ {
+ dto.SupportsSync = true;
+ }
+ }
+
+ if (fields.Contains(ItemFields.SeasonUserData))
+ {
+ var episode = item as Episode;
+
+ if (episode != null)
+ {
+ var season = episode.Season;
+
+ if (season != null)
+ {
+ dto.SeasonUserData = await _userDataRepository.GetUserDataDto(season, user).ConfigureAwait(false);
+ }
+ }
+ }
+
+ var userView = item as UserView;
+ if (userView != null)
+ {
+ dto.HasDynamicCategories = userView.ContainsDynamicCategories(user);
+ }
+
+ var collectionFolder = item as ICollectionFolder;
+ if (collectionFolder != null)
+ {
+ dto.HasDynamicCategories = false;
+ }
+ }
+
+ private int GetChildCount(Folder folder, User user)
+ {
+ // Right now this is too slow to calculate for top level folders on a per-user basis
+ // Just return something so that apps that are expecting a value won't think the folders are empty
+ if (folder is ICollectionFolder || folder is UserView)
+ {
+ return new Random().Next(1, 10);
+ }
+
+ return folder.GetChildCount(user);
+ }
+
+ /// <summary>
+ /// Gets client-side Id of a server-side BaseItem
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public string GetDtoId(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ return item.Id.ToString("N");
+ }
+
+ /// <summary>
+ /// Converts a UserItemData to a DTOUserItemData
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <returns>DtoUserItemData.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public UserItemDataDto GetUserItemDataDto(UserItemData data)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException("data");
+ }
+
+ return new UserItemDataDto
+ {
+ IsFavorite = data.IsFavorite,
+ Likes = data.Likes,
+ PlaybackPositionTicks = data.PlaybackPositionTicks,
+ PlayCount = data.PlayCount,
+ Rating = data.Rating,
+ Played = data.Played,
+ LastPlayedDate = data.LastPlayedDate,
+ Key = data.Key
+ };
+ }
+ private void SetBookProperties(BaseItemDto dto, Book item)
+ {
+ dto.SeriesName = item.SeriesName;
+ }
+ private void SetPhotoProperties(BaseItemDto dto, Photo item)
+ {
+ dto.Width = item.Width;
+ dto.Height = item.Height;
+ dto.CameraMake = item.CameraMake;
+ dto.CameraModel = item.CameraModel;
+ dto.Software = item.Software;
+ dto.ExposureTime = item.ExposureTime;
+ dto.FocalLength = item.FocalLength;
+ dto.ImageOrientation = item.Orientation;
+ dto.Aperture = item.Aperture;
+ dto.ShutterSpeed = item.ShutterSpeed;
+
+ dto.Latitude = item.Latitude;
+ dto.Longitude = item.Longitude;
+ dto.Altitude = item.Altitude;
+ dto.IsoSpeedRating = item.IsoSpeedRating;
+
+ var album = item.AlbumEntity;
+
+ if (album != null)
+ {
+ dto.Album = album.Name;
+ dto.AlbumId = album.Id.ToString("N");
+ }
+ }
+
+ private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
+ {
+ if (!string.IsNullOrEmpty(item.Album))
+ {
+ var parentAlbum = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ Name = item.Album
+
+ }).FirstOrDefault();
+
+ if (parentAlbum != null)
+ {
+ dto.AlbumId = GetDtoId(parentAlbum);
+ }
+ }
+
+ dto.Album = item.Album;
+ }
+
+ private void SetGameProperties(BaseItemDto dto, Game item)
+ {
+ dto.Players = item.PlayersSupported;
+ dto.GameSystem = item.GameSystem;
+ dto.MultiPartGameFiles = item.MultiPartGameFiles;
+ }
+
+ private void SetGameSystemProperties(BaseItemDto dto, GameSystem item)
+ {
+ dto.GameSystem = item.GameSystemName;
+ }
+
+ private List<string> GetImageTags(BaseItem item, List<ItemImageInfo> images)
+ {
+ return images
+ .Select(p => GetImageCacheTag(item, p))
+ .Where(i => i != null)
+ .ToList();
+ }
+
+ private string GetImageCacheTag(BaseItem item, ImageType type)
+ {
+ try
+ {
+ return _imageProcessor.GetImageCacheTag(item, type);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting {0} image info", ex, type);
+ return null;
+ }
+ }
+
+ private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
+ {
+ try
+ {
+ return _imageProcessor.GetImageCacheTag(item, image);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting {0} image info for {1}", ex, image.Type, image.Path);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Attaches People DTO's to a DTOBaseItem
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>Task.</returns>
+ private void AttachPeople(BaseItemDto dto, BaseItem item)
+ {
+ // Ordering by person type to ensure actors and artists are at the front.
+ // This is taking advantage of the fact that they both begin with A
+ // This should be improved in the future
+ var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
+ .ThenBy(i =>
+ {
+ if (i.IsType(PersonType.Actor))
+ {
+ return 0;
+ }
+ if (i.IsType(PersonType.GuestStar))
+ {
+ return 1;
+ }
+ if (i.IsType(PersonType.Director))
+ {
+ return 2;
+ }
+ if (i.IsType(PersonType.Writer))
+ {
+ return 3;
+ }
+ if (i.IsType(PersonType.Producer))
+ {
+ return 4;
+ }
+ if (i.IsType(PersonType.Composer))
+ {
+ return 4;
+ }
+
+ return 10;
+ })
+ .ToList();
+
+ var list = new List<BaseItemPerson>();
+
+ var dictionary = people.Select(p => p.Name)
+ .Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
+ {
+ try
+ {
+ return _libraryManager.GetPerson(c);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting person {0}", ex, c);
+ return null;
+ }
+
+ }).Where(i => i != null)
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
+
+ for (var i = 0; i < people.Count; i++)
+ {
+ var person = people[i];
+
+ var baseItemPerson = new BaseItemPerson
+ {
+ Name = person.Name,
+ Role = person.Role,
+ Type = person.Type
+ };
+
+ Person entity;
+
+ if (dictionary.TryGetValue(person.Name, out entity))
+ {
+ baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
+ baseItemPerson.Id = entity.Id.ToString("N");
+ list.Add(baseItemPerson);
+ }
+ }
+
+ dto.People = list.ToArray();
+ }
+
+ /// <summary>
+ /// Attaches the studios.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>Task.</returns>
+ private void AttachStudios(BaseItemDto dto, BaseItem item)
+ {
+ var studios = item.Studios.ToList();
+
+ dto.Studios = new StudioDto[studios.Count];
+
+ var dictionary = studios.Distinct(StringComparer.OrdinalIgnoreCase).Select(name =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(name);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error getting studio {0}", ex, name);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
+
+ for (var i = 0; i < studios.Count; i++)
+ {
+ var studio = studios[i];
+
+ var studioDto = new StudioDto
+ {
+ Name = studio
+ };
+
+ Studio entity;
+
+ if (dictionary.TryGetValue(studio, out entity))
+ {
+ studioDto.Id = entity.Id.ToString("N");
+ studioDto.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
+ }
+
+ dto.Studios[i] = studioDto;
+ }
+ }
+
+ /// <summary>
+ /// Gets the chapter info dto.
+ /// </summary>
+ /// <param name="chapterInfo">The chapter info.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>ChapterInfoDto.</returns>
+ private ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item)
+ {
+ var dto = new ChapterInfoDto
+ {
+ Name = chapterInfo.Name,
+ StartPositionTicks = chapterInfo.StartPositionTicks
+ };
+
+ if (!string.IsNullOrEmpty(chapterInfo.ImagePath))
+ {
+ dto.ImageTag = GetImageCacheTag(item, new ItemImageInfo
+ {
+ Path = chapterInfo.ImagePath,
+ Type = ImageType.Chapter,
+ DateModified = chapterInfo.ImageDateModified
+ });
+ }
+
+ return dto;
+ }
+
+ public List<ChapterInfoDto> GetChapterInfoDtos(BaseItem item)
+ {
+ return _itemRepo.GetChapters(item.Id)
+ .Select(c => GetChapterInfoDto(c, item))
+ .ToList();
+ }
+
+ /// <summary>
+ /// Sets simple property values on a DTOBaseItem
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="owner">The owner.</param>
+ /// <param name="options">The options.</param>
+ private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options)
+ {
+ var fields = options.Fields;
+
+ if (fields.Contains(ItemFields.DateCreated))
+ {
+ dto.DateCreated = item.DateCreated;
+ }
+
+ if (fields.Contains(ItemFields.DisplayMediaType))
+ {
+ dto.DisplayMediaType = item.DisplayMediaType;
+ }
+
+ if (fields.Contains(ItemFields.Settings))
+ {
+ dto.LockedFields = item.LockedFields;
+ dto.LockData = item.IsLocked;
+ dto.ForcedSortName = item.ForcedSortName;
+ }
+ dto.Container = item.Container;
+
+ var hasBudget = item as IHasBudget;
+ if (hasBudget != null)
+ {
+ if (fields.Contains(ItemFields.Budget))
+ {
+ dto.Budget = hasBudget.Budget;
+ }
+
+ if (fields.Contains(ItemFields.Revenue))
+ {
+ dto.Revenue = hasBudget.Revenue;
+ }
+ }
+
+ dto.EndDate = item.EndDate;
+
+ if (fields.Contains(ItemFields.HomePageUrl))
+ {
+ dto.HomePageUrl = item.HomePageUrl;
+ }
+
+ if (fields.Contains(ItemFields.ExternalUrls))
+ {
+ dto.ExternalUrls = _providerManager.GetExternalUrls(item).ToArray();
+ }
+
+ if (fields.Contains(ItemFields.Tags))
+ {
+ dto.Tags = item.Tags;
+ }
+
+ if (fields.Contains(ItemFields.Keywords))
+ {
+ dto.Keywords = item.Keywords;
+ }
+
+ var hasAspectRatio = item as IHasAspectRatio;
+ if (hasAspectRatio != null)
+ {
+ dto.AspectRatio = hasAspectRatio.AspectRatio;
+ }
+
+ if (fields.Contains(ItemFields.Metascore))
+ {
+ var hasMetascore = item as IHasMetascore;
+ if (hasMetascore != null)
+ {
+ dto.Metascore = hasMetascore.Metascore;
+ }
+ }
+
+ if (fields.Contains(ItemFields.AwardSummary))
+ {
+ var hasAwards = item as IHasAwards;
+ if (hasAwards != null)
+ {
+ dto.AwardSummary = hasAwards.AwardSummary;
+ }
+ }
+
+ var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
+ if (backdropLimit > 0)
+ {
+ dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList());
+ }
+
+ if (fields.Contains(ItemFields.ScreenshotImageTags))
+ {
+ var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
+ if (screenshotLimit > 0)
+ {
+ dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList());
+ }
+ }
+
+ if (fields.Contains(ItemFields.Genres))
+ {
+ dto.Genres = item.Genres;
+ }
+
+ if (options.EnableImages)
+ {
+ dto.ImageTags = new Dictionary<ImageType, string>();
+
+ // Prevent implicitly captured closure
+ var currentItem = item;
+ foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))
+ .ToList())
+ {
+ if (options.GetImageLimit(image.Type) > 0)
+ {
+ var tag = GetImageCacheTag(item, image);
+
+ if (tag != null)
+ {
+ dto.ImageTags[image.Type] = tag;
+ }
+ }
+ }
+ }
+
+ dto.Id = GetDtoId(item);
+ dto.IndexNumber = item.IndexNumber;
+ dto.ParentIndexNumber = item.ParentIndexNumber;
+
+ if (item.IsFolder)
+ {
+ dto.IsFolder = true;
+ }
+ else if (item is IHasMediaSources)
+ {
+ dto.IsFolder = false;
+ }
+
+ dto.MediaType = item.MediaType;
+ dto.LocationType = item.LocationType;
+ if (item.IsHD.HasValue && item.IsHD.Value)
+ {
+ dto.IsHD = item.IsHD;
+ }
+ dto.Audio = item.Audio;
+
+ if (fields.Contains(ItemFields.Settings))
+ {
+ dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
+ dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
+ }
+
+ dto.CriticRating = item.CriticRating;
+
+ if (fields.Contains(ItemFields.CriticRatingSummary))
+ {
+ dto.CriticRatingSummary = item.CriticRatingSummary;
+ }
+
+ var hasTrailers = item as IHasTrailers;
+ if (hasTrailers != null)
+ {
+ dto.LocalTrailerCount = hasTrailers.GetTrailerIds().Count;
+ }
+
+ var hasDisplayOrder = item as IHasDisplayOrder;
+ if (hasDisplayOrder != null)
+ {
+ dto.DisplayOrder = hasDisplayOrder.DisplayOrder;
+ }
+
+ var userView = item as UserView;
+ if (userView != null)
+ {
+ dto.CollectionType = userView.ViewType;
+ }
+
+ if (fields.Contains(ItemFields.RemoteTrailers))
+ {
+ dto.RemoteTrailers = hasTrailers != null ?
+ hasTrailers.RemoteTrailers :
+ new List<MediaUrl>();
+ }
+
+ dto.Name = item.Name;
+ dto.OfficialRating = item.OfficialRating;
+
+ if (fields.Contains(ItemFields.Overview))
+ {
+ dto.Overview = item.Overview;
+ }
+
+ if (fields.Contains(ItemFields.OriginalTitle))
+ {
+ dto.OriginalTitle = item.OriginalTitle;
+ }
+
+ if (fields.Contains(ItemFields.ShortOverview))
+ {
+ dto.ShortOverview = item.ShortOverview;
+ }
+
+ if (fields.Contains(ItemFields.ParentId))
+ {
+ var displayParentId = item.DisplayParentId;
+ if (displayParentId.HasValue)
+ {
+ dto.ParentId = displayParentId.Value.ToString("N");
+ }
+ }
+
+ AddInheritedImages(dto, item, options, owner);
+
+ if (fields.Contains(ItemFields.Path))
+ {
+ dto.Path = GetMappedPath(item);
+ }
+
+ dto.PremiereDate = item.PremiereDate;
+ dto.ProductionYear = item.ProductionYear;
+
+ if (fields.Contains(ItemFields.ProviderIds))
+ {
+ dto.ProviderIds = item.ProviderIds;
+ }
+
+ dto.RunTimeTicks = item.RunTimeTicks;
+
+ if (fields.Contains(ItemFields.SortName))
+ {
+ dto.SortName = item.SortName;
+ }
+
+ if (fields.Contains(ItemFields.CustomRating))
+ {
+ dto.CustomRating = item.CustomRating;
+ }
+
+ if (fields.Contains(ItemFields.Taglines))
+ {
+ if (!string.IsNullOrWhiteSpace(item.Tagline))
+ {
+ dto.Taglines = new List<string> { item.Tagline };
+ }
+
+ if (dto.Taglines == null)
+ {
+ dto.Taglines = new List<string>();
+ }
+ }
+
+ dto.Type = item.GetClientTypeName();
+ dto.CommunityRating = item.CommunityRating;
+
+ if (fields.Contains(ItemFields.VoteCount))
+ {
+ dto.VoteCount = item.VoteCount;
+ }
+
+ //if (item.IsFolder)
+ //{
+ // var folder = (Folder)item;
+
+ // if (fields.Contains(ItemFields.IndexOptions))
+ // {
+ // dto.IndexOptions = folder.IndexByOptionStrings.ToArray();
+ // }
+ //}
+
+ var supportsPlaceHolders = item as ISupportsPlaceHolders;
+ if (supportsPlaceHolders != null)
+ {
+ dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
+ }
+
+ // Add audio info
+ var audio = item as Audio;
+ if (audio != null)
+ {
+ dto.Album = audio.Album;
+ dto.ExtraType = audio.ExtraType;
+
+ var albumParent = audio.AlbumEntity;
+
+ if (albumParent != null)
+ {
+ dto.AlbumId = GetDtoId(albumParent);
+
+ dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary);
+ }
+
+ //if (fields.Contains(ItemFields.MediaSourceCount))
+ //{
+ // Songs always have one
+ //}
+ }
+
+ var hasArtist = item as IHasArtist;
+ if (hasArtist != null)
+ {
+ dto.Artists = hasArtist.Artists;
+
+ var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
+ {
+ EnableTotalRecordCount = false,
+ ItemIds = new[] { item.Id.ToString("N") }
+ });
+
+ dto.ArtistItems = artistItems.Items
+ .Select(i =>
+ {
+ var artist = i.Item1;
+ return new NameIdPair
+ {
+ Name = artist.Name,
+ Id = artist.Id.ToString("N")
+ };
+ })
+ .ToList();
+
+ // Include artists that are not in the database yet, e.g., just added via metadata editor
+ var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
+ dto.ArtistItems.AddRange(hasArtist.Artists
+ .Except(foundArtists, new DistinctNameComparer())
+ .Select(i =>
+ {
+ // This should not be necessary but we're seeing some cases of it
+ if (string.IsNullOrWhiteSpace(i))
+ {
+ return null;
+ }
+
+ var artist = _libraryManager.GetArtist(i);
+ if (artist != null)
+ {
+ return new NameIdPair
+ {
+ Name = artist.Name,
+ Id = artist.Id.ToString("N")
+ };
+ }
+
+ return null;
+
+ }).Where(i => i != null));
+ }
+
+ var hasAlbumArtist = item as IHasAlbumArtist;
+ if (hasAlbumArtist != null)
+ {
+ dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+
+ var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
+ {
+ EnableTotalRecordCount = false,
+ ItemIds = new[] { item.Id.ToString("N") }
+ });
+
+ dto.AlbumArtists = artistItems.Items
+ .Select(i =>
+ {
+ var artist = i.Item1;
+ return new NameIdPair
+ {
+ Name = artist.Name,
+ Id = artist.Id.ToString("N")
+ };
+ })
+ .ToList();
+ }
+
+ // Add video info
+ var video = item as Video;
+ if (video != null)
+ {
+ dto.VideoType = video.VideoType;
+ dto.Video3DFormat = video.Video3DFormat;
+ dto.IsoType = video.IsoType;
+
+ if (video.HasSubtitles)
+ {
+ dto.HasSubtitles = video.HasSubtitles;
+ }
+
+ if (video.AdditionalParts.Count != 0)
+ {
+ dto.PartCount = video.AdditionalParts.Count + 1;
+ }
+
+ if (fields.Contains(ItemFields.MediaSourceCount))
+ {
+ var mediaSourceCount = video.MediaSourceCount;
+ if (mediaSourceCount != 1)
+ {
+ dto.MediaSourceCount = mediaSourceCount;
+ }
+ }
+
+ if (fields.Contains(ItemFields.Chapters))
+ {
+ dto.Chapters = GetChapterInfoDtos(item);
+ }
+
+ dto.ExtraType = video.ExtraType;
+ }
+
+ if (fields.Contains(ItemFields.MediaStreams))
+ {
+ // Add VideoInfo
+ var iHasMediaSources = item as IHasMediaSources;
+
+ if (iHasMediaSources != null)
+ {
+ List<MediaStream> mediaStreams;
+
+ if (dto.MediaSources != null && dto.MediaSources.Count > 0)
+ {
+ mediaStreams = dto.MediaSources.Where(i => new Guid(i.Id) == item.Id)
+ .SelectMany(i => i.MediaStreams)
+ .ToList();
+ }
+ else
+ {
+ mediaStreams = _mediaSourceManager().GetStaticMediaSources(iHasMediaSources, true).First().MediaStreams;
+ }
+
+ dto.MediaStreams = mediaStreams;
+ }
+ }
+
+ var hasSpecialFeatures = item as IHasSpecialFeatures;
+ if (hasSpecialFeatures != null)
+ {
+ var specialFeatureCount = hasSpecialFeatures.SpecialFeatureIds.Count;
+
+ if (specialFeatureCount > 0)
+ {
+ dto.SpecialFeatureCount = specialFeatureCount;
+ }
+ }
+
+ // Add EpisodeInfo
+ var episode = item as Episode;
+ if (episode != null)
+ {
+ dto.IndexNumberEnd = episode.IndexNumberEnd;
+ dto.SeriesName = episode.SeriesName;
+
+ if (fields.Contains(ItemFields.AlternateEpisodeNumbers))
+ {
+ dto.DvdSeasonNumber = episode.DvdSeasonNumber;
+ dto.DvdEpisodeNumber = episode.DvdEpisodeNumber;
+ dto.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber;
+ }
+
+ if (fields.Contains(ItemFields.SpecialEpisodeNumbers))
+ {
+ dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber;
+ dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber;
+ dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber;
+ }
+
+ var seasonId = episode.SeasonId;
+ if (seasonId.HasValue)
+ {
+ dto.SeasonId = seasonId.Value.ToString("N");
+ }
+
+ dto.SeasonName = episode.SeasonName;
+
+ var seriesId = episode.SeriesId;
+ if (seriesId.HasValue)
+ {
+ dto.SeriesId = seriesId.Value.ToString("N");
+ }
+
+ Series episodeSeries = null;
+
+ if (fields.Contains(ItemFields.SeriesGenres))
+ {
+ episodeSeries = episodeSeries ?? episode.Series;
+ if (episodeSeries != null)
+ {
+ dto.SeriesGenres = episodeSeries.Genres.ToList();
+ }
+ }
+
+ //if (fields.Contains(ItemFields.SeriesPrimaryImage))
+ {
+ episodeSeries = episodeSeries ?? episode.Series;
+ if (episodeSeries != null)
+ {
+ dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary);
+ }
+ }
+
+ if (fields.Contains(ItemFields.SeriesStudio))
+ {
+ episodeSeries = episodeSeries ?? episode.Series;
+ if (episodeSeries != null)
+ {
+ dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
+ }
+ }
+ }
+
+ // Add SeriesInfo
+ var series = item as Series;
+ if (series != null)
+ {
+ dto.AirDays = series.AirDays;
+ dto.AirTime = series.AirTime;
+ dto.SeriesStatus = series.Status;
+
+ dto.AnimeSeriesIndex = series.AnimeSeriesIndex;
+ }
+
+ // Add SeasonInfo
+ var season = item as Season;
+ if (season != null)
+ {
+ dto.SeriesName = season.SeriesName;
+
+ var seriesId = season.SeriesId;
+ if (seriesId.HasValue)
+ {
+ dto.SeriesId = seriesId.Value.ToString("N");
+ }
+
+ series = null;
+
+ if (fields.Contains(ItemFields.SeriesStudio))
+ {
+ series = series ?? season.Series;
+ if (series != null)
+ {
+ dto.SeriesStudio = series.Studios.FirstOrDefault();
+ }
+ }
+
+ if (fields.Contains(ItemFields.SeriesPrimaryImage))
+ {
+ series = series ?? season.Series;
+ if (series != null)
+ {
+ dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary);
+ }
+ }
+ }
+
+ var game = item as Game;
+
+ if (game != null)
+ {
+ SetGameProperties(dto, game);
+ }
+
+ var gameSystem = item as GameSystem;
+
+ if (gameSystem != null)
+ {
+ SetGameSystemProperties(dto, gameSystem);
+ }
+
+ var musicVideo = item as MusicVideo;
+ if (musicVideo != null)
+ {
+ SetMusicVideoProperties(dto, musicVideo);
+ }
+
+ var book = item as Book;
+ if (book != null)
+ {
+ SetBookProperties(dto, book);
+ }
+
+ if (item.ProductionLocations.Count > 0 || item is Movie)
+ {
+ dto.ProductionLocations = item.ProductionLocations.ToArray();
+ }
+
+ var photo = item as Photo;
+ if (photo != null)
+ {
+ SetPhotoProperties(dto, photo);
+ }
+
+ dto.ChannelId = item.ChannelId;
+
+ if (item.SourceType == SourceType.Channel && !string.IsNullOrWhiteSpace(item.ChannelId))
+ {
+ var channel = _libraryManager.GetItemById(item.ChannelId);
+ if (channel != null)
+ {
+ dto.ChannelName = channel.Name;
+ }
+ }
+ }
+
+ private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner)
+ {
+ var logoLimit = options.GetImageLimit(ImageType.Logo);
+ var artLimit = options.GetImageLimit(ImageType.Art);
+ var thumbLimit = options.GetImageLimit(ImageType.Thumb);
+ var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
+
+ if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0)
+ {
+ return;
+ }
+
+ BaseItem parent = null;
+ var isFirst = true;
+
+ while (((!dto.HasLogo && logoLimit > 0) || (!dto.HasArtImage && artLimit > 0) || (!dto.HasThumb && thumbLimit > 0) || parent is Series) &&
+ (parent = parent ?? (isFirst ? item.GetParent() ?? owner : parent)) != null)
+ {
+ if (parent == null)
+ {
+ break;
+ }
+
+ var allImages = parent.ImageInfos;
+
+ if (logoLimit > 0 && !dto.HasLogo && dto.ParentLogoItemId == null)
+ {
+ var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
+
+ if (image != null)
+ {
+ dto.ParentLogoItemId = GetDtoId(parent);
+ dto.ParentLogoImageTag = GetImageCacheTag(parent, image);
+ }
+ }
+ if (artLimit > 0 && !dto.HasArtImage && dto.ParentArtItemId == null)
+ {
+ var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
+
+ if (image != null)
+ {
+ dto.ParentArtItemId = GetDtoId(parent);
+ dto.ParentArtImageTag = GetImageCacheTag(parent, image);
+ }
+ }
+ if (thumbLimit > 0 && !dto.HasThumb && (dto.ParentThumbItemId == null || parent is Series))
+ {
+ var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
+
+ if (image != null)
+ {
+ dto.ParentThumbItemId = GetDtoId(parent);
+ dto.ParentThumbImageTag = GetImageCacheTag(parent, image);
+ }
+ }
+ if (backdropLimit > 0 && !dto.HasBackdrop)
+ {
+ var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
+
+ if (images.Count > 0)
+ {
+ dto.ParentBackdropItemId = GetDtoId(parent);
+ dto.ParentBackdropImageTags = GetImageTags(parent, images);
+ }
+ }
+
+ isFirst = false;
+ parent = parent.GetParent();
+ }
+ }
+
+ private string GetMappedPath(BaseItem item)
+ {
+ var path = item.Path;
+
+ var locationType = item.LocationType;
+
+ if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
+ {
+ path = _libraryManager.GetPathAfterNetworkSubstitution(path, item);
+ }
+
+ return path;
+ }
+
+ /// <summary>
+ /// Attaches the primary image aspect ratio.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>Task.</returns>
+ public void AttachPrimaryImageAspectRatio(IItemDto dto, IHasImages item)
+ {
+ dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
+ }
+
+ public double? GetPrimaryImageAspectRatio(IHasImages item)
+ {
+ var imageInfo = item.GetImageInfo(ImageType.Primary, 0);
+
+ if (imageInfo == null || !imageInfo.IsLocalFile)
+ {
+ return null;
+ }
+
+ ImageSize size;
+
+ try
+ {
+ size = _imageProcessor.GetImageSize(imageInfo);
+ }
+ catch
+ {
+ //_logger.ErrorException("Failed to determine primary image aspect ratio for {0}", ex, path);
+ return null;
+ }
+
+ var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToList();
+
+ foreach (var enhancer in supportedEnhancers)
+ {
+ try
+ {
+ size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in image enhancer: {0}", ex, enhancer.GetType().Name);
+ }
+ }
+
+ var width = size.Width;
+ var height = size.Height;
+
+ if (width == 0 || height == 0)
+ {
+ return null;
+ }
+
+ var photo = item as Photo;
+ if (photo != null && photo.Orientation.HasValue)
+ {
+ switch (photo.Orientation.Value)
+ {
+ case ImageOrientation.LeftBottom:
+ case ImageOrientation.LeftTop:
+ case ImageOrientation.RightBottom:
+ case ImageOrientation.RightTop:
+ var temp = height;
+ height = width;
+ width = temp;
+ break;
+ }
+ }
+
+ return width / height;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index af093d53f..11b3393c8 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -13,7 +13,7 @@
<DefaultLanguage>en-US</DefaultLanguage>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
- <TargetFrameworkProfile>Profile75</TargetFrameworkProfile>
+ <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -52,6 +52,7 @@
<Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link>
</Compile>
+ <Compile Include="Activity\ActivityManager.cs" />
<Compile Include="Branding\BrandingConfigurationFactory.cs" />
<Compile Include="Channels\ChannelConfigurations.cs" />
<Compile Include="Channels\ChannelDynamicMediaSourceProvider.cs" />
@@ -60,18 +61,112 @@
<Compile Include="Channels\ChannelPostScanTask.cs" />
<Compile Include="Channels\RefreshChannelsScheduledTask.cs" />
<Compile Include="Collections\CollectionManager.cs" />
+ <Compile Include="Dto\DtoService.cs" />
+ <Compile Include="FileOrganization\EpisodeFileOrganizer.cs" />
+ <Compile Include="FileOrganization\Extensions.cs" />
+ <Compile Include="FileOrganization\FileOrganizationNotifier.cs" />
+ <Compile Include="FileOrganization\FileOrganizationService.cs" />
+ <Compile Include="FileOrganization\NameUtils.cs" />
+ <Compile Include="FileOrganization\OrganizerScheduledTask.cs" />
+ <Compile Include="FileOrganization\TvFolderOrganizer.cs" />
<Compile Include="Intros\DefaultIntroProvider.cs" />
+ <Compile Include="Library\CoreResolutionIgnoreRule.cs" />
+ <Compile Include="Library\LibraryManager.cs" />
+ <Compile Include="Library\LocalTrailerPostScanTask.cs" />
+ <Compile Include="Library\MediaSourceManager.cs" />
+ <Compile Include="Library\MusicManager.cs" />
+ <Compile Include="Library\PathExtensions.cs" />
+ <Compile Include="Library\ResolverHelper.cs" />
+ <Compile Include="Library\Resolvers\Audio\AudioResolver.cs" />
+ <Compile Include="Library\Resolvers\Audio\MusicAlbumResolver.cs" />
+ <Compile Include="Library\Resolvers\Audio\MusicArtistResolver.cs" />
+ <Compile Include="Library\Resolvers\BaseVideoResolver.cs" />
+ <Compile Include="Library\Resolvers\FolderResolver.cs" />
+ <Compile Include="Library\Resolvers\ItemResolver.cs" />
+ <Compile Include="Library\Resolvers\Movies\BoxSetResolver.cs" />
+ <Compile Include="Library\Resolvers\Movies\MovieResolver.cs" />
+ <Compile Include="Library\Resolvers\PhotoAlbumResolver.cs" />
+ <Compile Include="Library\Resolvers\PhotoResolver.cs" />
+ <Compile Include="Library\Resolvers\PlaylistResolver.cs" />
+ <Compile Include="Library\Resolvers\SpecialFolderResolver.cs" />
+ <Compile Include="Library\Resolvers\TV\EpisodeResolver.cs" />
+ <Compile Include="Library\Resolvers\TV\SeasonResolver.cs" />
+ <Compile Include="Library\Resolvers\TV\SeriesResolver.cs" />
+ <Compile Include="Library\Resolvers\VideoResolver.cs" />
+ <Compile Include="Library\SearchEngine.cs" />
+ <Compile Include="Library\UserViewManager.cs" />
+ <Compile Include="Library\Validators\ArtistsPostScanTask.cs" />
+ <Compile Include="Library\Validators\ArtistsValidator.cs" />
+ <Compile Include="Library\Validators\GameGenresPostScanTask.cs" />
+ <Compile Include="Library\Validators\GameGenresValidator.cs" />
+ <Compile Include="Library\Validators\GenresPostScanTask.cs" />
+ <Compile Include="Library\Validators\GenresValidator.cs" />
+ <Compile Include="Library\Validators\MusicGenresPostScanTask.cs" />
+ <Compile Include="Library\Validators\MusicGenresValidator.cs" />
+ <Compile Include="Library\Validators\PeopleValidator.cs" />
+ <Compile Include="Library\Validators\StudiosPostScanTask.cs" />
+ <Compile Include="Library\Validators\StudiosValidator.cs" />
+ <Compile Include="Library\Validators\YearsPostScanTask.cs" />
+ <Compile Include="Logging\PatternsLogger.cs" />
<Compile Include="News\NewsService.cs" />
+ <Compile Include="Persistence\CleanDatabaseScheduledTask.cs" />
<Compile Include="Playlists\PlaylistManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
<Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
<Compile Include="ScheduledTasks\PluginUpdateTask.cs" />
<Compile Include="ScheduledTasks\RefreshIntrosTask.cs" />
+ <Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" />
<Compile Include="ScheduledTasks\SystemUpdateTask.cs" />
+ <Compile Include="Sorting\AiredEpisodeOrderComparer.cs" />
+ <Compile Include="Sorting\AirTimeComparer.cs" />
+ <Compile Include="Sorting\AlbumArtistComparer.cs" />
+ <Compile Include="Sorting\AlbumComparer.cs" />
+ <Compile Include="Sorting\AlphanumComparator.cs" />
+ <Compile Include="Sorting\ArtistComparer.cs" />
+ <Compile Include="Sorting\BudgetComparer.cs" />
+ <Compile Include="Sorting\CommunityRatingComparer.cs" />
+ <Compile Include="Sorting\CriticRatingComparer.cs" />
+ <Compile Include="Sorting\DateCreatedComparer.cs" />
+ <Compile Include="Sorting\DateLastMediaAddedComparer.cs" />
+ <Compile Include="Sorting\DatePlayedComparer.cs" />
+ <Compile Include="Sorting\GameSystemComparer.cs" />
+ <Compile Include="Sorting\IsFavoriteOrLikeComparer.cs" />
+ <Compile Include="Sorting\IsFolderComparer.cs" />
+ <Compile Include="Sorting\IsPlayedComparer.cs" />
+ <Compile Include="Sorting\IsUnplayedComparer.cs" />
+ <Compile Include="Sorting\MetascoreComparer.cs" />
+ <Compile Include="Sorting\NameComparer.cs" />
+ <Compile Include="Sorting\OfficialRatingComparer.cs" />
+ <Compile Include="Sorting\PlayCountComparer.cs" />
+ <Compile Include="Sorting\PlayersComparer.cs" />
+ <Compile Include="Sorting\PremiereDateComparer.cs" />
+ <Compile Include="Sorting\ProductionYearComparer.cs" />
+ <Compile Include="Sorting\RandomComparer.cs" />
+ <Compile Include="Sorting\RevenueComparer.cs" />
+ <Compile Include="Sorting\RuntimeComparer.cs" />
+ <Compile Include="Sorting\SeriesSortNameComparer.cs" />
+ <Compile Include="Sorting\SortNameComparer.cs" />
+ <Compile Include="Sorting\StartDateComparer.cs" />
+ <Compile Include="Sorting\StudioComparer.cs" />
<Compile Include="Updates\InstallationManager.cs" />
</ItemGroup>
- <ItemGroup />
+ <ItemGroup>
+ <Reference Include="MediaBrowser.Naming, Version=1.0.6146.28476, Culture=neutral, processorArchitecture=MSIL">
+ <HintPath>..\packages\MediaBrowser.Naming.1.0.0.57\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
+ <Private>True</Private>
+ </Reference>
+ <Reference Include="Patterns.Logging, Version=1.0.6149.1756, Culture=neutral, processorArchitecture=MSIL">
+ <HintPath>..\packages\Patterns.Logging.1.0.0.4\lib\portable-net45+dnxcore50+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath>
+ <Private>True</Private>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="IO\" />
+ </ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
new file mode 100644
index 000000000..3f4ede478
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
@@ -0,0 +1,834 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Library;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Naming.TV;
+using EpisodeInfo = MediaBrowser.Controller.Providers.EpisodeInfo;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ public class EpisodeFileOrganizer
+ {
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly IFileOrganizationService _organizationService;
+ private readonly IServerConfigurationManager _config;
+ private readonly IProviderManager _providerManager;
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager)
+ {
+ _organizationService = organizationService;
+ _config = config;
+ _fileSystem = fileSystem;
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _libraryMonitor = libraryMonitor;
+ _providerManager = providerManager;
+ }
+
+ public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken)
+ {
+ _logger.Info("Sorting file {0}", path);
+
+ var result = new FileOrganizationResult
+ {
+ Date = DateTime.UtcNow,
+ OriginalPath = path,
+ OriginalFileName = Path.GetFileName(path),
+ Type = FileOrganizerType.Episode,
+ FileSize = _fileSystem.GetFileInfo(path).Length
+ };
+
+ try
+ {
+ if (_libraryMonitor.IsPathLocked(path))
+ {
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = "Path is locked by other processes. Please try again later.";
+ return result;
+ }
+
+ var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
+ var resolver = new EpisodeResolver(namingOptions, new PatternsLogger());
+
+ var episodeInfo = resolver.Resolve(path, false) ??
+ new MediaBrowser.Naming.TV.EpisodeInfo();
+
+ var seriesName = episodeInfo.SeriesName;
+
+ if (!string.IsNullOrEmpty(seriesName))
+ {
+ var seasonNumber = episodeInfo.SeasonNumber;
+
+ result.ExtractedSeasonNumber = seasonNumber;
+
+ // Passing in true will include a few extra regex's
+ var episodeNumber = episodeInfo.EpisodeNumber;
+
+ result.ExtractedEpisodeNumber = episodeNumber;
+
+ var premiereDate = episodeInfo.IsByDate ?
+ new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value) :
+ (DateTime?)null;
+
+ if (episodeInfo.IsByDate || (seasonNumber.HasValue && episodeNumber.HasValue))
+ {
+ if (episodeInfo.IsByDate)
+ {
+ _logger.Debug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value);
+ }
+ else
+ {
+ _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber);
+ }
+
+ var endingEpisodeNumber = episodeInfo.EndingEpsiodeNumber;
+
+ result.ExtractedEndingEpisodeNumber = endingEpisodeNumber;
+
+ await OrganizeEpisode(path,
+ seriesName,
+ seasonNumber,
+ episodeNumber,
+ endingEpisodeNumber,
+ premiereDate,
+ options,
+ overwriteExisting,
+ false,
+ result,
+ cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ var msg = string.Format("Unable to determine episode number from {0}", path);
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = msg;
+ _logger.Warn(msg);
+ }
+ }
+ else
+ {
+ var msg = string.Format("Unable to determine series name from {0}", path);
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = msg;
+ _logger.Warn(msg);
+ }
+
+ var previousResult = _organizationService.GetResultBySourcePath(path);
+
+ if (previousResult != null)
+ {
+ // Don't keep saving the same result over and over if nothing has changed
+ if (previousResult.Status == result.Status && previousResult.StatusMessage == result.StatusMessage && result.Status != FileSortingStatus.Success)
+ {
+ return previousResult;
+ }
+ }
+
+ await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = ex.Message;
+ }
+
+ return result;
+ }
+
+ public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken)
+ {
+ var result = _organizationService.GetResult(request.ResultId);
+
+ try
+ {
+ Series series = null;
+
+ if (request.NewSeriesProviderIds.Count > 0)
+ {
+ // We're having a new series here
+ SeriesInfo seriesRequest = new SeriesInfo();
+ seriesRequest.ProviderIds = request.NewSeriesProviderIds;
+
+ var refreshOptions = new MetadataRefreshOptions(_fileSystem);
+ series = new Series();
+ series.Id = Guid.NewGuid();
+ series.Name = request.NewSeriesName;
+
+ int year;
+ if (int.TryParse(request.NewSeriesYear, out year))
+ {
+ series.ProductionYear = year;
+ }
+
+ var seriesFolderName = series.Name;
+ if (series.ProductionYear.HasValue)
+ {
+ seriesFolderName = string.Format("{0} ({1})", seriesFolderName, series.ProductionYear);
+ }
+
+ series.Path = Path.Combine(request.TargetFolder, seriesFolderName);
+
+ series.ProviderIds = request.NewSeriesProviderIds;
+
+ await series.RefreshMetadata(refreshOptions, cancellationToken);
+ }
+
+ if (series == null)
+ {
+ // Existing Series
+ series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId));
+ }
+
+ await OrganizeEpisode(result.OriginalPath,
+ series,
+ request.SeasonNumber,
+ request.EpisodeNumber,
+ request.EndingEpisodeNumber,
+ null,
+ options,
+ true,
+ request.RememberCorrection,
+ result,
+ cancellationToken).ConfigureAwait(false);
+
+ await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = ex.Message;
+ }
+
+ return result;
+ }
+
+ private Task OrganizeEpisode(string sourcePath,
+ string seriesName,
+ int? seasonNumber,
+ int? episodeNumber,
+ int? endingEpiosdeNumber,
+ DateTime? premiereDate,
+ AutoOrganizeOptions options,
+ bool overwriteExisting,
+ bool rememberCorrection,
+ FileOrganizationResult result,
+ CancellationToken cancellationToken)
+ {
+ var series = GetMatchingSeries(seriesName, result, options);
+
+ if (series == null)
+ {
+ var msg = string.Format("Unable to find series in library matching name {0}", seriesName);
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = msg;
+ _logger.Warn(msg);
+ return Task.FromResult(true);
+ }
+
+ return OrganizeEpisode(sourcePath,
+ series,
+ seasonNumber,
+ episodeNumber,
+ endingEpiosdeNumber,
+ premiereDate,
+ options,
+ overwriteExisting,
+ rememberCorrection,
+ result,
+ cancellationToken);
+ }
+
+ private async Task OrganizeEpisode(string sourcePath,
+ Series series,
+ int? seasonNumber,
+ int? episodeNumber,
+ int? endingEpiosdeNumber,
+ DateTime? premiereDate,
+ AutoOrganizeOptions options,
+ bool overwriteExisting,
+ bool rememberCorrection,
+ FileOrganizationResult result,
+ CancellationToken cancellationToken)
+ {
+ _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path);
+
+ var originalExtractedSeriesString = result.ExtractedName;
+
+ bool isNew = string.IsNullOrWhiteSpace(result.Id);
+
+ if (isNew)
+ {
+ await _organizationService.SaveResult(result, cancellationToken);
+ }
+
+ if (!_organizationService.AddToInProgressList(result, isNew))
+ {
+ throw new Exception("File is currently processed otherwise. Please try again later.");
+ }
+
+ try
+ {
+ // Proceed to sort the file
+ var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(newPath))
+ {
+ var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath);
+ throw new Exception(msg);
+ }
+
+ _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath);
+ result.TargetPath = newPath;
+
+ var fileExists = _fileSystem.FileExists(result.TargetPath);
+ var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber);
+
+ if (!overwriteExisting)
+ {
+ if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath))
+ {
+ var msg = string.Format("File '{0}' already copied to new path '{1}', stopping organization", sourcePath, newPath);
+ _logger.Info(msg);
+ result.Status = FileSortingStatus.SkippedExisting;
+ result.StatusMessage = msg;
+ return;
+ }
+
+ if (fileExists)
+ {
+ var msg = string.Format("File '{0}' already exists as '{1}', stopping organization", sourcePath, newPath);
+ _logger.Info(msg);
+ result.Status = FileSortingStatus.SkippedExisting;
+ result.StatusMessage = msg;
+ result.TargetPath = newPath;
+ return;
+ }
+
+ if (otherDuplicatePaths.Count > 0)
+ {
+ var msg = string.Format("File '{0}' already exists as these:'{1}'. Stopping organization", sourcePath, string.Join("', '", otherDuplicatePaths));
+ _logger.Info(msg);
+ result.Status = FileSortingStatus.SkippedExisting;
+ result.StatusMessage = msg;
+ result.DuplicatePaths = otherDuplicatePaths;
+ return;
+ }
+ }
+
+ PerformFileSorting(options.TvOptions, result);
+
+ if (overwriteExisting)
+ {
+ var hasRenamedFiles = false;
+
+ foreach (var path in otherDuplicatePaths)
+ {
+ _logger.Debug("Removing duplicate episode {0}", path);
+
+ _libraryMonitor.ReportFileSystemChangeBeginning(path);
+
+ var renameRelatedFiles = !hasRenamedFiles &&
+ string.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase);
+
+ if (renameRelatedFiles)
+ {
+ hasRenamedFiles = true;
+ }
+
+ try
+ {
+ DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error removing duplicate episode", ex, path);
+ }
+ finally
+ {
+ _libraryMonitor.ReportFileSystemChangeComplete(path, true);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = ex.Message;
+ _logger.Warn(ex.Message);
+ return;
+ }
+ finally
+ {
+ _organizationService.RemoveFromInprogressList(result);
+ }
+
+ if (rememberCorrection)
+ {
+ SaveSmartMatchString(originalExtractedSeriesString, series, options);
+ }
+ }
+
+ private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options)
+ {
+ if (string.IsNullOrEmpty(matchString) || matchString.Length < 3)
+ {
+ return;
+ }
+
+ SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase));
+
+ if (info == null)
+ {
+ info = new SmartMatchInfo();
+ info.ItemName = series.Name;
+ info.OrganizerType = FileOrganizerType.Episode;
+ info.DisplayName = series.Name;
+ var list = options.SmartMatchInfos.ToList();
+ list.Add(info);
+ options.SmartMatchInfos = list.ToArray();
+ }
+
+ if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase))
+ {
+ var list = info.MatchStrings.ToList();
+ list.Add(matchString);
+ info.MatchStrings = list.ToArray();
+ _config.SaveAutoOrganizeOptions(options);
+ }
+ }
+
+ private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath)
+ {
+ _fileSystem.DeleteFile(path);
+
+ if (!renameRelatedFiles)
+ {
+ return;
+ }
+
+ // Now find other files
+ var originalFilenameWithoutExtension = Path.GetFileNameWithoutExtension(path);
+ var directory = Path.GetDirectoryName(path);
+
+ if (!string.IsNullOrWhiteSpace(originalFilenameWithoutExtension) && !string.IsNullOrWhiteSpace(directory))
+ {
+ // Get all related files, e.g. metadata, images, etc
+ var files = _fileSystem.GetFilePaths(directory)
+ .Where(i => (Path.GetFileNameWithoutExtension(i) ?? string.Empty).StartsWith(originalFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ var targetFilenameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath);
+
+ foreach (var file in files)
+ {
+ directory = Path.GetDirectoryName(file);
+ var filename = Path.GetFileName(file);
+
+ filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension,
+ StringComparison.OrdinalIgnoreCase);
+
+ var destination = Path.Combine(directory, filename);
+
+ _fileSystem.MoveFile(file, destination);
+ }
+ }
+ }
+
+ private List<string> GetOtherDuplicatePaths(string targetPath,
+ Series series,
+ int? seasonNumber,
+ int? episodeNumber,
+ int? endingEpisodeNumber)
+ {
+ // TODO: Support date-naming?
+ if (!seasonNumber.HasValue || !episodeNumber.HasValue)
+ {
+ return new List<string>();
+ }
+
+ var episodePaths = series.GetRecursiveChildren()
+ .OfType<Episode>()
+ .Where(i =>
+ {
+ var locationType = i.LocationType;
+
+ // Must be file system based and match exactly
+ if (locationType != LocationType.Remote &&
+ locationType != LocationType.Virtual &&
+ i.ParentIndexNumber.HasValue &&
+ i.ParentIndexNumber.Value == seasonNumber &&
+ i.IndexNumber.HasValue &&
+ i.IndexNumber.Value == episodeNumber)
+ {
+
+ if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue)
+ {
+ return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue &&
+ endingEpisodeNumber.Value == i.IndexNumberEnd.Value;
+ }
+
+ return true;
+ }
+
+ return false;
+ })
+ .Select(i => i.Path)
+ .ToList();
+
+ var folder = Path.GetDirectoryName(targetPath);
+ var targetFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(targetPath);
+
+ try
+ {
+ var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder)
+ .Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase));
+
+ episodePaths.AddRange(filesOfOtherExtensions);
+ }
+ catch (IOException)
+ {
+ // No big deal. Maybe the season folder doesn't already exist.
+ }
+
+ return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result)
+ {
+ _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath);
+
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath));
+
+ var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath);
+
+ try
+ {
+ if (targetAlreadyExists || options.CopyOriginalFile)
+ {
+ _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true);
+ }
+ else
+ {
+ _fileSystem.MoveFile(result.OriginalPath, result.TargetPath);
+ }
+
+ result.Status = FileSortingStatus.Success;
+ result.StatusMessage = string.Empty;
+ }
+ catch (Exception ex)
+ {
+ var errorMsg = string.Format("Failed to move file from {0} to {1}: {2}", result.OriginalPath, result.TargetPath, ex.Message);
+
+ result.Status = FileSortingStatus.Failure;
+ result.StatusMessage = errorMsg;
+ _logger.ErrorException(errorMsg, ex);
+
+ return;
+ }
+ finally
+ {
+ _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true);
+ }
+
+ if (targetAlreadyExists && !options.CopyOriginalFile)
+ {
+ try
+ {
+ _fileSystem.DeleteFile(result.OriginalPath);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
+ }
+ }
+ }
+
+ private Series GetMatchingSeries(string seriesName, FileOrganizationResult result, AutoOrganizeOptions options)
+ {
+ var parsedName = _libraryManager.ParseName(seriesName);
+
+ var yearInName = parsedName.Year;
+ var nameWithoutYear = parsedName.Name;
+
+ result.ExtractedName = nameWithoutYear;
+ result.ExtractedYear = yearInName;
+
+ var series = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Series).Name },
+ Recursive = true
+ })
+ .Cast<Series>()
+ .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i))
+ .Where(i => i.Item2 > 0)
+ .OrderByDescending(i => i.Item2)
+ .Select(i => i.Item1)
+ .FirstOrDefault();
+
+ if (series == null)
+ {
+ SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(e => e.MatchStrings.Contains(nameWithoutYear, StringComparer.OrdinalIgnoreCase));
+
+ if (info != null)
+ {
+ series = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Series).Name },
+ Recursive = true,
+ Name = info.ItemName
+
+ }).Cast<Series>().FirstOrDefault();
+ }
+ }
+
+ return series;
+ }
+
+ /// <summary>
+ /// Gets the new path.
+ /// </summary>
+ /// <param name="sourcePath">The source path.</param>
+ /// <param name="series">The series.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="episodeNumber">The episode number.</param>
+ /// <param name="endingEpisodeNumber">The ending episode number.</param>
+ /// <param name="premiereDate">The premiere date.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>System.String.</returns>
+ private async Task<string> GetNewPath(string sourcePath,
+ Series series,
+ int? seasonNumber,
+ int? episodeNumber,
+ int? endingEpisodeNumber,
+ DateTime? premiereDate,
+ TvFileOrganizationOptions options,
+ CancellationToken cancellationToken)
+ {
+ var episodeInfo = new EpisodeInfo
+ {
+ IndexNumber = episodeNumber,
+ IndexNumberEnd = endingEpisodeNumber,
+ MetadataCountryCode = series.GetPreferredMetadataCountryCode(),
+ MetadataLanguage = series.GetPreferredMetadataLanguage(),
+ ParentIndexNumber = seasonNumber,
+ SeriesProviderIds = series.ProviderIds,
+ PremiereDate = premiereDate
+ };
+
+ var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo>
+ {
+ SearchInfo = episodeInfo
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ var episode = searchResults.FirstOrDefault();
+
+ if (episode == null)
+ {
+ var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber);
+ _logger.Warn(msg);
+ throw new Exception(msg);
+ }
+
+ var episodeName = episode.Name;
+
+ //if (string.IsNullOrWhiteSpace(episodeName))
+ //{
+ // var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber);
+ // _logger.Warn(msg);
+ // return null;
+ //}
+
+ seasonNumber = seasonNumber ?? episode.ParentIndexNumber;
+ episodeNumber = episodeNumber ?? episode.IndexNumber;
+
+ var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options);
+
+ // MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256
+ // Usually newPath would include the drive component, but use 256 to be sure
+ var maxFilenameLength = 256 - newPath.Length;
+
+ if (!newPath.EndsWith(@"\"))
+ {
+ // Remove 1 for missing backslash combining path and filename
+ maxFilenameLength--;
+ }
+
+ // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt)
+ maxFilenameLength -= 4;
+
+ var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength);
+
+ if (string.IsNullOrEmpty(episodeFileName))
+ {
+ // cause failure
+ return string.Empty;
+ }
+
+ newPath = Path.Combine(newPath, episodeFileName);
+
+ return newPath;
+ }
+
+ /// <summary>
+ /// Gets the season folder path.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>System.String.</returns>
+ private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options)
+ {
+ // If there's already a season folder, use that
+ var season = series
+ .GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber)
+ .FirstOrDefault();
+
+ if (season != null)
+ {
+ return season.Path;
+ }
+
+ var path = series.Path;
+
+ if (series.ContainsEpisodesWithoutSeasonFolders)
+ {
+ return path;
+ }
+
+ if (seasonNumber == 0)
+ {
+ return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName));
+ }
+
+ var seasonFolderName = options.SeasonFolderPattern
+ .Replace("%s", seasonNumber.ToString(_usCulture))
+ .Replace("%0s", seasonNumber.ToString("00", _usCulture))
+ .Replace("%00s", seasonNumber.ToString("000", _usCulture));
+
+ return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName));
+ }
+
+ private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength)
+ {
+ seriesName = _fileSystem.GetValidFilename(seriesName).Trim();
+
+ if (string.IsNullOrWhiteSpace(episodeTitle))
+ {
+ episodeTitle = string.Empty;
+ }
+ else
+ {
+ episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim();
+ }
+
+ var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
+
+ var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern;
+
+ if (string.IsNullOrWhiteSpace(pattern))
+ {
+ throw new Exception("GetEpisodeFileName: Configured episode name pattern is empty!");
+ }
+
+ var result = pattern.Replace("%sn", seriesName)
+ .Replace("%s.n", seriesName.Replace(" ", "."))
+ .Replace("%s_n", seriesName.Replace(" ", "_"))
+ .Replace("%s", seasonNumber.ToString(_usCulture))
+ .Replace("%0s", seasonNumber.ToString("00", _usCulture))
+ .Replace("%00s", seasonNumber.ToString("000", _usCulture))
+ .Replace("%ext", sourceExtension)
+ .Replace("%en", "%#1")
+ .Replace("%e.n", "%#2")
+ .Replace("%e_n", "%#3");
+
+ if (endingEpisodeNumber.HasValue)
+ {
+ result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture))
+ .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture))
+ .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture));
+ }
+
+ result = result.Replace("%e", episodeNumber.ToString(_usCulture))
+ .Replace("%0e", episodeNumber.ToString("00", _usCulture))
+ .Replace("%00e", episodeNumber.ToString("000", _usCulture));
+
+ if (maxLength.HasValue && result.Contains("%#"))
+ {
+ // Substract 3 for the temp token length (%#1, %#2 or %#3)
+ int maxRemainingTitleLength = maxLength.Value - result.Length + 3;
+ string shortenedEpisodeTitle = string.Empty;
+
+ if (maxRemainingTitleLength > 5)
+ {
+ // A title with fewer than 5 letters wouldn't be of much value
+ shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length));
+ }
+
+ result = result.Replace("%#1", shortenedEpisodeTitle)
+ .Replace("%#2", shortenedEpisodeTitle.Replace(" ", "."))
+ .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_"));
+ }
+
+ if (maxLength.HasValue && result.Length > maxLength.Value)
+ {
+ // There may be cases where reducing the title length may still not be sufficient to
+ // stay below maxLength
+ var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength);
+ throw new Exception(msg);
+ }
+
+ return result;
+ }
+
+ private bool IsSameEpisode(string sourcePath, string newPath)
+ {
+ try
+ {
+ var sourceFileInfo = _fileSystem.GetFileInfo(sourcePath);
+ var destinationFileInfo = _fileSystem.GetFileInfo(newPath);
+
+ if (sourceFileInfo.Length == destinationFileInfo.Length)
+ {
+ return true;
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ return false;
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/FileOrganization/Extensions.cs b/Emby.Server.Implementations/FileOrganization/Extensions.cs
new file mode 100644
index 000000000..506bc0327
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/Extensions.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.FileOrganization;
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ public static class ConfigurationExtension
+ {
+ public static AutoOrganizeOptions GetAutoOrganizeOptions(this IConfigurationManager manager)
+ {
+ return manager.GetConfiguration<AutoOrganizeOptions>("autoorganize");
+ }
+ public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options)
+ {
+ manager.SaveConfiguration("autoorganize", options);
+ }
+ }
+
+ public class AutoOrganizeOptionsFactory : IConfigurationFactory
+ {
+ public IEnumerable<ConfigurationStore> GetConfigurations()
+ {
+ return new List<ConfigurationStore>
+ {
+ new ConfigurationStore
+ {
+ Key = "autoorganize",
+ ConfigurationType = typeof (AutoOrganizeOptions)
+ }
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs
new file mode 100644
index 000000000..2a0176547
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs
@@ -0,0 +1,80 @@
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ /// <summary>
+ /// Class SessionInfoWebSocketListener
+ /// </summary>
+ class FileOrganizationNotifier : IServerEntryPoint
+ {
+ private readonly IFileOrganizationService _organizationService;
+ private readonly ISessionManager _sessionManager;
+ private readonly ITaskManager _taskManager;
+
+ public FileOrganizationNotifier(ILogger logger, IFileOrganizationService organizationService, ISessionManager sessionManager, ITaskManager taskManager)
+ {
+ _organizationService = organizationService;
+ _sessionManager = sessionManager;
+ _taskManager = taskManager;
+ }
+
+ public void Run()
+ {
+ _organizationService.ItemAdded += _organizationService_ItemAdded;
+ _organizationService.ItemRemoved += _organizationService_ItemRemoved;
+ _organizationService.ItemUpdated += _organizationService_ItemUpdated;
+ _organizationService.LogReset += _organizationService_LogReset;
+
+ //_taskManager.TaskCompleted += _taskManager_TaskCompleted;
+ }
+
+ private void _organizationService_LogReset(object sender, EventArgs e)
+ {
+ _sessionManager.SendMessageToAdminSessions("AutoOrganize_LogReset", (FileOrganizationResult)null, CancellationToken.None);
+ }
+
+ private void _organizationService_ItemUpdated(object sender, GenericEventArgs<FileOrganizationResult> e)
+ {
+ _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemUpdated", e.Argument, CancellationToken.None);
+ }
+
+ private void _organizationService_ItemRemoved(object sender, GenericEventArgs<FileOrganizationResult> e)
+ {
+ _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemRemoved", e.Argument, CancellationToken.None);
+ }
+
+ private void _organizationService_ItemAdded(object sender, GenericEventArgs<FileOrganizationResult> e)
+ {
+ _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemAdded", e.Argument, CancellationToken.None);
+ }
+
+ //private void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
+ //{
+ // var taskWithKey = e.Task.ScheduledTask as IHasKey;
+ // if (taskWithKey != null && taskWithKey.Key == "AutoOrganize")
+ // {
+ // _sessionManager.SendMessageToAdminSessions("AutoOrganize_TaskCompleted", (FileOrganizationResult)null, CancellationToken.None);
+ // }
+ //}
+
+ public void Dispose()
+ {
+ _organizationService.ItemAdded -= _organizationService_ItemAdded;
+ _organizationService.ItemRemoved -= _organizationService_ItemRemoved;
+ _organizationService.ItemUpdated -= _organizationService_ItemUpdated;
+ _organizationService.LogReset -= _organizationService_LogReset;
+
+ //_taskManager.TaskCompleted -= _taskManager_TaskCompleted;
+ }
+
+
+ }
+}
diff --git a/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs
new file mode 100644
index 000000000..4094e6b9b
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs
@@ -0,0 +1,283 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ public class FileOrganizationService : IFileOrganizationService
+ {
+ private readonly ITaskManager _taskManager;
+ private readonly IFileOrganizationRepository _repo;
+ private readonly ILogger _logger;
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IProviderManager _providerManager;
+ private readonly ConcurrentDictionary<string, bool> _inProgressItemIds = new ConcurrentDictionary<string, bool>();
+
+ public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemAdded;
+ public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemUpdated;
+ public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemRemoved;
+ public event EventHandler LogReset;
+
+ public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager)
+ {
+ _taskManager = taskManager;
+ _repo = repo;
+ _logger = logger;
+ _libraryMonitor = libraryMonitor;
+ _libraryManager = libraryManager;
+ _config = config;
+ _fileSystem = fileSystem;
+ _providerManager = providerManager;
+ }
+
+ public void BeginProcessNewFiles()
+ {
+ _taskManager.CancelIfRunningAndQueue<OrganizerScheduledTask>();
+ }
+
+ public Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken)
+ {
+ if (result == null || string.IsNullOrEmpty(result.OriginalPath))
+ {
+ throw new ArgumentNullException("result");
+ }
+
+ result.Id = result.OriginalPath.GetMD5().ToString("N");
+
+ return _repo.SaveResult(result, cancellationToken);
+ }
+
+ public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query)
+ {
+ var results = _repo.GetResults(query);
+
+ foreach (var result in results.Items)
+ {
+ result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id);
+ }
+
+ return results;
+ }
+
+ public FileOrganizationResult GetResult(string id)
+ {
+ var result = _repo.GetResult(id);
+
+ if (result != null)
+ {
+ result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id);
+ }
+
+ return result;
+ }
+
+ public FileOrganizationResult GetResultBySourcePath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var id = path.GetMD5().ToString("N");
+
+ return GetResult(id);
+ }
+
+ public async Task DeleteOriginalFile(string resultId)
+ {
+ var result = _repo.GetResult(resultId);
+
+ _logger.Info("Requested to delete {0}", result.OriginalPath);
+
+ if (!AddToInProgressList(result, false))
+ {
+ throw new Exception("Path is currently processed otherwise. Please try again later.");
+ }
+
+ try
+ {
+ _fileSystem.DeleteFile(result.OriginalPath);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
+ }
+ finally
+ {
+ RemoveFromInprogressList(result);
+ }
+
+ await _repo.Delete(resultId);
+
+ EventHelper.FireEventIfNotNull(ItemRemoved, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
+ }
+
+ private AutoOrganizeOptions GetAutoOrganizeOptions()
+ {
+ return _config.GetAutoOrganizeOptions();
+ }
+
+ public async Task PerformOrganization(string resultId)
+ {
+ var result = _repo.GetResult(resultId);
+
+ if (string.IsNullOrEmpty(result.TargetPath))
+ {
+ throw new ArgumentException("No target path available.");
+ }
+
+ var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
+ _libraryMonitor, _providerManager);
+
+ var organizeResult = await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ if (organizeResult.Status != FileSortingStatus.Success)
+ {
+ throw new Exception(result.StatusMessage);
+ }
+ }
+
+ public async Task ClearLog()
+ {
+ await _repo.DeleteAll();
+ EventHelper.FireEventIfNotNull(LogReset, this, EventArgs.Empty, _logger);
+ }
+
+ public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request)
+ {
+ var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
+ _libraryMonitor, _providerManager);
+
+ var result = await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false);
+
+ if (result.Status != FileSortingStatus.Success)
+ {
+ throw new Exception(result.StatusMessage);
+ }
+ }
+
+ public QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query)
+ {
+ if (query == null)
+ {
+ throw new ArgumentNullException("query");
+ }
+
+ var options = GetAutoOrganizeOptions();
+
+ var items = options.SmartMatchInfos.Skip(query.StartIndex ?? 0).Take(query.Limit ?? Int32.MaxValue).ToArray();
+
+ return new QueryResult<SmartMatchInfo>()
+ {
+ Items = items,
+ TotalRecordCount = options.SmartMatchInfos.Length
+ };
+ }
+
+ public void DeleteSmartMatchEntry(string itemName, string matchString)
+ {
+ if (string.IsNullOrEmpty(itemName))
+ {
+ throw new ArgumentNullException("itemName");
+ }
+
+ if (string.IsNullOrEmpty(matchString))
+ {
+ throw new ArgumentNullException("matchString");
+ }
+
+ var options = GetAutoOrganizeOptions();
+
+ SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, itemName));
+
+ if (info != null && info.MatchStrings.Contains(matchString))
+ {
+ var list = info.MatchStrings.ToList();
+ list.Remove(matchString);
+ info.MatchStrings = list.ToArray();
+
+ if (info.MatchStrings.Length == 0)
+ {
+ var infos = options.SmartMatchInfos.ToList();
+ infos.Remove(info);
+ options.SmartMatchInfos = infos.ToArray();
+ }
+
+ _config.SaveAutoOrganizeOptions(options);
+ }
+ }
+
+ /// <summary>
+ /// Attempts to add a an item to the list of currently processed items.
+ /// </summary>
+ /// <param name="result">The result item.</param>
+ /// <param name="isNewItem">Passing true will notify the client to reload all items, otherwise only a single item will be refreshed.</param>
+ /// <returns>True if the item was added, False if the item is already contained in the list.</returns>
+ public bool AddToInProgressList(FileOrganizationResult result, bool isNewItem)
+ {
+ if (string.IsNullOrWhiteSpace(result.Id))
+ {
+ result.Id = result.OriginalPath.GetMD5().ToString("N");
+ }
+
+ if (!_inProgressItemIds.TryAdd(result.Id, false))
+ {
+ return false;
+ }
+
+ result.IsInProgress = true;
+
+ if (isNewItem)
+ {
+ EventHelper.FireEventIfNotNull(ItemAdded, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
+ }
+ else
+ {
+ EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Removes an item from the list of currently processed items.
+ /// </summary>
+ /// <param name="result">The result item.</param>
+ /// <returns>True if the item was removed, False if the item was not contained in the list.</returns>
+ public bool RemoveFromInprogressList(FileOrganizationResult result)
+ {
+ bool itemValue;
+ var retval = _inProgressItemIds.TryRemove(result.Id, out itemValue);
+
+ result.IsInProgress = false;
+
+ EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
+
+ return retval;
+ }
+
+ }
+}
diff --git a/Emby.Server.Implementations/FileOrganization/NameUtils.cs b/Emby.Server.Implementations/FileOrganization/NameUtils.cs
new file mode 100644
index 000000000..eb22ca4ea
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/NameUtils.cs
@@ -0,0 +1,81 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Globalization;
+using MediaBrowser.Controller.Extensions;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ public static class NameUtils
+ {
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ internal static Tuple<T, int> GetMatchScore<T>(string sortedName, int? year, T series)
+ where T : BaseItem
+ {
+ var score = 0;
+
+ var seriesNameWithoutYear = series.Name;
+ if (series.ProductionYear.HasValue)
+ {
+ seriesNameWithoutYear = seriesNameWithoutYear.Replace(series.ProductionYear.Value.ToString(UsCulture), String.Empty);
+ }
+
+ if (IsNameMatch(sortedName, seriesNameWithoutYear))
+ {
+ score++;
+
+ if (year.HasValue && series.ProductionYear.HasValue)
+ {
+ if (year.Value == series.ProductionYear.Value)
+ {
+ score++;
+ }
+ else
+ {
+ // Regardless of name, return a 0 score if the years don't match
+ return new Tuple<T, int>(series, 0);
+ }
+ }
+ }
+
+ return new Tuple<T, int>(series, score);
+ }
+
+
+ private static bool IsNameMatch(string name1, string name2)
+ {
+ name1 = GetComparableName(name1);
+ name2 = GetComparableName(name2);
+
+ return String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string GetComparableName(string name)
+ {
+ name = name.RemoveDiacritics();
+
+ name = " " + name + " ";
+
+ name = name.Replace(".", " ")
+ .Replace("_", " ")
+ .Replace(" and ", " ")
+ .Replace(".and.", " ")
+ .Replace("&", " ")
+ .Replace("!", " ")
+ .Replace("(", " ")
+ .Replace(")", " ")
+ .Replace(":", " ")
+ .Replace(",", " ")
+ .Replace("-", " ")
+ .Replace("'", " ")
+ .Replace("[", " ")
+ .Replace("]", " ")
+ .Replace(" a ", String.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace(" the ", String.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace(" ", String.Empty);
+
+ return name.Trim();
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs
new file mode 100644
index 000000000..5be7ba7ad
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs
@@ -0,0 +1,101 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileOrganizationService _organizationService;
+ private readonly IProviderManager _providerManager;
+
+ public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService, IProviderManager providerManager)
+ {
+ _libraryMonitor = libraryMonitor;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _config = config;
+ _organizationService = organizationService;
+ _providerManager = providerManager;
+ }
+
+ public string Name
+ {
+ get { return "Organize new media files"; }
+ }
+
+ public string Description
+ {
+ get { return "Processes new files available in the configured watch folder."; }
+ }
+
+ public string Category
+ {
+ get { return "Library"; }
+ }
+
+ private AutoOrganizeOptions GetAutoOrganizeOptions()
+ {
+ return _config.GetAutoOrganizeOptions();
+ }
+
+ public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ if (GetAutoOrganizeOptions().TvOptions.IsEnabled)
+ {
+ await new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _libraryMonitor, _organizationService, _config, _providerManager)
+ .Organize(GetAutoOrganizeOptions(), cancellationToken, progress).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks}
+ };
+ }
+
+ public bool IsHidden
+ {
+ get { return !GetAutoOrganizeOptions().TvOptions.IsEnabled; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return GetAutoOrganizeOptions().TvOptions.IsEnabled; }
+ }
+
+ public bool IsLogged
+ {
+ get { return false; }
+ }
+
+ public string Key
+ {
+ get { return "AutoOrganize"; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs
new file mode 100644
index 000000000..2850c3a61
--- /dev/null
+++ b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs
@@ -0,0 +1,210 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.FileOrganization
+{
+ public class TvFolderOrganizer
+ {
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly IFileOrganizationService _organizationService;
+ private readonly IServerConfigurationManager _config;
+ private readonly IProviderManager _providerManager;
+
+ public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IFileOrganizationService organizationService, IServerConfigurationManager config, IProviderManager providerManager)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _libraryMonitor = libraryMonitor;
+ _organizationService = organizationService;
+ _config = config;
+ _providerManager = providerManager;
+ }
+
+ private bool EnableOrganization(FileSystemMetadata fileInfo, TvFileOrganizationOptions options)
+ {
+ var minFileBytes = options.MinFileSizeMb * 1024 * 1024;
+
+ try
+ {
+ return _libraryManager.IsVideoFile(fileInfo.FullName) && fileInfo.Length >= minFileBytes;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error organizing file {0}", ex, fileInfo.Name);
+ }
+
+ return false;
+ }
+
+ public async Task Organize(AutoOrganizeOptions options, CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var watchLocations = options.TvOptions.WatchLocations.ToList();
+
+ var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize)
+ .OrderBy(_fileSystem.GetCreationTimeUtc)
+ .Where(i => EnableOrganization(i, options.TvOptions))
+ .ToList();
+
+ var processedFolders = new HashSet<string>();
+
+ progress.Report(10);
+
+ if (eligibleFiles.Count > 0)
+ {
+ var numComplete = 0;
+
+ foreach (var file in eligibleFiles)
+ {
+ var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager,
+ _libraryMonitor, _providerManager);
+
+ try
+ {
+ var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.TvOptions.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false);
+ if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase))
+ {
+ processedFolders.Add(file.DirectoryName);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error organizing episode {0}", ex, file.FullName);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= eligibleFiles.Count;
+
+ progress.Report(10 + 89 * percent);
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+ progress.Report(99);
+
+ foreach (var path in processedFolders)
+ {
+ var deleteExtensions = options.TvOptions.LeftOverFileExtensionsToDelete
+ .Select(i => i.Trim().TrimStart('.'))
+ .Where(i => !string.IsNullOrEmpty(i))
+ .Select(i => "." + i)
+ .ToList();
+
+ if (deleteExtensions.Count > 0)
+ {
+ DeleteLeftOverFiles(path, deleteExtensions);
+ }
+
+ if (options.TvOptions.DeleteEmptyFolders)
+ {
+ if (!IsWatchFolder(path, watchLocations))
+ {
+ DeleteEmptyFolders(path);
+ }
+ }
+ }
+
+ progress.Report(100);
+ }
+
+ /// <summary>
+ /// Gets the files to organize.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{FileInfo}.</returns>
+ private List<FileSystemMetadata> GetFilesToOrganize(string path)
+ {
+ try
+ {
+ return _fileSystem.GetFiles(path, true)
+ .ToList();
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error getting files from {0}", ex, path);
+
+ return new List<FileSystemMetadata>();
+ }
+ }
+
+ /// <summary>
+ /// Deletes the left over files.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="extensions">The extensions.</param>
+ private void DeleteLeftOverFiles(string path, IEnumerable<string> extensions)
+ {
+ var eligibleFiles = _fileSystem.GetFiles(path, true)
+ .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+
+ foreach (var file in eligibleFiles)
+ {
+ try
+ {
+ _fileSystem.DeleteFile(file.FullName);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting file {0}", ex, file.FullName);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deletes the empty folders.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ private void DeleteEmptyFolders(string path)
+ {
+ try
+ {
+ foreach (var d in _fileSystem.GetDirectoryPaths(path))
+ {
+ DeleteEmptyFolders(d);
+ }
+
+ var entries = _fileSystem.GetFileSystemEntryPaths(path);
+
+ if (!entries.Any())
+ {
+ try
+ {
+ _logger.Debug("Deleting empty directory {0}", path);
+ _fileSystem.DeleteDirectory(path, false);
+ }
+ catch (UnauthorizedAccessException) { }
+ catch (IOException) { }
+ }
+ }
+ catch (UnauthorizedAccessException) { }
+ }
+
+ /// <summary>
+ /// Determines if a given folder path is contained in a folder list
+ /// </summary>
+ /// <param name="path">The folder path to check.</param>
+ /// <param name="watchLocations">A list of folders.</param>
+ private bool IsWatchFolder(string path, IEnumerable<string> watchLocations)
+ {
+ return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
new file mode 100644
index 000000000..2e69cd2ef
--- /dev/null
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -0,0 +1,148 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Provides the core resolver ignore rules
+ /// </summary>
+ public class CoreResolutionIgnoreRule : IResolverIgnoreRule
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
+ /// </summary>
+ public static readonly List<string> IgnoreFolders = new List<string>
+ {
+ "metadata",
+ "ps3_update",
+ "ps3_vprm",
+ "extrafanart",
+ "extrathumbs",
+ ".actors",
+ ".wd_tv",
+
+ // Synology
+ "@eaDir",
+ "eaDir",
+ "#recycle"
+
+ };
+
+ public CoreResolutionIgnoreRule(IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Shoulds the ignore.
+ /// </summary>
+ /// <param name="fileInfo">The file information.</param>
+ /// <param name="parent">The parent.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
+ {
+ var filename = fileInfo.Name;
+ var isHidden = fileInfo.IsHidden;
+ var path = fileInfo.FullName;
+
+ // Handle mac .DS_Store
+ // https://github.com/MediaBrowser/MediaBrowser/issues/427
+ if (filename.IndexOf("._", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ return true;
+ }
+
+ // Ignore hidden files and folders
+ if (isHidden)
+ {
+ if (parent == null)
+ {
+ var parentFolderName = Path.GetFileName(Path.GetDirectoryName(path));
+
+ if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ // Sometimes these are marked hidden
+ if (_fileSystem.IsRootPath(path))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (fileInfo.IsDirectory)
+ {
+ // Ignore any folders in our list
+ if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ if (parent != null)
+ {
+ // Ignore trailer folders but allow it at the collection level
+ if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) &&
+ !(parent is AggregateFolder) && !(parent is UserRootFolder))
+ {
+ return true;
+ }
+
+ if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+ else
+ {
+ if (parent != null)
+ {
+ // Don't resolve these into audio files
+ if (string.Equals(_fileSystem.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) && _libraryManager.IsAudioFile(filename))
+ {
+ return true;
+ }
+ }
+
+ // Ignore samples
+ var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("-", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("_", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("!", " ", StringComparison.OrdinalIgnoreCase);
+
+ if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
new file mode 100644
index 000000000..7ae00d94c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -0,0 +1,3066 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Naming.Audio;
+using MediaBrowser.Naming.Common;
+using MediaBrowser.Naming.TV;
+using MediaBrowser.Naming.Video;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Library.Resolvers;
+using Emby.Server.Implementations.Library.Validators;
+using Emby.Server.Implementations.Logging;
+using Emby.Server.Implementations.ScheduledTasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Net;
+using SortOrder = MediaBrowser.Model.Entities.SortOrder;
+using VideoResolver = MediaBrowser.Naming.Video.VideoResolver;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Class LibraryManager
+ /// </summary>
+ public class LibraryManager : ILibraryManager
+ {
+ /// <summary>
+ /// Gets or sets the postscan tasks.
+ /// </summary>
+ /// <value>The postscan tasks.</value>
+ private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+ /// <summary>
+ /// Gets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ private IIntroProvider[] IntroProviders { get; set; }
+
+ /// <summary>
+ /// Gets the list of entity resolution ignore rules
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+ /// <summary>
+ /// Gets the list of BasePluginFolders added by plugins
+ /// </summary>
+ /// <value>The plugin folders.</value>
+ private IVirtualFolderCreator[] PluginFolderCreators { get; set; }
+
+ /// <summary>
+ /// Gets the list of currently registered entity resolvers
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ private IItemResolver[] EntityResolvers { get; set; }
+ private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comparers.
+ /// </summary>
+ /// <value>The comparers.</value>
+ private IBaseItemComparer[] Comparers { get; set; }
+
+ /// <summary>
+ /// Gets the active item repository
+ /// </summary>
+ /// <value>The item repository.</value>
+ public IItemRepository ItemRepository { get; set; }
+
+ /// <summary>
+ /// Occurs when [item added].
+ /// </summary>
+ public event EventHandler<ItemChangeEventArgs> ItemAdded;
+
+ /// <summary>
+ /// Occurs when [item updated].
+ /// </summary>
+ public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+
+ /// <summary>
+ /// Occurs when [item removed].
+ /// </summary>
+ public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The _task manager
+ /// </summary>
+ private readonly ITaskManager _taskManager;
+
+ /// <summary>
+ /// The _user manager
+ /// </summary>
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// The _user data repository
+ /// </summary>
+ private readonly IUserDataManager _userDataRepository;
+
+ /// <summary>
+ /// Gets or sets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ private IServerConfigurationManager ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// A collection of items that may be referenced from multiple physical places in the library
+ /// (typically, multiple user roots). We store them here and be sure they all reference a
+ /// single instance.
+ /// </summary>
+ /// <value>The by reference items.</value>
+ private ConcurrentDictionary<Guid, BaseItem> ByReferenceItems { get; set; }
+
+ private readonly Func<ILibraryMonitor> _libraryMonitorFactory;
+ private readonly Func<IProviderManager> _providerManagerFactory;
+ private readonly Func<IUserViewManager> _userviewManager;
+ public bool IsScanRunning { get; private set; }
+
+ /// <summary>
+ /// The _library items cache
+ /// </summary>
+ private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
+ /// <summary>
+ /// Gets the library items cache.
+ /// </summary>
+ /// <value>The library items cache.</value>
+ private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache
+ {
+ get
+ {
+ return _libraryItemsCache;
+ }
+ }
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryManager" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="taskManager">The task manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <param name="userDataRepository">The user data repository.</param>
+ public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager)
+ {
+ _logger = logger;
+ _taskManager = taskManager;
+ _userManager = userManager;
+ ConfigurationManager = configurationManager;
+ _userDataRepository = userDataRepository;
+ _libraryMonitorFactory = libraryMonitorFactory;
+ _fileSystem = fileSystem;
+ _providerManagerFactory = providerManagerFactory;
+ _userviewManager = userviewManager;
+ ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>();
+ _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+
+ ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
+
+ RecordConfigurationValues(configurationManager.Configuration);
+ }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="pluginFolders">The plugin folders.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <param name="introProviders">The intro providers.</param>
+ /// <param name="itemComparers">The item comparers.</param>
+ /// <param name="postscanTasks">The postscan tasks.</param>
+ public void AddParts(IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IVirtualFolderCreator> pluginFolders,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks)
+ {
+ EntityResolutionIgnoreRules = rules.ToArray();
+ PluginFolderCreators = pluginFolders.ToArray();
+ EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+ MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+ IntroProviders = introProviders.ToArray();
+ Comparers = itemComparers.ToArray();
+
+ PostscanTasks = postscanTasks.OrderBy(i =>
+ {
+ var hasOrder = i as IHasOrder;
+
+ return hasOrder == null ? 0 : hasOrder.Order;
+
+ }).ToArray();
+ }
+
+ /// <summary>
+ /// The _root folder
+ /// </summary>
+ private volatile AggregateFolder _rootFolder;
+ /// <summary>
+ /// The _root folder sync lock
+ /// </summary>
+ private readonly object _rootFolderSyncLock = new object();
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ public AggregateFolder RootFolder
+ {
+ get
+ {
+ if (_rootFolder == null)
+ {
+ lock (_rootFolderSyncLock)
+ {
+ if (_rootFolder == null)
+ {
+ _rootFolder = CreateRootFolder();
+ }
+ }
+ }
+ return _rootFolder;
+ }
+ }
+
+ /// <summary>
+ /// The _season zero display name
+ /// </summary>
+ private string _seasonZeroDisplayName;
+
+ private bool _wizardCompleted;
+ /// <summary>
+ /// Records the configuration values.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ private void RecordConfigurationValues(ServerConfiguration configuration)
+ {
+ _seasonZeroDisplayName = configuration.SeasonZeroDisplayName;
+ _wizardCompleted = configuration.IsStartupWizardCompleted;
+ }
+
+ /// <summary>
+ /// Configurations the updated.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ void ConfigurationUpdated(object sender, EventArgs e)
+ {
+ var config = ConfigurationManager.Configuration;
+
+ var newSeasonZeroName = ConfigurationManager.Configuration.SeasonZeroDisplayName;
+ var seasonZeroNameChanged = !string.Equals(_seasonZeroDisplayName, newSeasonZeroName, StringComparison.Ordinal);
+ var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
+
+ RecordConfigurationValues(config);
+
+ if (seasonZeroNameChanged || wizardChanged)
+ {
+ _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+ }
+
+ if (seasonZeroNameChanged)
+ {
+ Task.Run(async () =>
+ {
+ await UpdateSeasonZeroNames(newSeasonZeroName, CancellationToken.None).ConfigureAwait(false);
+
+ });
+ }
+ }
+
+ /// <summary>
+ /// Updates the season zero names.
+ /// </summary>
+ /// <param name="newName">The new name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken)
+ {
+ var seasons = GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Season).Name },
+ Recursive = true,
+ IndexNumber = 0
+
+ }).Cast<Season>()
+ .Where(i => !string.Equals(i.Name, newName, StringComparison.Ordinal))
+ .ToList();
+
+ foreach (var season in seasons)
+ {
+ season.Name = newName;
+
+ try
+ {
+ await UpdateItem(season, ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error saving {0}", ex, season.Path);
+ }
+ }
+ }
+
+ public void RegisterItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ RegisterItem(item.Id, item);
+ }
+
+ private void RegisterItem(Guid id, BaseItem item)
+ {
+ if (item is IItemByName)
+ {
+ if (!(item is MusicArtist))
+ {
+ return;
+ }
+ }
+
+ if (item.IsFolder)
+ {
+ if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder))
+ {
+ if (item.SourceType != SourceType.Library)
+ {
+ return;
+ }
+ }
+ }
+ else
+ {
+ if (item is Photo)
+ {
+ return;
+ }
+ }
+
+ LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; });
+ }
+
+ public async Task DeleteItem(BaseItem item, DeleteOptions options)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ _logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ item.Path ?? string.Empty,
+ item.Id);
+
+ var parent = item.Parent;
+
+ var locationType = item.LocationType;
+
+ var children = item.IsFolder
+ ? ((Folder)item).GetRecursiveChildren(false).ToList()
+ : new List<BaseItem>();
+
+ foreach (var metadataPath in GetMetadataPaths(item, children))
+ {
+ _logger.Debug("Deleting path {0}", metadataPath);
+
+ try
+ {
+ _fileSystem.DeleteDirectory(metadataPath, true);
+ }
+ catch (IOException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting {0}", ex, metadataPath);
+ }
+ }
+
+ if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual)
+ {
+ foreach (var path in item.GetDeletePaths().ToList())
+ {
+ if (_fileSystem.DirectoryExists(path))
+ {
+ _logger.Debug("Deleting path {0}", path);
+ _fileSystem.DeleteDirectory(path, true);
+ }
+ else if (_fileSystem.FileExists(path))
+ {
+ _logger.Debug("Deleting path {0}", path);
+ _fileSystem.DeleteFile(path);
+ }
+ }
+
+ if (parent != null)
+ {
+ await parent.ValidateChildren(new Progress<double>(), CancellationToken.None)
+ .ConfigureAwait(false);
+ }
+ }
+ else if (parent != null)
+ {
+ parent.RemoveChild(item);
+ }
+
+ await ItemRepository.DeleteItem(item.Id, CancellationToken.None).ConfigureAwait(false);
+ foreach (var child in children)
+ {
+ await ItemRepository.DeleteItem(child.Id, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ BaseItem removed;
+ _libraryItemsCache.TryRemove(item.Id, out removed);
+
+ ReportItemRemoved(item);
+ }
+
+ private IEnumerable<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ {
+ var list = new List<string>
+ {
+ item.GetInternalMetadataPath()
+ };
+
+ list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
+
+ return list;
+ }
+
+ /// <summary>
+ /// Resolves the item.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <returns>BaseItem.</returns>
+ private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers)
+ {
+ var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
+ .FirstOrDefault(i => i != null);
+
+ if (item != null)
+ {
+ ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
+ }
+
+ return item;
+ }
+
+ private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver)
+ {
+ try
+ {
+ return resolver.ResolvePath(args);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in {0} resolving {1}", ex, resolver.GetType().Name, args.Path);
+ return null;
+ }
+ }
+
+ public Guid GetNewItemId(string key, Type type)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new ArgumentNullException("key");
+ }
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (ConfigurationManager.Configuration.EnableLocalizedGuids && key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath))
+ {
+ // Try to normalize paths located underneath program-data in an attempt to make them more portable
+ key = key.Substring(ConfigurationManager.ApplicationPaths.ProgramDataPath.Length)
+ .TrimStart(new[] { '/', '\\' })
+ .Replace("/", "\\");
+ }
+
+ if (!ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
+ {
+ key = key.ToLower();
+ }
+
+ key = type.FullName + key;
+
+ return key.GetMD5();
+ }
+
+ /// <summary>
+ /// Ensure supplied item has only one instance throughout
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>The proper instance to the item</returns>
+ public BaseItem GetOrAddByReferenceItem(BaseItem item)
+ {
+ // Add this item to our list if not there already
+ if (!ByReferenceItems.TryAdd(item.Id, item))
+ {
+ // Already there - return the existing reference
+ item = ByReferenceItems[item.Id];
+ }
+ return item;
+ }
+
+ public BaseItem ResolvePath(FileSystemMetadata fileInfo,
+ Folder parent = null)
+ {
+ return ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent);
+ }
+
+ private BaseItem ResolvePath(FileSystemMetadata fileInfo,
+ IDirectoryService directoryService,
+ IItemResolver[] resolvers,
+ Folder parent = null,
+ string collectionType = null,
+ LibraryOptions libraryOptions = null)
+ {
+ if (fileInfo == null)
+ {
+ throw new ArgumentNullException("fileInfo");
+ }
+
+ var fullPath = fileInfo.FullName;
+
+ if (string.IsNullOrWhiteSpace(collectionType) && parent != null)
+ {
+ collectionType = GetContentTypeOverride(fullPath, true);
+ }
+
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ {
+ Parent = parent,
+ Path = fullPath,
+ FileInfo = fileInfo,
+ CollectionType = collectionType,
+ LibraryOptions = libraryOptions
+ };
+
+ // Return null if ignore rules deem that we should do so
+ if (IgnoreFile(args.FileInfo, args.Parent))
+ {
+ return null;
+ }
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ var isPhysicalRoot = args.IsPhysicalRoot;
+
+ // When resolving the root, we need it's grandchildren (children of user views)
+ var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
+
+ var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
+
+ // Need to remove subpaths that may have been resolved from shortcuts
+ // Example: if \\server\movies exists, then strip out \\server\movies\action
+ if (isPhysicalRoot)
+ {
+ var paths = NormalizeRootPathList(fileSystemDictionary.Values);
+
+ fileSystemDictionary = paths.ToDictionary(i => i.FullName);
+ }
+
+ args.FileSystemDictionary = fileSystemDictionary;
+ }
+
+ // Check to see if we should resolve based on our contents
+ if (args.IsDirectory && !ShouldResolvePathContents(args))
+ {
+ return null;
+ }
+
+ return ResolveItem(args, resolvers);
+ }
+
+ private readonly List<string> _ignoredPaths = new List<string>();
+
+ public void RegisterIgnoredPath(string path)
+ {
+ lock (_ignoredPaths)
+ {
+ _ignoredPaths.Add(path);
+ }
+ }
+ public void UnRegisterIgnoredPath(string path)
+ {
+ lock (_ignoredPaths)
+ {
+ _ignoredPaths.Remove(path);
+ }
+ }
+
+ public bool IgnoreFile(FileSystemMetadata file, BaseItem parent)
+ {
+ if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)))
+ {
+ return true;
+ }
+
+ //lock (_ignoredPaths)
+ {
+ if (_ignoredPaths.Contains(file.FullName, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public IEnumerable<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
+ {
+ var originalList = paths.ToList();
+
+ var list = originalList.Where(i => i.IsDirectory)
+ .Select(i => _fileSystem.NormalizePath(i.FullName))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath)))
+ .ToList();
+
+ foreach (var dupe in dupes)
+ {
+ _logger.Info("Found duplicate path: {0}", dupe);
+ }
+
+ var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList();
+ newList.AddRange(originalList.Where(i => !i.IsDirectory));
+ return newList;
+ }
+
+ /// <summary>
+ /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private static bool ShouldResolvePathContents(ItemResolveArgs args)
+ {
+ // Ignore any folders containing a file called .ignore
+ return !args.ContainsFileSystemEntryByName(".ignore");
+ }
+
+ public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType)
+ {
+ return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
+ }
+
+ public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files,
+ IDirectoryService directoryService,
+ Folder parent,
+ LibraryOptions libraryOptions,
+ string collectionType,
+ IItemResolver[] resolvers)
+ {
+ var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
+
+ if (parent != null)
+ {
+ var multiItemResolvers = resolvers == null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>().ToArray();
+
+ foreach (var resolver in multiItemResolvers)
+ {
+ var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
+
+ if (result != null && result.Items.Count > 0)
+ {
+ var items = new List<BaseItem>();
+ items.AddRange(result.Items);
+
+ foreach (var item in items)
+ {
+ ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService);
+ }
+ items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
+ return items;
+ }
+ }
+ }
+
+ return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
+ }
+
+ private IEnumerable<BaseItem> ResolveFileList(IEnumerable<FileSystemMetadata> fileList,
+ IDirectoryService directoryService,
+ Folder parent,
+ string collectionType,
+ IItemResolver[] resolvers,
+ LibraryOptions libraryOptions)
+ {
+ return fileList.Select(f =>
+ {
+ try
+ {
+ return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error resolving path {0}", ex, f.FullName);
+ return null;
+ }
+ }).Where(i => i != null);
+ }
+
+ /// <summary>
+ /// Creates the root media folder
+ /// </summary>
+ /// <returns>AggregateFolder.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot create the root folder until plugins have loaded</exception>
+ public AggregateFolder CreateRootFolder()
+ {
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
+
+ _fileSystem.CreateDirectory(rootFolderPath);
+
+ var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath));
+
+ // Add in the plug-in folders
+ foreach (var child in PluginFolderCreators)
+ {
+ var folder = child.GetFolder();
+
+ if (folder != null)
+ {
+ if (folder.Id == Guid.Empty)
+ {
+ if (string.IsNullOrWhiteSpace(folder.Path))
+ {
+ folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
+ }
+ else
+ {
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
+ }
+ }
+
+ var dbItem = GetItemById(folder.Id) as BasePluginFolder;
+
+ if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ folder = dbItem;
+ }
+
+ if (folder.ParentId != rootFolder.Id)
+ {
+ folder.ParentId = rootFolder.Id;
+ var task = folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
+ Task.WaitAll(task);
+ }
+
+ rootFolder.AddVirtualChild(folder);
+
+ RegisterItem(folder);
+ }
+ }
+
+ return rootFolder;
+ }
+
+ private volatile UserRootFolder _userRootFolder;
+ private readonly object _syncLock = new object();
+ public Folder GetUserRootFolder()
+ {
+ if (_userRootFolder == null)
+ {
+ lock (_syncLock)
+ {
+ if (_userRootFolder == null)
+ {
+ var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ _fileSystem.CreateDirectory(userRootPath);
+
+ var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder;
+
+ if (tmpItem == null)
+ {
+ tmpItem = (UserRootFolder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath));
+ }
+
+ _userRootFolder = tmpItem;
+ }
+ }
+ }
+
+ return _userRootFolder;
+ }
+
+ public BaseItem FindByPath(string path, bool? isFolder)
+ {
+ // If this returns multiple items it could be tricky figuring out which one is correct.
+ // In most cases, the newest one will be and the others obsolete but not yet cleaned up
+
+ var query = new InternalItemsQuery
+ {
+ Path = path,
+ IsFolder = isFolder,
+ SortBy = new[] { ItemSortBy.DateCreated },
+ SortOrder = SortOrder.Descending,
+ Limit = 1
+ };
+
+ return GetItemList(query)
+ .FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Person}.</returns>
+ public Person GetPerson(string name)
+ {
+ return CreateItemByName<Person>(Person.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Studio}.</returns>
+ public Studio GetStudio(string name)
+ {
+ return CreateItemByName<Studio>(Studio.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Genre}.</returns>
+ public Genre GetGenre(string name)
+ {
+ return CreateItemByName<Genre>(Genre.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets the genre.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{MusicGenre}.</returns>
+ public MusicGenre GetMusicGenre(string name)
+ {
+ return CreateItemByName<MusicGenre>(MusicGenre.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets the game genre.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{GameGenre}.</returns>
+ public GameGenre GetGameGenre(string name)
+ {
+ return CreateItemByName<GameGenre>(GameGenre.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>Task{Year}.</returns>
+ /// <exception cref="System.ArgumentOutOfRangeException"></exception>
+ public Year GetYear(int value)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException("Years less than or equal to 0 are invalid.");
+ }
+
+ var name = value.ToString(CultureInfo.InvariantCulture);
+
+ return CreateItemByName<Year>(Year.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Genre}.</returns>
+ public MusicArtist GetArtist(string name)
+ {
+ return CreateItemByName<MusicArtist>(MusicArtist.GetPath(name), name);
+ }
+
+ private T CreateItemByName<T>(string path, string name)
+ where T : BaseItem, new()
+ {
+ if (typeof(T) == typeof(MusicArtist))
+ {
+ var existing = GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(T).Name },
+ Name = name
+
+ }).Cast<MusicArtist>()
+ .OrderBy(i => i.IsAccessedByName ? 1 : 0)
+ .Cast<T>()
+ .FirstOrDefault();
+
+ if (existing != null)
+ {
+ return existing;
+ }
+ }
+
+ var id = GetNewItemId(path, typeof(T));
+
+ var item = GetItemById(id) as T;
+
+ if (item == null)
+ {
+ item = new T
+ {
+ Name = name,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
+ Path = path
+ };
+
+ var task = CreateItem(item, CancellationToken.None);
+ Task.WaitAll(task);
+ }
+
+ return item;
+ }
+
+ public IEnumerable<MusicArtist> GetAlbumArtists(IEnumerable<IHasAlbumArtist> items)
+ {
+ var names = items
+ .SelectMany(i => i.AlbumArtists)
+ .DistinctNames()
+ .Select(i =>
+ {
+ try
+ {
+ var artist = GetArtist(i);
+
+ return artist;
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return null;
+ }
+ })
+ .Where(i => i != null);
+
+ return names;
+ }
+
+ public IEnumerable<MusicArtist> GetArtists(IEnumerable<IHasArtist> items)
+ {
+ var names = items
+ .SelectMany(i => i.AllArtists)
+ .DistinctNames()
+ .Select(i =>
+ {
+ try
+ {
+ var artist = GetArtist(i);
+
+ return artist;
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return null;
+ }
+ })
+ .Where(i => i != null);
+
+ return names;
+ }
+
+ /// <summary>
+ /// Validate and refresh the People sub-set of the IBN.
+ /// The items are stored in the db but not loaded into memory until actually requested by an operation.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ // Ensure the location is available.
+ _fileSystem.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath);
+
+ return new PeopleValidator(this, _logger, ConfigurationManager, _fileSystem).ValidatePeople(cancellationToken, progress);
+ }
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ // Just run the scheduled task so that the user can see it
+ _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Queues the library scan.
+ /// </summary>
+ public void QueueLibraryScan()
+ {
+ // Just run the scheduled task so that the user can see it
+ _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
+ }
+
+ /// <summary>
+ /// Validates the media library internal.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ IsScanRunning = true;
+ _libraryMonitorFactory().Stop();
+
+ try
+ {
+ await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ _libraryMonitorFactory().Start();
+ IsScanRunning = false;
+ }
+ }
+
+ private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ _logger.Info("Validating media library");
+
+ await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+ progress.Report(.5);
+
+ // Start by just validating the children of the root, but go no further
+ await RootFolder.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false);
+
+ progress.Report(1);
+
+ var userRoot = GetUserRootFolder();
+
+ await userRoot.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+ await userRoot.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false);
+ progress.Report(2);
+
+ var innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(pct => progress.Report(2 + pct * .73));
+
+ // Now validate the entire media library
+ await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false);
+
+ progress.Report(75);
+
+ innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25));
+
+ // Run post-scan tasks
+ await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
+
+ progress.Report(100);
+ }
+
+ /// <summary>
+ /// Runs the post scan tasks.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var tasks = PostscanTasks.ToList();
+
+ var numComplete = 0;
+ var numTasks = tasks.Count;
+
+ foreach (var task in tasks)
+ {
+ var innerProgress = new ActionableProgress<double>();
+
+ // Prevent access to modified closure
+ var currentNumComplete = numComplete;
+
+ innerProgress.RegisterAction(pct =>
+ {
+ double innerPercent = currentNumComplete * 100 + pct;
+ innerPercent /= numTasks;
+ progress.Report(innerPercent);
+ });
+
+ _logger.Debug("Running post-scan task {0}", task.GetType().Name);
+
+ try
+ {
+ await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error running postscan task", ex);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= numTasks;
+ progress.Report(percent * 100);
+ }
+
+ progress.Report(100);
+ }
+
+ /// <summary>
+ /// Gets the default view.
+ /// </summary>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ public IEnumerable<VirtualFolderInfo> GetVirtualFolders()
+ {
+ return GetView(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath);
+ }
+
+ /// <summary>
+ /// Gets the view.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ private IEnumerable<VirtualFolderInfo> GetView(string path)
+ {
+ var topLibraryFolders = GetUserRootFolder().Children.ToList();
+
+ return _fileSystem.GetDirectoryPaths(path)
+ .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders));
+ }
+
+ private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders)
+ {
+ var info = new VirtualFolderInfo
+ {
+ Name = Path.GetFileName(dir),
+
+ Locations = _fileSystem.GetFilePaths(dir, false)
+ .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Select(_fileSystem.ResolveShortcut)
+ .OrderBy(i => i)
+ .ToList(),
+
+ CollectionType = GetCollectionType(dir)
+ };
+
+ var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase));
+
+ if (libraryFolder != null && libraryFolder.HasImage(ImageType.Primary))
+ {
+ info.PrimaryImageItemId = libraryFolder.Id.ToString("N");
+ }
+
+ if (libraryFolder != null)
+ {
+ info.ItemId = libraryFolder.Id.ToString("N");
+ info.LibraryOptions = GetLibraryOptions(libraryFolder);
+ }
+
+ return info;
+ }
+
+ private string GetCollectionType(string path)
+ {
+ return _fileSystem.GetFiles(path, false)
+ .Where(i => string.Equals(i.Extension, ".collection", StringComparison.OrdinalIgnoreCase))
+ .Select(i => _fileSystem.GetFileNameWithoutExtension(i))
+ .FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Gets the item by id.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public BaseItem GetItemById(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ BaseItem item;
+
+ if (LibraryItemsCache.TryGetValue(id, out item))
+ {
+ return item;
+ }
+
+ item = RetrieveItem(id);
+
+ //_logger.Debug("GetitemById {0}", id);
+
+ if (item != null)
+ {
+ RegisterItem(item);
+ }
+
+ return item;
+ }
+
+ public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query)
+ {
+ if (query.Recursive && query.ParentId.HasValue)
+ {
+ var parent = GetItemById(query.ParentId.Value);
+ if (parent != null)
+ {
+ SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ query.ParentId = null;
+ }
+ }
+
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ return ItemRepository.GetItemList(query);
+ }
+
+ public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query, IEnumerable<string> parentIds)
+ {
+ var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList();
+
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return ItemRepository.GetItemList(query);
+ }
+
+ public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ if (query.EnableTotalRecordCount)
+ {
+ return ItemRepository.GetItems(query);
+ }
+
+ return new QueryResult<BaseItem>
+ {
+ Items = ItemRepository.GetItemList(query).ToArray()
+ };
+ }
+
+ public List<Guid> GetItemIds(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ return ItemRepository.GetItemIdsList(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetStudios(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetGenres(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetGameGenres(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetMusicGenres(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetAllArtists(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetArtists(query);
+ }
+
+ private void SetTopParentOrAncestorIds(InternalItemsQuery query)
+ {
+ if (query.AncestorIds.Length == 0)
+ {
+ return;
+ }
+
+ var parents = query.AncestorIds.Select(i => GetItemById(new Guid(i))).ToList();
+
+ if (parents.All(i =>
+ {
+ if (i is ICollectionFolder || i is UserView)
+ {
+ return true;
+ }
+
+ //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name);
+ return false;
+
+ }))
+ {
+ // Optimize by querying against top level views
+ query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray();
+ query.AncestorIds = new string[] { };
+ }
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetAlbumArtists(query);
+ }
+
+ public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
+ {
+ if (query.Recursive && query.ParentId.HasValue)
+ {
+ var parent = GetItemById(query.ParentId.Value);
+ if (parent != null)
+ {
+ SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ query.ParentId = null;
+ }
+ }
+
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ if (query.EnableTotalRecordCount)
+ {
+ return ItemRepository.GetItems(query);
+ }
+
+ return new QueryResult<BaseItem>
+ {
+ Items = ItemRepository.GetItemList(query).ToArray()
+ };
+ }
+
+ private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents)
+ {
+ if (parents.All(i =>
+ {
+ if (i is ICollectionFolder || i is UserView)
+ {
+ return true;
+ }
+
+ //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name);
+ return false;
+
+ }))
+ {
+ // Optimize by querying against top level views
+ query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray();
+ }
+ else
+ {
+ // We need to be able to query from any arbitrary ancestor up the tree
+ query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray();
+ }
+ }
+
+ private void AddUserToQuery(InternalItemsQuery query, User user)
+ {
+ if (query.AncestorIds.Length == 0 &&
+ !query.ParentId.HasValue &&
+ query.ChannelIds.Length == 0 &&
+ query.TopParentIds.Length == 0 &&
+ string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)
+ && query.ItemIds.Length == 0)
+ {
+ var userViews = _userviewManager().GetUserViews(new UserViewQuery
+ {
+ UserId = user.Id.ToString("N"),
+ IncludeHidden = true
+
+ }, CancellationToken.None).Result.ToList();
+
+ query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray();
+ }
+ }
+
+ private IEnumerable<BaseItem> GetTopParentsForQuery(BaseItem item, User user)
+ {
+ var view = item as UserView;
+
+ if (view != null)
+ {
+ if (string.Equals(view.ViewType, CollectionType.LiveTv))
+ {
+ return new[] { view };
+ }
+ if (string.Equals(view.ViewType, CollectionType.Channels))
+ {
+ var channelResult = BaseItem.ChannelManager.GetChannelsInternal(new ChannelQuery
+ {
+ UserId = user.Id.ToString("N")
+
+ }, CancellationToken.None).Result;
+
+ return channelResult.Items;
+ }
+
+ // Translate view into folders
+ if (view.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(view.DisplayParentId);
+ if (displayParent != null)
+ {
+ return GetTopParentsForQuery(displayParent, user);
+ }
+ return new BaseItem[] { };
+ }
+ if (view.ParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(view.ParentId);
+ if (displayParent != null)
+ {
+ return GetTopParentsForQuery(displayParent, user);
+ }
+ return new BaseItem[] { };
+ }
+
+ // Handle grouping
+ if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType))
+ {
+ return user.RootFolder
+ .GetChildren(user, true)
+ .OfType<CollectionFolder>()
+ .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
+ .Where(i => user.IsFolderGrouped(i.Id))
+ .SelectMany(i => GetTopParentsForQuery(i, user));
+ }
+ return new BaseItem[] { };
+ }
+
+ var collectionFolder = item as CollectionFolder;
+ if (collectionFolder != null)
+ {
+ return collectionFolder.GetPhysicalParents();
+ }
+
+ var topParent = item.GetTopParent();
+ if (topParent != null)
+ {
+ return new[] { topParent };
+ }
+ return new BaseItem[] { };
+ }
+
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
+ {
+ var tasks = IntroProviders
+ .OrderBy(i => i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 0 : 1)
+ .Take(1)
+ .Select(i => GetIntros(i, item, user));
+
+ var items = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ return items
+ .SelectMany(i => i.ToArray())
+ .Select(ResolveIntro)
+ .Where(i => i != null);
+ }
+
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
+ private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
+ {
+ try
+ {
+ return await provider.GetIntros(item, user).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting intros", ex);
+
+ return new List<IntroInfo>();
+ }
+ }
+
+ /// <summary>
+ /// Gets all intro files.
+ /// </summary>
+ /// <returns>IEnumerable{System.String}.</returns>
+ public IEnumerable<string> GetAllIntroFiles()
+ {
+ return IntroProviders.SelectMany(i =>
+ {
+ try
+ {
+ return i.GetAllIntroFiles().ToList();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting intro files", ex);
+
+ return new List<string>();
+ }
+ });
+ }
+
+ /// <summary>
+ /// Resolves the intro.
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>Video.</returns>
+ private Video ResolveIntro(IntroInfo info)
+ {
+ Video video = null;
+
+ if (info.ItemId.HasValue)
+ {
+ // Get an existing item by Id
+ video = GetItemById(info.ItemId.Value) as Video;
+
+ if (video == null)
+ {
+ _logger.Error("Unable to locate item with Id {0}.", info.ItemId.Value);
+ }
+ }
+ else if (!string.IsNullOrEmpty(info.Path))
+ {
+ try
+ {
+ // Try to resolve the path into a video
+ video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
+
+ if (video == null)
+ {
+ _logger.Error("Intro resolver returned null for {0}.", info.Path);
+ }
+ else
+ {
+ // Pull the saved db item that will include metadata
+ var dbItem = GetItemById(video.Id) as Video;
+
+ if (dbItem != null)
+ {
+ video = dbItem;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error resolving path {0}.", ex, info.Path);
+ }
+ }
+ else
+ {
+ _logger.Error("IntroProvider returned an IntroInfo with null Path and ItemId.");
+ }
+
+ return video;
+ }
+
+ /// <summary>
+ /// Sorts the specified sort by.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="sortBy">The sort by.</param>
+ /// <param name="sortOrder">The sort order.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder)
+ {
+ var isFirst = true;
+
+ IOrderedEnumerable<BaseItem> orderedItems = null;
+
+ foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c != null))
+ {
+ if (isFirst)
+ {
+ orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
+ }
+ else
+ {
+ orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
+ }
+
+ isFirst = false;
+ }
+
+ return orderedItems ?? items;
+ }
+
+ /// <summary>
+ /// Gets the comparer.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IBaseItemComparer.</returns>
+ private IBaseItemComparer GetComparer(string name, User user)
+ {
+ var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
+
+ if (comparer != null)
+ {
+ // If it requires a user, create a new one, and assign the user
+ if (comparer is IUserBaseItemComparer)
+ {
+ var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType());
+
+ userComparer.User = user;
+ userComparer.UserManager = _userManager;
+ userComparer.UserDataRepository = _userDataRepository;
+
+ return userComparer;
+ }
+ }
+
+ return comparer;
+ }
+
+ /// <summary>
+ /// Creates the item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task CreateItem(BaseItem item, CancellationToken cancellationToken)
+ {
+ return CreateItems(new[] { item }, cancellationToken);
+ }
+
+ /// <summary>
+ /// Creates the items.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task CreateItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
+ {
+ var list = items.ToList();
+
+ await ItemRepository.SaveItems(list, cancellationToken).ConfigureAwait(false);
+
+ foreach (var item in list)
+ {
+ RegisterItem(item);
+ }
+
+ if (ItemAdded != null)
+ {
+ foreach (var item in list)
+ {
+ try
+ {
+ ItemAdded(this, new ItemChangeEventArgs { Item = item });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemAdded event handler", ex);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Updates the item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="updateReason">The update reason.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ var locationType = item.LocationType;
+ if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
+ {
+ await _providerManagerFactory().SaveMetadata(item, updateReason).ConfigureAwait(false);
+ }
+
+ item.DateLastSaved = DateTime.UtcNow;
+
+ var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name;
+ _logger.Debug("Saving {0} to database.", logName);
+
+ await ItemRepository.SaveItem(item, cancellationToken).ConfigureAwait(false);
+
+ RegisterItem(item);
+
+ if (ItemUpdated != null)
+ {
+ try
+ {
+ ItemUpdated(this, new ItemChangeEventArgs
+ {
+ Item = item,
+ UpdateReason = updateReason
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemUpdated event handler", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Reports the item removed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ public void ReportItemRemoved(BaseItem item)
+ {
+ if (ItemRemoved != null)
+ {
+ try
+ {
+ ItemRemoved(this, new ItemChangeEventArgs { Item = item });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemRemoved event handler", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Retrieves the item.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ public BaseItem RetrieveItem(Guid id)
+ {
+ return ItemRepository.RetrieveItem(id);
+ }
+
+ public IEnumerable<Folder> GetCollectionFolders(BaseItem item)
+ {
+ while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null)
+ {
+ item = item.GetParent();
+ }
+
+ if (item == null)
+ {
+ return new List<Folder>();
+ }
+
+ return GetUserRootFolder().Children
+ .OfType<Folder>()
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase));
+ }
+
+ public LibraryOptions GetLibraryOptions(BaseItem item)
+ {
+ var collectionFolder = item as CollectionFolder;
+ if (collectionFolder == null)
+ {
+ collectionFolder = GetCollectionFolders(item)
+ .OfType<CollectionFolder>()
+ .FirstOrDefault();
+ }
+
+ var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
+
+ if (options.SchemaVersion < 3)
+ {
+ options.SaveLocalMetadata = ConfigurationManager.Configuration.SaveLocalMeta;
+ options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders;
+ }
+
+ if (options.SchemaVersion < 2)
+ {
+ var chapterOptions = ConfigurationManager.GetConfiguration<ChapterOptions>("chapters");
+ options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan;
+
+ if (collectionFolder != null)
+ {
+ if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase))
+ {
+ options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction;
+ }
+ else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction;
+ }
+ }
+ }
+
+ return options;
+ }
+
+ public string GetContentType(BaseItem item)
+ {
+ string configuredContentType = GetConfiguredContentType(item, false);
+ if (!string.IsNullOrWhiteSpace(configuredContentType))
+ {
+ return configuredContentType;
+ }
+ configuredContentType = GetConfiguredContentType(item, true);
+ if (!string.IsNullOrWhiteSpace(configuredContentType))
+ {
+ return configuredContentType;
+ }
+ return GetInheritedContentType(item);
+ }
+
+ public string GetInheritedContentType(BaseItem item)
+ {
+ var type = GetTopFolderContentType(item);
+
+ if (!string.IsNullOrWhiteSpace(type))
+ {
+ return type;
+ }
+
+ return item.GetParents()
+ .Select(GetConfiguredContentType)
+ .LastOrDefault(i => !string.IsNullOrWhiteSpace(i));
+ }
+
+ public string GetConfiguredContentType(BaseItem item)
+ {
+ return GetConfiguredContentType(item, false);
+ }
+
+ public string GetConfiguredContentType(string path)
+ {
+ return GetContentTypeOverride(path, false);
+ }
+
+ public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
+ {
+ ICollectionFolder collectionFolder = item as ICollectionFolder;
+ if (collectionFolder != null)
+ {
+ return collectionFolder.CollectionType;
+ }
+ return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
+ }
+
+ private string GetContentTypeOverride(string path, bool inherit)
+ {
+ var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path)));
+ if (nameValuePair != null)
+ {
+ return nameValuePair.Value;
+ }
+ return null;
+ }
+
+ private string GetTopFolderContentType(BaseItem item)
+ {
+ if (item == null)
+ {
+ return null;
+ }
+
+ while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null)
+ {
+ item = item.GetParent();
+ }
+
+ return GetUserRootFolder().Children
+ .OfType<ICollectionFolder>()
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path))
+ .Select(i => i.CollectionType)
+ .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
+ }
+
+ private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
+ //private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromMinutes(1);
+
+ public Task<UserView> GetNamedView(User user,
+ string name,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ return GetNamedView(user, name, null, viewType, sortName, cancellationToken);
+ }
+
+ public async Task<UserView> GetNamedView(string name,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "views");
+
+ path = Path.Combine(path, _fileSystem.GetValidFilename(viewType));
+
+ var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
+
+ var item = GetItemById(id) as UserView;
+
+ var refresh = false;
+
+ if (item == null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName
+ };
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ refresh = true;
+ }
+
+ if (!refresh)
+ {
+ refresh = DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+ }
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ await item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None).ConfigureAwait(false);
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Not sure why this is necessary but need to figure it out
+ // View images are not getting utilized without this
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public async Task<UserView> GetNamedView(User user,
+ string name,
+ string parentId,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty);
+
+ var id = GetNewItemId(idValues, typeof(UserView));
+
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N"));
+
+ var item = GetItemById(id) as UserView;
+
+ var isNew = false;
+
+ if (item == null)
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName,
+ UserId = user.Id
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ item.DisplayParentId = new Guid(parentId);
+ }
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ isNew = true;
+ }
+
+ var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Need to force save to increment DateLastSaved
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public async Task<UserView> GetShadowView(BaseItem parent,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ if (parent == null)
+ {
+ throw new ArgumentNullException("parent");
+ }
+
+ var name = parent.Name;
+ var parentId = parent.Id;
+
+ var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty);
+
+ var id = GetNewItemId(idValues, typeof(UserView));
+
+ var path = parent.Path;
+
+ var item = GetItemById(id) as UserView;
+
+ var isNew = false;
+
+ if (item == null)
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName
+ };
+
+ item.DisplayParentId = parentId;
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ isNew = true;
+ }
+
+ var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Need to force save to increment DateLastSaved
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public async Task<UserView> GetNamedView(string name,
+ string parentId,
+ string viewType,
+ string sortName,
+ string uniqueId,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var idValues = "37_namedview_" + name + (parentId ?? string.Empty) + (viewType ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(uniqueId))
+ {
+ idValues += uniqueId;
+ }
+
+ var id = GetNewItemId(idValues, typeof(UserView));
+
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N"));
+
+ var item = GetItemById(id) as UserView;
+
+ var isNew = false;
+
+ if (item == null)
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ item.DisplayParentId = new Guid(parentId);
+ }
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ isNew = true;
+ }
+
+ if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
+ {
+ item.ViewType = viewType;
+ await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+
+ var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Need to force save to increment DateLastSaved
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public bool IsVideoFile(string path, LibraryOptions libraryOptions)
+ {
+ var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new PatternsLogger());
+ return resolver.IsVideoFile(path);
+ }
+
+ public bool IsVideoFile(string path)
+ {
+ return IsVideoFile(path, new LibraryOptions());
+ }
+
+ public bool IsAudioFile(string path, LibraryOptions libraryOptions)
+ {
+ var parser = new AudioFileParser(GetNamingOptions(libraryOptions));
+ return parser.IsAudioFile(path);
+ }
+
+ public bool IsAudioFile(string path)
+ {
+ return IsAudioFile(path, new LibraryOptions());
+ }
+
+ public int? GetSeasonNumberFromPath(string path)
+ {
+ return new SeasonPathParser(GetNamingOptions(), new RegexProvider()).Parse(path, true, true).SeasonNumber;
+ }
+
+ public bool FillMissingEpisodeNumbersFromPath(Episode episode)
+ {
+ var resolver = new EpisodeResolver(GetNamingOptions(),
+ new PatternsLogger());
+
+ var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd ||
+ episode.VideoType == VideoType.HdDvd;
+
+ var locationType = episode.LocationType;
+
+ var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ?
+ resolver.Resolve(episode.Path, isFolder) :
+ new MediaBrowser.Naming.TV.EpisodeInfo();
+
+ if (episodeInfo == null)
+ {
+ episodeInfo = new MediaBrowser.Naming.TV.EpisodeInfo();
+ }
+
+ var changed = false;
+
+ if (episodeInfo.IsByDate)
+ {
+ if (episode.IndexNumber.HasValue)
+ {
+ episode.IndexNumber = null;
+ changed = true;
+ }
+
+ if (episode.IndexNumberEnd.HasValue)
+ {
+ episode.IndexNumberEnd = null;
+ changed = true;
+ }
+
+ if (!episode.PremiereDate.HasValue)
+ {
+ if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
+ {
+ episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value).ToUniversalTime();
+ }
+
+ if (episode.PremiereDate.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.ProductionYear.HasValue)
+ {
+ episode.ProductionYear = episodeInfo.Year;
+
+ if (episode.ProductionYear.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.ParentIndexNumber.HasValue)
+ {
+ var season = episode.Season;
+
+ if (season != null)
+ {
+ episode.ParentIndexNumber = season.IndexNumber;
+ }
+
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ changed = true;
+ }
+ }
+ }
+ else
+ {
+ if (!episode.IndexNumber.HasValue)
+ {
+ episode.IndexNumber = episodeInfo.EpisodeNumber;
+
+ if (episode.IndexNumber.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.IndexNumberEnd.HasValue)
+ {
+ episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber;
+
+ if (episode.IndexNumberEnd.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.ParentIndexNumber.HasValue)
+ {
+ episode.ParentIndexNumber = episodeInfo.SeasonNumber;
+
+ if (!episode.ParentIndexNumber.HasValue)
+ {
+ var season = episode.Season;
+
+ if (season != null)
+ {
+ episode.ParentIndexNumber = season.IndexNumber;
+ }
+ }
+
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+ }
+
+ public NamingOptions GetNamingOptions()
+ {
+ return GetNamingOptions(new LibraryOptions());
+ }
+
+ public NamingOptions GetNamingOptions(LibraryOptions libraryOptions)
+ {
+ var options = new ExtendedNamingOptions();
+
+ // These cause apps to have problems
+ options.AudioFileExtensions.Remove(".m3u");
+ options.AudioFileExtensions.Remove(".wpl");
+
+ if (!libraryOptions.EnableArchiveMediaFiles)
+ {
+ options.AudioFileExtensions.Remove(".rar");
+ options.AudioFileExtensions.Remove(".zip");
+ }
+
+ if (!libraryOptions.EnableArchiveMediaFiles)
+ {
+ options.VideoFileExtensions.Remove(".rar");
+ options.VideoFileExtensions.Remove(".zip");
+ }
+
+ return options;
+ }
+
+ public ItemLookupInfo ParseName(string name)
+ {
+ var resolver = new VideoResolver(GetNamingOptions(), new PatternsLogger());
+
+ var result = resolver.CleanDateTime(name);
+ var cleanName = resolver.CleanString(result.Name);
+
+ return new ItemLookupInfo
+ {
+ Name = cleanName.Name,
+ Year = result.Year
+ };
+ }
+
+ public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ {
+ var files = owner.DetectIsInMixedFolder() ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
+ .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => _fileSystem.GetFiles(i.FullName, false))
+ .ToList();
+
+ var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger());
+
+ var videos = videoListResolver.Resolve(fileSystemChildren);
+
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+
+ if (currentVideo != null)
+ {
+ files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ }
+
+ var resolvers = new IItemResolver[]
+ {
+ new GenericVideoResolver<Trailer>(this)
+ };
+
+ return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
+ .OfType<Trailer>()
+ .Select(video =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = GetItemById(video.Id) as Trailer;
+
+ if (dbItem != null)
+ {
+ video = dbItem;
+ }
+
+ video.ExtraType = ExtraType.Trailer;
+ video.TrailerTypes = new List<TrailerType> { TrailerType.LocalTrailer };
+
+ return video;
+
+ // Sort them so that the list can be easily compared for changes
+ }).OrderBy(i => i.Path).ToList();
+ }
+
+ public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ {
+ var files = fileSystemChildren.Where(i => i.IsDirectory)
+ .Where(i => string.Equals(i.Name, "extras", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, "specials", StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => _fileSystem.GetFiles(i.FullName, false))
+ .ToList();
+
+ var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger());
+
+ var videos = videoListResolver.Resolve(fileSystemChildren);
+
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+
+ if (currentVideo != null)
+ {
+ files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ }
+
+ return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
+ .OfType<Video>()
+ .Select(video =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = GetItemById(video.Id) as Video;
+
+ if (dbItem != null)
+ {
+ video = dbItem;
+ }
+
+ SetExtraTypeFromFilename(video);
+
+ return video;
+
+ // Sort them so that the list can be easily compared for changes
+ }).OrderBy(i => i.Path).ToList();
+ }
+
+ public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
+ {
+ if (ownerItem != null)
+ {
+ var libraryOptions = GetLibraryOptions(ownerItem);
+ if (libraryOptions != null)
+ {
+ foreach (var pathInfo in libraryOptions.PathInfos)
+ {
+ if (string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
+ {
+ continue;
+ }
+
+ var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
+ if (substitutionResult.Item2)
+ {
+ return substitutionResult.Item1;
+ }
+ }
+ }
+ }
+
+ var metadataPath = ConfigurationManager.Configuration.MetadataPath;
+ var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath;
+
+ if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
+ {
+ var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
+ if (metadataSubstitutionResult.Item2)
+ {
+ return metadataSubstitutionResult.Item1;
+ }
+ }
+
+ foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
+ {
+ var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
+ if (substitutionResult.Item2)
+ {
+ return substitutionResult.Item1;
+ }
+ }
+
+ return path;
+ }
+
+ public string SubstitutePath(string path, string from, string to)
+ {
+ return SubstitutePathInternal(path, from, to).Item1;
+ }
+
+ private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+ if (string.IsNullOrWhiteSpace(from))
+ {
+ throw new ArgumentNullException("from");
+ }
+ if (string.IsNullOrWhiteSpace(to))
+ {
+ throw new ArgumentNullException("to");
+ }
+
+ from = from.Trim();
+ to = to.Trim();
+
+ var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
+ var changed = false;
+
+ if (!string.Equals(newPath, path))
+ {
+ if (to.IndexOf('/') != -1)
+ {
+ newPath = newPath.Replace('\\', '/');
+ }
+ else
+ {
+ newPath = newPath.Replace('/', '\\');
+ }
+
+ changed = true;
+ }
+
+ return new Tuple<string, bool>(newPath, changed);
+ }
+
+ private void SetExtraTypeFromFilename(Video item)
+ {
+ var resolver = new ExtraResolver(GetNamingOptions(), new PatternsLogger(), new RegexProvider());
+
+ var result = resolver.GetExtraInfo(item.Path);
+
+ if (string.Equals(result.ExtraType, "deletedscene", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.DeletedScene;
+ }
+ else if (string.Equals(result.ExtraType, "behindthescenes", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.BehindTheScenes;
+ }
+ else if (string.Equals(result.ExtraType, "interview", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.Interview;
+ }
+ else if (string.Equals(result.ExtraType, "scene", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.Scene;
+ }
+ else if (string.Equals(result.ExtraType, "sample", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.Sample;
+ }
+ else
+ {
+ item.ExtraType = ExtraType.Clip;
+ }
+ }
+
+ public List<PersonInfo> GetPeople(InternalPeopleQuery query)
+ {
+ return ItemRepository.GetPeople(query);
+ }
+
+ public List<PersonInfo> GetPeople(BaseItem item)
+ {
+ if (item.SupportsPeople)
+ {
+ var people = GetPeople(new InternalPeopleQuery
+ {
+ ItemId = item.Id
+ });
+
+ if (people.Count > 0)
+ {
+ return people;
+ }
+ }
+
+ return new List<PersonInfo>();
+ }
+
+ public List<Person> GetPeopleItems(InternalPeopleQuery query)
+ {
+ return ItemRepository.GetPeopleNames(query).Select(i =>
+ {
+ try
+ {
+ return GetPerson(i);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting person", ex);
+ return null;
+ }
+
+ }).Where(i => i != null).ToList();
+ }
+
+ public List<string> GetPeopleNames(InternalPeopleQuery query)
+ {
+ return ItemRepository.GetPeopleNames(query);
+ }
+
+ public Task UpdatePeople(BaseItem item, List<PersonInfo> people)
+ {
+ if (!item.SupportsPeople)
+ {
+ return Task.FromResult(true);
+ }
+
+ return ItemRepository.UpdatePeople(item.Id, people);
+ }
+
+ private readonly SemaphoreSlim _dynamicImageResourcePool = new SemaphoreSlim(1, 1);
+ public async Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex)
+ {
+ foreach (var url in image.Path.Split('|'))
+ {
+ try
+ {
+ _logger.Debug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
+
+ await _providerManagerFactory().SaveImage(item, url, _dynamicImageResourcePool, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
+
+ var newImage = item.GetImageInfo(image.Type, imageIndex);
+
+ if (newImage != null)
+ {
+ newImage.IsPlaceholder = image.IsPlaceholder;
+ }
+
+ await item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+ return item.GetImageInfo(image.Type, imageIndex);
+ }
+ catch (HttpException ex)
+ {
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ {
+ continue;
+ }
+ throw;
+ }
+ }
+
+ // Remove this image to prevent it from retrying over and over
+ item.RemoveImage(image);
+ await item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+ throw new InvalidOperationException();
+ }
+
+ public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ name = _fileSystem.GetValidFilename(name);
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ var virtualFolderPath = Path.Combine(rootFolderPath, name);
+ while (_fileSystem.DirectoryExists(virtualFolderPath))
+ {
+ name += "1";
+ virtualFolderPath = Path.Combine(rootFolderPath, name);
+ }
+
+ var mediaPathInfos = options.PathInfos;
+ if (mediaPathInfos != null)
+ {
+ var invalidpath = mediaPathInfos.FirstOrDefault(i => !_fileSystem.DirectoryExists(i.Path));
+ if (invalidpath != null)
+ {
+ throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
+ }
+ }
+
+ _libraryMonitorFactory().Stop();
+
+ try
+ {
+ _fileSystem.CreateDirectory(virtualFolderPath);
+
+ if (!string.IsNullOrEmpty(collectionType))
+ {
+ var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
+
+ _fileSystem.WriteAllBytes(path, new byte[] {});
+ }
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
+
+ if (mediaPathInfos != null)
+ {
+ foreach (var path in mediaPathInfos)
+ {
+ AddMediaPathInternal(name, path, false);
+ }
+ }
+ }
+ finally
+ {
+ Task.Run(() =>
+ {
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
+ {
+ ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ var task = Task.Delay(1000);
+ // Have to block here to allow exceptions to bubble
+ Task.WaitAll(task);
+
+ _libraryMonitorFactory().Start();
+ }
+ });
+ }
+ }
+
+ private bool ValidateNetworkPath(string path)
+ {
+ //if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ //{
+ // // We can't validate protocol-based paths, so just allow them
+ // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
+ // {
+ // return _fileSystem.DirectoryExists(path);
+ // }
+ //}
+
+ // Without native support for unc, we cannot validate this when running under mono
+ return true;
+ }
+
+ private const string ShortcutFileExtension = ".mblink";
+ public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
+ {
+ AddMediaPathInternal(virtualFolderName, pathInfo, true);
+ }
+
+ private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
+ {
+ if (pathInfo == null)
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var path = pathInfo.Path;
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ if (!_fileSystem.DirectoryExists(path))
+ {
+ throw new FileNotFoundException("The path does not exist.");
+ }
+
+ if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
+ {
+ throw new FileNotFoundException("The network path does not exist.");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+ var shortcutFilename = _fileSystem.GetFileNameWithoutExtension(path);
+
+ var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+
+ while (_fileSystem.FileExists(lnk))
+ {
+ shortcutFilename += "1";
+ lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+ }
+
+ _fileSystem.CreateShortcut(lnk, path);
+
+ RemoveContentTypeOverrides(path);
+
+ if (saveLibraryOptions)
+ {
+ var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
+
+ var list = libraryOptions.PathInfos.ToList();
+ list.Add(pathInfo);
+ libraryOptions.PathInfos = list.ToArray();
+
+ SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
+ }
+ }
+
+ public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
+ {
+ if (pathInfo == null)
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
+ {
+ throw new FileNotFoundException("The network path does not exist.");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+ var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
+
+ SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
+
+ var list = libraryOptions.PathInfos.ToList();
+ foreach (var originalPathInfo in list)
+ {
+ if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal))
+ {
+ originalPathInfo.NetworkPath = pathInfo.NetworkPath;
+ break;
+ }
+ }
+
+ libraryOptions.PathInfos = list.ToArray();
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
+ }
+
+ private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
+ {
+ var topLibraryFolders = GetUserRootFolder().Children.ToList();
+ var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders);
+
+ if (info.Locations.Count > 0 && info.Locations.Count != options.PathInfos.Length)
+ {
+ var list = options.PathInfos.ToList();
+
+ foreach (var location in info.Locations)
+ {
+ if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
+ {
+ list.Add(new MediaPathInfo
+ {
+ Path = location
+ });
+ }
+ }
+
+ options.PathInfos = list.ToArray();
+ }
+ }
+
+ public void RemoveVirtualFolder(string name, bool refreshLibrary)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ var path = Path.Combine(rootFolderPath, name);
+
+ if (!_fileSystem.DirectoryExists(path))
+ {
+ throw new FileNotFoundException("The media folder does not exist");
+ }
+
+ _libraryMonitorFactory().Stop();
+
+ try
+ {
+ _fileSystem.DeleteDirectory(path, true);
+ }
+ finally
+ {
+ Task.Run(() =>
+ {
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
+ {
+ ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ var task = Task.Delay(1000);
+ // Have to block here to allow exceptions to bubble
+ Task.WaitAll(task);
+
+ _libraryMonitorFactory().Start();
+ }
+ });
+ }
+ }
+
+ private void RemoveContentTypeOverrides(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var removeList = new List<NameValuePair>();
+
+ foreach (var contentType in ConfigurationManager.Configuration.ContentTypes)
+ {
+ if (string.Equals(path, contentType.Name, StringComparison.OrdinalIgnoreCase)
+ || _fileSystem.ContainsSubPath(path, contentType.Name))
+ {
+ removeList.Add(contentType);
+ }
+ }
+
+ if (removeList.Count > 0)
+ {
+ ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes
+ .Except(removeList)
+ .ToArray();
+
+ ConfigurationManager.SaveConfiguration();
+ }
+ }
+
+ public void RemoveMediaPath(string virtualFolderName, string mediaPath)
+ {
+ if (string.IsNullOrWhiteSpace(mediaPath))
+ {
+ throw new ArgumentNullException("mediaPath");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+ if (!_fileSystem.DirectoryExists(virtualFolderPath))
+ {
+ throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+ }
+
+ var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
+ .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
+
+ if (!string.IsNullOrEmpty(shortcut))
+ {
+ _fileSystem.DeleteFile(shortcut);
+ }
+
+ var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
+
+ libraryOptions.PathInfos = libraryOptions
+ .PathInfos
+ .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
+ .ToArray();
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs
new file mode 100644
index 000000000..7424ed5e5
--- /dev/null
+++ b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs
@@ -0,0 +1,102 @@
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class LocalTrailerPostScanTask : ILibraryPostScanTask
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IChannelManager _channelManager;
+
+ public LocalTrailerPostScanTask(ILibraryManager libraryManager, IChannelManager channelManager)
+ {
+ _libraryManager = libraryManager;
+ _channelManager = channelManager;
+ }
+
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(BoxSet).Name, typeof(Game).Name, typeof(Movie).Name, typeof(Series).Name },
+ Recursive = true
+
+ }).OfType<IHasTrailers>().ToList();
+
+ var trailerTypes = Enum.GetNames(typeof(TrailerType))
+ .Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true))
+ .Except(new[] { TrailerType.LocalTrailer })
+ .ToArray();
+
+ var trailers = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Trailer).Name },
+ TrailerTypes = trailerTypes,
+ Recursive = true
+
+ }).ToArray();
+
+ var numComplete = 0;
+
+ foreach (var item in items)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await AssignTrailers(item, trailers).ConfigureAwait(false);
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= items.Count;
+ progress.Report(percent * 100);
+ }
+
+ progress.Report(100);
+ }
+
+ private async Task AssignTrailers(IHasTrailers item, BaseItem[] channelTrailers)
+ {
+ if (item is Game)
+ {
+ return;
+ }
+
+ var imdbId = item.GetProviderId(MetadataProviders.Imdb);
+ var tmdbId = item.GetProviderId(MetadataProviders.Tmdb);
+
+ var trailers = channelTrailers.Where(i =>
+ {
+ if (!string.IsNullOrWhiteSpace(imdbId) &&
+ string.Equals(imdbId, i.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ if (!string.IsNullOrWhiteSpace(tmdbId) &&
+ string.Equals(tmdbId, i.GetProviderId(MetadataProviders.Tmdb), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ return false;
+ });
+
+ var trailerIds = trailers.Select(i => i.Id)
+ .ToList();
+
+ if (!trailerIds.SequenceEqual(item.RemoteTrailerIds))
+ {
+ item.RemoteTrailerIds = trailerIds;
+
+ var baseItem = (BaseItem)item;
+ await baseItem.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
new file mode 100644
index 000000000..93c406ebc
--- /dev/null
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -0,0 +1,651 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Threading;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class MediaSourceManager : IMediaSourceManager, IDisposable
+ {
+ private readonly IItemRepository _itemRepo;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+
+ private IMediaSourceProvider[] _providers;
+ private readonly ILogger _logger;
+ private readonly IUserDataManager _userDataManager;
+ private readonly ITimerFactory _timerFactory;
+
+ public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory)
+ {
+ _itemRepo = itemRepo;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _jsonSerializer = jsonSerializer;
+ _fileSystem = fileSystem;
+ _userDataManager = userDataManager;
+ _timerFactory = timerFactory;
+ }
+
+ public void AddParts(IEnumerable<IMediaSourceProvider> providers)
+ {
+ _providers = providers.ToArray();
+ }
+
+ public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query)
+ {
+ var list = _itemRepo.GetMediaStreams(query)
+ .ToList();
+
+ foreach (var stream in list)
+ {
+ stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
+ }
+
+ return list;
+ }
+
+ private bool StreamSupportsExternalStream(MediaStream stream)
+ {
+ if (stream.IsExternal)
+ {
+ return true;
+ }
+
+ if (stream.IsTextSubtitleStream)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId)
+ {
+ var list = GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = new Guid(mediaSourceId)
+ });
+
+ return GetMediaStreamsForItem(list);
+ }
+
+ public IEnumerable<MediaStream> GetMediaStreams(Guid itemId)
+ {
+ var list = GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = itemId
+ });
+
+ return GetMediaStreamsForItem(list);
+ }
+
+ private IEnumerable<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams)
+ {
+ var list = streams.ToList();
+
+ var subtitleStreams = list
+ .Where(i => i.Type == MediaStreamType.Subtitle)
+ .ToList();
+
+ if (subtitleStreams.Count > 0)
+ {
+ foreach (var subStream in subtitleStreams)
+ {
+ subStream.SupportsExternalStream = StreamSupportsExternalStream(subStream);
+ }
+ }
+
+ return list;
+ }
+
+ public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken)
+ {
+ var item = _libraryManager.GetItemById(id);
+
+ var hasMediaSources = (IHasMediaSources)item;
+ User user = null;
+
+ if (!string.IsNullOrWhiteSpace(userId))
+ {
+ user = _userManager.GetUserById(userId);
+ }
+
+ var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user);
+ var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false);
+
+ var list = new List<MediaSourceInfo>();
+
+ list.AddRange(mediaSources);
+
+ foreach (var source in dynamicMediaSources)
+ {
+ if (user != null)
+ {
+ SetUserProperties(hasMediaSources, source, user);
+ }
+ if (source.Protocol == MediaProtocol.File)
+ {
+ // TODO: Path substitution
+ if (!_fileSystem.FileExists(source.Path))
+ {
+ source.SupportsDirectStream = false;
+ }
+ }
+ else if (source.Protocol == MediaProtocol.Http)
+ {
+ // TODO: Allow this when the source is plain http, e.g. not HLS or Mpeg Dash
+ source.SupportsDirectStream = false;
+ }
+ else
+ {
+ source.SupportsDirectStream = false;
+ }
+
+ list.Add(source);
+ }
+
+ foreach (var source in list)
+ {
+ if (user != null)
+ {
+ if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!user.Policy.EnableAudioPlaybackTranscoding)
+ {
+ source.SupportsTranscoding = false;
+ }
+ }
+ }
+ }
+
+ return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder);
+ }
+
+ private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
+ {
+ var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
+ var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ return results.SelectMany(i => i.ToList());
+ }
+
+ private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var sources = await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false);
+ var list = sources.ToList();
+
+ foreach (var mediaSource in list)
+ {
+ SetKeyProperties(provider, mediaSource);
+ }
+
+ return list;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting media sources", ex);
+ return new List<MediaSourceInfo>();
+ }
+ }
+
+ private void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
+ {
+ var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter;
+
+ if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ mediaSource.OpenToken = prefix + mediaSource.OpenToken;
+ }
+
+ if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId;
+ }
+ }
+
+ public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(liveStreamId))
+ {
+ return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
+ }
+ //await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ //try
+ //{
+ // var stream = _openStreams.Values.FirstOrDefault(i => string.Equals(i.MediaSource.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
+
+ // if (stream != null)
+ // {
+ // return stream.MediaSource;
+ // }
+ //}
+ //finally
+ //{
+ // _liveStreamSemaphore.Release();
+ //}
+
+ var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video },
+ CancellationToken.None).ConfigureAwait(false);
+
+ return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ public IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (!(item is Video))
+ {
+ return item.GetMediaSources(enablePathSubstitution);
+ }
+
+ var sources = item.GetMediaSources(enablePathSubstitution).ToList();
+
+ if (user != null)
+ {
+ foreach (var source in sources)
+ {
+ SetUserProperties(item, source, user);
+ }
+ }
+
+ return sources;
+ }
+
+ private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user)
+ {
+ var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item);
+
+ var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections;
+
+ SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
+ SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
+ }
+
+ private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
+ {
+ if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+ {
+ var index = userData.SubtitleStreamIndex.Value;
+ // Make sure the saved index is still valid
+ if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
+ {
+ source.DefaultSubtitleStreamIndex = index;
+ return;
+ }
+ }
+
+ var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
+ ? new List<string>() : new List<string> { user.Configuration.SubtitleLanguagePreference };
+
+ var defaultAudioIndex = source.DefaultAudioStreamIndex;
+ var audioLangage = defaultAudioIndex == null
+ ? null
+ : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
+
+ source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
+ preferredSubs,
+ user.Configuration.SubtitleMode,
+ audioLangage);
+
+ MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
+ user.Configuration.SubtitleMode, audioLangage);
+ }
+
+ private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
+ {
+ if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
+ {
+ var index = userData.AudioStreamIndex.Value;
+ // Make sure the saved index is still valid
+ if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
+ {
+ source.DefaultAudioStreamIndex = index;
+ return;
+ }
+ }
+
+ var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+ ? new string[] { }
+ : new[] { user.Configuration.AudioLanguagePreference };
+
+ source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+ }
+
+ private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+ {
+ return sources.OrderBy(i =>
+ {
+ if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile)
+ {
+ return 0;
+ }
+
+ return 1;
+
+ }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
+ .ThenByDescending(i =>
+ {
+ var stream = i.VideoStream;
+
+ return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+ })
+ .ToList();
+ }
+
+ private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase);
+ private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
+ public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken)
+ {
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var tuple = GetProvider(request.OpenToken);
+ var provider = tuple.Item1;
+
+ var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+
+ var mediaSource = mediaSourceTuple.Item1;
+
+ if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
+ {
+ throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name));
+ }
+
+ SetKeyProperties(provider, mediaSource);
+
+ var info = new LiveStreamInfo
+ {
+ Date = DateTime.UtcNow,
+ EnableCloseTimer = enableAutoClose,
+ Id = mediaSource.LiveStreamId,
+ MediaSource = mediaSource,
+ DirectStreamProvider = mediaSourceTuple.Item2
+ };
+
+ _openStreams[mediaSource.LiveStreamId] = info;
+
+ if (enableAutoClose)
+ {
+ StartCloseTimer();
+ }
+
+ var json = _jsonSerializer.SerializeToString(mediaSource);
+ _logger.Debug("Live stream opened: " + json);
+ var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+
+ if (!string.IsNullOrWhiteSpace(request.UserId))
+ {
+ var user = _userManager.GetUserById(request.UserId);
+ var item = string.IsNullOrWhiteSpace(request.ItemId)
+ ? null
+ : _libraryManager.GetItemById(request.ItemId);
+ SetUserProperties(item, clone, user);
+ }
+
+ return new LiveStreamResponse
+ {
+ MediaSource = clone
+ };
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ _logger.Debug("Getting already opened live stream {0}", id);
+
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ LiveStreamInfo info;
+ if (_openStreams.TryGetValue(id, out info))
+ {
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider);
+ }
+ else
+ {
+ throw new ResourceNotFoundException();
+ }
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
+ {
+ var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
+ return result.Item1;
+ }
+
+ public async Task PingLiveStream(string id, CancellationToken cancellationToken)
+ {
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ LiveStreamInfo info;
+ if (_openStreams.TryGetValue(id, out info))
+ {
+ info.Date = DateTime.UtcNow;
+ }
+ else
+ {
+ _logger.Error("Failed to ping live stream {0}", id);
+ }
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId)
+ {
+ _logger.Info("Closing live stream {0} with provider {1}", streamId, provider.GetType().Name);
+
+ try
+ {
+ await provider.CloseMediaSource(streamId).ConfigureAwait(false);
+ }
+ catch (NotImplementedException)
+ {
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live stream {0}", ex, streamId);
+ }
+ }
+
+ public async Task CloseLiveStream(string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ LiveStreamInfo current;
+
+ if (_openStreams.TryGetValue(id, out current))
+ {
+ _openStreams.Remove(id);
+ current.Closed = true;
+
+ if (current.MediaSource.RequiresClosing)
+ {
+ var tuple = GetProvider(id);
+
+ await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false);
+ }
+
+ if (_openStreams.Count == 0)
+ {
+ StopCloseTimer();
+ }
+ }
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+ private const char LiveStreamIdDelimeter = '_';
+
+ private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new ArgumentException("key");
+ }
+
+ var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
+
+ var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), keys[0], StringComparison.OrdinalIgnoreCase));
+
+ var splitIndex = key.IndexOf(LiveStreamIdDelimeter);
+ var keyId = key.Substring(splitIndex + 1);
+
+ return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+ }
+
+ private ITimer _closeTimer;
+ private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(180);
+
+ private void StartCloseTimer()
+ {
+ StopCloseTimer();
+
+ _closeTimer = _timerFactory.Create(CloseTimerCallback, null, _openStreamMaxAge, _openStreamMaxAge);
+ }
+
+ private void StopCloseTimer()
+ {
+ var timer = _closeTimer;
+
+ if (timer != null)
+ {
+ _closeTimer = null;
+ timer.Dispose();
+ }
+ }
+
+ private async void CloseTimerCallback(object state)
+ {
+ List<LiveStreamInfo> infos;
+ await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ infos = _openStreams
+ .Values
+ .Where(i => i.EnableCloseTimer && DateTime.UtcNow - i.Date > _openStreamMaxAge)
+ .ToList();
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+
+ foreach (var info in infos)
+ {
+ if (!info.Closed)
+ {
+ try
+ {
+ await CloseLiveStream(info.Id).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing media source", ex);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ StopCloseTimer();
+ Dispose(true);
+ }
+
+ private readonly object _disposeLock = new object();
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ lock (_disposeLock)
+ {
+ foreach (var key in _openStreams.Keys.ToList())
+ {
+ var task = CloseLiveStream(key);
+
+ Task.WaitAll(task);
+ }
+ }
+ }
+ }
+
+ private class LiveStreamInfo
+ {
+ public DateTime Date;
+ public bool EnableCloseTimer;
+ public string Id;
+ public bool Closed;
+ public MediaSourceInfo MediaSource;
+ public IDirectStreamProvider DirectStreamProvider;
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
new file mode 100644
index 000000000..7669dd0bf
--- /dev/null
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -0,0 +1,157 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class MusicManager : IMusicManager
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicManager(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user)
+ {
+ var list = new List<Audio>
+ {
+ item
+ };
+
+ return list.Concat(GetInstantMixFromGenres(item.Genres, user));
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromArtist(MusicArtist artist, User user)
+ {
+ var genres = user.RootFolder
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name }
+ })
+ .Cast<Audio>()
+ .Where(i => i.HasAnyArtist(artist.Name))
+ .SelectMany(i => i.Genres)
+ .Concat(artist.Genres)
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user)
+ {
+ var genres = item
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name }
+ })
+ .Cast<Audio>()
+ .SelectMany(i => i.Genres)
+ .Concat(item.Genres)
+ .DistinctNames();
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromFolder(Folder item, User user)
+ {
+ var genres = item
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] {typeof(Audio).Name}
+ })
+ .Cast<Audio>()
+ .SelectMany(i => i.Genres)
+ .Concat(item.Genres)
+ .DistinctNames();
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromPlaylist(Playlist item, User user)
+ {
+ var genres = item
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name }
+ })
+ .Cast<Audio>()
+ .SelectMany(i => i.Genres)
+ .Concat(item.Genres)
+ .DistinctNames();
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user)
+ {
+ var genreList = genres.ToList();
+
+ var inputItems = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name },
+
+ Genres = genreList.ToArray()
+
+ });
+
+ var genresDictionary = genreList.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+
+ return inputItems
+ .Cast<Audio>()
+ .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey)))
+ .Where(i => i.Item2 > 0)
+ .OrderByDescending(i => i.Item2)
+ .ThenBy(i => Guid.NewGuid())
+ .Select(i => i.Item1)
+ .Take(100)
+ .OrderBy(i => Guid.NewGuid());
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromItem(BaseItem item, User user)
+ {
+ var genre = item as MusicGenre;
+ if (genre != null)
+ {
+ return GetInstantMixFromGenres(new[] { item.Name }, user);
+ }
+
+ var playlist = item as Playlist;
+ if (playlist != null)
+ {
+ return GetInstantMixFromPlaylist(playlist, user);
+ }
+
+ var album = item as MusicAlbum;
+ if (album != null)
+ {
+ return GetInstantMixFromAlbum(album, user);
+ }
+
+ var artist = item as MusicArtist;
+ if (artist != null)
+ {
+ return GetInstantMixFromArtist(artist, user);
+ }
+
+ var song = item as Audio;
+ if (song != null)
+ {
+ return GetInstantMixFromSong(song, user);
+ }
+
+ var folder = item as Folder;
+ if (folder != null)
+ {
+ return GetInstantMixFromFolder(folder, user);
+ }
+
+ return new Audio[] { };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
new file mode 100644
index 000000000..28ed2f53c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace Emby.Server.Implementations.Library
+{
+ public static class PathExtensions
+ {
+ /// <summary>
+ /// Gets the attribute value.
+ /// </summary>
+ /// <param name="str">The STR.</param>
+ /// <param name="attrib">The attrib.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">attrib</exception>
+ public static string GetAttributeValue(this string str, string attrib)
+ {
+ if (string.IsNullOrEmpty(str))
+ {
+ throw new ArgumentNullException("str");
+ }
+
+ if (string.IsNullOrEmpty(attrib))
+ {
+ throw new ArgumentNullException("attrib");
+ }
+
+ string srch = "[" + attrib + "=";
+ int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ if (start > -1)
+ {
+ start += srch.Length;
+ int end = str.IndexOf(']', start);
+ return str.Substring(start, end - start);
+ }
+ // for imdbid we also accept pattern matching
+ if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
+ {
+ var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase);
+ return m.Success ? m.Value : null;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
new file mode 100644
index 000000000..1d3cacc1d
--- /dev/null
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -0,0 +1,183 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Class ResolverHelper
+ /// </summary>
+ public static class ResolverHelper
+ {
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <exception cref="System.ArgumentException">Item must have a path</exception>
+ public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService)
+ {
+ // This version of the below method has no ItemResolveArgs, so we have to require the path already being set
+ if (string.IsNullOrWhiteSpace(item.Path))
+ {
+ throw new ArgumentException("Item must have a Path");
+ }
+
+ // If the resolver didn't specify this
+ if (parent != null)
+ {
+ item.SetParent(parent);
+ }
+
+ item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
+
+ item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.GetParents().Any(i => i.IsLocked);
+
+ // Make sure DateCreated and DateModified have values
+ var fileInfo = directoryService.GetFile(item.Path);
+ SetDateCreated(item, fileSystem, fileInfo);
+
+ EnsureName(item, fileInfo);
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ public static void SetInitialItemValues(BaseItem item, ItemResolveArgs args, IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ // If the resolver didn't specify this
+ if (string.IsNullOrEmpty(item.Path))
+ {
+ item.Path = args.Path;
+ }
+
+ // If the resolver didn't specify this
+ if (args.Parent != null)
+ {
+ item.SetParent(args.Parent);
+ }
+
+ item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
+
+ // Make sure the item has a name
+ EnsureName(item, args.FileInfo);
+
+ item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.GetParents().Any(i => i.IsLocked);
+
+ // Make sure DateCreated and DateModified have values
+ EnsureDates(fileSystem, item, args);
+ }
+
+ /// <summary>
+ /// Ensures the name.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="fileInfo">The file information.</param>
+ private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo)
+ {
+ // If the subclass didn't supply a name, add it here
+ if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
+ {
+ item.Name = GetDisplayName(fileInfo.Name, fileInfo.IsDirectory);
+ }
+ }
+
+ /// <summary>
+ /// Gets the display name.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
+ /// <returns>System.String.</returns>
+ private static string GetDisplayName(string path, bool isDirectory)
+ {
+ return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
+ }
+
+ /// <summary>
+ /// The MB name regex
+ /// </summary>
+ private static readonly Regex MbNameRegex = new Regex(@"(\[.*?\])");
+
+ internal static string StripBrackets(string inputString)
+ {
+ var output = MbNameRegex.Replace(inputString, string.Empty).Trim();
+ return Regex.Replace(output, @"\s+", " ");
+ }
+
+ /// <summary>
+ /// Ensures DateCreated and DateModified have values
+ /// </summary>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
+ {
+ if (fileSystem == null)
+ {
+ throw new ArgumentNullException("fileSystem");
+ }
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (args == null)
+ {
+ throw new ArgumentNullException("args");
+ }
+
+ // See if a different path came out of the resolver than what went in
+ if (!string.Equals(args.Path, item.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null;
+
+ if (childData != null)
+ {
+ SetDateCreated(item, fileSystem, childData);
+ }
+ else
+ {
+ var fileData = fileSystem.GetFileSystemInfo(item.Path);
+
+ if (fileData.Exists)
+ {
+ SetDateCreated(item, fileSystem, fileData);
+ }
+ }
+ }
+ else
+ {
+ SetDateCreated(item, fileSystem, args.FileInfo);
+ }
+ }
+
+ private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info)
+ {
+ var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
+
+ if (config.UseFileCreationTimeForDateAdded)
+ {
+ item.DateCreated = fileSystem.GetCreationTimeUtc(info);
+ }
+ else
+ {
+ item.DateCreated = DateTime.UtcNow;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
new file mode 100644
index 000000000..d8805355a
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -0,0 +1,68 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Audio
+{
+ /// <summary>
+ /// Class AudioResolver
+ /// </summary>
+ public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public AudioResolver(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Entities.Audio.Audio.</returns>
+ protected override MediaBrowser.Controller.Entities.Audio.Audio Resolve(ItemResolveArgs args)
+ {
+ // Return audio if the path is a file and has a matching extension
+
+ if (!args.IsDirectory)
+ {
+ var libraryOptions = args.GetLibraryOptions();
+
+ if (_libraryManager.IsAudioFile(args.Path, libraryOptions))
+ {
+ var collectionType = args.GetCollectionType();
+
+ var isMixed = string.IsNullOrWhiteSpace(collectionType);
+
+ // For conflicting extensions, give priority to videos
+ if (isMixed && _libraryManager.IsVideoFile(args.Path, libraryOptions))
+ {
+ return null;
+ }
+
+ var isStandalone = args.Parent == null;
+
+ if (isStandalone ||
+ string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) ||
+ isMixed)
+ {
+ return new MediaBrowser.Controller.Entities.Audio.Audio();
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
new file mode 100644
index 000000000..f8e105195
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -0,0 +1,173 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Naming.Audio;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Audio
+{
+ /// <summary>
+ /// Class MusicAlbumResolver
+ /// </summary>
+ public class MusicAlbumResolver : ItemResolver<MusicAlbum>
+ {
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Behind special folder resolver
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>MusicAlbum.</returns>
+ protected override MusicAlbum Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory) return null;
+
+ // Avoid mis-identifying top folders
+ if (args.HasParent<MusicAlbum>()) return null;
+ if (args.Parent.IsRoot) return null;
+
+ var collectionType = args.GetCollectionType();
+
+ var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+
+ // If there's a collection type and it's not music, don't allow it.
+ if (!isMusicMediaFolder)
+ {
+ return null;
+ }
+
+ return IsMusicAlbum(args) ? new MusicAlbum() : null;
+ }
+
+
+ /// <summary>
+ /// Determine if the supplied file data points to a music album
+ /// </summary>
+ public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions)
+ {
+ return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, libraryOptions, _libraryManager);
+ }
+
+ /// <summary>
+ /// Determine if the supplied resolve args should be considered a music album
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns>
+ private bool IsMusicAlbum(ItemResolveArgs args)
+ {
+ // Args points to an album if parent is an Artist folder or it directly contains music
+ if (args.IsDirectory)
+ {
+ //if (args.Parent is MusicArtist) return true; //saves us from testing children twice
+ if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, args.GetLibraryOptions(), _libraryManager)) return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determine if the supplied list contains what we should consider music
+ /// </summary>
+ private bool ContainsMusic(IEnumerable<FileSystemMetadata> list,
+ bool allowSubfolders,
+ IDirectoryService directoryService,
+ ILogger logger,
+ IFileSystem fileSystem,
+ LibraryOptions libraryOptions,
+ ILibraryManager libraryManager)
+ {
+ var discSubfolderCount = 0;
+ var notMultiDisc = false;
+
+ foreach (var fileSystemInfo in list)
+ {
+ if (fileSystemInfo.IsDirectory)
+ {
+ if (allowSubfolders)
+ {
+ var path = fileSystemInfo.FullName;
+ var isMultiDisc = IsMultiDiscFolder(path, libraryOptions);
+
+ if (isMultiDisc)
+ {
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
+
+ if (hasMusic)
+ {
+ logger.Debug("Found multi-disc folder: " + path);
+ discSubfolderCount++;
+ }
+ }
+ else
+ {
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
+
+ if (hasMusic)
+ {
+ // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+ notMultiDisc = true;
+ }
+ }
+ }
+ }
+
+ var fullName = fileSystemInfo.FullName;
+
+ if (libraryManager.IsAudioFile(fullName, libraryOptions))
+ {
+ return true;
+ }
+ }
+
+ if (notMultiDisc)
+ {
+ return false;
+ }
+
+ return discSubfolderCount > 0;
+ }
+
+ private bool IsMultiDiscFolder(string path, LibraryOptions libraryOptions)
+ {
+ var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(libraryOptions);
+
+ var parser = new AlbumParser(namingOptions, new PatternsLogger());
+ var result = parser.ParseMultiPart(path);
+
+ return result.IsMultiPart;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
new file mode 100644
index 000000000..2971405b9
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -0,0 +1,94 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Audio
+{
+ /// <summary>
+ /// Class MusicArtistResolver
+ /// </summary>
+ public class MusicArtistResolver : ItemResolver<MusicArtist>
+ {
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+
+ public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _config = config;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Behind special folder resolver
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>MusicArtist.</returns>
+ protected override MusicArtist Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory) return null;
+
+ // Don't allow nested artists
+ if (args.HasParent<MusicArtist>() || args.HasParent<MusicAlbum>())
+ {
+ return null;
+ }
+
+ var collectionType = args.GetCollectionType();
+
+ var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+
+ // If there's a collection type and it's not music, it can't be a series
+ if (!isMusicMediaFolder)
+ {
+ return null;
+ }
+
+ if (args.ContainsFileSystemEntryByName("artist.nfo"))
+ {
+ return new MusicArtist();
+ }
+
+ if (_config.Configuration.EnableSimpleArtistDetection)
+ {
+ return null;
+ }
+
+ // Avoid mis-identifying top folders
+ if (args.Parent.IsRoot) return null;
+
+ var directoryService = args.DirectoryService;
+
+ var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
+
+ // If we contain an album assume we are an artist folder
+ return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
+ }
+
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
new file mode 100644
index 000000000..b7819eb68
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -0,0 +1,297 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Naming.Video;
+using System;
+using System.IO;
+using Emby.Server.Implementations.Logging;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video or Video subclass
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
+ where T : Video, new()
+ {
+ protected readonly ILibraryManager LibraryManager;
+
+ protected BaseVideoResolver(ILibraryManager libraryManager)
+ {
+ LibraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>`0.</returns>
+ protected override T Resolve(ItemResolveArgs args)
+ {
+ return ResolveVideo<T>(args, false);
+ }
+
+ /// <summary>
+ /// Resolves the video.
+ /// </summary>
+ /// <typeparam name="TVideoType">The type of the T video type.</typeparam>
+ /// <param name="args">The args.</param>
+ /// <param name="parseName">if set to <c>true</c> [parse name].</param>
+ /// <returns>``0.</returns>
+ protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
+ where TVideoType : Video, new()
+ {
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+
+ // If the path is a file check for a matching extensions
+ var parser = new MediaBrowser.Naming.Video.VideoResolver(namingOptions, new PatternsLogger());
+
+ if (args.IsDirectory)
+ {
+ TVideoType video = null;
+ VideoFileInfo videoInfo = null;
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in args.FileSystemChildren)
+ {
+ var filename = child.Name;
+
+ if (child.IsDirectory)
+ {
+ if (IsDvdDirectory(filename))
+ {
+ videoInfo = parser.ResolveDirectory(args.Path);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ video = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd,
+ ProductionYear = videoInfo.Year
+ };
+ break;
+ }
+ if (IsBluRayDirectory(filename))
+ {
+ videoInfo = parser.ResolveDirectory(args.Path);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ video = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay,
+ ProductionYear = videoInfo.Year
+ };
+ break;
+ }
+ }
+ else if (IsDvdFile(filename))
+ {
+ videoInfo = parser.ResolveDirectory(args.Path);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ video = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd,
+ ProductionYear = videoInfo.Year
+ };
+ break;
+ }
+ }
+
+ if (video != null)
+ {
+ video.Name = parseName ?
+ videoInfo.Name :
+ Path.GetFileName(args.Path);
+
+ Set3DFormat(video, videoInfo);
+ }
+
+ return video;
+ }
+ else
+ {
+ var videoInfo = parser.Resolve(args.Path, false, false);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub)
+ {
+ var path = args.Path;
+
+ var video = new TVideoType
+ {
+ Path = path,
+ IsInMixedFolder = true,
+ ProductionYear = videoInfo.Year
+ };
+
+ SetVideoType(video, videoInfo);
+
+ video.Name = parseName ?
+ videoInfo.Name :
+ Path.GetFileNameWithoutExtension(args.Path);
+
+ Set3DFormat(video, videoInfo);
+
+ return video;
+ }
+ }
+
+ return null;
+ }
+
+ protected void SetVideoType(Video video, VideoFileInfo videoInfo)
+ {
+ var extension = Path.GetExtension(video.Path);
+ video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
+ VideoType.Iso :
+ VideoType.VideoFile;
+
+ video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ video.IsPlaceHolder = videoInfo.IsStub;
+
+ if (videoInfo.IsStub)
+ {
+ if (string.Equals(videoInfo.StubType, "dvd", StringComparison.OrdinalIgnoreCase))
+ {
+ video.VideoType = VideoType.Dvd;
+ }
+ else if (string.Equals(videoInfo.StubType, "hddvd", StringComparison.OrdinalIgnoreCase))
+ {
+ video.VideoType = VideoType.HdDvd;
+ video.IsHD = true;
+ }
+ else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase))
+ {
+ video.VideoType = VideoType.BluRay;
+ video.IsHD = true;
+ }
+ else if (string.Equals(videoInfo.StubType, "hdtv", StringComparison.OrdinalIgnoreCase))
+ {
+ video.IsHD = true;
+ }
+ }
+
+ SetIsoType(video);
+ }
+
+ protected void SetIsoType(Video video)
+ {
+ if (video.VideoType == VideoType.Iso)
+ {
+ if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ video.IsoType = IsoType.Dvd;
+ }
+ else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ video.IsoType = IsoType.BluRay;
+ }
+ }
+ }
+
+ protected void Set3DFormat(Video video, bool is3D, string format3D)
+ {
+ if (is3D)
+ {
+ if (string.Equals(format3D, "fsbs", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullSideBySide;
+ }
+ else if (string.Equals(format3D, "ftab", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullTopAndBottom;
+ }
+ else if (string.Equals(format3D, "hsbs", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals(format3D, "htab", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals(format3D, "sbs", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals(format3D, "sbs3d", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals(format3D, "tab", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals(format3D, "mvc", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.MVC;
+ }
+ }
+ }
+
+ protected void Set3DFormat(Video video, VideoFileInfo videoInfo)
+ {
+ Set3DFormat(video, videoInfo.Is3D, videoInfo.Format3D);
+ }
+
+ protected void Set3DFormat(Video video)
+ {
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+
+ var resolver = new Format3DParser(namingOptions, new PatternsLogger());
+ var result = resolver.Parse(video.Path);
+
+ Set3DFormat(video, result.Is3D, result.Format3D);
+ }
+
+ /// <summary>
+ /// Determines whether [is DVD directory] [the specified directory name].
+ /// </summary>
+ /// <param name="directoryName">Name of the directory.</param>
+ /// <returns><c>true</c> if [is DVD directory] [the specified directory name]; otherwise, <c>false</c>.</returns>
+ protected bool IsDvdDirectory(string directoryName)
+ {
+ return string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is DVD file] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [is DVD file] [the specified name]; otherwise, <c>false</c>.</returns>
+ protected bool IsDvdFile(string name)
+ {
+ return string.Equals(name, "video_ts.ifo", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is blu ray directory] [the specified directory name].
+ /// </summary>
+ /// <param name="directoryName">Name of the directory.</param>
+ /// <returns><c>true</c> if [is blu ray directory] [the specified directory name]; otherwise, <c>false</c>.</returns>
+ protected bool IsBluRayDirectory(string directoryName)
+ {
+ return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
new file mode 100644
index 000000000..5e73baa5c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -0,0 +1,56 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Class FolderResolver
+ /// </summary>
+ public class FolderResolver : FolderResolver<Folder>
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Folder.</returns>
+ protected override Folder Resolve(ItemResolveArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ return new Folder();
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Class FolderResolver
+ /// </summary>
+ /// <typeparam name="TItemType">The type of the T item type.</typeparam>
+ public abstract class FolderResolver<TItemType> : ItemResolver<TItemType>
+ where TItemType : Folder, new()
+ {
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
new file mode 100644
index 000000000..b4a37be5f
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -0,0 +1,62 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Class ItemResolver
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class ItemResolver<T> : IItemResolver
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>`0.</returns>
+ protected virtual T Resolve(ItemResolveArgs args)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public virtual ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.First;
+ }
+ }
+
+ /// <summary>
+ /// Sets initial values on the newly resolved item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected virtual void SetInitialItemValues(T item, ItemResolveArgs args)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem IItemResolver.ResolvePath(ItemResolveArgs args)
+ {
+ var item = Resolve(args);
+
+ if (item != null)
+ {
+ SetInitialItemValues(item, args);
+ }
+
+ return item;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
new file mode 100644
index 000000000..df441c5ed
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -0,0 +1,77 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Movies
+{
+ /// <summary>
+ /// Class BoxSetResolver
+ /// </summary>
+ public class BoxSetResolver : FolderResolver<BoxSet>
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BoxSet.</returns>
+ protected override BoxSet Resolve(ItemResolveArgs args)
+ {
+ // It's a boxset if all of the following conditions are met:
+ // Is a Directory
+ // Contains [boxset] in the path
+ if (args.IsDirectory)
+ {
+ var filename = Path.GetFileName(args.Path);
+
+ if (string.IsNullOrEmpty(filename))
+ {
+ return null;
+ }
+
+ if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ args.ContainsFileSystemEntryByName("collection.xml"))
+ {
+ return new BoxSet
+ {
+ Path = args.Path,
+ Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path))
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(BoxSet item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void SetProviderIdFromPath(BaseItem item)
+ {
+ //we need to only look at the name of this actual item (not parents)
+ var justName = Path.GetFileName(item.Path);
+
+ var id = justName.GetAttributeValue("tmdbid");
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, id);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
new file mode 100644
index 000000000..d8c8b2024
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -0,0 +1,541 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Naming.Video;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Movies
+{
+ /// <summary>
+ /// Class MovieResolver
+ /// </summary>
+ public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
+ {
+ public MovieResolver(ILibraryManager libraryManager)
+ : base(libraryManager)
+ {
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Give plugins a chance to catch iso's first
+ // Also since we have to loop through child files looking for videos,
+ // see if we can avoid some of that by letting other resolvers claim folders first
+ // Also run after series resolver
+ return ResolverPriority.Third;
+ }
+ }
+
+ public MultiItemResolverResult ResolveMultiple(Folder parent,
+ List<FileSystemMetadata> files,
+ string collectionType,
+ IDirectoryService directoryService)
+ {
+ var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+
+ if (result != null)
+ {
+ foreach (var item in result.Items)
+ {
+ SetInitialItemValues((Video)item, null);
+ }
+ }
+
+ return result;
+ }
+
+ private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+ List<FileSystemMetadata> files,
+ string collectionType,
+ IDirectoryService directoryService)
+ {
+ if (IsInvalid(parent, collectionType))
+ {
+ return null;
+ }
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return ResolveVideos<MusicVideo>(parent, files, directoryService, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ return ResolveVideos<Video>(parent, files, directoryService, false);
+ }
+
+ if (string.IsNullOrEmpty(collectionType))
+ {
+ // Owned items should just use the plain video type
+ if (parent == null)
+ {
+ return ResolveVideos<Video>(parent, files, directoryService, false);
+ }
+
+ if (parent is Series || parent.GetParents().OfType<Series>().Any())
+ {
+ return null;
+ }
+
+ return ResolveVideos<Movie>(parent, files, directoryService, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ return ResolveVideos<Movie>(parent, files, directoryService, true);
+ }
+
+ return null;
+ }
+
+ private MultiItemResolverResult ResolveVideos<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions)
+ where T : Video, new()
+ {
+ var files = new List<FileSystemMetadata>();
+ var videos = new List<BaseItem>();
+ var leftOver = new List<FileSystemMetadata>();
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in fileSystemEntries)
+ {
+ if (child.IsDirectory)
+ {
+ leftOver.Add(child);
+ }
+ else if (IsIgnored(child.Name))
+ {
+
+ }
+ else
+ {
+ files.Add(child);
+ }
+ }
+
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+
+ var resolver = new VideoListResolver(namingOptions, new PatternsLogger());
+ var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
+
+ var result = new MultiItemResolverResult
+ {
+ ExtraFiles = leftOver,
+ Items = videos
+ };
+
+ var isInMixedFolder = resolverResult.Count > 1;
+
+ foreach (var video in resolverResult)
+ {
+ var firstVideo = video.Files.First();
+
+ var videoItem = new T
+ {
+ Path = video.Files[0].Path,
+ IsInMixedFolder = isInMixedFolder,
+ ProductionYear = video.Year,
+ Name = video.Name,
+ AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToList(),
+ LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToList()
+ };
+
+ SetVideoType(videoItem, firstVideo);
+ Set3DFormat(videoItem, firstVideo);
+
+ result.Items.Add(videoItem);
+ }
+
+ result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
+
+ return result;
+ }
+
+ private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
+ {
+ return result.Any(i => ContainsFile(i, file));
+ }
+
+ private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
+ {
+ return result.Files.Any(i => ContainsFile(i, file)) ||
+ result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
+ result.Extras.Any(i => ContainsFile(i, file));
+ }
+
+ private bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
+ {
+ return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Video.</returns>
+ protected override Video Resolve(ItemResolveArgs args)
+ {
+ var collectionType = args.GetCollectionType();
+
+ if (IsInvalid(args.Parent, collectionType))
+ {
+ return null;
+ }
+
+ // Find movies with their own folders
+ if (args.IsDirectory)
+ {
+ var files = args.FileSystemChildren
+ .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
+ .ToList();
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<MusicVideo>(args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ }
+
+ if (string.IsNullOrEmpty(collectionType))
+ {
+ // Owned items will be caught by the plain video resolver
+ if (args.Parent == null)
+ {
+ //return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
+ return null;
+ }
+
+ if (args.HasParent<Series>())
+ {
+ return null;
+ }
+
+ {
+ return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ }
+ }
+
+ if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ }
+
+ return null;
+ }
+
+ // Owned items will be caught by the plain video resolver
+ if (args.Parent == null)
+ {
+ return null;
+ }
+
+ Video item = null;
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<MusicVideo>(args, false);
+ }
+
+ // To find a movie file, the collection type must be movies or boxsets
+ else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<Movie>(args, true);
+ }
+
+ else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<Video>(args, false);
+ }
+ else if (string.IsNullOrEmpty(collectionType))
+ {
+ if (args.HasParent<Series>())
+ {
+ return null;
+ }
+
+ item = ResolveVideo<Video>(args, false);
+ }
+
+ if (item != null)
+ {
+ item.IsInMixedFolder = true;
+ }
+
+ return item;
+ }
+
+ private bool IsIgnored(string filename)
+ {
+ // Ignore samples
+ var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("-", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("_", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("!", " ", StringComparison.OrdinalIgnoreCase);
+
+ if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(Video item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdsFromPath(item);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void SetProviderIdsFromPath(Video item)
+ {
+ if (item is Movie || item is MusicVideo)
+ {
+ //we need to only look at the name of this actual item (not parents)
+ var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath);
+
+ if (!string.IsNullOrWhiteSpace(justName))
+ {
+ // check for tmdb id
+ var tmdbid = justName.GetAttributeValue("tmdbid");
+
+ if (!string.IsNullOrWhiteSpace(tmdbid))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, tmdbid);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(item.Path))
+ {
+ // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
+ var imdbid = item.Path.GetAttributeValue("imdbid");
+
+ if (!string.IsNullOrWhiteSpace(imdbid))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, imdbid);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Finds a movie based on a child file system entries
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>Movie.</returns>
+ private T FindMovie<T>(string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool allowFilesAsFolders)
+ where T : Video, new()
+ {
+ var multiDiscFolders = new List<FileSystemMetadata>();
+
+ // Search for a folder rip
+ foreach (var child in fileSystemEntries)
+ {
+ var filename = child.Name;
+
+ if (child.IsDirectory)
+ {
+ if (IsDvdDirectory(filename))
+ {
+ var movie = new T
+ {
+ Path = path,
+ VideoType = VideoType.Dvd
+ };
+ Set3DFormat(movie);
+ return movie;
+ }
+ if (IsBluRayDirectory(filename))
+ {
+ var movie = new T
+ {
+ Path = path,
+ VideoType = VideoType.BluRay
+ };
+ Set3DFormat(movie);
+ return movie;
+ }
+
+ multiDiscFolders.Add(child);
+ }
+ else if (IsDvdFile(filename))
+ {
+ var movie = new T
+ {
+ Path = path,
+ VideoType = VideoType.Dvd
+ };
+ Set3DFormat(movie);
+ return movie;
+ }
+ }
+
+ if (allowFilesAsFolders)
+ {
+ // TODO: Allow GetMultiDiscMovie in here
+ var supportsMultiVersion = !string.Equals(collectionType, CollectionType.HomeVideos) &&
+ !string.Equals(collectionType, CollectionType.Photos) &&
+ !string.Equals(collectionType, CollectionType.MusicVideos);
+
+ var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion);
+
+ if (result.Items.Count == 1)
+ {
+ var movie = (T)result.Items[0];
+ movie.IsInMixedFolder = false;
+ movie.Name = Path.GetFileName(movie.ContainingFolderPath);
+ return movie;
+ }
+
+ if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
+ {
+ return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the multi disc movie.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="multiDiscFolders">The folders.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns>``0.</returns>
+ private T GetMultiDiscMovie<T>(List<FileSystemMetadata> multiDiscFolders, IDirectoryService directoryService)
+ where T : Video, new()
+ {
+ var videoTypes = new List<VideoType>();
+
+ var folderPaths = multiDiscFolders.Select(i => i.FullName).Where(i =>
+ {
+ var subFileEntries = directoryService.GetFileSystemEntries(i)
+ .ToList();
+
+ var subfolders = subFileEntries
+ .Where(e => e.IsDirectory)
+ .Select(d => d.Name)
+ .ToList();
+
+ if (subfolders.Any(IsDvdDirectory))
+ {
+ videoTypes.Add(VideoType.Dvd);
+ return true;
+ }
+ if (subfolders.Any(IsBluRayDirectory))
+ {
+ videoTypes.Add(VideoType.BluRay);
+ return true;
+ }
+
+ var subFiles = subFileEntries
+ .Where(e => !e.IsDirectory)
+ .Select(d => d.Name);
+
+ if (subFiles.Any(IsDvdFile))
+ {
+ videoTypes.Add(VideoType.Dvd);
+ return true;
+ }
+
+ return false;
+
+ }).OrderBy(i => i).ToList();
+
+ // If different video types were found, don't allow this
+ if (videoTypes.Distinct().Count() > 1)
+ {
+ return null;
+ }
+
+ if (folderPaths.Count == 0)
+ {
+ return null;
+ }
+
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var resolver = new StackResolver(namingOptions, new PatternsLogger());
+
+ var result = resolver.ResolveDirectories(folderPaths);
+
+ if (result.Stacks.Count != 1)
+ {
+ return null;
+ }
+
+ var returnVideo = new T
+ {
+ Path = folderPaths[0],
+
+ AdditionalParts = folderPaths.Skip(1).ToList(),
+
+ VideoType = videoTypes[0],
+
+ Name = result.Stacks[0].Name
+ };
+
+ SetIsoType(returnVideo);
+
+ return returnVideo;
+ }
+
+ private bool IsInvalid(Folder parent, string collectionType)
+ {
+ if (parent != null)
+ {
+ if (parent.IsRoot)
+ {
+ return true;
+ }
+ }
+
+ var validCollectionTypes = new[]
+ {
+ CollectionType.Movies,
+ CollectionType.HomeVideos,
+ CollectionType.MusicVideos,
+ CollectionType.Movies,
+ CollectionType.Photos
+ };
+
+ if (string.IsNullOrWhiteSpace(collectionType))
+ {
+ return false;
+ }
+
+ return !validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
new file mode 100644
index 000000000..3d7ede879
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -0,0 +1,56 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
+ {
+ private readonly IImageProcessor _imageProcessor;
+ public PhotoAlbumResolver(IImageProcessor imageProcessor)
+ {
+ _imageProcessor = imageProcessor;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Trailer.</returns>
+ protected override PhotoAlbum Resolve(ItemResolveArgs args)
+ {
+ // Must be an image file within a photo collection
+ if (args.IsDirectory && string.Equals(args.GetCollectionType(), CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ if (HasPhotos(args))
+ {
+ return new PhotoAlbum
+ {
+ Path = args.Path
+ };
+ }
+ }
+
+ return null;
+ }
+
+ private bool HasPhotos(ItemResolveArgs args)
+ {
+ return args.FileSystemChildren.Any(i => (!i.IsDirectory) && PhotoResolver.IsImageFile(i.FullName, _imageProcessor));
+ }
+
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Behind special folder resolver
+ return ResolverPriority.Second;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
new file mode 100644
index 000000000..df39e57ad
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -0,0 +1,103 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ public class PhotoResolver : ItemResolver<Photo>
+ {
+ private readonly IImageProcessor _imageProcessor;
+ private readonly ILibraryManager _libraryManager;
+
+ public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ {
+ _imageProcessor = imageProcessor;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Trailer.</returns>
+ protected override Photo Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory)
+ {
+ // Must be an image file within a photo collection
+ var collectionType = args.GetCollectionType();
+
+
+ if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
+ (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
+ {
+ if (IsImageFile(args.Path, _imageProcessor))
+ {
+ var filename = Path.GetFileNameWithoutExtension(args.Path);
+
+ // Make sure the image doesn't belong to a video file
+ if (args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)).Any(i => IsOwnedByMedia(args.GetLibraryOptions(), i, filename)))
+ {
+ return null;
+ }
+
+ return new Photo
+ {
+ Path = args.Path
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private bool IsOwnedByMedia(LibraryOptions libraryOptions, FileSystemMetadata file, string imageFilename)
+ {
+ if (_libraryManager.IsVideoFile(file.FullName, libraryOptions) && imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file.Name), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static readonly string[] IgnoreFiles =
+ {
+ "folder",
+ "thumb",
+ "landscape",
+ "fanart",
+ "backdrop",
+ "poster",
+ "cover"
+ };
+
+ internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
+ {
+ var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty;
+
+ if (IgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+ {
+ return false;
+ }
+
+ return imageProcessor.SupportedInputFormats.Contains((Path.GetExtension(path) ?? string.Empty).TrimStart('.'), StringComparer.OrdinalIgnoreCase);
+ }
+
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
new file mode 100644
index 000000000..8c59cf20f
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -0,0 +1,42 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using System;
+using System.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ public class PlaylistResolver : FolderResolver<Playlist>
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BoxSet.</returns>
+ protected override Playlist Resolve(ItemResolveArgs args)
+ {
+ // It's a boxset if all of the following conditions are met:
+ // Is a Directory
+ // Contains [playlist] in the path
+ if (args.IsDirectory)
+ {
+ var filename = Path.GetFileName(args.Path);
+
+ if (string.IsNullOrEmpty(filename))
+ {
+ return null;
+ }
+
+ if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new Playlist
+ {
+ Path = args.Path,
+ Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path))
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
new file mode 100644
index 000000000..1bec1073d
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -0,0 +1,85 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ class SpecialFolderResolver : FolderResolver<Folder>
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerApplicationPaths _appPaths;
+
+ public SpecialFolderResolver(IFileSystem fileSystem, IServerApplicationPaths appPaths)
+ {
+ _fileSystem = fileSystem;
+ _appPaths = appPaths;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.First; }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Folder.</returns>
+ protected override Folder Resolve(ItemResolveArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ if (args.IsPhysicalRoot)
+ {
+ return new AggregateFolder();
+ }
+ if (string.Equals(args.Path, _appPaths.DefaultUserViewsPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return new UserRootFolder(); //if we got here and still a root - must be user root
+ }
+ if (args.IsVf)
+ {
+ return new CollectionFolder
+ {
+ CollectionType = GetCollectionType(args),
+ PhysicalLocationsList = args.PhysicalLocations.ToList()
+ };
+ }
+ }
+
+ return null;
+ }
+
+ private string GetCollectionType(ItemResolveArgs args)
+ {
+ return args.FileSystemChildren
+ .Where(i =>
+ {
+
+ try
+ {
+ return !i.IsDirectory &&
+ string.Equals(".collection", i.Extension, StringComparison.OrdinalIgnoreCase);
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+
+ })
+ .Select(i => _fileSystem.GetFileNameWithoutExtension(i))
+ .FirstOrDefault();
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
new file mode 100644
index 000000000..2a4cc49b7
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -0,0 +1,75 @@
+using System;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.Linq;
+using MediaBrowser.Model.Entities;
+
+namespace Emby.Server.Implementations.Library.Resolvers.TV
+{
+ /// <summary>
+ /// Class EpisodeResolver
+ /// </summary>
+ public class EpisodeResolver : BaseVideoResolver<Episode>
+ {
+ public EpisodeResolver(ILibraryManager libraryManager) : base(libraryManager)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Episode.</returns>
+ protected override Episode Resolve(ItemResolveArgs args)
+ {
+ var parent = args.Parent;
+
+ if (parent == null)
+ {
+ return null;
+ }
+
+ var season = parent as Season;
+ // Just in case the user decided to nest episodes.
+ // Not officially supported but in some cases we can handle it.
+ if (season == null)
+ {
+ season = parent.GetParents().OfType<Season>().FirstOrDefault();
+ }
+
+ // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ // Also handle flat tv folders
+ if (season != null ||
+ string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+ args.HasParent<Series>())
+ {
+ var episode = ResolveVideo<Episode>(args, false);
+
+ if (episode != null)
+ {
+ var series = parent as Series;
+ if (series == null)
+ {
+ series = parent.GetParents().OfType<Series>().FirstOrDefault();
+ }
+
+ if (series != null)
+ {
+ episode.SeriesId = series.Id;
+ episode.SeriesName = series.Name;
+ episode.SeriesSortName = series.SortName;
+ }
+ if (season != null)
+ {
+ episode.SeasonId = season.Id;
+ episode.SeasonName = season.Name;
+ }
+ }
+
+ return episode;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
new file mode 100644
index 000000000..c065feda1
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -0,0 +1,62 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Naming.Common;
+using MediaBrowser.Naming.TV;
+
+namespace Emby.Server.Implementations.Library.Resolvers.TV
+{
+ /// <summary>
+ /// Class SeasonResolver
+ /// </summary>
+ public class SeasonResolver : FolderResolver<Season>
+ {
+ /// <summary>
+ /// The _config
+ /// </summary>
+ private readonly IServerConfigurationManager _config;
+
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
+ /// </summary>
+ /// <param name="config">The config.</param>
+ public SeasonResolver(IServerConfigurationManager config, ILibraryManager libraryManager)
+ {
+ _config = config;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Season.</returns>
+ protected override Season Resolve(ItemResolveArgs args)
+ {
+ if (args.Parent is Series && args.IsDirectory)
+ {
+ var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
+ var series = ((Series)args.Parent);
+
+ var season = new Season
+ {
+ IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(args.Path, true, true).SeasonNumber,
+ SeriesId = series.Id,
+ SeriesSortName = series.SortName,
+ SeriesName = series.Name
+ };
+
+ if (season.IndexNumber.HasValue && season.IndexNumber.Value == 0)
+ {
+ season.Name = _config.Configuration.SeasonZeroDisplayName;
+ }
+
+ return season;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
new file mode 100644
index 000000000..44eb0e3e2
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -0,0 +1,251 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Naming.Common;
+using MediaBrowser.Naming.TV;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.Resolvers.TV
+{
+ /// <summary>
+ /// Class SeriesResolver
+ /// </summary>
+ public class SeriesResolver : FolderResolver<Series>
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+
+ public SeriesResolver(IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Series.</returns>
+ protected override Series Resolve(ItemResolveArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ if (args.HasParent<Series>() || args.HasParent<Season>())
+ {
+ return null;
+ }
+
+ var collectionType = args.GetCollectionType();
+ if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ //if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
+ //{
+ // return new Series
+ // {
+ // Path = args.Path,
+ // Name = Path.GetFileName(args.Path)
+ // };
+ //}
+
+ var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
+ if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Series
+ {
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
+ }
+ }
+ else if (string.IsNullOrWhiteSpace(collectionType))
+ {
+ if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
+ {
+ if (args.Parent.IsRoot)
+ {
+ // For now, return null, but if we want to allow this in the future then add some additional checks to guard against a misplaced tvshow.nfo
+ return null;
+ }
+
+ return new Series
+ {
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
+ }
+
+ if (args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
+ {
+ return new Series
+ {
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static bool IsSeriesFolder(string path,
+ IEnumerable<FileSystemMetadata> fileSystemChildren,
+ IDirectoryService directoryService,
+ IFileSystem fileSystem,
+ ILogger logger,
+ ILibraryManager libraryManager,
+ LibraryOptions libraryOptions,
+ bool isTvContentType)
+ {
+ foreach (var child in fileSystemChildren)
+ {
+ //if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+ //{
+ // //logger.Debug("Igoring series file or folder marked hidden: {0}", child.FullName);
+ // continue;
+ //}
+
+ // Can't enforce this because files saved by Bitcasa are always marked System
+ //if ((attributes & FileAttributes.System) == FileAttributes.System)
+ //{
+ // logger.Debug("Igoring series subfolder marked system: {0}", child.FullName);
+ // continue;
+ //}
+
+ if (child.IsDirectory)
+ {
+ if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
+ {
+ //logger.Debug("{0} is a series because of season folder {1}.", path, child.FullName);
+ return true;
+ }
+ }
+ else
+ {
+ string fullName = child.FullName;
+ if (libraryManager.IsVideoFile(fullName, libraryOptions))
+ {
+ if (isTvContentType)
+ {
+ return true;
+ }
+
+ var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
+
+ // In mixed folders we need to be conservative and avoid expressions that may result in false positives (e.g. movies with numbers in the title)
+ if (!isTvContentType)
+ {
+ namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions
+ .Where(i => i.IsNamed && !i.IsOptimistic)
+ .ToList();
+ }
+
+ var episodeResolver = new MediaBrowser.Naming.TV.EpisodeResolver(namingOptions, new PatternsLogger());
+ var episodeInfo = episodeResolver.Resolve(fullName, false, false);
+ if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue)
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ logger.Debug("{0} is not a series folder.", path);
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [is place holder] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is place holder] [the specified path]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">path</exception>
+ private static bool IsVideoPlaceHolder(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var extension = Path.GetExtension(path);
+
+ return string.Equals(extension, ".disc", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is season folder] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
+ private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
+ {
+ var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
+
+ var seasonNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+
+ return seasonNumber.HasValue;
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(Series item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item, args.Path);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="path">The path.</param>
+ private void SetProviderIdFromPath(Series item, string path)
+ {
+ var justName = Path.GetFileName(path);
+
+ var id = justName.GetAttributeValue("tvdbid");
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
new file mode 100644
index 000000000..b5e1bf5f7
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video
+ /// </summary>
+ public class VideoResolver : BaseVideoResolver<Video>
+ {
+ public VideoResolver(ILibraryManager libraryManager)
+ : base(libraryManager)
+ {
+ }
+
+ protected override Video Resolve(ItemResolveArgs args)
+ {
+ if (args.Parent != null)
+ {
+ // The movie resolver will handle this
+ return null;
+ }
+
+ return base.Resolve(args);
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+ }
+
+ public class GenericVideoResolver<T> : BaseVideoResolver<T>
+ where T : Video, new ()
+ {
+ public GenericVideoResolver(ILibraryManager libraryManager) : base(libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
new file mode 100644
index 000000000..afdf65c06
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -0,0 +1,275 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Search;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Extensions;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// </summary>
+ public class SearchEngine : ISearchEngine
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly ILogger _logger;
+
+ public SearchEngine(ILogManager logManager, ILibraryManager libraryManager, IUserManager userManager)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+
+ _logger = logManager.GetLogger("Lucene");
+ }
+
+ public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query)
+ {
+ User user = null;
+
+ if (string.IsNullOrWhiteSpace(query.UserId))
+ {
+ }
+ else
+ {
+ user = _userManager.GetUserById(query.UserId);
+ }
+
+ var results = await GetSearchHints(query, user).ConfigureAwait(false);
+
+ var searchResultArray = results.ToArray();
+ results = searchResultArray;
+
+ var count = searchResultArray.Length;
+
+ if (query.StartIndex.HasValue)
+ {
+ results = results.Skip(query.StartIndex.Value);
+ }
+
+ if (query.Limit.HasValue)
+ {
+ results = results.Take(query.Limit.Value);
+ }
+
+ return new QueryResult<SearchHintInfo>
+ {
+ TotalRecordCount = count,
+
+ Items = results.ToArray()
+ };
+ }
+
+ private void AddIfMissing(List<string> list, string value)
+ {
+ if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
+ {
+ list.Add(value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the search hints.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{SearchHintResult}.</returns>
+ /// <exception cref="System.ArgumentNullException">searchTerm</exception>
+ private Task<IEnumerable<SearchHintInfo>> GetSearchHints(SearchQuery query, User user)
+ {
+ var searchTerm = query.SearchTerm;
+
+ if (searchTerm != null)
+ {
+ searchTerm = searchTerm.Trim().RemoveDiacritics();
+ }
+
+ if (string.IsNullOrWhiteSpace(searchTerm))
+ {
+ throw new ArgumentNullException("searchTerm");
+ }
+
+ var terms = GetWords(searchTerm);
+
+ var hints = new List<Tuple<BaseItem, string, int>>();
+
+ var excludeItemTypes = new List<string>();
+ var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList();
+
+ excludeItemTypes.Add(typeof(Year).Name);
+ excludeItemTypes.Add(typeof(Folder).Name);
+
+ if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(Genre).Name);
+ AddIfMissing(includeItemTypes, typeof(GameGenre).Name);
+ AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(Genre).Name);
+ AddIfMissing(excludeItemTypes, typeof(GameGenre).Name);
+ AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
+ }
+
+ if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(Person).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(Person).Name);
+ }
+
+ if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(Studio).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(Studio).Name);
+ }
+
+ if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
+ }
+
+ AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
+
+ var mediaItems = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ NameContains = searchTerm,
+ ExcludeItemTypes = excludeItemTypes.ToArray(),
+ IncludeItemTypes = includeItemTypes.ToArray(),
+ Limit = query.Limit,
+ IncludeItemsByName = true,
+ IsVirtualItem = false
+ });
+
+ // Add search hints based on item name
+ hints.AddRange(mediaItems.Select(item =>
+ {
+ var index = GetIndex(item.Name, searchTerm, terms);
+
+ return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2);
+ }));
+
+ var returnValue = hints.Where(i => i.Item3 >= 0).OrderBy(i => i.Item3).Select(i => new SearchHintInfo
+ {
+ Item = i.Item1,
+ MatchedTerm = i.Item2
+ });
+
+ return Task.FromResult(returnValue);
+ }
+
+ /// <summary>
+ /// Gets the index.
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="searchInput">The search input.</param>
+ /// <param name="searchWords">The search input.</param>
+ /// <returns>System.Int32.</returns>
+ private Tuple<string, int> GetIndex(string input, string searchInput, List<string> searchWords)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ input = input.RemoveDiacritics();
+
+ if (string.Equals(input, searchInput, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Tuple<string, int>(searchInput, 0);
+ }
+
+ var index = input.IndexOf(searchInput, StringComparison.OrdinalIgnoreCase);
+
+ if (index == 0)
+ {
+ return new Tuple<string, int>(searchInput, 1);
+ }
+ if (index > 0)
+ {
+ return new Tuple<string, int>(searchInput, 2);
+ }
+
+ var items = GetWords(input);
+
+ for (var i = 0; i < searchWords.Count; i++)
+ {
+ var searchTerm = searchWords[i];
+
+ for (var j = 0; j < items.Count; j++)
+ {
+ var item = items[j];
+
+ if (string.Equals(item, searchTerm, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Tuple<string, int>(searchTerm, 3 + (i + 1) * (j + 1));
+ }
+
+ index = item.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase);
+
+ if (index == 0)
+ {
+ return new Tuple<string, int>(searchTerm, 4 + (i + 1) * (j + 1));
+ }
+ if (index > 0)
+ {
+ return new Tuple<string, int>(searchTerm, 5 + (i + 1) * (j + 1));
+ }
+ }
+ }
+ return new Tuple<string, int>(null, -1);
+ }
+
+ /// <summary>
+ /// Gets the words.
+ /// </summary>
+ /// <param name="term">The term.</param>
+ /// <returns>System.String[][].</returns>
+ private List<string> GetWords(string term)
+ {
+ var stoplist = GetStopList().ToList();
+
+ return term.Split()
+ .Where(i => !string.IsNullOrWhiteSpace(i) && !stoplist.Contains(i, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+ }
+
+ private IEnumerable<string> GetStopList()
+ {
+ return new[]
+ {
+ "the",
+ "a",
+ "of",
+ "an"
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
new file mode 100644
index 000000000..b93f565a3
--- /dev/null
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -0,0 +1,292 @@
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Globalization;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class UserViewManager : IUserViewManager
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localizationManager;
+ private readonly IUserManager _userManager;
+
+ private readonly IChannelManager _channelManager;
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly IServerConfigurationManager _config;
+
+ public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _localizationManager = localizationManager;
+ _userManager = userManager;
+ _channelManager = channelManager;
+ _liveTvManager = liveTvManager;
+ _config = config;
+ }
+
+ public async Task<IEnumerable<Folder>> GetUserViews(UserViewQuery query, CancellationToken cancellationToken)
+ {
+ var user = _userManager.GetUserById(query.UserId);
+
+ var folders = user.RootFolder
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .ToList();
+
+ if (!query.IncludeHidden)
+ {
+ folders = folders.Where(i =>
+ {
+ var hidden = i as IHiddenFromDisplay;
+ return hidden == null || !hidden.IsHiddenFromUser(user);
+ }).ToList();
+ }
+
+ var plainFolderIds = user.Configuration.PlainFolderViews.Select(i => new Guid(i)).ToList();
+
+ var groupedFolders = new List<ICollectionFolder>();
+
+ var list = new List<Folder>();
+
+ foreach (var folder in folders)
+ {
+ var collectionFolder = folder as ICollectionFolder;
+ var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType;
+
+ if (UserView.IsUserSpecific(folder))
+ {
+ list.Add(await _libraryManager.GetNamedView(user, folder.Name, folder.Id.ToString("N"), folderViewType, null, cancellationToken).ConfigureAwait(false));
+ continue;
+ }
+
+ if (plainFolderIds.Contains(folder.Id) && UserView.IsEligibleForEnhancedView(folderViewType))
+ {
+ list.Add(folder);
+ continue;
+ }
+
+ if (collectionFolder != null && UserView.IsEligibleForGrouping(folder) && user.IsFolderGrouped(folder.Id))
+ {
+ groupedFolders.Add(collectionFolder);
+ continue;
+ }
+
+ if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ list.Add(await GetUserView(folder, folderViewType, string.Empty, cancellationToken).ConfigureAwait(false));
+ }
+ else
+ {
+ list.Add(folder);
+ }
+ }
+
+ foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
+ {
+ var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType))
+ .ToList();
+
+ if (parents.Count > 0)
+ {
+ list.Add(await GetUserView(parents, viewType, string.Empty, user, query.PresetViews, cancellationToken).ConfigureAwait(false));
+ }
+ }
+
+ if (_config.Configuration.EnableFolderView)
+ {
+ var name = _localizationManager.GetLocalizedString("ViewType" + CollectionType.Folders);
+ list.Add(await _libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken).ConfigureAwait(false));
+ }
+
+ if (query.IncludeExternalContent)
+ {
+ var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery
+ {
+ UserId = query.UserId
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ var channels = channelResult.Items;
+
+ if (_config.Configuration.EnableChannelView && channels.Length > 0)
+ {
+ list.Add(await _channelManager.GetInternalChannelFolder(cancellationToken).ConfigureAwait(false));
+ }
+ else
+ {
+ list.AddRange(channels);
+ }
+
+ if (_liveTvManager.GetEnabledUsers().Select(i => i.Id.ToString("N")).Contains(query.UserId))
+ {
+ list.Add(await _liveTvManager.GetInternalLiveTvFolder(CancellationToken.None).ConfigureAwait(false));
+ }
+ }
+
+ var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+
+ var orders = user.Configuration.OrderedViews.ToList();
+
+ return list
+ .OrderBy(i =>
+ {
+ var index = orders.IndexOf(i.Id.ToString("N"));
+
+ if (index == -1)
+ {
+ var view = i as UserView;
+ if (view != null)
+ {
+ if (view.DisplayParentId != Guid.Empty)
+ {
+ index = orders.IndexOf(view.DisplayParentId.ToString("N"));
+ }
+ }
+ }
+
+ return index == -1 ? int.MaxValue : index;
+ })
+ .ThenBy(sorted.IndexOf)
+ .ThenBy(i => i.SortName);
+ }
+
+ public Task<UserView> GetUserSubView(string name, string parentId, string type, string sortName, CancellationToken cancellationToken)
+ {
+ var uniqueId = parentId + "subview" + type;
+
+ return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId, cancellationToken);
+ }
+
+ public Task<UserView> GetUserSubView(string parentId, string type, string sortName, CancellationToken cancellationToken)
+ {
+ var name = _localizationManager.GetLocalizedString("ViewType" + type);
+
+ return GetUserSubView(name, parentId, type, sortName, cancellationToken);
+ }
+
+ private async Task<Folder> GetUserView(List<ICollectionFolder> parents, string viewType, string sortName, User user, string[] presetViews, CancellationToken cancellationToken)
+ {
+ if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
+ {
+ if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase))
+ {
+ return (Folder)parents[0];
+ }
+
+ return await GetUserView((Folder)parents[0], viewType, string.Empty, cancellationToken).ConfigureAwait(false);
+ }
+
+ var name = _localizationManager.GetLocalizedString("ViewType" + viewType);
+ return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false);
+ }
+
+ public Task<UserView> GetUserView(Folder parent, string viewType, string sortName, CancellationToken cancellationToken)
+ {
+ return _libraryManager.GetShadowView(parent, viewType, sortName, cancellationToken);
+ }
+
+ public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request)
+ {
+ var user = _userManager.GetUserById(request.UserId);
+
+ var libraryItems = GetItemsForLatestItems(user, request);
+
+ var list = new List<Tuple<BaseItem, List<BaseItem>>>();
+
+ foreach (var item in libraryItems)
+ {
+ // Only grab the index container for media
+ var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer;
+
+ if (container == null)
+ {
+ list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
+ }
+ else
+ {
+ var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id);
+
+ if (current != null)
+ {
+ current.Item2.Add(item);
+ }
+ else
+ {
+ list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
+ }
+ }
+
+ if (list.Count >= request.Limit)
+ {
+ break;
+ }
+ }
+
+ return list;
+ }
+
+ private IEnumerable<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request)
+ {
+ var parentId = request.ParentId;
+
+ var includeItemTypes = request.IncludeItemTypes;
+ var limit = request.Limit ?? 10;
+
+ var parentIds = string.IsNullOrEmpty(parentId)
+ ? new string[] { }
+ : new[] { parentId };
+
+ if (parentIds.Length == 0)
+ {
+ parentIds = user.RootFolder.GetChildren(user, true)
+ .OfType<Folder>()
+ .Select(i => i.Id.ToString("N"))
+ .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i))
+ .ToArray();
+ }
+
+ if (parentIds.Length == 0)
+ {
+ return new List<BaseItem>();
+ }
+
+ var excludeItemTypes = includeItemTypes.Length == 0 ? new[]
+ {
+ typeof(Person).Name,
+ typeof(Studio).Name,
+ typeof(Year).Name,
+ typeof(GameGenre).Name,
+ typeof(MusicGenre).Name,
+ typeof(Genre).Name
+
+ } : new string[] { };
+
+ return _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = includeItemTypes,
+ SortOrder = SortOrder.Descending,
+ SortBy = new[] { ItemSortBy.DateCreated },
+ IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
+ ExcludeItemTypes = excludeItemTypes,
+ ExcludeLocationTypes = new[] { LocationType.Virtual },
+ Limit = limit * 5,
+ SourceTypes = parentIds.Length == 0 ? new[] { SourceType.Library } : new SourceType[] { },
+ IsPlayed = request.IsPlayed
+
+ }, parentIds);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
new file mode 100644
index 000000000..4d718dbee
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
@@ -0,0 +1,44 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class ArtistsPostScanTask
+ /// </summary>
+ public class ArtistsPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public ArtistsPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
new file mode 100644
index 000000000..643c5970e
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -0,0 +1,84 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class ArtistsValidator
+ /// </summary>
+ public class ArtistsValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetAllArtistNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetArtist(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs
new file mode 100644
index 000000000..ee6c4461c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class GameGenresPostScanTask
+ /// </summary>
+ public class GameGenresPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GameGenresPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public GameGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new GameGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs
new file mode 100644
index 000000000..b1820bb91
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class GameGenresValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ public GameGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetGameGenreNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetGameGenre(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
new file mode 100644
index 000000000..be46decfb
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
@@ -0,0 +1,42 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ public class GenresPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public GenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
new file mode 100644
index 000000000..d8956f78a
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -0,0 +1,75 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class GenresValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetGenreNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetGenre(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
new file mode 100644
index 000000000..cd4021548
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class MusicGenresPostScanTask
+ /// </summary>
+ public class MusicGenresPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public MusicGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
new file mode 100644
index 000000000..983c881b7
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
@@ -0,0 +1,75 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class MusicGenresValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetMusicGenreNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
new file mode 100644
index 000000000..813f07fff
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -0,0 +1,172 @@
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class PeopleValidator
+ /// </summary>
+ public class PeopleValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ }
+
+ private bool DownloadMetadata(PersonInfo i, PeopleMetadataOptions options)
+ {
+ if (i.IsType(PersonType.Actor))
+ {
+ return options.DownloadActorMetadata;
+ }
+ if (i.IsType(PersonType.Director))
+ {
+ return options.DownloadDirectorMetadata;
+ }
+ if (i.IsType(PersonType.Composer))
+ {
+ return options.DownloadComposerMetadata;
+ }
+ if (i.IsType(PersonType.Writer))
+ {
+ return options.DownloadWriterMetadata;
+ }
+ if (i.IsType(PersonType.Producer))
+ {
+ return options.DownloadProducerMetadata;
+ }
+ if (i.IsType(PersonType.GuestStar))
+ {
+ return options.DownloadGuestStarMetadata;
+ }
+
+ return options.DownloadOtherPeopleMetadata;
+ }
+
+ /// <summary>
+ /// Validates the people.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(pct => progress.Report(pct * .15));
+
+ var peopleOptions = _config.Configuration.PeopleMetadataOptions;
+
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery());
+
+ var dict = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var person in people)
+ {
+ var isMetadataEnabled = DownloadMetadata(person, peopleOptions);
+
+ bool currentValue;
+ if (dict.TryGetValue(person.Name, out currentValue))
+ {
+ if (!currentValue && isMetadataEnabled)
+ {
+ dict[person.Name] = true;
+ }
+ }
+ else
+ {
+ dict[person.Name] = isMetadataEnabled;
+ }
+ }
+
+ var numComplete = 0;
+
+ _logger.Debug("Will refresh {0} people", dict.Count);
+
+ var numPeople = dict.Count;
+
+ foreach (var person in dict)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ var item = _libraryManager.GetPerson(person.Key);
+
+ var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview);
+ var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 30;
+
+ var defaultMetadataRefreshMode = performFullRefresh
+ ? MetadataRefreshMode.FullRefresh
+ : MetadataRefreshMode.Default;
+
+ var imageRefreshMode = performFullRefresh
+ ? ImageRefreshMode.FullRefresh
+ : ImageRefreshMode.Default;
+
+ var options = new MetadataRefreshOptions(_fileSystem)
+ {
+ MetadataRefreshMode = person.Value ? defaultMetadataRefreshMode : MetadataRefreshMode.ValidationOnly,
+ ImageRefreshMode = person.Value ? imageRefreshMode : ImageRefreshMode.ValidationOnly,
+ ForceSave = performFullRefresh
+ };
+
+ await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error validating IBN entry {0}", ex, person);
+ }
+
+ // Update progress
+ numComplete++;
+ double percent = numComplete;
+ percent /= numPeople;
+
+ progress.Report(100 * percent);
+ }
+
+ progress.Report(100);
+
+ _logger.Info("People validation complete");
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
new file mode 100644
index 000000000..d23efb6d3
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class MusicGenresPostScanTask
+ /// </summary>
+ public class StudiosPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public StudiosPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
new file mode 100644
index 000000000..6faab7bb9
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class StudiosValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ private readonly IItemRepository _itemRepo;
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetStudioNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetStudio(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs
new file mode 100644
index 000000000..ae43c77f0
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs
@@ -0,0 +1,55 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ public class YearsPostScanTask : ILibraryPostScanTask
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+
+ public YearsPostScanTask(ILibraryManager libraryManager, ILogger logger)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
+
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var yearNumber = 1900;
+ var maxYear = DateTime.UtcNow.Year + 3;
+ var count = maxYear - yearNumber + 1;
+ var numComplete = 0;
+
+ while (yearNumber < maxYear)
+ {
+ try
+ {
+ var year = _libraryManager.GetYear(yearNumber);
+
+ await year.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing year {0}", ex, yearNumber);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ yearNumber++;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Logging/PatternsLogger.cs b/Emby.Server.Implementations/Logging/PatternsLogger.cs
new file mode 100644
index 000000000..6dbf33d61
--- /dev/null
+++ b/Emby.Server.Implementations/Logging/PatternsLogger.cs
@@ -0,0 +1,63 @@
+using Patterns.Logging;
+using System;
+
+namespace Emby.Server.Implementations.Logging
+{
+ public class PatternsLogger : ILogger
+ {
+ private readonly MediaBrowser.Model.Logging.ILogger _logger;
+
+ public PatternsLogger()
+ : this(new MediaBrowser.Model.Logging.NullLogger())
+ {
+ }
+
+ public PatternsLogger(MediaBrowser.Model.Logging.ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void Debug(string message, params object[] paramList)
+ {
+ _logger.Debug(message, paramList);
+ }
+
+ public void Error(string message, params object[] paramList)
+ {
+ _logger.Error(message, paramList);
+ }
+
+ public void ErrorException(string message, Exception exception, params object[] paramList)
+ {
+ _logger.ErrorException(message, exception, paramList);
+ }
+
+ public void Fatal(string message, params object[] paramList)
+ {
+ _logger.Fatal(message, paramList);
+ }
+
+ public void FatalException(string message, Exception exception, params object[] paramList)
+ {
+ _logger.FatalException(message, exception, paramList);
+ }
+
+ public void Info(string message, params object[] paramList)
+ {
+ _logger.Info(message, paramList);
+ }
+
+ public void Warn(string message, params object[] paramList)
+ {
+ _logger.Warn(message, paramList);
+ }
+
+ public void Log(LogSeverity severity, string message, params object[] paramList)
+ {
+ }
+
+ public void LogMultiline(string message, LogSeverity severity, System.Text.StringBuilder additionalContent)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs
new file mode 100644
index 000000000..88f4f1f81
--- /dev/null
+++ b/Emby.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs
@@ -0,0 +1,361 @@
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Emby.Server.Implementations.ScheduledTasks;
+
+namespace Emby.Server.Implementations.Persistence
+{
+ public class CleanDatabaseScheduledTask : IScheduledTask
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemRepository _itemRepo;
+ private readonly ILogger _logger;
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IHttpServer _httpServer;
+ private readonly ILocalizationManager _localization;
+ private readonly ITaskManager _taskManager;
+
+ public const int MigrationVersion = 23;
+ public static bool EnableUnavailableMessage = false;
+ const int LatestSchemaVersion = 109;
+
+ public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager)
+ {
+ _libraryManager = libraryManager;
+ _itemRepo = itemRepo;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ _httpServer = httpServer;
+ _localization = localization;
+ _taskManager = taskManager;
+ }
+
+ public string Name
+ {
+ get { return "Clean Database"; }
+ }
+
+ public string Description
+ {
+ get { return "Deletes obsolete content from the database."; }
+ }
+
+ public string Category
+ {
+ get { return "Library"; }
+ }
+
+ public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ OnProgress(0);
+
+ // Ensure these objects are lazy loaded.
+ // Without this there is a deadlock that will need to be investigated
+ var rootChildren = _libraryManager.RootFolder.Children.ToList();
+ rootChildren = _libraryManager.GetUserRootFolder().Children.ToList();
+
+ var innerProgress = new ActionableProgress<double>();
+ innerProgress.RegisterAction(p =>
+ {
+ double newPercentCommplete = .4 * p;
+ OnProgress(newPercentCommplete);
+
+ progress.Report(newPercentCommplete);
+ });
+
+ await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false);
+
+ innerProgress = new ActionableProgress<double>();
+ innerProgress.RegisterAction(p =>
+ {
+ double newPercentCommplete = 40 + .05 * p;
+ OnProgress(newPercentCommplete);
+ progress.Report(newPercentCommplete);
+ });
+ await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false);
+ progress.Report(45);
+
+ innerProgress = new ActionableProgress<double>();
+ innerProgress.RegisterAction(p =>
+ {
+ double newPercentCommplete = 45 + .55 * p;
+ OnProgress(newPercentCommplete);
+ progress.Report(newPercentCommplete);
+ });
+ await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false);
+ progress.Report(100);
+
+ await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false);
+
+ if (_config.Configuration.MigrationVersion < MigrationVersion)
+ {
+ _config.Configuration.MigrationVersion = MigrationVersion;
+ _config.SaveConfiguration();
+ }
+
+ if (_config.Configuration.SchemaVersion < LatestSchemaVersion)
+ {
+ _config.Configuration.SchemaVersion = LatestSchemaVersion;
+ _config.SaveConfiguration();
+ }
+
+ if (EnableUnavailableMessage)
+ {
+ EnableUnavailableMessage = false;
+ _httpServer.GlobalResponse = null;
+ _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
+ }
+
+ _taskManager.SuspendTriggers = false;
+ }
+
+ private void OnProgress(double newPercentCommplete)
+ {
+ if (EnableUnavailableMessage)
+ {
+ var html = "<!doctype html><html><head><title>Emby</title></head><body>";
+ var text = _localization.GetLocalizedString("DbUpgradeMessage");
+ html += string.Format(text, newPercentCommplete.ToString("N2", CultureInfo.InvariantCulture));
+
+ html += "<script>setTimeout(function(){window.location.reload(true);}, 5000);</script>";
+ html += "</body></html>";
+
+ _httpServer.GlobalResponse = html;
+ }
+ }
+
+ private async Task UpdateToLatestSchema(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IsCurrentSchema = false,
+ ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name }
+ });
+
+ var numComplete = 0;
+ var numItems = itemIds.Count;
+
+ _logger.Debug("Upgrading schema for {0} items", numItems);
+
+ var list = new List<BaseItem>();
+
+ foreach (var itemId in itemIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (itemId != Guid.Empty)
+ {
+ // Somehow some invalid data got into the db. It probably predates the boundary checking
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item != null)
+ {
+ list.Add(item);
+ }
+ }
+
+ if (list.Count >= 1000)
+ {
+ try
+ {
+ await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error saving item", ex);
+ }
+
+ list.Clear();
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= numItems;
+ progress.Report(percent * 100);
+ }
+
+ if (list.Count > 0)
+ {
+ try
+ {
+ await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error saving item", ex);
+ }
+ }
+
+ progress.Report(100);
+ }
+
+ private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ HasDeadParentId = true
+ });
+
+ var numComplete = 0;
+ var numItems = itemIds.Count;
+
+ _logger.Debug("Cleaning {0} items with dead parent links", numItems);
+
+ foreach (var itemId in itemIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item != null)
+ {
+ _logger.Info("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
+
+ await item.Delete(new DeleteOptions
+ {
+ DeleteFileLocation = false
+
+ }).ConfigureAwait(false);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= numItems;
+ progress.Report(percent * 100);
+ }
+
+ progress.Report(100);
+ }
+
+ private async Task CleanDeletedItems(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var result = _itemRepo.GetItemIdsWithPath(new InternalItemsQuery
+ {
+ LocationTypes = new[] { LocationType.FileSystem },
+ //Limit = limit,
+
+ // These have their own cleanup routines
+ ExcludeItemTypes = new[]
+ {
+ typeof(Person).Name,
+ typeof(Genre).Name,
+ typeof(MusicGenre).Name,
+ typeof(GameGenre).Name,
+ typeof(Studio).Name,
+ typeof(Year).Name,
+ typeof(Channel).Name,
+ typeof(AggregateFolder).Name,
+ typeof(CollectionFolder).Name
+ }
+ });
+
+ var numComplete = 0;
+ var numItems = result.Items.Length;
+
+ foreach (var item in result.Items)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var path = item.Item2;
+
+ try
+ {
+ if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path))
+ {
+ continue;
+ }
+
+ var libraryItem = _libraryManager.GetItemById(item.Item1);
+
+ if (libraryItem.IsTopParent)
+ {
+ continue;
+ }
+
+ var hasDualAccess = libraryItem as IHasDualAccess;
+ if (hasDualAccess != null && hasDualAccess.IsAccessedByName)
+ {
+ continue;
+ }
+
+ var libraryItemPath = libraryItem.Path;
+ if (!string.Equals(libraryItemPath, path, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.Error("CleanDeletedItems aborting delete for item {0}-{1} because paths don't match. {2}---{3}", libraryItem.Id, libraryItem.Name, libraryItem.Path ?? string.Empty, path ?? string.Empty);
+ continue;
+ }
+
+ if (Folder.IsPathOffline(path))
+ {
+ await libraryItem.UpdateIsOffline(true).ConfigureAwait(false);
+ continue;
+ }
+
+ _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty);
+
+ await libraryItem.OnFileDeleted().ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in CleanDeletedItems. File {0}", ex, path);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= numItems;
+ progress.Report(percent * 100);
+ }
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ };
+ }
+
+ public string Key
+ {
+ get { return "CleanDatabase"; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs
new file mode 100644
index 000000000..fb07b8e99
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs
@@ -0,0 +1,96 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Library;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class RefreshMediaLibraryTask
+ /// </summary>
+ public class RefreshMediaLibraryTask : IScheduledTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public RefreshMediaLibraryTask(ILibraryManager libraryManager, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _config = config;
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(12).Ticks}
+ };
+ }
+
+ /// <summary>
+ /// Executes the internal.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ progress.Report(0);
+
+ return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Scan media library"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return "Scans your media library and refreshes metatata based on configuration."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get
+ {
+ return "Library";
+ }
+ }
+
+ public string Key
+ {
+ get { return "RefreshLibrary"; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/AirTimeComparer.cs b/Emby.Server.Implementations/Sorting/AirTimeComparer.cs
new file mode 100644
index 000000000..bc05e9af3
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/AirTimeComparer.cs
@@ -0,0 +1,71 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class AirTimeComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return DateTime.Compare(GetValue(x), GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private DateTime GetValue(BaseItem x)
+ {
+ var series = x as Series;
+
+ if (series == null)
+ {
+ var season = x as Season;
+
+ if (season != null)
+ {
+ series = season.Series;
+ }
+ else
+ {
+ var episode = x as Episode;
+
+ if (episode != null)
+ {
+ series = episode.Series;
+ }
+ }
+ }
+
+ if (series != null)
+ {
+ DateTime result;
+ if (DateTime.TryParse(series.AirTime, out result))
+ {
+ return result;
+ }
+ }
+
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.AirTime; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
new file mode 100644
index 000000000..494668cb9
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -0,0 +1,160 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ class AiredEpisodeOrderComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ if (x.PremiereDate.HasValue && y.PremiereDate.HasValue)
+ {
+ var val = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
+
+ if (val != 0)
+ {
+ //return val;
+ }
+ }
+
+ var episode1 = x as Episode;
+ var episode2 = y as Episode;
+
+ if (episode1 == null)
+ {
+ if (episode2 == null)
+ {
+ return 0;
+ }
+
+ return 1;
+ }
+
+ if (episode2 == null)
+ {
+ return -1;
+ }
+
+ return Compare(episode1, episode2);
+ }
+
+ private int Compare(Episode x, Episode y)
+ {
+ var isXSpecial = (x.ParentIndexNumber ?? -1) == 0;
+ var isYSpecial = (y.ParentIndexNumber ?? -1) == 0;
+
+ if (isXSpecial && isYSpecial)
+ {
+ return CompareSpecials(x, y);
+ }
+
+ if (!isXSpecial && !isYSpecial)
+ {
+ return CompareEpisodes(x, y);
+ }
+
+ if (!isXSpecial)
+ {
+ return CompareEpisodeToSpecial(x, y);
+ }
+
+ return CompareEpisodeToSpecial(y, x) * -1;
+ }
+
+ private int CompareEpisodeToSpecial(Episode x, Episode y)
+ {
+ // http://thetvdb.com/wiki/index.php?title=Special_Episodes
+
+ var xSeason = x.ParentIndexNumber ?? -1;
+ var ySeason = y.AirsAfterSeasonNumber ?? y.AirsBeforeSeasonNumber ?? -1;
+
+ if (xSeason != ySeason)
+ {
+ return xSeason.CompareTo(ySeason);
+ }
+
+ // Special comes after episode
+ if (y.AirsAfterSeasonNumber.HasValue)
+ {
+ return -1;
+ }
+
+ var yEpisode = y.AirsBeforeEpisodeNumber;
+
+ // Special comes before the season
+ if (!yEpisode.HasValue)
+ {
+ return 1;
+ }
+
+ // Compare episode number
+ var xEpisode = x.IndexNumber;
+
+ if (!xEpisode.HasValue)
+ {
+ // Can't really compare if this happens
+ return 0;
+ }
+
+ // Special comes before episode
+ if (xEpisode.Value == yEpisode.Value)
+ {
+ return 1;
+ }
+
+ return xEpisode.Value.CompareTo(yEpisode.Value);
+ }
+
+ private int CompareSpecials(Episode x, Episode y)
+ {
+ return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
+ }
+
+ private int GetSpecialCompareValue(Episode item)
+ {
+ // First sort by season number
+ // Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
+ var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000;
+
+ // Second sort order is if it airs after the season
+ if (item.AirsAfterSeasonNumber.HasValue)
+ {
+ val += 1000000;
+ }
+
+ // Third level is the episode number
+ val += (item.AirsBeforeEpisodeNumber ?? 0) * 1000;
+
+ // Finally, if that's still the same, last resort is the special number itself
+ val += item.IndexNumber ?? 0;
+
+ return val;
+ }
+
+ private int CompareEpisodes(Episode x, Episode y)
+ {
+ var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
+ var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
+
+ return xValue.CompareTo(yValue);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.AiredEpisodeOrder; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
new file mode 100644
index 000000000..cd3834080
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -0,0 +1,47 @@
+using System.Linq;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class AlbumArtistComparer
+ /// </summary>
+ public class AlbumArtistComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private string GetValue(BaseItem x)
+ {
+ var audio = x as IHasAlbumArtist;
+
+ return audio != null ? audio.AlbumArtists.FirstOrDefault() : null;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.AlbumArtist; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
new file mode 100644
index 000000000..68f5f173e
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -0,0 +1,46 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class AlbumComparer
+ /// </summary>
+ public class AlbumComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private string GetValue(BaseItem x)
+ {
+ var audio = x as Audio;
+
+ return audio == null ? string.Empty : audio.Album;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Album; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/AlphanumComparator.cs b/Emby.Server.Implementations/Sorting/AlphanumComparator.cs
new file mode 100644
index 000000000..4bfcda1ac
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/AlphanumComparator.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Text;
+using MediaBrowser.Controller.Sorting;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class AlphanumComparator : IComparer<string>
+ {
+ public static int CompareValues(string s1, string s2)
+ {
+ if (s1 == null || s2 == null)
+ {
+ return 0;
+ }
+
+ int thisMarker = 0, thisNumericChunk = 0;
+ int thatMarker = 0, thatNumericChunk = 0;
+
+ while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
+ {
+ if (thisMarker >= s1.Length)
+ {
+ return -1;
+ }
+ else if (thatMarker >= s2.Length)
+ {
+ return 1;
+ }
+ char thisCh = s1[thisMarker];
+ char thatCh = s2[thatMarker];
+
+ StringBuilder thisChunk = new StringBuilder();
+ StringBuilder thatChunk = new StringBuilder();
+
+ while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0])))
+ {
+ thisChunk.Append(thisCh);
+ thisMarker++;
+
+ if (thisMarker < s1.Length)
+ {
+ thisCh = s1[thisMarker];
+ }
+ }
+
+ while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || SortHelper.InChunk(thatCh, thatChunk[0])))
+ {
+ thatChunk.Append(thatCh);
+ thatMarker++;
+
+ if (thatMarker < s2.Length)
+ {
+ thatCh = s2[thatMarker];
+ }
+ }
+
+ int result = 0;
+ // If both chunks contain numeric characters, sort them numerically
+ if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
+ {
+ if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk))
+ {
+ return 0;
+ }
+ if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk))
+ {
+ return 0;
+ }
+
+ if (thisNumericChunk < thatNumericChunk)
+ {
+ result = -1;
+ }
+
+ if (thisNumericChunk > thatNumericChunk)
+ {
+ result = 1;
+ }
+ }
+ else
+ {
+ result = thisChunk.ToString().CompareTo(thatChunk.ToString());
+ }
+
+ if (result != 0)
+ {
+ return result;
+ }
+ }
+
+ return 0;
+ }
+
+ public int Compare(string x, string y)
+ {
+ return CompareValues(x, y);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
new file mode 100644
index 000000000..edb195820
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -0,0 +1,51 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class ArtistComparer
+ /// </summary>
+ public class ArtistComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private string GetValue(BaseItem x)
+ {
+ var audio = x as Audio;
+
+ if (audio == null)
+ {
+ return string.Empty;
+ }
+
+ return audio.Artists.Count == 0 ? null : audio.Artists[0];
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Artist; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/BudgetComparer.cs b/Emby.Server.Implementations/Sorting/BudgetComparer.cs
new file mode 100644
index 000000000..f3aef69f1
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/BudgetComparer.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class BudgetComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ private double GetValue(BaseItem x)
+ {
+ var hasBudget = x as IHasBudget;
+ if (hasBudget != null)
+ {
+ return hasBudget.Budget ?? 0;
+ }
+ return 0;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Budget; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
new file mode 100644
index 000000000..396bbbdb9
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class CommunityRatingComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return (x.CommunityRating ?? 0).CompareTo(y.CommunityRating ?? 0);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.CommunityRating; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
new file mode 100644
index 000000000..877dbfcc1
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -0,0 +1,37 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class CriticRatingComparer
+ /// </summary>
+ public class CriticRatingComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ private float GetValue(BaseItem x)
+ {
+ return x.CriticRating ?? 0;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.CriticRating; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
new file mode 100644
index 000000000..c436fcb4a
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class DateCreatedComparer
+ /// </summary>
+ public class DateCreatedComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return DateTime.Compare(x.DateCreated, y.DateCreated);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.DateCreated; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
new file mode 100644
index 000000000..fc92505ac
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -0,0 +1,69 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class DateLastMediaAddedComparer : IUserBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public User User { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetDate(x).CompareTo(GetDate(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private DateTime GetDate(BaseItem x)
+ {
+ var folder = x as Folder;
+
+ if (folder != null)
+ {
+ if (folder.DateLastMediaAdded.HasValue)
+ {
+ return folder.DateLastMediaAdded.Value;
+ }
+ }
+
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.DateLastContentAdded; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
new file mode 100644
index 000000000..388d2772e
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -0,0 +1,69 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class DatePlayedComparer
+ /// </summary>
+ public class DatePlayedComparer : IUserBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public User User { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetDate(x).CompareTo(GetDate(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private DateTime GetDate(BaseItem x)
+ {
+ var userdata = UserDataRepository.GetUserData(User, x);
+
+ if (userdata != null && userdata.LastPlayedDate.HasValue)
+ {
+ return userdata.LastPlayedDate.Value;
+ }
+
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.DatePlayed; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/GameSystemComparer.cs b/Emby.Server.Implementations/Sorting/GameSystemComparer.cs
new file mode 100644
index 000000000..4ee30397d
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/GameSystemComparer.cs
@@ -0,0 +1,54 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class GameSystemComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private string GetValue(BaseItem x)
+ {
+ var game = x as Game;
+
+ if (game != null)
+ {
+ return game.GameSystem;
+ }
+
+ var system = x as GameSystem;
+
+ if (system != null)
+ {
+ return system.GameSystemName;
+ }
+
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.GameSystem; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
new file mode 100644
index 000000000..27485f09e
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -0,0 +1,58 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class IsFavoriteOrLikeComparer : IUserBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public User User { get; set; }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private int GetValue(BaseItem x)
+ {
+ return x.IsFavoriteOrLiked(User) ? 0 : 1;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.IsFavoriteOrLiked; }
+ }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
new file mode 100644
index 000000000..756d13bd8
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class IsFolderComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private int GetValue(BaseItem x)
+ {
+ return x.IsFolder ? 0 : 1;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.IsFolder; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
new file mode 100644
index 000000000..987dc54a5
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -0,0 +1,58 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class IsPlayedComparer : IUserBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public User User { get; set; }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private int GetValue(BaseItem x)
+ {
+ return x.IsPlayed(User) ? 0 : 1;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.IsUnplayed; }
+ }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
new file mode 100644
index 000000000..0f4e4c37e
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -0,0 +1,58 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class IsUnplayedComparer : IUserBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public User User { get; set; }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private int GetValue(BaseItem x)
+ {
+ return x.IsUnplayed(User) ? 0 : 1;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.IsUnplayed; }
+ }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/MetascoreComparer.cs b/Emby.Server.Implementations/Sorting/MetascoreComparer.cs
new file mode 100644
index 000000000..9759e0228
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/MetascoreComparer.cs
@@ -0,0 +1,41 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class MetascoreComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ private float GetValue(BaseItem x)
+ {
+ var hasMetascore = x as IHasMetascore;
+
+ if (hasMetascore != null)
+ {
+ return hasMetascore.Metascore ?? 0;
+ }
+
+ return 0;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Metascore; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
new file mode 100644
index 000000000..8ab5e5172
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class NameComparer
+ /// </summary>
+ public class NameComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Name; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
new file mode 100644
index 000000000..3eab4fccc
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -0,0 +1,40 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class OfficialRatingComparer : IBaseItemComparer
+ {
+ private readonly ILocalizationManager _localization;
+
+ public OfficialRatingComparer(ILocalizationManager localization)
+ {
+ _localization = localization;
+ }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ var levelX = string.IsNullOrEmpty(x.OfficialRating) ? 0 : _localization.GetRatingLevel(x.OfficialRating) ?? 0;
+ var levelY = string.IsNullOrEmpty(y.OfficialRating) ? 0 : _localization.GetRatingLevel(y.OfficialRating) ?? 0;
+
+ return levelX.CompareTo(levelY);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.OfficialRating; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
new file mode 100644
index 000000000..aecad7c58
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -0,0 +1,63 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class PlayCountComparer
+ /// </summary>
+ public class PlayCountComparer : IUserBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public User User { get; set; }
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private int GetValue(BaseItem x)
+ {
+ var userdata = UserDataRepository.GetUserData(User, x);
+
+ return userdata == null ? 0 : userdata.PlayCount;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.PlayCount; }
+ }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/PlayersComparer.cs b/Emby.Server.Implementations/Sorting/PlayersComparer.cs
new file mode 100644
index 000000000..3b54517c3
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/PlayersComparer.cs
@@ -0,0 +1,46 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class PlayersComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the value.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>System.String.</returns>
+ private int GetValue(BaseItem x)
+ {
+ var game = x as Game;
+
+ if (game != null)
+ {
+ return game.PlayersSupported ?? 0;
+ }
+
+ return 0;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Players; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
new file mode 100644
index 000000000..d7219c86f
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -0,0 +1,59 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class PremiereDateComparer
+ /// </summary>
+ public class PremiereDateComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetDate(x).CompareTo(GetDate(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private DateTime GetDate(BaseItem x)
+ {
+ if (x.PremiereDate.HasValue)
+ {
+ return x.PremiereDate.Value;
+ }
+
+ if (x.ProductionYear.HasValue)
+ {
+ try
+ {
+ return new DateTime(x.ProductionYear.Value, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ // Don't blow up if the item has a bad ProductionYear, just return MinValue
+ }
+ }
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.PremiereDate; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
new file mode 100644
index 000000000..ea479419a
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -0,0 +1,52 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class ProductionYearComparer
+ /// </summary>
+ public class ProductionYearComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private int GetValue(BaseItem x)
+ {
+ if (x.ProductionYear.HasValue)
+ {
+ return x.ProductionYear.Value;
+ }
+
+ if (x.PremiereDate.HasValue)
+ {
+ return x.PremiereDate.Value.Year;
+ }
+
+ return 0;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.ProductionYear; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
new file mode 100644
index 000000000..1fbecde56
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class RandomComparer
+ /// </summary>
+ public class RandomComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return Guid.NewGuid().CompareTo(Guid.NewGuid());
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Random; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/RevenueComparer.cs b/Emby.Server.Implementations/Sorting/RevenueComparer.cs
new file mode 100644
index 000000000..62e43eac1
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/RevenueComparer.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class RevenueComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetValue(x).CompareTo(GetValue(y));
+ }
+
+ private double GetValue(BaseItem x)
+ {
+ var hasBudget = x as IHasBudget;
+ if (hasBudget != null)
+ {
+ return hasBudget.Revenue ?? 0;
+ }
+ return 0;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Revenue; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
new file mode 100644
index 000000000..63c4758cb
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class RuntimeComparer
+ /// </summary>
+ public class RuntimeComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Runtime; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
new file mode 100644
index 000000000..b315d33c3
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -0,0 +1,37 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ class SeriesSortNameComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ private string GetValue(BaseItem item)
+ {
+ var hasSeries = item as IHasSeries;
+
+ return hasSeries != null ? hasSeries.SeriesSortName : null;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.SeriesSortName; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
new file mode 100644
index 000000000..f2a764840
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class SortNameComparer
+ /// </summary>
+ public class SortNameComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.SortName; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
new file mode 100644
index 000000000..6be5f4883
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -0,0 +1,47 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class StartDateComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return GetDate(x).CompareTo(GetDate(y));
+ }
+
+ /// <summary>
+ /// Gets the date.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <returns>DateTime.</returns>
+ private DateTime GetDate(BaseItem x)
+ {
+ var hasStartDate = x as LiveTvProgram;
+
+ if (hasStartDate != null)
+ {
+ return hasStartDate.StartDate;
+ }
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.StartDate; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
new file mode 100644
index 000000000..6735022af
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ public class StudioComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem x, BaseItem y)
+ {
+ return AlphanumComparator.CompareValues(x.Studios.FirstOrDefault() ?? string.Empty, y.Studios.FirstOrDefault() ?? string.Empty);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ItemSortBy.Studio; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/packages.config b/Emby.Server.Implementations/packages.config
new file mode 100644
index 000000000..97e40a2f4
--- /dev/null
+++ b/Emby.Server.Implementations/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="MediaBrowser.Naming" version="1.0.0.57" targetFramework="portable45-net45+win8" />
+ <package id="Patterns.Logging" version="1.0.0.4" targetFramework="portable45-net45+win8" />
+</packages> \ No newline at end of file