diff options
Diffstat (limited to 'Emby.Server.Implementations')
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<IEnumerable<IntroInfo>>.</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 |
