aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Drawing/DrawingUtils.cs81
-rw-r--r--MediaBrowser.Controller/Drawing/ImageManager.cs600
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessor.cs148
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs68
-rw-r--r--MediaBrowser.Controller/Entities/Audio.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs78
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs133
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs10
-rw-r--r--MediaBrowser.Controller/Entities/BaseEntity.cs94
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1584
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs49
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs100
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs1666
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs17
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs10
-rw-r--r--MediaBrowser.Controller/Entities/ISupportsSpecialFeatures.cs105
-rw-r--r--MediaBrowser.Controller/Entities/IndexFolder.cs204
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs175
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs66
-rw-r--r--MediaBrowser.Controller/Entities/PlaybackProgressEventArgs.cs13
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs17
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs170
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs177
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs101
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs51
-rw-r--r--MediaBrowser.Controller/Entities/User.cs467
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs183
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs21
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs129
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs17
-rw-r--r--MediaBrowser.Controller/FFMpeg/FFProbe.cs137
-rw-r--r--MediaBrowser.Controller/FFMpeg/FFProbeResult.cs119
-rw-r--r--MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id1
-rw-r--r--MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id1
-rw-r--r--MediaBrowser.Controller/FFMpeg/readme.txt3
-rw-r--r--MediaBrowser.Controller/IO/DirectoryWatchers.cs697
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs381
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs132
-rw-r--r--MediaBrowser.Controller/IO/FileSystemManager.cs112
-rw-r--r--MediaBrowser.Controller/IO/Shortcut.cs185
-rw-r--r--MediaBrowser.Controller/Kernel.cs985
-rw-r--r--MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs171
-rw-r--r--MediaBrowser.Controller/Library/DtoBuilder.cs934
-rw-r--r--MediaBrowser.Controller/Library/ItemController.cs136
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs397
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveEventArgs.cs104
-rw-r--r--MediaBrowser.Controller/Library/LibraryManager.cs511
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs69
-rw-r--r--MediaBrowser.Controller/Library/ResourcePool.cs79
-rw-r--r--MediaBrowser.Controller/Library/UserDataManager.cs219
-rw-r--r--MediaBrowser.Controller/Library/UserManager.cs395
-rw-r--r--MediaBrowser.Controller/Localization/AURatingsDictionary.cs23
-rw-r--r--MediaBrowser.Controller/Localization/BaseStrings.cs290
-rw-r--r--MediaBrowser.Controller/Localization/GBRatingsDictionary.cs24
-rw-r--r--MediaBrowser.Controller/Localization/LocalizedStrings.cs155
-rw-r--r--MediaBrowser.Controller/Localization/NLRatingsDictionary.cs23
-rw-r--r--MediaBrowser.Controller/Localization/Ratings.cs162
-rw-r--r--MediaBrowser.Controller/Localization/RatingsDefinition.cs122
-rw-r--r--MediaBrowser.Controller/Localization/USRatingsDictionary.cs39
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj427
-rw-r--r--MediaBrowser.Controller/MediaInfo/BDInfoResult.cs41
-rw-r--r--MediaBrowser.Controller/MediaInfo/FFMpegManager.cs1078
-rw-r--r--MediaBrowser.Controller/MediaInfo/FFProbeResult.cs354
-rw-r--r--MediaBrowser.Controller/MediaInfo/ffmpeg20130209.zip.REMOVED.git-id1
-rw-r--r--MediaBrowser.Controller/MediaInfo/readme.txt5
-rw-r--r--MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs29
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs45
-rw-r--r--MediaBrowser.Controller/Persistence/IRepository.cs23
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs28
-rw-r--r--MediaBrowser.Controller/Persistence/IUserRepository.cs35
-rw-r--r--MediaBrowser.Controller/Persistence/SQLite/SQLiteDisplayPreferencesRepository.cs139
-rw-r--r--MediaBrowser.Controller/Persistence/SQLite/SQLiteExtensions.cs61
-rw-r--r--MediaBrowser.Controller/Persistence/SQLite/SQLiteItemRepository.cs268
-rw-r--r--MediaBrowser.Controller/Persistence/SQLite/SQLiteRepository.cs301
-rw-r--r--MediaBrowser.Controller/Persistence/SQLite/SQLiteUserDataRepository.cs138
-rw-r--r--MediaBrowser.Controller/Persistence/SQLite/SQLiteUserRepository.cs147
-rw-r--r--MediaBrowser.Controller/Persistence/TypeMapper.cs47
-rw-r--r--MediaBrowser.Controller/Playback/BaseIntroProvider.cs19
-rw-r--r--MediaBrowser.Controller/Plugins/BaseConfigurationPage.cs81
-rw-r--r--MediaBrowser.Controller/Plugins/PluginSecurityManager.cs65
-rw-r--r--MediaBrowser.Controller/Properties/AssemblyInfo.cs69
-rw-r--r--MediaBrowser.Controller/Providers/AudioInfoProvider.cs262
-rw-r--r--MediaBrowser.Controller/Providers/BaseImageEnhancer.cs113
-rw-r--r--MediaBrowser.Controller/Providers/BaseItemXmlParser.cs1332
-rw-r--r--MediaBrowser.Controller/Providers/BaseMetadataProvider.cs512
-rw-r--r--MediaBrowser.Controller/Providers/BaseProviderInfo.cs75
-rw-r--r--MediaBrowser.Controller/Providers/FanartBaseProvider.cs84
-rw-r--r--MediaBrowser.Controller/Providers/FolderProviderFromXml.cs121
-rw-r--r--MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs359
-rw-r--r--MediaBrowser.Controller/Providers/ImagesByNameProvider.cs103
-rw-r--r--MediaBrowser.Controller/Providers/LocalTrailerProvider.cs47
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs265
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs17
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs74
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs358
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs84
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs137
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs208
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs291
-rw-r--r--MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs220
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs1607
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs100
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs134
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs45
-rw-r--r--MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs113
-rw-r--r--MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs465
-rw-r--r--MediaBrowser.Controller/Providers/ProviderManager.cs332
-rw-r--r--MediaBrowser.Controller/Providers/SortNameProvider.cs129
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs194
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs181
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs164
-rw-r--r--MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs140
-rw-r--r--MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs288
-rw-r--r--MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs287
-rw-r--r--MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs545
-rw-r--r--MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs122
-rw-r--r--MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs159
-rw-r--r--MediaBrowser.Controller/Providers/VideoInfoProvider.cs168
-rw-r--r--MediaBrowser.Controller/Resolvers/Audio/AudioResolver.cs29
-rw-r--r--MediaBrowser.Controller/Resolvers/Audio/MusicAlbumResolver.cs27
-rw-r--r--MediaBrowser.Controller/Resolvers/Audio/MusicArtistResolver.cs29
-rw-r--r--MediaBrowser.Controller/Resolvers/AudioResolver.cs54
-rw-r--r--MediaBrowser.Controller/Resolvers/BaseItemResolver.cs291
-rw-r--r--MediaBrowser.Controller/Resolvers/BaseResolutionIgnoreRule.cs12
-rw-r--r--MediaBrowser.Controller/Resolvers/CoreResolutionIgnoreRule.cs52
-rw-r--r--MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs281
-rw-r--r--MediaBrowser.Controller/Resolvers/FolderResolver.cs107
-rw-r--r--MediaBrowser.Controller/Resolvers/LocalTrailerResolver.cs40
-rw-r--r--MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs71
-rw-r--r--MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs326
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs86
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs59
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs164
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/TVUtils.cs415
-rw-r--r--MediaBrowser.Controller/Resolvers/VideoResolver.cs173
-rw-r--r--MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs100
-rw-r--r--MediaBrowser.Controller/ScheduledTasks/ImageCleanupTask.cs202
-rw-r--r--MediaBrowser.Controller/ScheduledTasks/PeopleValidationTask.cs72
-rw-r--r--MediaBrowser.Controller/ScheduledTasks/PluginUpdateTask.cs113
-rw-r--r--MediaBrowser.Controller/ScheduledTasks/RefreshMediaLibraryTask.cs78
-rw-r--r--MediaBrowser.Controller/ServerApplicationPaths.cs612
-rw-r--r--MediaBrowser.Controller/Sorting/BaseItemComparer.cs231
-rw-r--r--MediaBrowser.Controller/Sorting/SortOrder.cs33
-rw-r--r--MediaBrowser.Controller/Updates/InstallationManager.cs486
-rw-r--r--MediaBrowser.Controller/Weather/BaseWeatherProvider.cs71
-rw-r--r--MediaBrowser.Controller/Weather/WeatherProvider.cs500
-rw-r--r--MediaBrowser.Controller/Xml/XmlExtensions.cs46
-rw-r--r--MediaBrowser.Controller/packages.config14
149 files changed, 24713 insertions, 6469 deletions
diff --git a/MediaBrowser.Controller/Drawing/DrawingUtils.cs b/MediaBrowser.Controller/Drawing/DrawingUtils.cs
deleted file mode 100644
index 8e2f829b98..0000000000
--- a/MediaBrowser.Controller/Drawing/DrawingUtils.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Drawing;
-
-namespace MediaBrowser.Controller.Drawing
-{
- public static class DrawingUtils
- {
- /// <summary>
- /// Resizes a set of dimensions
- /// </summary>
- public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
- {
- return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
- }
-
- /// <summary>
- /// Resizes a set of dimensions
- /// </summary>
- /// <param name="size">The original size object</param>
- /// <param name="width">A new fixed width, if desired</param>
- /// <param name="height">A new fixed neight, if desired</param>
- /// <param name="maxWidth">A max fixed width, if desired</param>
- /// <param name="maxHeight">A max fixed height, if desired</param>
- /// <returns>A new size object</returns>
- public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
- {
- decimal newWidth = size.Width;
- decimal newHeight = size.Height;
-
- if (width.HasValue && height.HasValue)
- {
- newWidth = width.Value;
- newHeight = height.Value;
- }
-
- else if (height.HasValue)
- {
- newWidth = GetNewWidth(newHeight, newWidth, height.Value);
- newHeight = height.Value;
- }
-
- else if (width.HasValue)
- {
- newHeight = GetNewHeight(newHeight, newWidth, width.Value);
- newWidth = width.Value;
- }
-
- if (maxHeight.HasValue && maxHeight < newHeight)
- {
- newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
- newHeight = maxHeight.Value;
- }
-
- if (maxWidth.HasValue && maxWidth < newWidth)
- {
- newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
- newWidth = maxWidth.Value;
- }
-
- return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
- }
-
- private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
- {
- decimal scaleFactor = newHeight;
- scaleFactor /= currentHeight;
- scaleFactor *= currentWidth;
-
- return scaleFactor;
- }
-
- private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
- {
- decimal scaleFactor = newWidth;
- scaleFactor /= currentWidth;
- scaleFactor *= currentHeight;
-
- return scaleFactor;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Drawing/ImageManager.cs b/MediaBrowser.Controller/Drawing/ImageManager.cs
new file mode 100644
index 0000000000..b493a97afe
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/ImageManager.cs
@@ -0,0 +1,600 @@
+using MediaBrowser.Common.Drawing;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ /// <summary>
+ /// Class ImageManager
+ /// </summary>
+ public class ImageManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// Gets the image size cache.
+ /// </summary>
+ /// <value>The image size cache.</value>
+ private FileSystemRepository ImageSizeCache { get; set; }
+
+ /// <summary>
+ /// Gets or sets the resized image cache.
+ /// </summary>
+ /// <value>The resized image cache.</value>
+ private FileSystemRepository ResizedImageCache { get; set; }
+ /// <summary>
+ /// Gets the cropped image cache.
+ /// </summary>
+ /// <value>The cropped image cache.</value>
+ private FileSystemRepository CroppedImageCache { get; set; }
+
+ /// <summary>
+ /// Gets the cropped image cache.
+ /// </summary>
+ /// <value>The cropped image cache.</value>
+ private FileSystemRepository EnhancedImageCache { get; set; }
+
+ /// <summary>
+ /// The cached imaged sizes
+ /// </summary>
+ private readonly ConcurrentDictionary<string, Task<ImageSize>> _cachedImagedSizes = new ConcurrentDictionary<string, Task<ImageSize>>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ImageManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public ImageManager(Kernel kernel)
+ : base(kernel)
+ {
+ ImageSizeCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "image-sizes"));
+ ResizedImageCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "resized-images"));
+ CroppedImageCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "cropped-images"));
+ EnhancedImageCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "enhanced-images"));
+ }
+
+ /// <summary>
+ /// Processes an image by resizing to target dimensions
+ /// </summary>
+ /// <param name="entity">The entity that owns the image</param>
+ /// <param name="imageType">The image type</param>
+ /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
+ /// <param name="cropWhitespace">if set to <c>true</c> [crop whitespace].</param>
+ /// <param name="dateModified">The last date modified of the original image file</param>
+ /// <param name="toStream">The stream to save the new image to</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">entity</exception>
+ public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, bool cropWhitespace, DateTime dateModified, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
+ {
+ if (entity == null)
+ {
+ throw new ArgumentNullException("entity");
+ }
+
+ if (toStream == null)
+ {
+ throw new ArgumentNullException("toStream");
+ }
+
+ var originalImagePath = GetImagePath(entity, imageType, imageIndex);
+
+ if (cropWhitespace)
+ {
+ try
+ {
+ originalImagePath = GetCroppedImage(originalImagePath, dateModified);
+ }
+ catch (Exception ex)
+ {
+ // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
+ Logger.ErrorException("Error cropping image", ex);
+ }
+ }
+
+ try
+ {
+ // Enhance if we have enhancers
+ var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex).ConfigureAwait(false);
+
+ // If the path changed update dateModified
+ if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase))
+ {
+ dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath);
+ originalImagePath = ehnancedImagePath;
+ }
+ }
+ catch
+ {
+ Logger.Error("Error enhancing image");
+ }
+
+ var originalImageSize = await GetImageSize(originalImagePath, dateModified).ConfigureAwait(false);
+
+ // Determine the output size based on incoming parameters
+ var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight);
+
+ if (!quality.HasValue)
+ {
+ quality = 90;
+ }
+
+ var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified);
+
+ // Grab the cache file if it already exists
+ try
+ {
+ using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
+ }
+ return;
+ }
+ catch (FileNotFoundException)
+ {
+ // Cache file doesn't exist. No biggie.
+ }
+
+ using (var fileStream = File.OpenRead(originalImagePath))
+ {
+ using (var originalImage = Bitmap.FromStream(fileStream, true, false))
+ {
+ var newWidth = Convert.ToInt32(newSize.Width);
+ var newHeight = Convert.ToInt32(newSize.Height);
+
+ // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+ var thumbnail = originalImage.PixelFormat.HasFlag(PixelFormat.Indexed) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat);
+
+ // Preserve the original resolution
+ thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+
+ var thumbnailGraph = Graphics.FromImage(thumbnail);
+
+ thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+ thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+ thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+ thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+
+ thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
+
+ var outputFormat = originalImage.RawFormat;
+
+ using (var memoryStream = new MemoryStream { })
+ {
+ // Save to the memory stream
+ thumbnail.Save(outputFormat, memoryStream, quality.Value);
+
+ var bytes = memoryStream.ToArray();
+
+ var outputTask = Task.Run(async () => await toStream.WriteAsync(bytes, 0, bytes.Length));
+
+ // Save to the cache location
+ using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ // Save to the filestream
+ await cacheFileStream.WriteAsync(bytes, 0, bytes.Length);
+ }
+
+ await outputTask.ConfigureAwait(false);
+ }
+
+ thumbnailGraph.Dispose();
+ thumbnail.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the cache file path based on a set of parameters
+ /// </summary>
+ /// <param name="originalPath">The path to the original image file</param>
+ /// <param name="outputSize">The size to output the image in</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ /// <param name="dateModified">The last modified date of the image</param>
+ /// <returns>System.String.</returns>
+ private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified)
+ {
+ var filename = originalPath;
+
+ filename += "width=" + outputSize.Width;
+
+ filename += "height=" + outputSize.Height;
+
+ filename += "quality=" + quality;
+
+ filename += "datemodified=" + dateModified.Ticks;
+
+ return ResizedImageCache.GetResourcePath(filename, Path.GetExtension(originalPath));
+ }
+
+
+ /// <summary>
+ /// Gets image dimensions
+ /// </summary>
+ /// <param name="imagePath">The image path.</param>
+ /// <param name="dateModified">The date modified.</param>
+ /// <returns>Task{ImageSize}.</returns>
+ /// <exception cref="System.ArgumentNullException">imagePath</exception>
+ public Task<ImageSize> GetImageSize(string imagePath, DateTime dateModified)
+ {
+ if (string.IsNullOrEmpty(imagePath))
+ {
+ throw new ArgumentNullException("imagePath");
+ }
+
+ var name = imagePath + "datemodified=" + dateModified.Ticks;
+
+ return _cachedImagedSizes.GetOrAdd(name, keyName => GetImageSizeTask(keyName, imagePath));
+ }
+
+ /// <summary>
+ /// Gets cached image dimensions, or results null if non-existant
+ /// </summary>
+ /// <param name="keyName">Name of the key.</param>
+ /// <param name="imagePath">The image path.</param>
+ /// <returns>Task{ImageSize}.</returns>
+ private Task<ImageSize> GetImageSizeTask(string keyName, string imagePath)
+ {
+ return Task.Run(() => GetImageSize(keyName, imagePath));
+ }
+
+ /// <summary>
+ /// Gets the size of the image.
+ /// </summary>
+ /// <param name="keyName">Name of the key.</param>
+ /// <param name="imagePath">The image path.</param>
+ /// <returns>ImageSize.</returns>
+ private ImageSize GetImageSize(string keyName, string imagePath)
+ {
+ // Now check the file system cache
+ var fullCachePath = ImageSizeCache.GetResourcePath(keyName, ".pb");
+
+ try
+ {
+ var result = Kernel.ProtobufSerializer.DeserializeFromFile<int[]>(fullCachePath);
+
+ return new ImageSize { Width = result[0], Height = result[1] };
+ }
+ catch (FileNotFoundException)
+ {
+ // Cache file doesn't exist no biggie
+ }
+
+ var size = ImageHeader.GetDimensions(imagePath);
+
+ var imageSize = new ImageSize { Width = size.Width, Height = size.Height };
+
+ // Update the file system cache
+ CacheImageSize(fullCachePath, size.Width, size.Height);
+
+ return imageSize;
+ }
+
+ /// <summary>
+ /// Caches image dimensions
+ /// </summary>
+ /// <param name="cachePath">The cache path.</param>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ private void CacheImageSize(string cachePath, int width, int height)
+ {
+ var output = new[] { width, height };
+
+ Kernel.ProtobufSerializer.SerializeToFile(output, cachePath);
+ }
+
+ /// <summary>
+ /// Gets the image path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ /// <exception cref="System.InvalidOperationException"></exception>
+ public string GetImagePath(BaseItem item, ImageType imageType, int imageIndex)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (imageType == ImageType.Backdrop)
+ {
+ if (item.BackdropImagePaths == null)
+ {
+ throw new InvalidOperationException(string.Format("Item {0} does not have any Backdrops.", item.Name));
+ }
+
+ return item.BackdropImagePaths[imageIndex];
+ }
+
+ if (imageType == ImageType.Screenshot)
+ {
+ if (item.ScreenshotImagePaths == null)
+ {
+ throw new InvalidOperationException(string.Format("Item {0} does not have any Screenshots.", item.Name));
+ }
+
+ return item.ScreenshotImagePaths[imageIndex];
+ }
+
+ if (imageType == ImageType.ChapterImage)
+ {
+ var video = (Video)item;
+
+ if (video.Chapters == null)
+ {
+ throw new InvalidOperationException(string.Format("Item {0} does not have any Chapters.", item.Name));
+ }
+
+ return video.Chapters[imageIndex].ImagePath;
+ }
+
+ return item.GetImage(imageType);
+ }
+
+ /// <summary>
+ /// Gets the image date modified.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>DateTime.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public DateTime GetImageDateModified(BaseItem item, ImageType imageType, int imageIndex)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var imagePath = GetImagePath(item, imageType, imageIndex);
+
+ return GetImageDateModified(item, imagePath);
+ }
+
+ /// <summary>
+ /// Gets the image date modified.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imagePath">The image path.</param>
+ /// <returns>DateTime.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public DateTime GetImageDateModified(BaseItem item, string imagePath)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (string.IsNullOrEmpty(imagePath))
+ {
+ throw new ArgumentNullException("imagePath");
+ }
+
+ var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(imagePath);
+
+ // If we didn't the metafile entry, check the Season
+ if (!metaFileEntry.HasValue)
+ {
+ var episode = item as Episode;
+
+ if (episode != null && episode.Season != null)
+ {
+ episode.Season.ResolveArgs.GetMetaFileByPath(imagePath);
+ }
+ }
+
+ // See if we can avoid a file system lookup by looking for the file in ResolveArgs
+ return metaFileEntry == null ? File.GetLastWriteTimeUtc(imagePath) : metaFileEntry.Value.LastWriteTimeUtc;
+ }
+
+ /// <summary>
+ /// Crops whitespace from an image, caches the result, and returns the cached path
+ /// </summary>
+ /// <param name="originalImagePath">The original image path.</param>
+ /// <param name="dateModified">The date modified.</param>
+ /// <returns>System.String.</returns>
+ private string GetCroppedImage(string originalImagePath, DateTime dateModified)
+ {
+ var name = originalImagePath;
+ name += "datemodified=" + dateModified.Ticks;
+
+ var croppedImagePath = CroppedImageCache.GetResourcePath(name, Path.GetExtension(originalImagePath));
+
+ if (!CroppedImageCache.ContainsFilePath(croppedImagePath))
+ {
+ using (var fileStream = File.OpenRead(originalImagePath))
+ {
+ using (var originalImage = (Bitmap)Bitmap.FromStream(fileStream, true, false))
+ {
+ var outputFormat = originalImage.RawFormat;
+
+ using (var croppedImage = originalImage.CropWhitespace())
+ {
+ using (var cacheFileStream = new FileStream(croppedImagePath, FileMode.Create))
+ {
+ croppedImage.Save(outputFormat, cacheFileStream, 100);
+ }
+ }
+ }
+ }
+ }
+
+ return croppedImagePath;
+ }
+
+ /// <summary>
+ /// Runs an image through the image enhancers, caches the result, and returns the cached path
+ /// </summary>
+ /// <param name="originalImagePath">The original image path.</param>
+ /// <param name="dateModified">The date modified of the original image file.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">originalImagePath</exception>
+ public async Task<string> GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex)
+ {
+ if (string.IsNullOrEmpty(originalImagePath))
+ {
+ throw new ArgumentNullException("originalImagePath");
+ }
+
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var supportedEnhancers = Kernel.ImageEnhancers.Where(i => i.Supports(item, imageType)).ToList();
+
+ // No enhancement - don't cache
+ if (supportedEnhancers.Count == 0)
+ {
+ return originalImagePath;
+ }
+
+ var cacheGuid = GetImageCacheTag(originalImagePath, dateModified, supportedEnhancers, item, imageType);
+
+ // All enhanced images are saved as png to allow transparency
+ var enhancedImagePath = EnhancedImageCache.GetResourcePath(cacheGuid + ".png");
+
+ if (!EnhancedImageCache.ContainsFilePath(enhancedImagePath))
+ {
+ using (var fileStream = File.OpenRead(originalImagePath))
+ {
+ using (var originalImage = Image.FromStream(fileStream, true, false))
+ {
+ //Pass the image through registered enhancers
+ using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
+ {
+ //And then save it in the cache
+ newImage.Save(enhancedImagePath, ImageFormat.Png);
+ }
+ }
+ }
+ }
+
+ return enhancedImagePath;
+ }
+
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imagePath">The image path.</param>
+ /// <returns>Guid.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (string.IsNullOrEmpty(imagePath))
+ {
+ throw new ArgumentNullException("imagePath");
+ }
+
+ var dateModified = GetImageDateModified(item, imagePath);
+
+ var supportedEnhancers = Kernel.ImageEnhancers.Where(i => i.Supports(item, imageType));
+
+ return GetImageCacheTag(imagePath, dateModified, supportedEnhancers, item, imageType);
+ }
+
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
+ /// <param name="originalImagePath">The original image path.</param>
+ /// <param name="dateModified">The date modified of the original image file.</param>
+ /// <param name="imageEnhancers">The image enhancers.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns>Guid.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public Guid GetImageCacheTag(string originalImagePath, DateTime dateModified, IEnumerable<BaseImageEnhancer> imageEnhancers, BaseItem item, ImageType imageType)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (imageEnhancers == null)
+ {
+ throw new ArgumentNullException("imageEnhancers");
+ }
+
+ if (string.IsNullOrEmpty(originalImagePath))
+ {
+ throw new ArgumentNullException("originalImagePath");
+ }
+
+ // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
+ var cacheKeys = imageEnhancers.Select(i => i.GetType().Name + i.LastConfigurationChange(item, imageType).Ticks).ToList();
+ cacheKeys.Add(originalImagePath + dateModified.Ticks);
+
+ return string.Join("|", cacheKeys.ToArray()).GetMD5();
+ }
+
+ /// <summary>
+ /// Executes the image enhancers.
+ /// </summary>
+ /// <param name="imageEnhancers">The image enhancers.</param>
+ /// <param name="originalImage">The original image.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{EnhancedImage}.</returns>
+ private async Task<Image> ExecuteImageEnhancers(IEnumerable<BaseImageEnhancer> imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex)
+ {
+ var result = originalImage;
+
+ // Run the enhancers sequentially in order of priority
+ foreach (var enhancer in imageEnhancers)
+ {
+ result = await enhancer.EnhanceImageAsync(item, result, imageType, imageIndex).ConfigureAwait(false);
+ }
+
+ return result;
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ ImageSizeCache.Dispose();
+ ResizedImageCache.Dispose();
+ CroppedImageCache.Dispose();
+ EnhancedImageCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessor.cs b/MediaBrowser.Controller/Drawing/ImageProcessor.cs
deleted file mode 100644
index 29e40d17d7..0000000000
--- a/MediaBrowser.Controller/Drawing/ImageProcessor.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.Drawing.Imaging;
-using System.IO;
-using System.Linq;
-
-namespace MediaBrowser.Controller.Drawing
-{
- public static class ImageProcessor
- {
- /// <summary>
- /// Processes an image by resizing to target dimensions
- /// </summary>
- /// <param name="entity">The entity that owns the image</param>
- /// <param name="imageType">The image type</param>
- /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
- /// <param name="toStream">The stream to save the new image to</param>
- /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
- /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
- /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
- /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
- /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
- public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
- {
- Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
-
- // Determine the output size based on incoming parameters
- Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
-
- Bitmap thumbnail;
-
- // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
- if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
- {
- thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
- }
- else
- {
- thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
- }
-
- thumbnail.MakeTransparent();
-
- // Preserve the original resolution
- thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
-
- Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
-
- thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
- thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
- thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
- thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
- thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
-
- thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
-
- ImageFormat outputFormat = originalImage.RawFormat;
-
- // Write to the output stream
- SaveImage(outputFormat, thumbnail, toStream, quality);
-
- thumbnailGraph.Dispose();
- thumbnail.Dispose();
- originalImage.Dispose();
- }
-
- public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
- {
- var item = entity as BaseItem;
-
- if (item != null)
- {
- if (imageType == ImageType.Logo)
- {
- return item.LogoImagePath;
- }
- if (imageType == ImageType.Backdrop)
- {
- return item.BackdropImagePaths.ElementAt(imageIndex);
- }
- if (imageType == ImageType.Banner)
- {
- return item.BannerImagePath;
- }
- if (imageType == ImageType.Art)
- {
- return item.ArtImagePath;
- }
- if (imageType == ImageType.Thumbnail)
- {
- return item.ThumbnailImagePath;
- }
- }
-
- return entity.PrimaryImagePath;
- }
-
- public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
- {
- // Use special save methods for jpeg and png that will result in a much higher quality image
- // All other formats use the generic Image.Save
- if (ImageFormat.Jpeg.Equals(outputFormat))
- {
- SaveJpeg(newImage, toStream, quality);
- }
- else if (ImageFormat.Png.Equals(outputFormat))
- {
- newImage.Save(toStream, ImageFormat.Png);
- }
- else
- {
- newImage.Save(toStream, outputFormat);
- }
- }
-
- public static void SaveJpeg(Image image, Stream target, int? quality)
- {
- if (!quality.HasValue)
- {
- quality = 90;
- }
-
- using (var encoderParameters = new EncoderParameters(1))
- {
- encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
- image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
- }
- }
-
- public static ImageCodecInfo GetImageCodecInfo(string mimeType)
- {
- ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
-
- for (int i = 0; i < info.Length; i++)
- {
- ImageCodecInfo ici = info[i];
- if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
- {
- return ici;
- }
- }
- return info[1];
- }
- }
-}
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
new file mode 100644
index 0000000000..c4fda4fa27
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Specialized folder that can have items added to it's children by external entities.
+ /// Used for our RootFolder so plug-ins can add items.
+ /// </summary>
+ public class AggregateFolder : Folder
+ {
+ /// <summary>
+ /// The _virtual children
+ /// </summary>
+ private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
+
+ /// <summary>
+ /// Gets the virtual children.
+ /// </summary>
+ /// <value>The virtual children.</value>
+ public ConcurrentBag<BaseItem> VirtualChildren
+ {
+ get { return _virtualChildren; }
+ }
+
+ /// <summary>
+ /// Adds the virtual child.
+ /// </summary>
+ /// <param name="child">The child.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddVirtualChild(BaseItem child)
+ {
+ if (child == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ _virtualChildren.Add(child);
+ }
+
+ /// <summary>
+ /// Get the children of this folder from the actual file system
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected override IEnumerable<BaseItem> GetNonCachedChildren()
+ {
+ return base.GetNonCachedChildren().Concat(_virtualChildren);
+ }
+
+ /// <summary>
+ /// Finds the virtual child.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public BaseItem FindVirtualChild(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ return _virtualChildren.FirstOrDefault(i => i.Id == id);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio.cs b/MediaBrowser.Controller/Entities/Audio.cs
deleted file mode 100644
index 61e901dd22..0000000000
--- a/MediaBrowser.Controller/Entities/Audio.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Audio : BaseItem
- {
- public int BitRate { get; set; }
- public int Channels { get; set; }
- public int SampleRate { get; set; }
-
- public string Artist { get; set; }
- public string Album { get; set; }
- public string AlbumArtist { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
new file mode 100644
index 0000000000..511b589e53
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -0,0 +1,78 @@
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class Audio
+ /// </summary>
+ public class Audio : BaseItem, IHasMediaStreams
+ {
+ /// <summary>
+ /// Gets or sets the media streams.
+ /// </summary>
+ /// <value>The media streams.</value>
+ public List<MediaStream> MediaStreams { get; set; }
+
+ /// <summary>
+ /// Override this to true if class should be grouped under a container in indicies
+ /// The container class should be defined via IndexContainer
+ /// </summary>
+ /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool GroupInIndex
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// The unknown album
+ /// </summary>
+ private static readonly MusicAlbum UnknownAlbum = new MusicAlbum {Name = "<Unknown>"};
+ /// <summary>
+ /// Override this to return the folder that should be used to construct a container
+ /// for this item in an index. GroupInIndex should be true as well.
+ /// </summary>
+ /// <value>The index container.</value>
+ [IgnoreDataMember]
+ public override Folder IndexContainer
+ {
+ get
+ {
+ return Parent is MusicAlbum ? Parent : Album != null ? new MusicAlbum {Name = Album, PrimaryImagePath = PrimaryImagePath } : UnknownAlbum;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the artist.
+ /// </summary>
+ /// <value>The artist.</value>
+ public string Artist { get; set; }
+ /// <summary>
+ /// Gets or sets the album.
+ /// </summary>
+ /// <value>The album.</value>
+ public string Album { get; set; }
+ /// <summary>
+ /// Gets or sets the album artist.
+ /// </summary>
+ /// <value>The album artist.</value>
+ public string AlbumArtist { get; set; }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ public override string MediaType
+ {
+ get
+ {
+ return Model.Entities.MediaType.Audio;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
new file mode 100644
index 0000000000..9e4e3c5424
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -0,0 +1,133 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class MusicAlbum
+ /// </summary>
+ public class MusicAlbum : Folder
+ {
+ /// <summary>
+ /// Songs will group into us so don't also include us in the index
+ /// </summary>
+ /// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool IncludeInIndex
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Override this to true if class should be grouped under a container in indicies
+ /// The container class should be defined via IndexContainer
+ /// </summary>
+ /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool GroupInIndex
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// The unknwon artist
+ /// </summary>
+ private static readonly MusicArtist UnknwonArtist = new MusicArtist {Name = "<Unknown>"};
+
+ /// <summary>
+ /// Override this to return the folder that should be used to construct a container
+ /// for this item in an index. GroupInIndex should be true as well.
+ /// </summary>
+ /// <value>The index container.</value>
+ [IgnoreDataMember]
+ public override Folder IndexContainer
+ {
+ get { return Parent as MusicArtist ?? UnknwonArtist; }
+ }
+
+ /// <summary>
+ /// Override to point to first child (song) if not explicitly defined
+ /// </summary>
+ /// <value>The primary image path.</value>
+ [IgnoreDataMember]
+ public override string PrimaryImagePath
+ {
+ get
+ {
+ if (base.PrimaryImagePath == null)
+ {
+ var child = Children.FirstOrDefault();
+
+ return child == null ? base.PrimaryImagePath : child.PrimaryImagePath;
+ }
+
+ return base.PrimaryImagePath;
+ }
+ set
+ {
+ base.PrimaryImagePath = value;
+ }
+ }
+
+ /// <summary>
+ /// Override to point to first child (song)
+ /// </summary>
+ /// <value>The production year.</value>
+ public override int? ProductionYear
+ {
+ get
+ {
+ var child = Children.FirstOrDefault();
+
+ return child == null ? base.ProductionYear : child.ProductionYear;
+ }
+ set
+ {
+ base.ProductionYear = value;
+ }
+ }
+
+ /// <summary>
+ /// Override to point to first child (song)
+ /// </summary>
+ /// <value>The genres.</value>
+ public override List<string> Genres
+ {
+ get
+ {
+ var child = Children.FirstOrDefault();
+
+ return child == null ? base.Genres : child.Genres;
+ }
+ set
+ {
+ base.Genres = value;
+ }
+ }
+
+ /// <summary>
+ /// Override to point to first child (song)
+ /// </summary>
+ /// <value>The studios.</value>
+ public override List<string> Studios
+ {
+ get
+ {
+ var child = Children.FirstOrDefault();
+
+ return child == null ? base.Studios : child.Studios;
+ }
+ set
+ {
+ base.Studios = value;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
new file mode 100644
index 0000000000..b2fc04873f
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class MusicArtist
+ /// </summary>
+ public class MusicArtist : Folder
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseEntity.cs b/MediaBrowser.Controller/Entities/BaseEntity.cs
deleted file mode 100644
index 5b4a360c1f..0000000000
--- a/MediaBrowser.Controller/Entities/BaseEntity.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Providers;
-
-namespace MediaBrowser.Controller.Entities
-{
- /// <summary>
- /// Provides a base entity for all of our types
- /// </summary>
- public abstract class BaseEntity
- {
- public string Name { get; set; }
-
- public Guid Id { get; set; }
-
- public string Path { get; set; }
-
- public Folder Parent { get; set; }
-
- public string PrimaryImagePath { get; set; }
-
- public DateTime DateCreated { get; set; }
-
- public DateTime DateModified { get; set; }
-
- public override string ToString()
- {
- return Name;
- }
- protected Dictionary<Guid, BaseProviderInfo> _providerData;
- /// <summary>
- /// Holds persistent data for providers like last refresh date.
- /// Providers can use this to determine if they need to refresh.
- /// The BaseProviderInfo class can be extended to hold anything a provider may need.
- ///
- /// Keyed by a unique provider ID.
- /// </summary>
- public Dictionary<Guid, BaseProviderInfo> ProviderData
- {
- get
- {
- if (_providerData == null) _providerData = new Dictionary<Guid, BaseProviderInfo>();
- return _providerData;
- }
- set
- {
- _providerData = value;
- }
- }
-
- protected ItemResolveEventArgs _resolveArgs;
- /// <summary>
- /// We attach these to the item so that we only ever have to hit the file system once
- /// (this includes the children of the containing folder)
- /// Use ResolveArgs.FileSystemChildren to check for the existence of files instead of File.Exists
- /// </summary>
- public ItemResolveEventArgs ResolveArgs
- {
- get
- {
- if (_resolveArgs == null)
- {
- _resolveArgs = new ItemResolveEventArgs()
- {
- FileInfo = FileData.GetFileData(this.Path),
- Parent = this.Parent,
- Cancel = false,
- Path = this.Path
- };
- _resolveArgs = FileSystemHelper.FilterChildFileSystemEntries(_resolveArgs, (this.Parent != null && this.Parent.IsRoot));
- }
- return _resolveArgs;
- }
- set
- {
- _resolveArgs = value;
- }
- }
-
- /// <summary>
- /// Refresh metadata on us by execution our provider chain
- /// </summary>
- /// <returns>true if a provider reports we changed</returns>
- public bool RefreshMetadata()
- {
- Kernel.Instance.ExecuteMetadataProviders(this).ConfigureAwait(false);
- return true;
- }
-
- }
-}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 4c9008b22c..e583e3a6a0 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1,202 +1,1382 @@
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.IO;
-using System;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace MediaBrowser.Controller.Entities
-{
- public abstract class BaseItem : BaseEntity, IHasProviderIds
- {
-
- public IEnumerable<string> PhysicalLocations
- {
- get
- {
- return _resolveArgs.PhysicalLocations;
- }
- }
-
- public string SortName { get; set; }
-
- /// <summary>
- /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
- /// </summary>
- public DateTime? PremiereDate { get; set; }
-
- public string LogoImagePath { get; set; }
-
- public string ArtImagePath { get; set; }
-
- public string ThumbnailImagePath { get; set; }
-
- public string BannerImagePath { get; set; }
-
- public IEnumerable<string> BackdropImagePaths { get; set; }
-
- public string OfficialRating { get; set; }
-
- public string CustomRating { get; set; }
- public string CustomPin { get; set; }
-
- public string Language { get; set; }
- public string Overview { get; set; }
- public List<string> Taglines { get; set; }
-
- /// <summary>
- /// Using a Dictionary to prevent duplicates
- /// </summary>
- public Dictionary<string,PersonInfo> People { get; set; }
-
- public List<string> Studios { get; set; }
-
- public List<string> Genres { get; set; }
-
- public string DisplayMediaType { get; set; }
-
- public float? CommunityRating { get; set; }
- public long? RunTimeTicks { get; set; }
-
- public string AspectRatio { get; set; }
- public int? ProductionYear { get; set; }
-
- /// <summary>
- /// If the item is part of a series, this is it's number in the series.
- /// This could be episode number, album track number, etc.
- /// </summary>
- public int? IndexNumber { get; set; }
-
- /// <summary>
- /// For an episode this could be the season number, or for a song this could be the disc number.
- /// </summary>
- public int? ParentIndexNumber { get; set; }
-
- public IEnumerable<Video> LocalTrailers { get; set; }
-
- public string TrailerUrl { get; set; }
-
- public Dictionary<string, string> ProviderIds { get; set; }
-
- public Dictionary<Guid, UserItemData> UserData { get; set; }
-
- public UserItemData GetUserData(User user, bool createIfNull)
- {
- if (UserData == null || !UserData.ContainsKey(user.Id))
- {
- if (createIfNull)
- {
- AddUserData(user, new UserItemData());
- }
- else
- {
- return null;
- }
- }
-
- return UserData[user.Id];
- }
-
- private void AddUserData(User user, UserItemData data)
- {
- if (UserData == null)
- {
- UserData = new Dictionary<Guid, UserItemData>();
- }
-
- UserData[user.Id] = data;
- }
-
- /// <summary>
- /// Determines if a given user has access to this item
- /// </summary>
- internal bool IsParentalAllowed(User user)
- {
- return true;
- }
-
- /// <summary>
- /// Finds an item by ID, recursively
- /// </summary>
- public virtual BaseItem FindItemById(Guid id)
- {
- if (Id == id)
- {
- return this;
- }
-
- if (LocalTrailers != null)
- {
- return LocalTrailers.FirstOrDefault(i => i.Id == id);
- }
-
- return null;
- }
-
- public virtual bool IsFolder
- {
- get
- {
- return false;
- }
- }
-
- /// <summary>
- /// Determine if we have changed vs the passed in copy
- /// </summary>
- /// <param name="copy"></param>
- /// <returns></returns>
- public virtual bool IsChanged(BaseItem copy)
- {
- bool changed = copy.DateModified != this.DateModified;
- if (changed) MediaBrowser.Common.Logging.Logger.LogDebugInfo(this.Name + " changed - original creation: " + this.DateCreated + " new creation: " + copy.DateCreated + " original modified: " + this.DateModified + " new modified: " + copy.DateModified);
- return changed;
- }
-
- /// <summary>
- /// Determines if the item is considered new based on user settings
- /// </summary>
- public bool IsRecentlyAdded(User user)
- {
- return (DateTime.UtcNow - DateCreated).TotalDays < user.RecentItemDays;
- }
-
- public void AddPerson(PersonInfo person)
- {
- if (People == null)
- {
- People = new Dictionary<string, PersonInfo>(StringComparer.OrdinalIgnoreCase);
- }
-
- People[person.Name] = person;
- }
-
- /// <summary>
- /// Marks the item as either played or unplayed
- /// </summary>
- public virtual void SetPlayedStatus(User user, bool wasPlayed)
- {
- UserItemData data = GetUserData(user, true);
-
- if (wasPlayed)
- {
- data.PlayCount = Math.Max(data.PlayCount, 1);
- }
- else
- {
- data.PlayCount = 0;
- data.PlaybackPositionTicks = 0;
- }
- }
-
- /// <summary>
- /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
- /// </summary>
- /// <returns></returns>
- public virtual Task ChangedExternally()
- {
- return Task.Run(() => RefreshMetadata());
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class BaseItem
+ /// </summary>
+ public abstract class BaseItem : IHasProviderIds
+ {
+ /// <summary>
+ /// The trailer folder name
+ /// </summary>
+ public const string TrailerFolderName = "trailers";
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public virtual string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public virtual Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public virtual string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the location.
+ /// </summary>
+ /// <value>The type of the location.</value>
+ public virtual LocationType LocationType
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Path))
+ {
+ return LocationType.Virtual;
+ }
+
+ return System.IO.Path.IsPathRooted(Path) ? LocationType.FileSystem : LocationType.Remote;
+ }
+ }
+
+ /// <summary>
+ /// This is just a helper for convenience
+ /// </summary>
+ /// <value>The primary image path.</value>
+ [IgnoreDataMember]
+ public virtual string PrimaryImagePath
+ {
+ get { return GetImage(ImageType.Primary); }
+ set { SetImage(ImageType.Primary, value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the images.
+ /// </summary>
+ /// <value>The images.</value>
+ public Dictionary<string, string> Images { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <value>The date modified.</value>
+ public DateTime DateModified { get; set; }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ /// <summary>
+ /// Returns true if this item should not attempt to fetch metadata
+ /// </summary>
+ /// <value><c>true</c> if [dont fetch meta]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public virtual bool DontFetchMeta
+ {
+ get
+ {
+ if (Path != null)
+ {
+ return Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the item has a saved local image of the specified name (jpg or png).
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [has local image] [the specified item]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">name</exception>
+ public bool HasLocalImage(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ return ResolveArgs.ContainsMetaFileByName(name + ".jpg") ||
+ ResolveArgs.ContainsMetaFileByName(name + ".png");
+ }
+
+ /// <summary>
+ /// Should be overridden to return the proper folder where metadata lives
+ /// </summary>
+ /// <value>The meta location.</value>
+ [IgnoreDataMember]
+ public virtual string MetaLocation
+ {
+ get
+ {
+ return Path ?? "";
+ }
+ }
+
+ /// <summary>
+ /// The _provider data
+ /// </summary>
+ private Dictionary<Guid, BaseProviderInfo> _providerData;
+ /// <summary>
+ /// Holds persistent data for providers like last refresh date.
+ /// Providers can use this to determine if they need to refresh.
+ /// The BaseProviderInfo class can be extended to hold anything a provider may need.
+ /// Keyed by a unique provider ID.
+ /// </summary>
+ /// <value>The provider data.</value>
+ public Dictionary<Guid, BaseProviderInfo> ProviderData
+ {
+ get
+ {
+ return _providerData ?? (_providerData = new Dictionary<Guid, BaseProviderInfo>());
+ }
+ set
+ {
+ _providerData = value;
+ }
+ }
+
+ /// <summary>
+ /// The _file system stamp
+ /// </summary>
+ private Guid? _fileSystemStamp;
+ /// <summary>
+ /// Gets a directory stamp, in the form of a string, that can be used for
+ /// comparison purposes to determine if the file system entries for this item have changed.
+ /// </summary>
+ /// <value>The file system stamp.</value>
+ [IgnoreDataMember]
+ public Guid FileSystemStamp
+ {
+ get
+ {
+ if (!_fileSystemStamp.HasValue)
+ {
+ _fileSystemStamp = GetFileSystemStamp();
+ }
+
+ return _fileSystemStamp.Value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [IgnoreDataMember]
+ public virtual string MediaType
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets a directory stamp, in the form of a string, that can be used for
+ /// comparison purposes to determine if the file system entries for this item have changed.
+ /// </summary>
+ /// <returns>Guid.</returns>
+ private Guid GetFileSystemStamp()
+ {
+ // If there's no path or the item is a file, there's nothing to do
+ if (LocationType != LocationType.FileSystem || !ResolveArgs.IsDirectory)
+ {
+ return Guid.Empty;
+ }
+
+ var sb = new StringBuilder();
+
+ // Record the name of each file
+ // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order
+ foreach (var file in ResolveArgs.FileSystemChildren.OrderBy(f => f.cFileName))
+ {
+ sb.Append(file.cFileName);
+ }
+ foreach (var file in ResolveArgs.MetadataFiles.OrderBy(f => f.cFileName))
+ {
+ sb.Append(file.cFileName);
+ }
+
+ return sb.ToString().GetMD5();
+ }
+
+ /// <summary>
+ /// The _resolve args
+ /// </summary>
+ private ItemResolveArgs _resolveArgs;
+ /// <summary>
+ /// The _resolve args initialized
+ /// </summary>
+ private bool _resolveArgsInitialized;
+ /// <summary>
+ /// The _resolve args sync lock
+ /// </summary>
+ private object _resolveArgsSyncLock = new object();
+ /// <summary>
+ /// We attach these to the item so that we only ever have to hit the file system once
+ /// (this includes the children of the containing folder)
+ /// Use ResolveArgs.FileSystemDictionary to check for the existence of files instead of File.Exists
+ /// </summary>
+ /// <value>The resolve args.</value>
+ [IgnoreDataMember]
+ public ItemResolveArgs ResolveArgs
+ {
+ get
+ {
+ try
+ {
+ LazyInitializer.EnsureInitialized(ref _resolveArgs, ref _resolveArgsInitialized, ref _resolveArgsSyncLock, () => CreateResolveArgs());
+
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error creating resolve args for ", ex, Path);
+
+ throw;
+ }
+
+ return _resolveArgs;
+ }
+ set
+ {
+ _resolveArgs = value;
+ _resolveArgsInitialized = value != null;
+
+ // Null this out so that it can be lazy loaded again
+ _fileSystemStamp = null;
+ }
+ }
+
+ /// <summary>
+ /// Resets the resolve args.
+ /// </summary>
+ /// <param name="pathInfo">The path info.</param>
+ public void ResetResolveArgs(WIN32_FIND_DATA? pathInfo)
+ {
+ ResolveArgs = CreateResolveArgs(pathInfo);
+ }
+
+ /// <summary>
+ /// Creates ResolveArgs on demand
+ /// </summary>
+ /// <param name="pathInfo">The path info.</param>
+ /// <returns>ItemResolveArgs.</returns>
+ /// <exception cref="System.IO.IOException">Unable to retrieve file system info for + path</exception>
+ protected internal virtual ItemResolveArgs CreateResolveArgs(WIN32_FIND_DATA? pathInfo = null)
+ {
+ var path = Path;
+
+ // non file-system entries will not have a path
+ if (string.IsNullOrEmpty(path))
+ {
+ return new ItemResolveArgs
+ {
+ FileInfo = new WIN32_FIND_DATA()
+ };
+ }
+
+ if (UseParentPathToCreateResolveArgs)
+ {
+ path = System.IO.Path.GetDirectoryName(path);
+ }
+
+ pathInfo = pathInfo ?? FileSystem.GetFileData(path);
+
+ if (!pathInfo.HasValue)
+ {
+ throw new IOException("Unable to retrieve file system info for " + path);
+ }
+
+ var args = new ItemResolveArgs
+ {
+ FileInfo = pathInfo.Value,
+ Path = path,
+ Parent = Parent
+ };
+
+ // Gather child folder and files
+
+ if (args.IsDirectory)
+ {
+ // When resolving the root, we need it's grandchildren (children of user views)
+ var flattenFolderDepth = args.IsPhysicalRoot ? 2 : 0;
+
+ args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, flattenFolderDepth: flattenFolderDepth, args: args);
+ }
+
+ //update our dates
+ EntityResolutionHelper.EnsureDates(this, args);
+
+ return args;
+ }
+
+ /// <summary>
+ /// Some subclasses will stop resolving at a directory and point their Path to a file within. This will help ensure the on-demand resolve args are identical to the
+ /// original ones.
+ /// </summary>
+ /// <value><c>true</c> if [use parent path to create resolve args]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ protected virtual bool UseParentPathToCreateResolveArgs
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the name of the sort.
+ /// </summary>
+ /// <value>The name of the sort.</value>
+ public string SortName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent.
+ /// </summary>
+ /// <value>The parent.</value>
+ [IgnoreDataMember]
+ public Folder Parent { get; set; }
+
+ /// <summary>
+ /// Gets the collection folder parent.
+ /// </summary>
+ /// <value>The collection folder parent.</value>
+ [IgnoreDataMember]
+ public Folder CollectionFolder
+ {
+ get
+ {
+ if (this is AggregateFolder)
+ {
+ return null;
+ }
+
+ if (IsFolder)
+ {
+ var iCollectionFolder = this as ICollectionFolder;
+
+ if (iCollectionFolder != null)
+ {
+ return (Folder)this;
+ }
+ }
+
+ var parent = Parent;
+
+ while (parent != null)
+ {
+ var iCollectionFolder = parent as ICollectionFolder;
+
+ if (iCollectionFolder != null)
+ {
+ return parent;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+ /// </summary>
+ /// <value>The premiere date.</value>
+ public DateTime? PremiereDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display type of the media.
+ /// </summary>
+ /// <value>The display type of the media.</value>
+ public virtual string DisplayMediaType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backdrop image paths.
+ /// </summary>
+ /// <value>The backdrop image paths.</value>
+ public List<string> BackdropImagePaths { get; set; }
+
+ /// <summary>
+ /// Gets or sets the screenshot image paths.
+ /// </summary>
+ /// <value>The screenshot image paths.</value>
+ public List<string> ScreenshotImagePaths { get; set; }
+
+ /// <summary>
+ /// Gets or sets the official rating.
+ /// </summary>
+ /// <value>The official rating.</value>
+ public string OfficialRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom rating.
+ /// </summary>
+ /// <value>The custom rating.</value>
+ public string CustomRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language.
+ /// </summary>
+ /// <value>The language.</value>
+ public string Language { get; set; }
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string Overview { get; set; }
+ /// <summary>
+ /// Gets or sets the taglines.
+ /// </summary>
+ /// <value>The taglines.</value>
+ public List<string> Taglines { get; set; }
+
+ /// <summary>
+ /// Gets or sets the people.
+ /// </summary>
+ /// <value>The people.</value>
+ public List<PersonInfo> People { get; set; }
+
+ /// <summary>
+ /// Override this if you need to combine/collapse person information
+ /// </summary>
+ /// <value>All people.</value>
+ [IgnoreDataMember]
+ public virtual IEnumerable<PersonInfo> AllPeople
+ {
+ get { return People; }
+ }
+
+ /// <summary>
+ /// Gets or sets the studios.
+ /// </summary>
+ /// <value>The studios.</value>
+ public virtual List<string> Studios { get; set; }
+
+ /// <summary>
+ /// Gets or sets the genres.
+ /// </summary>
+ /// <value>The genres.</value>
+ public virtual List<string> Genres { get; set; }
+
+ /// <summary>
+ /// Gets or sets the community rating.
+ /// </summary>
+ /// <value>The community rating.</value>
+ public float? CommunityRating { get; set; }
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the aspect ratio.
+ /// </summary>
+ /// <value>The aspect ratio.</value>
+ public string AspectRatio { get; set; }
+ /// <summary>
+ /// Gets or sets the production year.
+ /// </summary>
+ /// <value>The production year.</value>
+ public virtual int? ProductionYear { get; set; }
+
+ /// <summary>
+ /// If the item is part of a series, this is it's number in the series.
+ /// This could be episode number, album track number, etc.
+ /// </summary>
+ /// <value>The index number.</value>
+ public int? IndexNumber { get; set; }
+
+ /// <summary>
+ /// For an episode this could be the season number, or for a song this could be the disc number.
+ /// </summary>
+ /// <value>The parent index number.</value>
+ public int? ParentIndexNumber { get; set; }
+
+ /// <summary>
+ /// The _local trailers
+ /// </summary>
+ private List<Video> _localTrailers;
+ /// <summary>
+ /// The _local trailers initialized
+ /// </summary>
+ private bool _localTrailersInitialized;
+ /// <summary>
+ /// The _local trailers sync lock
+ /// </summary>
+ private object _localTrailersSyncLock = new object();
+ /// <summary>
+ /// Gets the local trailers.
+ /// </summary>
+ /// <value>The local trailers.</value>
+ [IgnoreDataMember]
+ public List<Video> LocalTrailers
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _localTrailers, ref _localTrailersInitialized, ref _localTrailersSyncLock, LoadLocalTrailers);
+ return _localTrailers;
+ }
+ private set
+ {
+ _localTrailers = value;
+
+ if (value == null)
+ {
+ _localTrailersInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Loads local trailers from the file system
+ /// </summary>
+ /// <returns>List{Video}.</returns>
+ private List<Video> LoadLocalTrailers()
+ {
+ ItemResolveArgs resolveArgs;
+
+ try
+ {
+ resolveArgs = ResolveArgs;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting ResolveArgs for {0}", ex, Path);
+ return new List<Video> { };
+ }
+
+ if (!resolveArgs.IsDirectory)
+ {
+ return new List<Video> { };
+ }
+
+ var folder = resolveArgs.GetFileSystemEntryByName(TrailerFolderName);
+
+ // Path doesn't exist. No biggie
+ if (folder == null)
+ {
+ return new List<Video> { };
+ }
+
+ IEnumerable<WIN32_FIND_DATA> files;
+
+ try
+ {
+ files = FileSystem.GetFiles(folder.Value.Path);
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error loading trailers for {0}", ex, Name);
+ return new List<Video> { };
+ }
+
+ return Kernel.Instance.LibraryManager.GetItems<Video>(files, null).Select(video =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = Kernel.Instance.ItemRepository.RetrieveItem(video.Id) as Video;
+
+ if (dbItem != null)
+ {
+ dbItem.ResolveArgs = video.ResolveArgs;
+ video = dbItem;
+ }
+
+ return video;
+ }).ToList();
+ }
+
+ /// <summary>
+ /// Overrides the base implementation to refresh metadata for local trailers
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
+ /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
+ /// <returns>true if a provider reports we changed</returns>
+ public virtual async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+ {
+ if (resetResolveArgs)
+ {
+ ResolveArgs = null;
+ }
+
+ // Lazy load these again
+ LocalTrailers = null;
+
+ // Refresh for the item
+ var itemRefreshTask = Kernel.Instance.ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Refresh metadata for local trailers
+ var trailerTasks = LocalTrailers.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Await the trailer tasks
+ await Task.WhenAll(trailerTasks).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Get the result from the item task
+ var changed = await itemRefreshTask.ConfigureAwait(false);
+
+ if (changed || forceSave)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Kernel.Instance.ItemRepository.SaveItem(this, cancellationToken).ConfigureAwait(false);
+ }
+
+ return changed;
+ }
+
+ /// <summary>
+ /// Clear out all metadata properties. Extend for sub-classes.
+ /// </summary>
+ public virtual void ClearMetaValues()
+ {
+ Images = null;
+ SortName = null;
+ PremiereDate = null;
+ BackdropImagePaths = null;
+ OfficialRating = null;
+ CustomRating = null;
+ Overview = null;
+ Taglines = null;
+ Language = null;
+ Studios = null;
+ Genres = null;
+ CommunityRating = null;
+ RunTimeTicks = null;
+ AspectRatio = null;
+ ProductionYear = null;
+ ProviderIds = null;
+ DisplayMediaType = GetType().Name;
+ ResolveArgs = null;
+ }
+
+ /// <summary>
+ /// Gets or sets the trailer URL.
+ /// </summary>
+ /// <value>The trailer URL.</value>
+ public List<string> TrailerUrls { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ /// <summary>
+ /// Override this to false if class should be ignored for indexing purposes
+ /// </summary>
+ /// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public virtual bool IncludeInIndex
+ {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Override this to true if class should be grouped under a container in indicies
+ /// The container class should be defined via IndexContainer
+ /// </summary>
+ /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public virtual bool GroupInIndex
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Override this to return the folder that should be used to construct a container
+ /// for this item in an index. GroupInIndex should be true as well.
+ /// </summary>
+ /// <value>The index container.</value>
+ [IgnoreDataMember]
+ public virtual Folder IndexContainer
+ {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// The _user data
+ /// </summary>
+ private IEnumerable<UserItemData> _userData;
+ /// <summary>
+ /// The _user data initialized
+ /// </summary>
+ private bool _userDataInitialized;
+ /// <summary>
+ /// The _user data sync lock
+ /// </summary>
+ private object _userDataSyncLock = new object();
+ /// <summary>
+ /// Gets the user data.
+ /// </summary>
+ /// <value>The user data.</value>
+ [IgnoreDataMember]
+ public IEnumerable<UserItemData> UserData
+ {
+ get
+ {
+ // Call ToList to exhaust the stream because we'll be iterating over this multiple times
+ LazyInitializer.EnsureInitialized(ref _userData, ref _userDataInitialized, ref _userDataSyncLock, () => Kernel.Instance.UserDataRepository.RetrieveUserData(this).ToList());
+ return _userData;
+ }
+ private set
+ {
+ _userData = value;
+
+ if (value == null)
+ {
+ _userDataInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the user data.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="createIfNull">if set to <c>true</c> [create if null].</param>
+ /// <returns>UserItemData.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public UserItemData GetUserData(User user, bool createIfNull)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (UserData == null)
+ {
+ if (!createIfNull)
+ {
+ return null;
+ }
+
+ AddOrUpdateUserData(user, new UserItemData { UserId = user.Id });
+ }
+
+ var data = UserData.FirstOrDefault(u => u.UserId == user.Id);
+
+ if (data == null && createIfNull)
+ {
+ data = new UserItemData { UserId = user.Id };
+ AddOrUpdateUserData(user, data);
+ }
+
+ return data;
+ }
+
+ /// <summary>
+ /// Adds the or update user data.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="data">The data.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddOrUpdateUserData(User user, UserItemData data)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (data == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ data.UserId = user.Id;
+
+ if (UserData == null)
+ {
+ UserData = new[] { data };
+ }
+ else
+ {
+ var list = UserData.Where(u => u.UserId != user.Id).ToList();
+ list.Add(data);
+ UserData = list;
+ }
+ }
+
+ /// <summary>
+ /// The _user data id
+ /// </summary>
+ protected Guid _userDataId; //cache this so it doesn't have to be re-constructed on every reference
+ /// <summary>
+ /// Return the id that should be used to key user data for this item.
+ /// Default is just this Id but subclasses can use provider Ids for transportability.
+ /// </summary>
+ /// <value>The user data id.</value>
+ [IgnoreDataMember]
+ public virtual Guid UserDataId
+ {
+ get
+ {
+ return _userDataId == Guid.Empty ? (_userDataId = Id) : _userDataId;
+ }
+ }
+
+ /// <summary>
+ /// Determines if a given user has access to this item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public bool IsParentalAllowed(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return user.Configuration.MaxParentalRating == null || Ratings.Level(CustomRating ?? OfficialRating) <= user.Configuration.MaxParentalRating;
+ }
+
+ /// <summary>
+ /// Determines if this folder should be visible to a given user.
+ /// Default is just parental allowed. Can be overridden for more functionality.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public virtual bool IsVisible(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ return IsParentalAllowed(user);
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public virtual BaseItem FindItemById(Guid id, User user)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ if (Id == id)
+ {
+ return this;
+ }
+
+ if (LocalTrailers != null)
+ {
+ return LocalTrailers.FirstOrDefault(i => i.Id == id);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Finds the particular item by searching through our parents and, if not found there, loading from repo
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentException"></exception>
+ protected BaseItem FindParentItem(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentException();
+ }
+
+ var parent = Parent;
+ while (parent != null && !parent.IsRoot)
+ {
+ if (parent.Id == id) return parent;
+ parent = parent.Parent;
+ }
+
+ //not found - load from repo
+ return Kernel.Instance.ItemRepository.RetrieveItem(id);
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public virtual bool IsFolder
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determine if we have changed vs the passed in copy
+ /// </summary>
+ /// <param name="copy">The copy.</param>
+ /// <returns><c>true</c> if the specified copy has changed; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public virtual bool HasChanged(BaseItem copy)
+ {
+ if (copy == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var changed = copy.DateModified != DateModified;
+ if (changed)
+ {
+ Logger.LogDebugInfo(Name + " changed - original creation: " + DateCreated + " new creation: " + copy.DateCreated + " original modified: " + DateModified + " new modified: " + copy.DateModified);
+ }
+ return changed;
+ }
+
+ /// <summary>
+ /// Determines if the item is considered new based on user settings
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if [is recently added] [the specified user]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public bool IsRecentlyAdded(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return (DateTime.UtcNow - DateCreated).TotalDays < Kernel.Instance.Configuration.RecentItemDays;
+ }
+
+ /// <summary>
+ /// Determines if the item is considered recently played on user settings
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if [is recently played] [the specified user]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public bool IsRecentlyPlayed(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var data = GetUserData(user, false);
+
+ if (data == null || data.LastPlayedDate == null || data.PlayCount == 0)
+ {
+ return false;
+ }
+
+ return (DateTime.UtcNow - data.LastPlayedDate.Value).TotalDays < Kernel.Instance.Configuration.RecentlyPlayedDays;
+ }
+
+ /// <summary>
+ /// Adds people to the item
+ /// </summary>
+ /// <param name="people">The people.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddPeople(IEnumerable<PersonInfo> people)
+ {
+ if (people == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ foreach (var person in people)
+ {
+ AddPerson(person);
+ }
+ }
+
+ /// <summary>
+ /// Adds a person to the item
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddPerson(PersonInfo person)
+ {
+ if (person == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (string.IsNullOrWhiteSpace(person.Name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (People == null)
+ {
+ People = new List<PersonInfo>();
+ }
+
+ // Check for dupes based on the combination of Name and Type
+
+ if (!People.Any(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(person.Type, StringComparison.OrdinalIgnoreCase)))
+ {
+ People.Add(person);
+ }
+ }
+
+ /// <summary>
+ /// Adds studios to the item
+ /// </summary>
+ /// <param name="studios">The studios.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddStudios(IEnumerable<string> studios)
+ {
+ if (studios == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ foreach (var name in studios)
+ {
+ AddStudio(name);
+ }
+ }
+
+ /// <summary>
+ /// Adds a studio to the item
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddStudio(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (Studios == null)
+ {
+ Studios = new List<string>();
+ }
+
+ if (!Studios.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ Studios.Add(name);
+ }
+ }
+
+ /// <summary>
+ /// Adds a tagline to the item
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddTagline(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (Taglines == null)
+ {
+ Taglines = new List<string>();
+ }
+
+ if (!Taglines.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ Taglines.Add(name);
+ }
+ }
+
+ /// <summary>
+ /// Adds a TrailerUrl to the item
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddTrailerUrl(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (TrailerUrls == null)
+ {
+ TrailerUrls = new List<string>();
+ }
+
+ if (!TrailerUrls.Contains(url, StringComparer.OrdinalIgnoreCase))
+ {
+ TrailerUrls.Add(url);
+ }
+ }
+
+ /// <summary>
+ /// Adds a genre to the item
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddGenre(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (Genres == null)
+ {
+ Genres = new List<string>();
+ }
+
+ if (!Genres.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ Genres.Add(name);
+ }
+ }
+
+ /// <summary>
+ /// Adds genres to the item
+ /// </summary>
+ /// <param name="genres">The genres.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddGenres(IEnumerable<string> genres)
+ {
+ if (genres == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ foreach (var name in genres)
+ {
+ AddGenre(name);
+ }
+ }
+
+ /// <summary>
+ /// Marks the item as either played or unplayed
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public virtual Task SetPlayedStatus(User user, bool wasPlayed)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var data = GetUserData(user, true);
+
+ if (wasPlayed)
+ {
+ data.PlayCount = Math.Max(data.PlayCount, 1);
+
+ if (!data.LastPlayedDate.HasValue)
+ {
+ data.LastPlayedDate = DateTime.UtcNow;
+ }
+ }
+ else
+ {
+ //I think it is okay to do this here.
+ // if this is only called when a user is manually forcing something to un-played
+ // then it probably is what we want to do...
+ data.PlayCount = 0;
+ data.PlaybackPositionTicks = 0;
+ data.LastPlayedDate = null;
+ }
+
+ data.Played = wasPlayed;
+
+ return Kernel.Instance.UserDataManager.SaveUserDataForItem(user, this, data);
+ }
+
+ /// <summary>
+ /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
+ /// </summary>
+ /// <returns>Task.</returns>
+ public virtual Task ChangedExternally()
+ {
+ return RefreshMetadata(CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Finds a parent of a given type
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ public T FindParent<T>()
+ where T : Folder
+ {
+ var parent = Parent;
+
+ while (parent != null)
+ {
+ var result = parent as T;
+ if (result != null)
+ {
+ return result;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets an image
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ public string GetImage(ImageType type)
+ {
+ if (type == ImageType.Backdrop)
+ {
+ throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
+ }
+ if (type == ImageType.Screenshot)
+ {
+ throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
+ }
+
+ if (Images == null)
+ {
+ return null;
+ }
+
+ string val;
+ Images.TryGetValue(type.ToString(), out val);
+ return val;
+ }
+
+ /// <summary>
+ /// Gets an image
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ public bool HasImage(ImageType type)
+ {
+ if (type == ImageType.Backdrop)
+ {
+ throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
+ }
+ if (type == ImageType.Screenshot)
+ {
+ throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
+ }
+
+ return !string.IsNullOrEmpty(GetImage(type));
+ }
+
+ /// <summary>
+ /// Sets an image
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="path">The path.</param>
+ /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ public void SetImage(ImageType type, string path)
+ {
+ if (type == ImageType.Backdrop)
+ {
+ throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
+ }
+ if (type == ImageType.Screenshot)
+ {
+ throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
+ }
+
+ var typeKey = type.ToString();
+
+ // If it's null remove the key from the dictionary
+ if (string.IsNullOrEmpty(path))
+ {
+ if (Images != null)
+ {
+ if (Images.ContainsKey(typeKey))
+ {
+ Images.Remove(typeKey);
+ }
+ }
+ }
+ else
+ {
+ // Ensure it exists
+ if (Images == null)
+ {
+ Images = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ Images[typeKey] = path;
+ }
+ }
+
+ /// <summary>
+ /// Deletes the image.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>Task.</returns>
+ public async Task DeleteImage(ImageType type)
+ {
+ if (!HasImage(type))
+ {
+ return;
+ }
+
+ // Delete the source file
+ File.Delete(GetImage(type));
+
+ // Remove it from the item
+ SetImage(type, null);
+
+ // Refresh metadata
+ await RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
new file mode 100644
index 0000000000..7cabcf9f01
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
@@ -0,0 +1,49 @@
+using MediaBrowser.Common.Extensions;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Plugins derive from and export this class to create a folder that will appear in the root along
+ /// with all the other actual physical folders in the system.
+ /// </summary>
+ public abstract class BasePluginFolder : Folder, ICollectionFolder
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public override Guid Id
+ {
+ get
+ {
+ // This doesn't get populated through the normal resolving process
+ if (base.Id == Guid.Empty)
+ {
+ base.Id = (Path ?? Name).GetMBId(GetType());
+ }
+ return base.Id;
+ }
+ set
+ {
+ base.Id = value;
+ }
+ }
+
+ /// <summary>
+ /// We don't resolve normally so need to fill this in
+ /// </summary>
+ public override string DisplayMediaType
+ {
+ get
+ {
+ return "CollectionFolder"; // Plug-in folders are collection folders
+ }
+ set
+ {
+ base.DisplayMediaType = value;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
new file mode 100644
index 0000000000..5684f8e802
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Specialized Folder class that points to a subset of the physical folders in the system.
+ /// It is created from the user-specific folders within the system root
+ /// </summary>
+ public class CollectionFolder : Folder, ICollectionFolder
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is virtual folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is virtual folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool IsVirtualFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Allow different display preferences for each collection folder
+ /// </summary>
+ /// <value>The display prefs id.</value>
+ public override Guid DisplayPrefsId
+ {
+ get
+ {
+ return Id;
+ }
+ }
+
+ // Cache this since it will be used a lot
+ /// <summary>
+ /// The null task result
+ /// </summary>
+ private static readonly Task NullTaskResult = Task.FromResult<object>(null);
+
+ /// <summary>
+ /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>Task.</returns>
+ protected override Task ValidateChildrenInternal(IProgress<TaskProgress> progress, CancellationToken cancellationToken, bool? recursive = null)
+ {
+ //we don't directly validate our children
+ //but we do need to clear out the index cache...
+ IndexCache = new ConcurrentDictionary<string, List<BaseItem>>(StringComparer.OrdinalIgnoreCase);
+
+ return NullTaskResult;
+ }
+
+ /// <summary>
+ /// Our children are actually just references to the ones in the physical root...
+ /// </summary>
+ /// <value>The actual children.</value>
+ protected override ConcurrentBag<BaseItem> ActualChildren
+ {
+ get
+ {
+ IEnumerable<Guid> folderIds;
+
+ try
+ {
+ // Accessing ResolveArgs could involve file system access
+ folderIds = ResolveArgs.PhysicalLocations.Select(f => (f.GetMBId(typeof(Folder))));
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error creating FolderIds for {0}", ex, Path);
+
+ folderIds = new Guid[] {};
+ }
+
+ var ourChildren =
+ Kernel.Instance.RootFolder.Children.OfType<Folder>()
+ .Where(i => folderIds.Contains(i.Id))
+ .SelectMany(c => c.Children);
+
+ return new ConcurrentBag<BaseItem>(ourChildren);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 07529c80f6..cef79fe560 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1,619 +1,1047 @@
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Controller.Resolvers;
-using System;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Folder : BaseItem
- {
- #region Events
- /// <summary>
- /// Fires whenever a validation routine updates our children. The added and removed children are properties of the args.
- /// *** Will fire asynchronously. ***
- /// </summary>
- public event EventHandler<ChildrenChangedEventArgs> ChildrenChanged;
- protected void OnChildrenChanged(ChildrenChangedEventArgs args)
- {
- if (ChildrenChanged != null)
- {
- Task.Run( () =>
- {
- ChildrenChanged(this, args);
- Kernel.Instance.OnLibraryChanged(args);
- });
- }
- }
-
- #endregion
-
- public override bool IsFolder
- {
- get
- {
- return true;
- }
- }
-
- public bool IsRoot { get; set; }
-
- public bool IsVirtualFolder
- {
- get
- {
- return Parent != null && Parent.IsRoot;
- }
- }
- protected object childLock = new object();
- protected List<BaseItem> children;
- protected virtual List<BaseItem> ActualChildren
- {
- get
- {
- if (children == null)
- {
- LoadChildren();
- }
- return children;
- }
-
- set
- {
- children = value;
- }
- }
-
- /// <summary>
- /// thread-safe access to the actual children of this folder - without regard to user
- /// </summary>
- public IEnumerable<BaseItem> Children
- {
- get
- {
- lock (childLock)
- return ActualChildren.ToList();
- }
- }
-
- /// <summary>
- /// thread-safe access to all recursive children of this folder - without regard to user
- /// </summary>
- public IEnumerable<BaseItem> RecursiveChildren
- {
- get
- {
- foreach (var item in Children)
- {
- yield return item;
-
- var subFolder = item as Folder;
-
- if (subFolder != null)
- {
- foreach (var subitem in subFolder.RecursiveChildren)
- {
- yield return subitem;
- }
- }
- }
- }
- }
-
-
- /// <summary>
- /// Loads and validates our children
- /// </summary>
- protected virtual void LoadChildren()
- {
- //first - load our children from the repo
- lock (childLock)
- children = GetCachedChildren();
-
- //then kick off a validation against the actual file system
- Task.Run(() => ValidateChildren());
- }
-
- protected bool ChildrenValidating = false;
-
- /// <summary>
- /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
- /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
- /// </summary>
- /// <returns></returns>
- protected async virtual void ValidateChildren()
- {
- if (ChildrenValidating) return; //only ever want one of these going at once and don't want them to fire off in sequence so don't use lock
- ChildrenValidating = true;
- bool changed = false; //this will save us a little time at the end if nothing changes
- var changedArgs = new ChildrenChangedEventArgs(this);
- //get the current valid children from filesystem (or wherever)
- var nonCachedChildren = await GetNonCachedChildren();
- if (nonCachedChildren == null) return; //nothing to validate
- //build a dictionary of the current children we have now by Id so we can compare quickly and easily
- Dictionary<Guid, BaseItem> currentChildren;
- lock (childLock)
- currentChildren = ActualChildren.ToDictionary(i => i.Id);
-
- //create a list for our validated children
- var validChildren = new List<BaseItem>();
- //now traverse the valid children and find any changed or new items
- foreach (var child in nonCachedChildren)
- {
- BaseItem currentChild;
- currentChildren.TryGetValue(child.Id, out currentChild);
- if (currentChild == null)
- {
- //brand new item - needs to be added
- changed = true;
- changedArgs.ItemsAdded.Add(child);
- //refresh it
- child.RefreshMetadata();
- Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+") "+ child.Name + " (" + child.Path + ")");
- //save it in repo...
-
- //and add it to our valid children
- validChildren.Add(child);
- //fire an added event...?
- //if it is a folder we need to validate its children as well
- Folder folder = child as Folder;
- if (folder != null)
- {
- folder.ValidateChildren();
- //probably need to refresh too...
- }
- }
- else
- {
- //existing item - check if it has changed
- if (currentChild.IsChanged(child))
- {
- changed = true;
- //update resolve args and refresh meta
- // Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
- // will identify this item as the same one
- currentChild.ResolveArgs = child.ResolveArgs;
- currentChild.RefreshMetadata();
- Logger.LogInfo("Item Changed: ("+currentChild.GetType().Name+") "+ currentChild.Name + " (" + currentChild.Path + ")");
- //save it in repo...
- validChildren.Add(currentChild);
- }
- else
- {
- //current child that didn't change - just put it in the valid children
- validChildren.Add(currentChild);
- }
- }
- }
-
- //that's all the new and changed ones - now see if there are any that are missing
- changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
- changed |= changedArgs.ItemsRemoved != null;
-
- //now, if anything changed - replace our children
- if (changed)
- {
- if (changedArgs.ItemsRemoved != null) foreach (var item in changedArgs.ItemsRemoved) Logger.LogDebugInfo("** " + item.Name + " Removed from library.");
-
- lock (childLock)
- ActualChildren = validChildren;
- //and save children in repo...
-
- //and fire event
- this.OnChildrenChanged(changedArgs);
- }
- ChildrenValidating = false;
-
- }
-
- /// <summary>
- /// Get the children of this folder from the actual file system
- /// </summary>
- /// <returns></returns>
- protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
- {
- ItemResolveEventArgs args = new ItemResolveEventArgs()
- {
- FileInfo = FileData.GetFileData(this.Path),
- Parent = this.Parent,
- Cancel = false,
- Path = this.Path
- };
-
- // Gather child folder and files
- if (args.IsDirectory)
- {
- args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
-
- bool isVirtualFolder = Parent != null && Parent.IsRoot;
- args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
- }
- else
- {
- Logger.LogError("Folder has a path that is not a directory: " + this.Path);
- return null;
- }
-
- if (!EntityResolutionHelper.ShouldResolvePathContents(args))
- {
- return null;
- }
- return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
- .Where(i => i != null).OrderBy(f =>
- {
- return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
-
- });
-
- }
-
- /// <summary>
- /// Resolves a path into a BaseItem
- /// </summary>
- protected async Task<BaseItem> GetChild(string path, WIN32_FIND_DATA? fileInfo = null)
- {
- ItemResolveEventArgs args = new ItemResolveEventArgs()
- {
- FileInfo = fileInfo ?? FileData.GetFileData(path),
- Parent = this,
- Cancel = false,
- Path = path
- };
-
- args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
- args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
-
- return Kernel.Instance.ResolveItem(args);
-
- }
-
- /// <summary>
- /// Finds child BaseItems for us
- /// </summary>
- protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
- {
- Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
-
- for (int i = 0; i < fileSystemChildren.Length; i++)
- {
- var child = fileSystemChildren[i];
-
- tasks[i] = GetChild(child.Path, child);
- }
-
- return tasks;
- }
-
-
- /// <summary>
- /// Get our children from the repo - stubbed for now
- /// </summary>
- /// <returns></returns>
- protected virtual List<BaseItem> GetCachedChildren()
- {
- return new List<BaseItem>();
- }
-
- /// <summary>
- /// Gets allowed children of an item
- /// </summary>
- public IEnumerable<BaseItem> GetChildren(User user)
- {
- lock(childLock)
- return ActualChildren.Where(c => c.IsParentalAllowed(user));
- }
-
- /// <summary>
- /// Gets allowed recursive children of an item
- /// </summary>
- public IEnumerable<BaseItem> GetRecursiveChildren(User user)
- {
- foreach (var item in GetChildren(user))
- {
- yield return item;
-
- var subFolder = item as Folder;
-
- if (subFolder != null)
- {
- foreach (var subitem in subFolder.GetRecursiveChildren(user))
- {
- yield return subitem;
- }
- }
- }
- }
-
- /// <summary>
- /// Folders need to validate and refresh
- /// </summary>
- /// <returns></returns>
- public override Task ChangedExternally()
- {
- return Task.Run(() =>
- {
- if (this.IsRoot)
- {
- Kernel.Instance.ReloadRoot().ConfigureAwait(false);
- }
- else
- {
- RefreshMetadata();
- ValidateChildren();
- }
- });
- }
-
- /// <summary>
- /// Since it can be slow to make all of these calculations at once, this method will provide a way to get them all back together
- /// </summary>
- public ItemSpecialCounts GetSpecialCounts(User user)
- {
- var counts = new ItemSpecialCounts();
-
- IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
-
- var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
-
- counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
- counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
- counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
- counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
-
- return counts;
- }
-
- /// <summary>
- /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
- /// </summary>
- public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
- {
- return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
- }
-
- /// <summary>
- /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
- /// </summary>
- public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
- {
- return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
- }
-
- /// <summary>
- /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
- /// </summary>
- public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
- {
- return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
- }
-
- /// <summary>
- /// Finds all recursive items within a top-level parent that the user has marked as a favorite
- /// </summary>
- public IEnumerable<BaseItem> GetFavoriteItems(User user)
- {
- return GetRecursiveChildren(user).Where(c =>
- {
- UserItemData data = c.GetUserData(user, false);
-
- if (data != null)
- {
- return data.IsFavorite;
- }
-
- return false;
- });
- }
-
- /// <summary>
- /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
- /// </summary>
- public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
- {
- return GetRecursiveChildren(user).Where(c =>
- {
- if (c.People != null)
- {
- return c.People.ContainsKey(person);
- }
-
- return false;
- });
- }
-
- /// <summary>
- /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
- /// </summary>
- /// <param name="personType">Specify this to limit results to a specific PersonType</param>
- public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
- {
- return GetRecursiveChildren(user).Where(c =>
- {
- if (c.People != null)
- {
- return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
- }
-
- return false;
- });
- }
-
- /// <summary>
- /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
- /// </summary>
- public List<BaseItem> GetRecentlyAddedItems(User user)
- {
- return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
- }
-
- /// <summary>
- /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
- /// </summary>
- public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
- {
- return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
- }
-
- /// <summary>
- /// Gets all in-progress items (recursive) within a folder
- /// </summary>
- public List<BaseItem> GetInProgressItems(User user)
- {
- return GetInProgressItems(GetRecursiveChildren(user), user);
- }
-
- /// <summary>
- /// Takes a list of items and returns the ones that are recently added
- /// </summary>
- private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
- {
- var list = new List<BaseItem>();
-
- foreach (var item in itemSet)
- {
- if (!item.IsFolder && item.IsRecentlyAdded(user))
- {
- list.Add(item);
- }
- }
-
- return list;
- }
-
- /// <summary>
- /// Takes a list of items and returns the ones that are recently added and unplayed
- /// </summary>
- private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
- {
- var list = new List<BaseItem>();
-
- foreach (var item in itemSet)
- {
- if (!item.IsFolder && item.IsRecentlyAdded(user))
- {
- var userdata = item.GetUserData(user, false);
-
- if (userdata == null || userdata.PlayCount == 0)
- {
- list.Add(item);
- }
- }
- }
-
- return list;
- }
-
- /// <summary>
- /// Takes a list of items and returns the ones that are in progress
- /// </summary>
- private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
- {
- var list = new List<BaseItem>();
-
- foreach (var item in itemSet)
- {
- if (!item.IsFolder)
- {
- var userdata = item.GetUserData(user, false);
-
- if (userdata != null && userdata.PlaybackPositionTicks > 0)
- {
- list.Add(item);
- }
- }
- }
-
- return list;
- }
-
- /// <summary>
- /// Gets the total played percentage for a set of items
- /// </summary>
- private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
- {
- itemSet = itemSet.Where(i => !(i.IsFolder));
-
- decimal totalPercent = 0;
-
- int count = 0;
-
- foreach (BaseItem item in itemSet)
- {
- count++;
-
- UserItemData data = item.GetUserData(user, false);
-
- if (data == null)
- {
- continue;
- }
-
- if (data.PlayCount > 0)
- {
- totalPercent += 100;
- }
- else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
- {
- decimal itemPercent = data.PlaybackPositionTicks;
- itemPercent /= item.RunTimeTicks.Value;
- totalPercent += itemPercent;
- }
- }
-
- if (count == 0)
- {
- return 0;
- }
-
- return totalPercent / count;
- }
-
- /// <summary>
- /// Marks the item as either played or unplayed
- /// </summary>
- public override void SetPlayedStatus(User user, bool wasPlayed)
- {
- base.SetPlayedStatus(user, wasPlayed);
-
- // Now sweep through recursively and update status
- foreach (BaseItem item in GetChildren(user))
- {
- item.SetPlayedStatus(user, wasPlayed);
- }
- }
-
- /// <summary>
- /// Finds an item by ID, recursively
- /// </summary>
- public override BaseItem FindItemById(Guid id)
- {
- var result = base.FindItemById(id);
-
- if (result != null)
- {
- return result;
- }
-
- //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
- return RecursiveChildren.FirstOrDefault(i => i.Id == id);
- }
-
- /// <summary>
- /// Finds an item by path, recursively
- /// </summary>
- public BaseItem FindByPath(string path)
- {
- if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
- {
- return this;
- }
-
- //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
- return RecursiveChildren.FirstOrDefault(i => i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Tasks;
+using SortOrder = MediaBrowser.Controller.Sorting.SortOrder;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Folder
+ /// </summary>
+ public class Folder : BaseItem
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool IsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is physical root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
+ public bool IsPhysicalRoot { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
+ public bool IsRoot { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is virtual folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is virtual folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public virtual bool IsVirtualFolder
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Return the id that should be used to key display prefs for this item.
+ /// Default is based on the type for everything except actual generic folders.
+ /// </summary>
+ /// <value>The display prefs id.</value>
+ [IgnoreDataMember]
+ public virtual Guid DisplayPrefsId
+ {
+ get
+ {
+ var thisType = GetType();
+ return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
+ }
+ }
+
+ /// <summary>
+ /// The _display prefs
+ /// </summary>
+ private IEnumerable<DisplayPreferences> _displayPrefs;
+ /// <summary>
+ /// The _display prefs initialized
+ /// </summary>
+ private bool _displayPrefsInitialized;
+ /// <summary>
+ /// The _display prefs sync lock
+ /// </summary>
+ private object _displayPrefsSyncLock = new object();
+ /// <summary>
+ /// Gets the display prefs.
+ /// </summary>
+ /// <value>The display prefs.</value>
+ [IgnoreDataMember]
+ public IEnumerable<DisplayPreferences> DisplayPrefs
+ {
+ get
+ {
+ // Call ToList to exhaust the stream because we'll be iterating over this multiple times
+ LazyInitializer.EnsureInitialized(ref _displayPrefs, ref _displayPrefsInitialized, ref _displayPrefsSyncLock, () => Kernel.Instance.DisplayPreferencesRepository.RetrieveDisplayPrefs(this).ToList());
+ return _displayPrefs;
+ }
+ private set
+ {
+ _displayPrefs = value;
+
+ if (value == null)
+ {
+ _displayPrefsInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the display prefs.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="createIfNull">if set to <c>true</c> [create if null].</param>
+ /// <returns>DisplayPreferences.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public DisplayPreferences GetDisplayPrefs(User user, bool createIfNull)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (DisplayPrefs == null)
+ {
+ if (!createIfNull)
+ {
+ return null;
+ }
+
+ AddOrUpdateDisplayPrefs(user, new DisplayPreferences { UserId = user.Id });
+ }
+
+ var data = DisplayPrefs.FirstOrDefault(u => u.UserId == user.Id);
+
+ if (data == null && createIfNull)
+ {
+ data = new DisplayPreferences { UserId = user.Id };
+ AddOrUpdateDisplayPrefs(user, data);
+ }
+
+ return data;
+ }
+
+ /// <summary>
+ /// Adds the or update display prefs.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="data">The data.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddOrUpdateDisplayPrefs(User user, DisplayPreferences data)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (data == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ data.UserId = user.Id;
+
+ if (DisplayPrefs == null)
+ {
+ DisplayPrefs = new[] { data };
+ }
+ else
+ {
+ var list = DisplayPrefs.Where(u => u.UserId != user.Id).ToList();
+ list.Add(data);
+ DisplayPrefs = list;
+ }
+ }
+
+ #region Sorting
+
+ /// <summary>
+ /// The _sort by options
+ /// </summary>
+ private Dictionary<string, IComparer<BaseItem>> _sortByOptions;
+ /// <summary>
+ /// Dictionary of sort options - consists of a display value (localized) and an IComparer of BaseItem
+ /// </summary>
+ /// <value>The sort by options.</value>
+ [IgnoreDataMember]
+ public Dictionary<string, IComparer<BaseItem>> SortByOptions
+ {
+ get { return _sortByOptions ?? (_sortByOptions = GetSortByOptions()); }
+ }
+
+ /// <summary>
+ /// Returns the valid set of sort by options for this folder type.
+ /// Override or extend to modify.
+ /// </summary>
+ /// <returns>Dictionary{System.StringIComparer{BaseItem}}.</returns>
+ protected virtual Dictionary<string, IComparer<BaseItem>> GetSortByOptions()
+ {
+ return new Dictionary<string, IComparer<BaseItem>> {
+ {LocalizedStrings.Instance.GetString("NameDispPref"), new BaseItemComparer(SortOrder.Name)},
+ {LocalizedStrings.Instance.GetString("DateDispPref"), new BaseItemComparer(SortOrder.Date)},
+ {LocalizedStrings.Instance.GetString("RatingDispPref"), new BaseItemComparer(SortOrder.Rating)},
+ {LocalizedStrings.Instance.GetString("RuntimeDispPref"), new BaseItemComparer(SortOrder.Runtime)},
+ {LocalizedStrings.Instance.GetString("YearDispPref"), new BaseItemComparer(SortOrder.Year)}
+ };
+
+ }
+
+ /// <summary>
+ /// Get a sorting comparer by name
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>IComparer{BaseItem}.</returns>
+ private IComparer<BaseItem> GetSortingFunction(string name)
+ {
+ IComparer<BaseItem> sorting;
+ SortByOptions.TryGetValue(name ?? "", out sorting);
+ return sorting ?? new BaseItemComparer(SortOrder.Name);
+ }
+
+ /// <summary>
+ /// Get the list of sort by choices for this folder (localized).
+ /// </summary>
+ /// <value>The sort by option strings.</value>
+ [IgnoreDataMember]
+ public IEnumerable<string> SortByOptionStrings
+ {
+ get { return SortByOptions.Keys; }
+ }
+
+ #endregion
+
+ #region Indexing
+
+ /// <summary>
+ /// The _index by options
+ /// </summary>
+ private Dictionary<string, Func<User, IEnumerable<BaseItem>>> _indexByOptions;
+ /// <summary>
+ /// Dictionary of index options - consists of a display value and an indexing function
+ /// which takes User as a parameter and returns an IEnum of BaseItem
+ /// </summary>
+ /// <value>The index by options.</value>
+ [IgnoreDataMember]
+ public Dictionary<string, Func<User, IEnumerable<BaseItem>>> IndexByOptions
+ {
+ get { return _indexByOptions ?? (_indexByOptions = GetIndexByOptions()); }
+ }
+
+ /// <summary>
+ /// Returns the valid set of index by options for this folder type.
+ /// Override or extend to modify.
+ /// </summary>
+ /// <returns>Dictionary{System.StringFunc{UserIEnumerable{BaseItem}}}.</returns>
+ protected virtual Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions()
+ {
+ return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> {
+ {LocalizedStrings.Instance.GetString("NoneDispPref"), null},
+ {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer},
+ {LocalizedStrings.Instance.GetString("GenreDispPref"), GetIndexByGenre},
+ {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector},
+ {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear},
+ {LocalizedStrings.Instance.GetString("OfficialRatingDispPref"), null},
+ {LocalizedStrings.Instance.GetString("StudioDispPref"), GetIndexByStudio}
+ };
+
+ }
+
+ /// <summary>
+ /// Gets the index by actor.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GetIndexByPerformer(User user)
+ {
+ return GetIndexByPerson(user, new List<string> { PersonType.Actor, PersonType.MusicArtist }, LocalizedStrings.Instance.GetString("PerformerDispPref"));
+ }
+
+ /// <summary>
+ /// Gets the index by director.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GetIndexByDirector(User user)
+ {
+ return GetIndexByPerson(user, new List<string> { PersonType.Director }, LocalizedStrings.Instance.GetString("DirectorDispPref"));
+ }
+
+ /// <summary>
+ /// Gets the index by person.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="personTypes">The person types we should match on</param>
+ /// <param name="indexName">Name of the index.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GetIndexByPerson(User user, List<string> personTypes, string indexName)
+ {
+
+ // Even though this implementation means multiple iterations over the target list - it allows us to defer
+ // the retrieval of the individual children for each index value until they are requested.
+ using (new Profiler(indexName + " Index Build for " + Name))
+ {
+ // Put this in a local variable to avoid an implicitly captured closure
+ var currentIndexName = indexName;
+
+ var us = this;
+ var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.AllPeople != null).ToList();
+
+ return candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type))
+ .Select(a => a.Name))
+ .Distinct()
+ .Select(i =>
+ {
+ try
+ {
+ return Kernel.Instance.LibraryManager.GetPerson(i).Result;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting person {0}", ex, i);
+ return null;
+ }
+ catch (AggregateException ex)
+ {
+ Logger.LogException("Error getting person {0}", ex, i);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+ .Select(a => new IndexFolder(us, a,
+ candidates.Where(i => i.AllPeople.Any(p => personTypes.Contains(p.Type) && p.Name.Equals(a.Name, StringComparison.OrdinalIgnoreCase))
+ ), currentIndexName));
+
+ }
+ }
+
+ /// <summary>
+ /// Gets the index by studio.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GetIndexByStudio(User user)
+ {
+ // Even though this implementation means multiple iterations over the target list - it allows us to defer
+ // the retrieval of the individual children for each index value until they are requested.
+ using (new Profiler("Studio Index Build for " + Name))
+ {
+ var indexName = LocalizedStrings.Instance.GetString("StudioDispPref");
+
+ var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.Studios != null).ToList();
+
+ return candidates.AsParallel().SelectMany(i => i.Studios)
+ .Distinct()
+ .Select(i =>
+ {
+ try
+ {
+ return Kernel.Instance.LibraryManager.GetStudio(i).Result;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting studio {0}", ex, i);
+ return null;
+ }
+ catch (AggregateException ex)
+ {
+ Logger.LogException("Error getting studio {0}", ex, i);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+ .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.Studios.Any(s => s.Equals(ndx.Name, StringComparison.OrdinalIgnoreCase))), indexName));
+ }
+ }
+
+ /// <summary>
+ /// Gets the index by genre.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GetIndexByGenre(User user)
+ {
+ // Even though this implementation means multiple iterations over the target list - it allows us to defer
+ // the retrieval of the individual children for each index value until they are requested.
+ using (new Profiler("Genre Index Build for " + Name))
+ {
+ var indexName = LocalizedStrings.Instance.GetString("GenreDispPref");
+
+ //we need a copy of this so we don't double-recurse
+ var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.Genres != null).ToList();
+
+ return candidates.AsParallel().SelectMany(i => i.Genres)
+ .Distinct()
+ .Select(i =>
+ {
+ try
+ {
+ return Kernel.Instance.LibraryManager.GetGenre(i).Result;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting genre {0}", ex, i);
+ return null;
+ }
+ catch (AggregateException ex)
+ {
+ Logger.LogException("Error getting genre {0}", ex, i);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+ .Select(genre => new IndexFolder(this, genre, candidates.Where(i => i.Genres.Any(g => g.Equals(genre.Name, StringComparison.OrdinalIgnoreCase))), indexName)
+ );
+ }
+ }
+
+ /// <summary>
+ /// Gets the index by year.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GetIndexByYear(User user)
+ {
+ // Even though this implementation means multiple iterations over the target list - it allows us to defer
+ // the retrieval of the individual children for each index value until they are requested.
+ using (new Profiler("Production Year Index Build for " + Name))
+ {
+ var indexName = LocalizedStrings.Instance.GetString("YearDispPref");
+
+ //we need a copy of this so we don't double-recurse
+ var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList();
+
+ return candidates.AsParallel().Select(i => i.ProductionYear.Value)
+ .Distinct()
+ .Select(i =>
+ {
+ try
+ {
+ return Kernel.Instance.LibraryManager.GetYear(i).Result;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting year {0}", ex, i);
+ return null;
+ }
+ catch (AggregateException ex)
+ {
+ Logger.LogException("Error getting year {0}", ex, i);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+
+ .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.ProductionYear == int.Parse(ndx.Name)), indexName));
+
+ }
+ }
+
+ /// <summary>
+ /// Returns the indexed children for this user from the cache. Caches them if not already there.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="indexBy">The index by.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ private IEnumerable<BaseItem> GetIndexedChildren(User user, string indexBy)
+ {
+ List<BaseItem> result;
+ var cacheKey = user.Name + indexBy;
+ IndexCache.TryGetValue(cacheKey, out result);
+
+ if (result == null)
+ {
+ //not cached - cache it
+ Func<User, IEnumerable<BaseItem>> indexing;
+ IndexByOptions.TryGetValue(indexBy, out indexing);
+ result = BuildIndex(indexBy, indexing, user);
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Get the list of indexy by choices for this folder (localized).
+ /// </summary>
+ /// <value>The index by option strings.</value>
+ [IgnoreDataMember]
+ public IEnumerable<string> IndexByOptionStrings
+ {
+ get { return IndexByOptions.Keys; }
+ }
+
+ /// <summary>
+ /// The index cache
+ /// </summary>
+ protected ConcurrentDictionary<string, List<BaseItem>> IndexCache = new ConcurrentDictionary<string, List<BaseItem>>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Builds the index.
+ /// </summary>
+ /// <param name="indexKey">The index key.</param>
+ /// <param name="indexFunction">The index function.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>List{BaseItem}.</returns>
+ protected virtual List<BaseItem> BuildIndex(string indexKey, Func<User, IEnumerable<BaseItem>> indexFunction, User user)
+ {
+ return indexFunction != null
+ ? IndexCache[user.Name + indexKey] = indexFunction(user).ToList()
+ : null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// The children
+ /// </summary>
+ private ConcurrentBag<BaseItem> _children;
+ /// <summary>
+ /// The _children initialized
+ /// </summary>
+ private bool _childrenInitialized;
+ /// <summary>
+ /// The _children sync lock
+ /// </summary>
+ private object _childrenSyncLock = new object();
+ /// <summary>
+ /// Gets or sets the actual children.
+ /// </summary>
+ /// <value>The actual children.</value>
+ protected virtual ConcurrentBag<BaseItem> ActualChildren
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _children, ref _childrenInitialized, ref _childrenSyncLock, LoadChildren);
+ return _children;
+ }
+ private set
+ {
+ _children = value;
+
+ if (value == null)
+ {
+ _childrenInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to the actual children of this folder - without regard to user
+ /// </summary>
+ /// <value>The children.</value>
+ [IgnoreDataMember]
+ public ConcurrentBag<BaseItem> Children
+ {
+ get
+ {
+ return ActualChildren;
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to all recursive children of this folder - without regard to user
+ /// </summary>
+ /// <value>The recursive children.</value>
+ [IgnoreDataMember]
+ public IEnumerable<BaseItem> RecursiveChildren
+ {
+ get
+ {
+ foreach (var item in Children)
+ {
+ yield return item;
+
+ if (item.IsFolder)
+ {
+ var subFolder = (Folder)item;
+
+ foreach (var subitem in subFolder.RecursiveChildren)
+ {
+ yield return subitem;
+ }
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Loads our children. Validation will occur externally.
+ /// We want this sychronous.
+ /// </summary>
+ /// <returns>ConcurrentBag{BaseItem}.</returns>
+ protected virtual ConcurrentBag<BaseItem> LoadChildren()
+ {
+ //just load our children from the repo - the library will be validated and maintained in other processes
+ return new ConcurrentBag<BaseItem>(GetCachedChildren());
+ }
+
+ /// <summary>
+ /// Gets or sets the current validation cancellation token source.
+ /// </summary>
+ /// <value>The current validation cancellation token source.</value>
+ private CancellationTokenSource CurrentValidationCancellationTokenSource { get; set; }
+
+ /// <summary>
+ /// Validates that the children of the folder still exist
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>Task.</returns>
+ public async Task ValidateChildren(IProgress<TaskProgress> progress, CancellationToken cancellationToken, bool? recursive = null)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Cancel the current validation, if any
+ if (CurrentValidationCancellationTokenSource != null)
+ {
+ CurrentValidationCancellationTokenSource.Cancel();
+ }
+
+ // Create an inner cancellation token. This can cancel all validations from this level on down,
+ // but nothing above this
+ var innerCancellationTokenSource = new CancellationTokenSource();
+
+ try
+ {
+ CurrentValidationCancellationTokenSource = innerCancellationTokenSource;
+
+ var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken);
+
+ await ValidateChildrenInternal(progress, linkedCancellationTokenSource.Token, recursive).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.LogInfo("ValidateChildren cancelled for " + Name);
+
+ // If the outer cancelletion token in the cause for the cancellation, throw it
+ if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken)
+ {
+ throw;
+ }
+ }
+ finally
+ {
+ // Null out the token source
+ if (CurrentValidationCancellationTokenSource == innerCancellationTokenSource)
+ {
+ CurrentValidationCancellationTokenSource = null;
+ }
+
+ innerCancellationTokenSource.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>Task.</returns>
+ protected async virtual Task ValidateChildrenInternal(IProgress<TaskProgress> progress, CancellationToken cancellationToken, bool? recursive = null)
+ {
+ // Nothing to do here
+ if (LocationType != LocationType.FileSystem)
+ {
+ return;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var changedArgs = new ChildrenChangedEventArgs(this);
+
+ //get the current valid children from filesystem (or wherever)
+ var nonCachedChildren = GetNonCachedChildren();
+
+ if (nonCachedChildren == null) return; //nothing to validate
+
+ progress.Report(new TaskProgress { PercentComplete = 5 });
+
+ //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+ var currentChildren = ActualChildren.ToDictionary(i => i.Id);
+
+ //create a list for our validated children
+ var validChildren = new ConcurrentBag<Tuple<BaseItem, bool>>();
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Parallel.ForEach(nonCachedChildren, child =>
+ {
+ BaseItem currentChild;
+
+ if (currentChildren.TryGetValue(child.Id, out currentChild))
+ {
+ currentChild.ResolveArgs = child.ResolveArgs;
+
+ //existing item - check if it has changed
+ if (currentChild.HasChanged(child))
+ {
+ EntityResolutionHelper.EnsureDates(currentChild, child.ResolveArgs);
+
+ changedArgs.AddUpdatedItem(currentChild);
+ validChildren.Add(new Tuple<BaseItem, bool>(currentChild, true));
+ }
+ else
+ {
+ validChildren.Add(new Tuple<BaseItem, bool>(currentChild, false));
+ }
+ }
+ else
+ {
+ //brand new item - needs to be added
+ changedArgs.AddNewItem(child);
+
+ validChildren.Add(new Tuple<BaseItem, bool>(child, true));
+ }
+ });
+
+ // If any items were added or removed....
+ if (!changedArgs.ItemsAdded.IsEmpty || currentChildren.Count != validChildren.Count)
+ {
+ var newChildren = validChildren.Select(c => c.Item1).ToList();
+
+ //that's all the new and changed ones - now see if there are any that are missing
+ changedArgs.ItemsRemoved = currentChildren.Values.Except(newChildren).ToList();
+
+ foreach (var item in changedArgs.ItemsRemoved)
+ {
+ Logger.LogInfo("** " + item.Name + " Removed from library.");
+ }
+
+ var childrenReplaced = false;
+
+ if (changedArgs.ItemsRemoved.Count > 0)
+ {
+ ActualChildren = new ConcurrentBag<BaseItem>(newChildren);
+ childrenReplaced = true;
+ }
+
+ var saveTasks = new List<Task>();
+
+ foreach (var item in changedArgs.ItemsAdded)
+ {
+ Logger.LogInfo("** " + item.Name + " Added to library.");
+
+ if (!childrenReplaced)
+ {
+ _children.Add(item);
+ }
+
+ saveTasks.Add(Kernel.Instance.ItemRepository.SaveItem(item, CancellationToken.None));
+ }
+
+ await Task.WhenAll(saveTasks).ConfigureAwait(false);
+
+ //and save children in repo...
+ Logger.LogInfo("*** Saving " + newChildren.Count + " children for " + Name);
+ await Kernel.Instance.ItemRepository.SaveChildren(Id, newChildren, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ if (changedArgs.HasChange)
+ {
+ //force the indexes to rebuild next time
+ IndexCache.Clear();
+
+ //and fire event
+ Kernel.Instance.LibraryManager.OnLibraryChanged(changedArgs);
+ }
+
+ progress.Report(new TaskProgress { PercentComplete = 15 });
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await RefreshChildren(validChildren, progress, cancellationToken, recursive).ConfigureAwait(false);
+
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+ }
+
+ /// <summary>
+ /// Refreshes the children.
+ /// </summary>
+ /// <param name="children">The children.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>Task.</returns>
+ private Task RefreshChildren(IEnumerable<Tuple<BaseItem, bool>> children, IProgress<TaskProgress> progress, CancellationToken cancellationToken, bool? recursive)
+ {
+ var numComplete = 0;
+
+ var list = children.ToList();
+
+ var tasks = list.Select(tuple => Task.Run(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var child = tuple.Item1;
+
+ //refresh it
+ await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder).ConfigureAwait(false);
+
+ //and add it to our valid children
+ //fire an added event...?
+ //if it is a folder we need to validate its children as well
+
+ // Refresh children if a folder and the item changed or recursive is set to true
+ var refreshChildren = child.IsFolder && (tuple.Item2 || (recursive.HasValue && recursive.Value));
+
+ if (refreshChildren)
+ {
+ // Don't refresh children if explicitly set to false
+ if (recursive.HasValue && recursive.Value == false)
+ {
+ refreshChildren = false;
+ }
+ }
+
+ if (refreshChildren)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await ((Folder)child).ValidateChildren(new Progress<TaskProgress> { }, cancellationToken, recursive: recursive).ConfigureAwait(false);
+ }
+
+ lock (progress)
+ {
+ numComplete++;
+
+ double percent = numComplete;
+ percent /= list.Count;
+
+ progress.Report(new TaskProgress { PercentComplete = (85 * percent) + 15 });
+ }
+ }));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return Task.WhenAll(tasks);
+ }
+
+ /// <summary>
+ /// Get the children of this folder from the actual file system
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected virtual IEnumerable<BaseItem> GetNonCachedChildren()
+ {
+ IEnumerable<WIN32_FIND_DATA> fileSystemChildren;
+
+ try
+ {
+ fileSystemChildren = ResolveArgs.FileSystemChildren;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting ResolveArgs for {0}", ex, Path);
+ return new List<BaseItem> { };
+ }
+
+ return Kernel.Instance.LibraryManager.GetItems<BaseItem>(fileSystemChildren, this);
+ }
+
+ /// <summary>
+ /// Get our children from the repo - stubbed for now
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected virtual IEnumerable<BaseItem> GetCachedChildren()
+ {
+ return Kernel.Instance.ItemRepository.RetrieveChildren(this);
+ }
+
+ /// <summary>
+ /// Gets allowed children of an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="indexBy">The index by.</param>
+ /// <param name="sortBy">The sort by.</param>
+ /// <param name="sortOrder">The sort order.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public virtual IEnumerable<BaseItem> GetChildren(User user, string indexBy = null, string sortBy = null, Model.Entities.SortOrder? sortOrder = null)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ //the true root should return our users root folder children
+ if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, indexBy, sortBy, sortOrder);
+
+ IEnumerable<BaseItem> result = null;
+
+ if (!string.IsNullOrEmpty(indexBy))
+ {
+ result = GetIndexedChildren(user, indexBy);
+ }
+
+ // If indexed is false or the indexing function is null
+ if (result == null)
+ {
+ result = ActualChildren.Where(c => c.IsVisible(user));
+ }
+
+ if (string.IsNullOrEmpty(sortBy))
+ {
+ return result;
+ }
+
+ return sortOrder.HasValue && sortOrder.Value == Model.Entities.SortOrder.Descending
+ ? result.OrderByDescending(i => i, GetSortingFunction(sortBy))
+ : result.OrderBy(i => i, GetSortingFunction(sortBy));
+ }
+
+ /// <summary>
+ /// Gets allowed recursive children of an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public IEnumerable<BaseItem> GetRecursiveChildren(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ foreach (var item in GetChildren(user))
+ {
+ yield return item;
+
+ var subFolder = item as Folder;
+
+ if (subFolder != null)
+ {
+ foreach (var subitem in subFolder.GetRecursiveChildren(user))
+ {
+ yield return subitem;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Folders need to validate and refresh
+ /// </summary>
+ /// <returns>Task.</returns>
+ public override async Task ChangedExternally()
+ {
+ await base.ChangedExternally().ConfigureAwait(false);
+
+ var progress = new Progress<TaskProgress> { };
+
+ await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Marks the item as either played or unplayed
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
+ /// <returns>Task.</returns>
+ public override async Task SetPlayedStatus(User user, bool wasPlayed)
+ {
+ await base.SetPlayedStatus(user, wasPlayed).ConfigureAwait(false);
+
+ // Now sweep through recursively and update status
+ var tasks = GetChildren(user).Select(c => c.SetPlayedStatus(user, wasPlayed));
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>BaseItem.</returns>
+ public override BaseItem FindItemById(Guid id, User user)
+ {
+ var result = base.FindItemById(id, user);
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ var children = user == null ? ActualChildren : GetChildren(user);
+
+ foreach (var child in children)
+ {
+ result = child.FindItemById(id, user);
+
+ if (result != null)
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Finds an item by path, recursively
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public BaseItem FindByPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ try
+ {
+ if (ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
+ {
+ return this;
+ }
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting ResolveArgs for {0}", ex, Path);
+ }
+
+ //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+ return RecursiveChildren.FirstOrDefault(i =>
+ {
+ try
+ {
+ return i.ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase);
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting ResolveArgs for {0}", ex, Path);
+ return false;
+ }
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index ba343a2bc6..d5e8afb202 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -1,7 +1,10 @@
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Genre : BaseEntity
- {
- }
-}
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Genre
+ /// </summary>
+ public class Genre : BaseItem
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
new file mode 100644
index 0000000000..4ea531331e
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// This is just a marker interface to denote top level folders
+ /// </summary>
+ public interface ICollectionFolder
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/ISupportsSpecialFeatures.cs b/MediaBrowser.Controller/Entities/ISupportsSpecialFeatures.cs
new file mode 100644
index 0000000000..9b52c2f7fc
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/ISupportsSpecialFeatures.cs
@@ -0,0 +1,105 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Allows some code sharing between entities that support special features
+ /// </summary>
+ public interface ISupportsSpecialFeatures
+ {
+ /// <summary>
+ /// Gets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ string Path { get; }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the resolve args.
+ /// </summary>
+ /// <value>The resolve args.</value>
+ ItemResolveArgs ResolveArgs { get; }
+
+ /// <summary>
+ /// Gets the special features.
+ /// </summary>
+ /// <value>The special features.</value>
+ List<Video> SpecialFeatures { get; }
+ }
+
+ /// <summary>
+ /// Class SpecialFeatures
+ /// </summary>
+ public static class SpecialFeatures
+ {
+ /// <summary>
+ /// Loads special features from the file system
+ /// </summary>
+ /// <param name="entity">The entity.</param>
+ /// <returns>List{Video}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public static IEnumerable<Video> LoadSpecialFeatures(ISupportsSpecialFeatures entity)
+ {
+ if (entity == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ WIN32_FIND_DATA? folder;
+
+ try
+ {
+ folder = entity.ResolveArgs.GetFileSystemEntryByName("specials");
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting ResolveArgs for {0}", ex, entity.Path);
+ return new List<Video> { };
+ }
+
+ // Path doesn't exist. No biggie
+ if (folder == null)
+ {
+ return new List<Video> {};
+ }
+
+ IEnumerable<WIN32_FIND_DATA> files;
+
+ try
+ {
+ files = FileSystem.GetFiles(folder.Value.Path);
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error loading trailers for {0}", ex, entity.Name);
+ return new List<Video> { };
+ }
+
+ return Kernel.Instance.LibraryManager.GetItems<Video>(files, null).Select(video =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = Kernel.Instance.ItemRepository.RetrieveItem(video.Id) as Video;
+
+ if (dbItem != null)
+ {
+ dbItem.ResolveArgs = video.ResolveArgs;
+ video = dbItem;
+ }
+
+ return video;
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IndexFolder.cs b/MediaBrowser.Controller/Entities/IndexFolder.cs
new file mode 100644
index 0000000000..013db48536
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IndexFolder.cs
@@ -0,0 +1,204 @@
+using MediaBrowser.Common.Extensions;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class IndexFolder
+ /// </summary>
+ public class IndexFolder : Folder
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IndexFolder" /> class.
+ /// </summary>
+ /// <param name="parent">The parent.</param>
+ /// <param name="shadow">The shadow.</param>
+ /// <param name="children">The children.</param>
+ /// <param name="indexName">Name of the index.</param>
+ /// <param name="groupContents">if set to <c>true</c> [group contents].</param>
+ public IndexFolder(Folder parent, BaseItem shadow, IEnumerable<BaseItem> children, string indexName, bool groupContents = true)
+ {
+ ChildSource = children;
+ ShadowItem = shadow;
+ GroupContents = groupContents;
+ if (shadow == null)
+ {
+ Name = SortName = "<Unknown>";
+ }
+ else
+ {
+ SetShadowValues();
+ }
+ Id = (parent.Id.ToString() + Name).GetMBId(typeof(IndexFolder));
+
+ IndexName = indexName;
+ Parent = parent;
+ }
+
+ /// <summary>
+ /// Resets the parent.
+ /// </summary>
+ /// <param name="parent">The parent.</param>
+ public void ResetParent(Folder parent)
+ {
+ Parent = parent;
+ Id = (parent.Id.ToString() + Name).GetMBId(typeof(IndexFolder));
+ }
+
+ /// <summary>
+ /// Override this to true if class should be grouped under a container in indicies
+ /// The container class should be defined via IndexContainer
+ /// </summary>
+ /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool GroupInIndex
+ {
+ get
+ {
+ return ShadowItem != null && ShadowItem.GroupInIndex;
+ }
+ }
+
+ /// <summary>
+ /// Override this to return the folder that should be used to construct a container
+ /// for this item in an index. GroupInIndex should be true as well.
+ /// </summary>
+ /// <value>The index container.</value>
+ [IgnoreDataMember]
+ public override Folder IndexContainer
+ {
+ get { return ShadowItem != null ? ShadowItem.IndexContainer : new IndexFolder(this, null, null, "<Unknown>", false); }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [group contents].
+ /// </summary>
+ /// <value><c>true</c> if [group contents]; otherwise, <c>false</c>.</value>
+ protected bool GroupContents { get; set; }
+ /// <summary>
+ /// Gets or sets the child source.
+ /// </summary>
+ /// <value>The child source.</value>
+ protected IEnumerable<BaseItem> ChildSource { get; set; }
+ /// <summary>
+ /// Gets or sets our children.
+ /// </summary>
+ /// <value>Our children.</value>
+ protected ConcurrentBag<BaseItem> OurChildren { get; set; }
+ /// <summary>
+ /// Gets the name of the index.
+ /// </summary>
+ /// <value>The name of the index.</value>
+ public string IndexName { get; private set; }
+
+ /// <summary>
+ /// Override to return the children defined to us when we were created
+ /// </summary>
+ /// <value>The actual children.</value>
+ protected override ConcurrentBag<BaseItem> LoadChildren()
+ {
+ var originalChildSource = ChildSource.ToList();
+
+ var kids = originalChildSource;
+ if (GroupContents)
+ {
+ // Recursively group up the chain
+ var group = true;
+ var isSubsequentLoop = false;
+
+ while (group)
+ {
+ kids = isSubsequentLoop || kids.Any(i => i.GroupInIndex)
+ ? GroupedSource(kids).ToList()
+ : originalChildSource;
+
+ group = kids.Any(i => i.GroupInIndex);
+ isSubsequentLoop = true;
+ }
+ }
+
+ // Now - since we built the index grouping from the bottom up - we now need to properly set Parents from the top down
+ SetParents(this, kids.OfType<IndexFolder>());
+
+ return new ConcurrentBag<BaseItem>(kids);
+ }
+
+ /// <summary>
+ /// Sets the parents.
+ /// </summary>
+ /// <param name="parent">The parent.</param>
+ /// <param name="kids">The kids.</param>
+ private void SetParents(Folder parent, IEnumerable<IndexFolder> kids)
+ {
+ foreach (var child in kids)
+ {
+ child.ResetParent(parent);
+ child.SetParents(child, child.Children.OfType<IndexFolder>());
+ }
+ }
+
+ /// <summary>
+ /// Groupeds the source.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected IEnumerable<BaseItem> GroupedSource(IEnumerable<BaseItem> source)
+ {
+ return source.GroupBy(i => i.IndexContainer).Select(container => new IndexFolder(this, container.Key, container, null, false));
+ }
+
+ /// <summary>
+ /// The item we are shadowing as a folder (Genre, Actor, etc.)
+ /// We inherit the images and other meta from this item
+ /// </summary>
+ /// <value>The shadow item.</value>
+ protected BaseItem ShadowItem { get; set; }
+
+ /// <summary>
+ /// Sets the shadow values.
+ /// </summary>
+ protected void SetShadowValues()
+ {
+ if (ShadowItem != null)
+ {
+ Name = ShadowItem.Name;
+ SortName = ShadowItem.SortName;
+ Genres = ShadowItem.Genres;
+ Studios = ShadowItem.Studios;
+ OfficialRating = ShadowItem.OfficialRating;
+ BackdropImagePaths = ShadowItem.BackdropImagePaths;
+ Images = ShadowItem.Images;
+ Overview = ShadowItem.Overview;
+ DisplayMediaType = ShadowItem.GetType().Name;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the base implementation to refresh metadata for local trailers
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
+ /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+ {
+ if (ShadowItem != null)
+ {
+ var changed = await ShadowItem.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SetShadowValues();
+ return changed;
+ }
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index cb841530ee..34f09b4b09 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -1,7 +1,11 @@
-
-namespace MediaBrowser.Controller.Entities.Movies
-{
- public class BoxSet : Folder
- {
- }
-}
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ /// <summary>
+ /// Class BoxSet
+ /// </summary>
+ public class BoxSet : Folder
+ {
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 2d98fa06e8..114e10d37a 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -1,31 +1,144 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace MediaBrowser.Controller.Entities.Movies
-{
- public class Movie : Video
- {
- public IEnumerable<Video> SpecialFeatures { get; set; }
-
- /// <summary>
- /// Finds an item by ID, recursively
- /// </summary>
- public override BaseItem FindItemById(Guid id)
- {
- var item = base.FindItemById(id);
-
- if (item != null)
- {
- return item;
- }
-
- if (SpecialFeatures != null)
- {
- return SpecialFeatures.FirstOrDefault(i => i.Id == id);
- }
-
- return null;
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ /// <summary>
+ /// Class Movie
+ /// </summary>
+ public class Movie : Video, ISupportsSpecialFeatures
+ {
+ /// <summary>
+ /// Should be overridden to return the proper folder where metadata lives
+ /// </summary>
+ /// <value>The meta location.</value>
+ [IgnoreDataMember]
+ public override string MetaLocation
+ {
+ get
+ {
+ return VideoType == VideoType.VideoFile || VideoType == VideoType.Iso ? System.IO.Path.GetDirectoryName(Path) : Path;
+ }
+ }
+
+ /// <summary>
+ /// Override to use tmdb or imdb id so it will stick if the item moves physical locations
+ /// </summary>
+ /// <value>The user data id.</value>
+ [IgnoreDataMember]
+ public override Guid UserDataId
+ {
+ get
+ {
+ if (_userDataId == Guid.Empty)
+ {
+ var baseId = this.GetProviderId(MetadataProviders.Tmdb) ?? this.GetProviderId(MetadataProviders.Imdb);
+ _userDataId = baseId != null ? baseId.GetMD5() : Id;
+ }
+ return _userDataId;
+ }
+ }
+
+ /// <summary>
+ /// The _special features
+ /// </summary>
+ private List<Video> _specialFeatures;
+ /// <summary>
+ /// The _special features initialized
+ /// </summary>
+ private bool _specialFeaturesInitialized;
+ /// <summary>
+ /// The _special features sync lock
+ /// </summary>
+ private object _specialFeaturesSyncLock = new object();
+ /// <summary>
+ /// Gets the special features.
+ /// </summary>
+ /// <value>The special features.</value>
+ [IgnoreDataMember]
+ public List<Video> SpecialFeatures
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _specialFeatures, ref _specialFeaturesInitialized, ref _specialFeaturesSyncLock, () => Entities.SpecialFeatures.LoadSpecialFeatures(this).ToList());
+ return _specialFeatures;
+ }
+ private set
+ {
+ _specialFeatures = value;
+
+ if (value == null)
+ {
+ _specialFeaturesInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Needed because the resolver stops at the movie folder and we find the video inside.
+ /// </summary>
+ /// <value><c>true</c> if [use parent path to create resolve args]; otherwise, <c>false</c>.</value>
+ protected override bool UseParentPathToCreateResolveArgs
+ {
+ get
+ {
+ return VideoType == VideoType.VideoFile || VideoType == VideoType.Iso;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the base implementation to refresh metadata for special features
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
+ /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+ {
+ // Lazy load these again
+ SpecialFeatures = null;
+
+ // Kick off a task to refresh the main item
+ var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
+
+ var tasks = SpecialFeatures.Select(item => item.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return result;
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>BaseItem.</returns>
+ public override BaseItem FindItemById(Guid id, User user)
+ {
+ var item = base.FindItemById(id, user);
+
+ if (item != null)
+ {
+ return item;
+ }
+
+ if (SpecialFeatures != null)
+ {
+ return SpecialFeatures.FirstOrDefault(i => i.Id == id);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index a12b9e38e2..5c92c019b8 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -1,25 +1,41 @@
-
-namespace MediaBrowser.Controller.Entities
-{
- /// <summary>
- /// This is the full Person object that can be retrieved with all of it's data.
- /// </summary>
- public class Person : BaseEntity
- {
- }
-
- /// <summary>
- /// This is the small Person stub that is attached to BaseItems
- /// </summary>
- public class PersonInfo
- {
- public string Name { get; set; }
- public string Overview { get; set; }
- public string Type { get; set; }
-
- public override string ToString()
- {
- return Name;
- }
- }
-}
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// This is the full Person object that can be retrieved with all of it's data.
+ /// </summary>
+ public class Person : BaseItem
+ {
+ }
+
+ /// <summary>
+ /// This is the small Person stub that is attached to BaseItems
+ /// </summary>
+ public class PersonInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ /// <value>The role.</value>
+ public string Role { get; set; }
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Entities/PlaybackProgressEventArgs.cs
new file mode 100644
index 0000000000..bbec606ae2
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/PlaybackProgressEventArgs.cs
@@ -0,0 +1,13 @@
+using MediaBrowser.Common.Events;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Holds information about a playback progress event
+ /// </summary>
+ public class PlaybackProgressEventArgs : GenericEventArgs<BaseItem>
+ {
+ public User User { get; set; }
+ public long? PlaybackPositionTicks { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
index b7c6e6aa43..a255849e61 100644
--- a/MediaBrowser.Controller/Entities/Studio.cs
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -1,7 +1,10 @@
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Studio : BaseEntity
- {
- }
-}
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Studio
+ /// </summary>
+ public class Studio : BaseItem
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 5d599fca7f..854b9d0183 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -1,7 +1,163 @@
-
-namespace MediaBrowser.Controller.Entities.TV
-{
- public class Episode : Video
- {
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ /// <summary>
+ /// Class Episode
+ /// </summary>
+ public class Episode : Video
+ {
+ /// <summary>
+ /// Episodes have a special Metadata folder
+ /// </summary>
+ /// <value>The meta location.</value>
+ [IgnoreDataMember]
+ public override string MetaLocation
+ {
+ get
+ {
+ return System.IO.Path.Combine(Parent.Path, "metadata");
+ }
+ }
+
+ /// <summary>
+ /// We want to group into series not show individually in an index
+ /// </summary>
+ /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool GroupInIndex
+ {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// We roll up into series
+ /// </summary>
+ /// <value>The index container.</value>
+ [IgnoreDataMember]
+ public override Folder IndexContainer
+ {
+ get
+ {
+ return Season;
+ }
+ }
+
+ /// <summary>
+ /// Override to use the provider Ids + season and episode number so it will be portable
+ /// </summary>
+ /// <value>The user data id.</value>
+ [IgnoreDataMember]
+ public override Guid UserDataId
+ {
+ get
+ {
+ if (_userDataId == Guid.Empty)
+ {
+ var baseId = Series != null ? Series.GetProviderId(MetadataProviders.Tvdb) ?? Series.GetProviderId(MetadataProviders.Tvcom) : null;
+ if (baseId != null)
+ {
+ var seasonNo = Season != null ? Season.IndexNumber ?? 0 : 0;
+ var epNo = IndexNumber ?? 0;
+ baseId = baseId + seasonNo.ToString("000") + epNo.ToString("000");
+ }
+ _userDataId = baseId != null ? baseId.GetMD5() : Id;
+ }
+ return _userDataId;
+ }
+ }
+
+ /// <summary>
+ /// Override this if you need to combine/collapse person information
+ /// </summary>
+ /// <value>All people.</value>
+ [IgnoreDataMember]
+ public override IEnumerable<PersonInfo> AllPeople
+ {
+ get
+ {
+ if (People == null) return Series != null ? Series.People : People;
+ return Series != null && Series.People != null ? People.Concat(Series.People) : base.AllPeople;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the studios.
+ /// </summary>
+ /// <value>The studios.</value>
+ [IgnoreDataMember]
+ public override List<string> Studios
+ {
+ get
+ {
+ return Series != null ? Series.Studios : null;
+ }
+ set
+ {
+ base.Studios = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the genres.
+ /// </summary>
+ /// <value>The genres.</value>
+ [IgnoreDataMember]
+ public override List<string> Genres
+ {
+ get { return Series != null ? Series.Genres : null; }
+ set
+ {
+ base.Genres = value;
+ }
+ }
+
+ /// <summary>
+ /// We persist the MB Id of our series object so we can always find it no matter
+ /// what context we happen to be loaded from.
+ /// </summary>
+ /// <value>The series item id.</value>
+ public Guid SeriesItemId { get; set; }
+
+ /// <summary>
+ /// We persist the MB Id of our season object so we can always find it no matter
+ /// what context we happen to be loaded from.
+ /// </summary>
+ /// <value>The season item id.</value>
+ public Guid SeasonItemId { get; set; }
+
+ /// <summary>
+ /// The _series
+ /// </summary>
+ private Series _series;
+ /// <summary>
+ /// This Episode's Series Instance
+ /// </summary>
+ /// <value>The series.</value>
+ [IgnoreDataMember]
+ public Series Series
+ {
+ get { return _series ?? (_series = FindParent<Series>()); }
+ }
+
+ /// <summary>
+ /// The _season
+ /// </summary>
+ private Season _season;
+ /// <summary>
+ /// This Episode's Season Instance
+ /// </summary>
+ /// <value>The season.</value>
+ [IgnoreDataMember]
+ public Season Season
+ {
+ get { return _season ?? (_season = FindParent<Season>()); }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index f9c7fecb32..140c90814e 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -1,34 +1,143 @@
-using System;
-
-namespace MediaBrowser.Controller.Entities.TV
-{
- public class Season : Folder
- {
- /// <summary>
- /// Store these to reduce disk access in Episode Resolver
- /// </summary>
- public string[] MetadataFiles
- {
- get
- {
- return ResolveArgs.MetadataFiles ?? new string[] { };
- }
- }
-
- /// <summary>
- /// Determines if the metafolder contains a given file
- /// </summary>
- public bool ContainsMetadataFile(string file)
- {
- for (int i = 0; i < MetadataFiles.Length; i++)
- {
- if (MetadataFiles[i].Equals(file, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
- }
-}
+using System.Collections.Generic;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ /// <summary>
+ /// Class Season
+ /// </summary>
+ public class Season : Folder
+ {
+
+ /// <summary>
+ /// Seasons are just containers
+ /// </summary>
+ /// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool IncludeInIndex
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// We want to group into our Series
+ /// </summary>
+ /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool GroupInIndex
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Override this to return the folder that should be used to construct a container
+ /// for this item in an index. GroupInIndex should be true as well.
+ /// </summary>
+ /// <value>The index container.</value>
+ [IgnoreDataMember]
+ public override Folder IndexContainer
+ {
+ get
+ {
+ return Series;
+ }
+ }
+
+ // Genre, Rating and Stuido will all be the same
+ protected override Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions()
+ {
+ return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> {
+ {LocalizedStrings.Instance.GetString("NoneDispPref"), null},
+ {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer},
+ {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector},
+ {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear},
+ };
+ }
+
+ /// <summary>
+ /// Override to use the provider Ids + season number so it will be portable
+ /// </summary>
+ /// <value>The user data id.</value>
+ [IgnoreDataMember]
+ public override Guid UserDataId
+ {
+ get
+ {
+ if (_userDataId == Guid.Empty)
+ {
+ var baseId = Series != null ? Series.GetProviderId(MetadataProviders.Tvdb) ?? Series.GetProviderId(MetadataProviders.Tvcom) : null;
+ if (baseId != null)
+ {
+ var seasonNo = IndexNumber ?? 0;
+ baseId = baseId + seasonNo.ToString("000");
+ }
+
+ _userDataId = baseId != null ? baseId.GetMD5() : Id;
+ }
+ return _userDataId;
+ }
+ }
+
+ /// <summary>
+ /// We persist the MB Id of our series object so we can always find it no matter
+ /// what context we happen to be loaded from.
+ /// </summary>
+ /// <value>The series item id.</value>
+ public Guid SeriesItemId { get; set; }
+
+ /// <summary>
+ /// The _series
+ /// </summary>
+ private Series _series;
+ /// <summary>
+ /// This Episode's Series Instance
+ /// </summary>
+ /// <value>The series.</value>
+ [IgnoreDataMember]
+ public Series Series
+ {
+ get { return _series ?? (_series = FindParent<Series>()); }
+ }
+
+ /// <summary>
+ /// Add files from the metadata folder to ResolveArgs
+ /// </summary>
+ /// <param name="args">The args.</param>
+ internal static void AddMetadataFiles(ItemResolveArgs args)
+ {
+ var folder = args.GetFileSystemEntryByName("metadata");
+
+ if (folder.HasValue)
+ {
+ args.AddMetadataFiles(FileSystem.GetFiles(folder.Value.Path));
+ }
+ }
+
+ /// <summary>
+ /// Creates ResolveArgs on demand
+ /// </summary>
+ /// <param name="pathInfo">The path info.</param>
+ /// <returns>ItemResolveArgs.</returns>
+ protected internal override ItemResolveArgs CreateResolveArgs(WIN32_FIND_DATA? pathInfo = null)
+ {
+ var args = base.CreateResolveArgs(pathInfo);
+
+ AddMetadataFiles(args);
+
+ return args;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 7c228a53df..152dcbac87 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -1,12 +1,89 @@
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Entities.TV
-{
- public class Series : Folder
- {
- public string Status { get; set; }
- public IEnumerable<DayOfWeek> AirDays { get; set; }
- public string AirTime { get; set; }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ /// <summary>
+ /// Class Series
+ /// </summary>
+ public class Series : Folder
+ {
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public SeriesStatus? Status { get; set; }
+ /// <summary>
+ /// Gets or sets the air days.
+ /// </summary>
+ /// <value>The air days.</value>
+ public List<DayOfWeek> AirDays { get; set; }
+ /// <summary>
+ /// Gets or sets the air time.
+ /// </summary>
+ /// <value>The air time.</value>
+ public string AirTime { get; set; }
+
+ /// <summary>
+ /// Series aren't included directly in indices - Their Episodes will roll up to them
+ /// </summary>
+ /// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool IncludeInIndex
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Override to use the provider Ids so it will be portable
+ /// </summary>
+ /// <value>The user data id.</value>
+ [IgnoreDataMember]
+ public override Guid UserDataId
+ {
+ get
+ {
+ if (_userDataId == Guid.Empty)
+ {
+ var baseId = this.GetProviderId(MetadataProviders.Tvdb) ?? this.GetProviderId(MetadataProviders.Tvcom);
+ _userDataId = baseId != null ? baseId.GetMD5() : Id;
+ }
+ return _userDataId;
+ }
+ }
+
+ // Studio, Genre and Rating will all be the same so makes no sense to index by these
+ protected override Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions()
+ {
+ return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> {
+ {LocalizedStrings.Instance.GetString("NoneDispPref"), null},
+ {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer},
+ {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector},
+ {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear},
+ };
+ }
+
+ /// <summary>
+ /// Creates ResolveArgs on demand
+ /// </summary>
+ /// <param name="pathInfo">The path info.</param>
+ /// <returns>ItemResolveArgs.</returns>
+ protected internal override ItemResolveArgs CreateResolveArgs(WIN32_FIND_DATA? pathInfo = null)
+ {
+ var args = base.CreateResolveArgs(pathInfo);
+
+ Season.AddMetadataFiles(args);
+
+ return args;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
new file mode 100644
index 0000000000..ed20d05b04
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -0,0 +1,51 @@
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Trailer
+ /// </summary>
+ public class Trailer : Video
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is local trailer.
+ /// </summary>
+ /// <value><c>true</c> if this instance is local trailer; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsLocalTrailer
+ {
+ get
+ {
+ // Local trailers are not part of children
+ return Parent == null;
+ }
+ }
+
+ /// <summary>
+ /// Should be overridden to return the proper folder where metadata lives
+ /// </summary>
+ /// <value>The meta location.</value>
+ [IgnoreDataMember]
+ public override string MetaLocation
+ {
+ get
+ {
+ if (!IsLocalTrailer)
+ {
+ return System.IO.Path.GetDirectoryName(Path);
+ }
+
+ return base.MetaLocation;
+ }
+ }
+
+ /// <summary>
+ /// Needed because the resolver stops at the trailer folder and we find the video inside.
+ /// </summary>
+ /// <value><c>true</c> if [use parent path to create resolve args]; otherwise, <c>false</c>.</value>
+ protected override bool UseParentPathToCreateResolveArgs
+ {
+ get { return !IsLocalTrailer; }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs
index 01eadfafb2..92e268d4c5 100644
--- a/MediaBrowser.Controller/Entities/User.cs
+++ b/MediaBrowser.Controller/Entities/User.cs
@@ -1,21 +1,446 @@
-using System;
-
-namespace MediaBrowser.Controller.Entities
-{
- public class User : BaseEntity
- {
- public string Password { get; set; }
-
- public string MaxParentalRating { get; set; }
-
- public int RecentItemDays { get; set; }
-
- public User()
- {
- RecentItemDays = 14;
- }
-
- public DateTime? LastLoginDate { get; set; }
- public DateTime? LastActivityDate { get; set; }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class User
+ /// </summary>
+ public class User : BaseItem
+ {
+ /// <summary>
+ /// The _root folder path
+ /// </summary>
+ private string _rootFolderPath;
+ /// <summary>
+ /// Gets the root folder path.
+ /// </summary>
+ /// <value>The root folder path.</value>
+ [IgnoreDataMember]
+ public string RootFolderPath
+ {
+ get
+ {
+ if (_rootFolderPath == null)
+ {
+ if (Configuration.UseCustomLibrary)
+ {
+ _rootFolderPath = GetRootFolderPath(Name);
+
+ if (!Directory.Exists(_rootFolderPath))
+ {
+ Directory.CreateDirectory(_rootFolderPath);
+ }
+ }
+ else
+ {
+ _rootFolderPath = Kernel.Instance.ApplicationPaths.DefaultUserViewsPath;
+ }
+ }
+ return _rootFolderPath;
+ }
+ }
+
+ /// <summary>
+ /// Gets the root folder path based on a given username
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <returns>System.String.</returns>
+ private string GetRootFolderPath(string username)
+ {
+ var safeFolderName = FileSystem.GetValidFilename(username);
+
+ return System.IO.Path.Combine(Kernel.Instance.ApplicationPaths.RootFolderPath, safeFolderName);
+ }
+
+ /// <summary>
+ /// Gets or sets the password.
+ /// </summary>
+ /// <value>The password.</value>
+ public string Password { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public override string Path
+ {
+ get
+ {
+ // Return this so that metadata providers will look in here
+ return ConfigurationDirectoryPath;
+ }
+ set
+ {
+ base.Path = value;
+ }
+ }
+
+ /// <summary>
+ /// Ensure this has a value
+ /// </summary>
+ /// <value>The display type of the media.</value>
+ public override string DisplayMediaType
+ {
+ get
+ {
+ return base.DisplayMediaType ?? GetType().Name;
+ }
+ set
+ {
+ base.DisplayMediaType = value;
+ }
+ }
+
+ /// <summary>
+ /// The _root folder
+ /// </summary>
+ private UserRootFolder _rootFolder;
+ /// <summary>
+ /// The _user root folder initialized
+ /// </summary>
+ private bool _userRootFolderInitialized;
+ /// <summary>
+ /// The _user root folder sync lock
+ /// </summary>
+ private object _userRootFolderSyncLock = new object();
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ [IgnoreDataMember]
+ public UserRootFolder RootFolder
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _rootFolder, ref _userRootFolderInitialized, ref _userRootFolderSyncLock, () => (UserRootFolder)Kernel.Instance.LibraryManager.GetItem(RootFolderPath));
+ return _rootFolder;
+ }
+ private set
+ {
+ _rootFolder = value;
+
+ if (_rootFolder == null)
+ {
+ _userRootFolderInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the last login date.
+ /// </summary>
+ /// <value>The last login date.</value>
+ public DateTime? LastLoginDate { get; set; }
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ public DateTime? LastActivityDate { get; set; }
+
+ /// <summary>
+ /// The _configuration
+ /// </summary>
+ private UserConfiguration _configuration;
+ /// <summary>
+ /// The _configuration initialized
+ /// </summary>
+ private bool _configurationInitialized;
+ /// <summary>
+ /// The _configuration sync lock
+ /// </summary>
+ private object _configurationSyncLock = new object();
+ /// <summary>
+ /// Gets the user's configuration
+ /// </summary>
+ /// <value>The configuration.</value>
+ [IgnoreDataMember]
+ public UserConfiguration Configuration
+ {
+ get
+ {
+ // Lazy load
+ LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationInitialized, ref _configurationSyncLock, () => XmlSerializer.GetXmlConfiguration<UserConfiguration>(ConfigurationFilePath));
+ return _configuration;
+ }
+ private set
+ {
+ _configuration = value;
+
+ if (value == null)
+ {
+ _configurationInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the last date modified of the configuration
+ /// </summary>
+ /// <value>The configuration date last modified.</value>
+ [IgnoreDataMember]
+ public DateTime ConfigurationDateLastModified
+ {
+ get
+ {
+ // Ensure it's been lazy loaded
+ var config = Configuration;
+
+ return File.GetLastWriteTimeUtc(ConfigurationFilePath);
+ }
+ }
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidateMediaLibrary(IProgress<TaskProgress> progress, CancellationToken cancellationToken)
+ {
+ Logger.LogInfo("Validating media library for {0}", Name);
+ await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await RootFolder.ValidateChildren(progress, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Validates only the collection folders for a User and goes no further
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidateCollectionFolders(IProgress<TaskProgress> progress, CancellationToken cancellationToken)
+ {
+ Logger.LogInfo("Validating collection folders for {0}", Name);
+ await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await RootFolder.ValidateChildren(progress, cancellationToken, recursive: false).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Renames the user.
+ /// </summary>
+ /// <param name="newName">The new name.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ internal Task Rename(string newName)
+ {
+ if (string.IsNullOrEmpty(newName))
+ {
+ throw new ArgumentNullException();
+ }
+
+ // If only the casing is changing, leave the file system alone
+ if (!newName.Equals(Name, StringComparison.OrdinalIgnoreCase))
+ {
+ // Move configuration
+ var newConfigDirectory = GetConfigurationDirectoryPath(newName);
+
+ // Exceptions will be thrown if these paths already exist
+ if (Directory.Exists(newConfigDirectory))
+ {
+ Directory.Delete(newConfigDirectory, true);
+ }
+ Directory.Move(ConfigurationDirectoryPath, newConfigDirectory);
+
+ var customLibraryPath = GetRootFolderPath(Name);
+
+ // Move the root folder path if using a custom library
+ if (Directory.Exists(customLibraryPath))
+ {
+ var newRootFolderPath = GetRootFolderPath(newName);
+ if (Directory.Exists(newRootFolderPath))
+ {
+ Directory.Delete(newRootFolderPath, true);
+ }
+ Directory.Move(customLibraryPath, newRootFolderPath);
+ }
+ }
+
+ Name = newName;
+
+ // Force these to be lazy loaded again
+ _configurationDirectoryPath = null;
+ _rootFolderPath = null;
+ RootFolder = null;
+
+ // Kick off a task to validate the media library
+ Task.Run(() => ValidateMediaLibrary(new Progress<TaskProgress> { }, CancellationToken.None));
+
+ return RefreshMetadata(CancellationToken.None, forceSave: true, forceRefresh: true);
+ }
+
+ /// <summary>
+ /// The _configuration directory path
+ /// </summary>
+ private string _configurationDirectoryPath;
+ /// <summary>
+ /// Gets the path to the user's configuration directory
+ /// </summary>
+ /// <value>The configuration directory path.</value>
+ private string ConfigurationDirectoryPath
+ {
+ get
+ {
+ if (_configurationDirectoryPath == null)
+ {
+ _configurationDirectoryPath = GetConfigurationDirectoryPath(Name);
+
+ if (!Directory.Exists(_configurationDirectoryPath))
+ {
+ Directory.CreateDirectory(_configurationDirectoryPath);
+ }
+ }
+
+ return _configurationDirectoryPath;
+ }
+ }
+
+ /// <summary>
+ /// Gets the configuration directory path.
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <returns>System.String.</returns>
+ private string GetConfigurationDirectoryPath(string username)
+ {
+ var safeFolderName = FileSystem.GetValidFilename(username);
+
+ return System.IO.Path.Combine(Kernel.Instance.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName);
+ }
+
+ /// <summary>
+ /// Gets the path to the user's configuration file
+ /// </summary>
+ /// <value>The configuration file path.</value>
+ private string ConfigurationFilePath
+ {
+ get
+ {
+ return System.IO.Path.Combine(ConfigurationDirectoryPath, "config.xml");
+ }
+ }
+
+ /// <summary>
+ /// Saves the current configuration to the file system
+ /// </summary>
+ public void SaveConfiguration()
+ {
+ XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
+ }
+
+ /// <summary>
+ /// Refresh metadata on us by execution our provider chain
+ /// The item will be persisted if a change is made by a provider, or if it's new or changed.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
+ /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
+ /// <returns>true if a provider reports we changed</returns>
+ public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+ {
+ if (resetResolveArgs)
+ {
+ ResolveArgs = null;
+ }
+
+ var changed = await Kernel.Instance.ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+
+ if (changed || forceSave)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Kernel.Instance.UserManager.UpdateUser(this).ConfigureAwait(false);
+ }
+
+ return changed;
+ }
+
+ /// <summary>
+ /// Updates the configuration.
+ /// </summary>
+ /// <param name="config">The config.</param>
+ /// <exception cref="System.ArgumentNullException">config</exception>
+ public void UpdateConfiguration(UserConfiguration config)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException("config");
+ }
+
+ var customLibraryChanged = config.UseCustomLibrary != Configuration.UseCustomLibrary;
+
+ Configuration = config;
+ SaveConfiguration();
+
+ // Force these to be lazy loaded again
+ if (customLibraryChanged)
+ {
+ _rootFolderPath = null;
+ RootFolder = null;
+
+ if (config.UseCustomLibrary)
+ {
+ CopyDefaultLibraryPathsIfNeeded();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Copies the default library paths if needed.
+ /// </summary>
+ private void CopyDefaultLibraryPathsIfNeeded()
+ {
+ var userPath = RootFolderPath;
+
+ var defaultPath = Kernel.Instance.ApplicationPaths.DefaultUserViewsPath;
+
+ if (userPath.Equals(defaultPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ if (!Directory.EnumerateFileSystemEntries(userPath, "*.lnk", SearchOption.AllDirectories).Any())
+ {
+ FileSystem.CopyAll(defaultPath, userPath);
+ }
+ }
+
+ /// <summary>
+ /// Resets the password by clearing it.
+ /// </summary>
+ /// <returns>Task.</returns>
+ public Task ResetPassword()
+ {
+ return ChangePassword(string.Empty);
+ }
+
+ /// <summary>
+ /// Changes the password.
+ /// </summary>
+ /// <param name="newPassword">The new password.</param>
+ /// <returns>Task.</returns>
+ public Task ChangePassword(string newPassword)
+ {
+ Password = string.IsNullOrEmpty(newPassword) ? string.Empty : newPassword.GetMD5().ToString();
+
+ return Kernel.Instance.UserManager.UpdateUser(this);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
index bb4950046b..3e960d5275 100644
--- a/MediaBrowser.Controller/Entities/UserItemData.cs
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -1,67 +1,116 @@
-using System;
-using System.Runtime.Serialization;
-
-namespace MediaBrowser.Controller.Entities
-{
- public class UserItemData
- {
- private float? _rating;
- /// <summary>
- /// Gets or sets the users 0-10 rating
- /// </summary>
- public float? Rating
- {
- get
- {
- return _rating;
- }
- set
- {
- if (value.HasValue)
- {
- if (value.Value < 0 || value.Value > 10)
- {
- throw new InvalidOperationException("A 0-10 rating is required for UserItemData.");
- }
- }
-
- _rating = value;
- }
- }
-
- public long PlaybackPositionTicks { get; set; }
-
- public int PlayCount { get; set; }
-
- public bool IsFavorite { get; set; }
-
- /// <summary>
- /// This is an interpreted property to indicate likes or dislikes
- /// This should never be serialized.
- /// </summary>
- [IgnoreDataMember]
- public bool? Likes
- {
- get
- {
- if (Rating != null)
- {
- return Rating >= 6.5;
- }
-
- return null;
- }
- set
- {
- if (value.HasValue)
- {
- Rating = value.Value ? 10 : 1;
- }
- else
- {
- Rating = null;
- }
- }
- }
- }
-}
+using ProtoBuf;
+using System;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class UserItemData
+ /// </summary>
+ [ProtoContract]
+ public class UserItemData
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ [ProtoMember(1)]
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// The _rating
+ /// </summary>
+ private float? _rating;
+ /// <summary>
+ /// Gets or sets the users 0-10 rating
+ /// </summary>
+ /// <value>The rating.</value>
+ /// <exception cref="System.ArgumentOutOfRangeException">A 0-10 rating is required for UserItemData.</exception>
+ /// <exception cref="System.InvalidOperationException">A 0-10 rating is required for UserItemData.</exception>
+ [ProtoMember(2)]
+ public float? Rating
+ {
+ get
+ {
+ return _rating;
+ }
+ set
+ {
+ if (value.HasValue)
+ {
+ if (value.Value < 0 || value.Value > 10)
+ {
+ throw new ArgumentOutOfRangeException("A 0-10 rating is required for UserItemData.");
+ }
+ }
+
+ _rating = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the playback position ticks.
+ /// </summary>
+ /// <value>The playback position ticks.</value>
+ [ProtoMember(3)]
+ public long PlaybackPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play count.
+ /// </summary>
+ /// <value>The play count.</value>
+ [ProtoMember(4)]
+ public int PlayCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+ [ProtoMember(5)]
+ public bool IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last played date.
+ /// </summary>
+ /// <value>The last played date.</value>
+ [ProtoMember(6)]
+ public DateTime? LastPlayedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserItemData" /> is played.
+ /// </summary>
+ /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+ [ProtoMember(7)]
+ public bool Played { get; set; }
+
+ /// <summary>
+ /// This is an interpreted property to indicate likes or dislikes
+ /// This should never be serialized.
+ /// </summary>
+ /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool? Likes
+ {
+ get
+ {
+ if (Rating != null)
+ {
+ return Rating >= 6.5;
+ }
+
+ return null;
+ }
+ set
+ {
+ if (value.HasValue)
+ {
+ Rating = value.Value ? 10 : 1;
+ }
+ else
+ {
+ Rating = null;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
new file mode 100644
index 0000000000..4ffd3468d9
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Special class used for User Roots. Children contain actual ones defined for this user
+ /// PLUS the virtual folders from the physical root (added by plug-ins).
+ /// </summary>
+ public class UserRootFolder : Folder
+ {
+ /// <summary>
+ /// Get the children of this folder from the actual file system
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected override IEnumerable<BaseItem> GetNonCachedChildren()
+ {
+ return base.GetNonCachedChildren().Concat(Kernel.Instance.RootFolder.VirtualChildren);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 8dd82fab99..b3ed21b19a 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -1,20 +1,109 @@
-using MediaBrowser.Model.Entities;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Video : BaseItem
- {
- public VideoType VideoType { get; set; }
-
- public List<SubtitleStream> Subtitles { get; set; }
- public List<AudioStream> AudioStreams { get; set; }
-
- public int Height { get; set; }
- public int Width { get; set; }
- public string ScanType { get; set; }
- public float FrameRate { get; set; }
- public int BitRate { get; set; }
- public string Codec { get; set; }
- }
-}
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Video
+ /// </summary>
+ public class Video : BaseItem, IHasMediaStreams
+ {
+ /// <summary>
+ /// Gets or sets the type of the video.
+ /// </summary>
+ /// <value>The type of the video.</value>
+ public VideoType VideoType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the iso.
+ /// </summary>
+ /// <value>The type of the iso.</value>
+ public IsoType? IsoType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the format of the video.
+ /// </summary>
+ /// <value>The format of the video.</value>
+ public VideoFormat VideoFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the media streams.
+ /// </summary>
+ /// <value>The media streams.</value>
+ public List<MediaStream> MediaStreams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapters.
+ /// </summary>
+ /// <value>The chapters.</value>
+ public List<ChapterInfo> Chapters { get; set; }
+
+ /// <summary>
+ /// If the video is a folder-rip, this will hold the file list for the largest playlist
+ /// </summary>
+ public List<string> PlayableStreamFileNames { get; set; }
+
+ /// <summary>
+ /// Gets the playable stream files.
+ /// </summary>
+ /// <returns>List{System.String}.</returns>
+ public List<string> GetPlayableStreamFiles()
+ {
+ return GetPlayableStreamFiles(Path);
+ }
+
+ /// <summary>
+ /// Gets the playable stream files.
+ /// </summary>
+ /// <param name="rootPath">The root path.</param>
+ /// <returns>List{System.String}.</returns>
+ public List<string> GetPlayableStreamFiles(string rootPath)
+ {
+ if (PlayableStreamFileNames == null)
+ {
+ return null;
+ }
+
+ var allFiles = Directory.EnumerateFiles(rootPath, "*", SearchOption.AllDirectories).ToList();
+
+ return PlayableStreamFileNames.Select(name => allFiles.FirstOrDefault(f => string.Equals(System.IO.Path.GetFileName(f), name, System.StringComparison.OrdinalIgnoreCase)))
+ .Where(f => !string.IsNullOrEmpty(f))
+ .ToList();
+ }
+
+ /// <summary>
+ /// The default video stream for this video. Use this to determine media info for this item.
+ /// </summary>
+ /// <value>The default video stream.</value>
+ [IgnoreDataMember]
+ public MediaStream DefaultVideoStream
+ {
+ get { return MediaStreams != null ? MediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video) : null; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [is3 D].
+ /// </summary>
+ /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool Is3D
+ {
+ get { return VideoFormat > 0; }
+ }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ public override string MediaType
+ {
+ get
+ {
+ return Model.Entities.MediaType.Video;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index d0b29de56c..9150057c82 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -1,7 +1,10 @@
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Year : BaseEntity
- {
- }
-}
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Year
+ /// </summary>
+ public class Year : BaseItem
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/FFProbe.cs b/MediaBrowser.Controller/FFMpeg/FFProbe.cs
deleted file mode 100644
index f16f0142d6..0000000000
--- a/MediaBrowser.Controller/FFMpeg/FFProbe.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Common.Serialization;
-using MediaBrowser.Controller.Entities;
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.FFMpeg
-{
- /// <summary>
- /// Runs FFProbe against a media file and returns metadata.
- /// </summary>
- public static class FFProbe
- {
- /// <summary>
- /// Runs FFProbe against an Audio file, caches the result and returns the output
- /// </summary>
- public static FFProbeResult Run(BaseItem item, string cacheDirectory)
- {
- string cachePath = GetFfProbeCachePath(item, cacheDirectory);
-
- // Use try catch to avoid having to use File.Exists
- try
- {
- return GetCachedResult(cachePath);
- }
- catch (FileNotFoundException)
- {
- }
- catch (Exception ex)
- {
- Logger.LogException(ex);
- }
-
- FFProbeResult result = Run(item.Path);
-
- if (result != null)
- {
- // Fire and forget
- CacheResult(result, cachePath);
- }
-
- return result;
- }
-
- /// <summary>
- /// Gets the cached result of an FFProbe operation
- /// </summary>
- private static FFProbeResult GetCachedResult(string path)
- {
- return ProtobufSerializer.DeserializeFromFile<FFProbeResult>(path);
- }
-
- /// <summary>
- /// Caches the result of an FFProbe operation
- /// </summary>
- private static async void CacheResult(FFProbeResult result, string outputCachePath)
- {
- await Task.Run(() =>
- {
- try
- {
- ProtobufSerializer.SerializeToFile(result, outputCachePath);
- }
- catch (Exception ex)
- {
- Logger.LogException(ex);
- }
- }).ConfigureAwait(false);
- }
-
- private static FFProbeResult Run(string input)
- {
- var startInfo = new ProcessStartInfo { };
-
- startInfo.CreateNoWindow = true;
-
- startInfo.UseShellExecute = false;
-
- // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
- startInfo.RedirectStandardOutput = true;
- startInfo.RedirectStandardError = true;
-
- startInfo.FileName = Kernel.Instance.ApplicationPaths.FFProbePath;
- startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
- startInfo.Arguments = string.Format("\"{0}\" -v quiet -print_format json -show_streams -show_format", input);
-
- //Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
-
- var process = new Process { };
- process.StartInfo = startInfo;
-
- process.EnableRaisingEvents = true;
-
- process.Exited += ProcessExited;
-
- try
- {
- process.Start();
-
- // MUST read both stdout and stderr asynchronously or a deadlock may occurr
- // If we ever decide to disable the ffmpeg log then you must uncomment the below line.
- process.BeginErrorReadLine();
-
- return JsonSerializer.DeserializeFromStream<FFProbeResult>(process.StandardOutput.BaseStream);
- }
- catch (Exception ex)
- {
- Logger.LogException(ex);
-
- // Hate having to do this
- try
- {
- process.Kill();
- }
- catch
- {
- }
-
- return null;
- }
- }
-
- static void ProcessExited(object sender, EventArgs e)
- {
- (sender as Process).Dispose();
- }
-
- private static string GetFfProbeCachePath(BaseItem item, string cacheDirectory)
- {
- string outputDirectory = Path.Combine(cacheDirectory, item.Id.ToString().Substring(0, 1));
-
- return Path.Combine(outputDirectory, item.Id + "-" + item.DateModified.Ticks + ".pb");
- }
- }
-}
diff --git a/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs b/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs
deleted file mode 100644
index db7c9dd3c5..0000000000
--- a/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System.Collections.Generic;
-using ProtoBuf;
-
-namespace MediaBrowser.Controller.FFMpeg
-{
- /// <summary>
- /// Provides a class that we can use to deserialize the ffprobe json output
- /// Sample output:
- /// http://stackoverflow.com/questions/7708373/get-ffmpeg-information-in-friendly-way
- /// </summary>
- [ProtoContract]
- public class FFProbeResult
- {
- [ProtoMember(1)]
- public MediaStream[] streams { get; set; }
-
- [ProtoMember(2)]
- public MediaFormat format { get; set; }
- }
-
- /// <summary>
- /// Represents a stream within the output
- /// A number of properties are commented out to improve deserialization performance
- /// Enable them as needed.
- /// </summary>
- [ProtoContract]
- public class MediaStream
- {
- [ProtoMember(1)]
- public int index { get; set; }
-
- [ProtoMember(2)]
- public string profile { get; set; }
-
- [ProtoMember(3)]
- public string codec_name { get; set; }
-
- [ProtoMember(4)]
- public string codec_long_name { get; set; }
-
- [ProtoMember(5)]
- public string codec_type { get; set; }
-
- //public string codec_time_base { get; set; }
- //public string codec_tag { get; set; }
- //public string codec_tag_string { get; set; }
- //public string sample_fmt { get; set; }
-
- [ProtoMember(6)]
- public string sample_rate { get; set; }
-
- [ProtoMember(7)]
- public int channels { get; set; }
-
- //public int bits_per_sample { get; set; }
- //public string r_frame_rate { get; set; }
-
- [ProtoMember(8)]
- public string avg_frame_rate { get; set; }
-
- //public string time_base { get; set; }
- //public string start_time { get; set; }
-
- [ProtoMember(9)]
- public string duration { get; set; }
-
- [ProtoMember(10)]
- public string bit_rate { get; set; }
-
- [ProtoMember(11)]
- public int width { get; set; }
-
- [ProtoMember(12)]
- public int height { get; set; }
-
- //public int has_b_frames { get; set; }
- //public string sample_aspect_ratio { get; set; }
-
- [ProtoMember(13)]
- public string display_aspect_ratio { get; set; }
-
- //public string pix_fmt { get; set; }
- //public int level { get; set; }
-
- [ProtoMember(14)]
- public Dictionary<string, string> tags { get; set; }
- }
-
- [ProtoContract]
- public class MediaFormat
- {
- [ProtoMember(1)]
- public string filename { get; set; }
-
- [ProtoMember(2)]
- public int nb_streams { get; set; }
-
- [ProtoMember(3)]
- public string format_name { get; set; }
-
- [ProtoMember(4)]
- public string format_long_name { get; set; }
-
- [ProtoMember(5)]
- public string start_time { get; set; }
-
- [ProtoMember(6)]
- public string duration { get; set; }
-
- [ProtoMember(7)]
- public string size { get; set; }
-
- [ProtoMember(8)]
- public string bit_rate { get; set; }
-
- [ProtoMember(9)]
- public Dictionary<string, string> tags { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id b/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id
deleted file mode 100644
index 73a37bd556..0000000000
--- a/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id
+++ /dev/null
@@ -1 +0,0 @@
-84ac1c51e84cfbfb20e7b96c9f1a4442a8cfadf2 \ No newline at end of file
diff --git a/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id b/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id
deleted file mode 100644
index 682ead74d1..0000000000
--- a/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id
+++ /dev/null
@@ -1 +0,0 @@
-331e241e29f1b015e303b301c17c37883e39f39d \ No newline at end of file
diff --git a/MediaBrowser.Controller/FFMpeg/readme.txt b/MediaBrowser.Controller/FFMpeg/readme.txt
deleted file mode 100644
index cdb039bdca..0000000000
--- a/MediaBrowser.Controller/FFMpeg/readme.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-This is the 32-bit static build of ffmpeg, located at:
-
-http://ffmpeg.zeranoe.com/builds/ \ No newline at end of file
diff --git a/MediaBrowser.Controller/IO/DirectoryWatchers.cs b/MediaBrowser.Controller/IO/DirectoryWatchers.cs
index eb1358e16f..29353bc251 100644
--- a/MediaBrowser.Controller/IO/DirectoryWatchers.cs
+++ b/MediaBrowser.Controller/IO/DirectoryWatchers.cs
@@ -1,172 +1,525 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Common.Extensions;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.IO
-{
- public class DirectoryWatchers
- {
- private readonly List<FileSystemWatcher> FileSystemWatchers = new List<FileSystemWatcher>();
- private Timer updateTimer;
- private List<string> affectedPaths = new List<string>();
-
- private const int TimerDelayInSeconds = 30;
-
- public void Start()
- {
- var pathsToWatch = new List<string>();
-
- var rootFolder = Kernel.Instance.RootFolder;
-
- pathsToWatch.Add(rootFolder.Path);
-
- foreach (Folder folder in rootFolder.Children.OfType<Folder>())
- {
- foreach (string path in folder.PhysicalLocations)
- {
- if (Path.IsPathRooted(path) && !pathsToWatch.ContainsParentFolder(path))
- {
- pathsToWatch.Add(path);
- }
- }
- }
-
- foreach (string path in pathsToWatch)
- {
- Logger.LogInfo("Watching directory " + path + " for changes.");
-
- var watcher = new FileSystemWatcher(path, "*") { };
- watcher.IncludeSubdirectories = true;
-
- //watcher.Changed += watcher_Changed;
-
- // All the others seem to trigger change events on the parent, so let's keep it simple for now.
- // Actually, we really need to only watch created, deleted and renamed as changed fires too much -ebr
- watcher.Created += watcher_Changed;
- watcher.Deleted += watcher_Changed;
- watcher.Renamed += watcher_Changed;
-
- watcher.EnableRaisingEvents = true;
- FileSystemWatchers.Add(watcher);
- }
- }
-
- void watcher_Changed(object sender, FileSystemEventArgs e)
- {
- Logger.LogDebugInfo("****** Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath);
- lock (affectedPaths)
- {
- //Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path
- var affectedPath = Path.GetDirectoryName(e.FullPath);
-
- if (e.ChangeType == WatcherChangeTypes.Renamed)
- {
- var renamedArgs = e as RenamedEventArgs;
- if (affectedPaths.Contains(renamedArgs.OldFullPath))
- {
- Logger.LogDebugInfo("****** Removing " + renamedArgs.OldFullPath + " from affected paths.");
- affectedPaths.Remove(renamedArgs.OldFullPath);
- }
- }
-
- //If anything underneath this path was already marked as affected - remove it as it will now get captured by this one
- affectedPaths.RemoveAll(p => p.StartsWith(e.FullPath, StringComparison.OrdinalIgnoreCase));
-
- if (!affectedPaths.ContainsParentFolder(affectedPath))
- {
- Logger.LogDebugInfo("****** Adding " + affectedPath + " to affected paths.");
- affectedPaths.Add(affectedPath);
- }
- }
-
- if (updateTimer == null)
- {
- updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1));
- }
- else
- {
- updateTimer.Change(TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1));
- }
- }
-
- private async void TimerStopped(object stateInfo)
- {
- updateTimer.Dispose();
- updateTimer = null;
- List<string> paths;
- lock (affectedPaths)
- {
- paths = affectedPaths;
- affectedPaths = new List<string>();
- }
-
- await ProcessPathChanges(paths).ConfigureAwait(false);
- }
-
- private Task ProcessPathChanges(IEnumerable<string> paths)
- {
- var itemsToRefresh = new List<BaseItem>();
-
- foreach (BaseItem item in paths.Select(p => GetAffectedBaseItem(p)))
- {
- if (item != null && !itemsToRefresh.Contains(item))
- {
- itemsToRefresh.Add(item);
- }
- }
-
- if (itemsToRefresh.Any(i =>
- {
- var folder = i as Folder;
-
- return folder != null && folder.IsRoot;
- }))
- {
- return Kernel.Instance.ReloadRoot();
- }
-
- foreach (var p in paths) Logger.LogDebugInfo("********* "+ p + " reports change.");
- foreach (var i in itemsToRefresh) Logger.LogDebugInfo("********* "+i.Name + " ("+ i.Path + ") will be refreshed.");
- return Task.WhenAll(itemsToRefresh.Select(i => i.ChangedExternally()));
- }
-
- private BaseItem GetAffectedBaseItem(string path)
- {
- BaseItem item = null;
-
- while (item == null && !string.IsNullOrEmpty(path))
- {
- item = Kernel.Instance.RootFolder.FindByPath(path);
-
- path = Path.GetDirectoryName(path);
- }
-
- return item;
- }
-
- public void Stop()
- {
- foreach (FileSystemWatcher watcher in FileSystemWatchers)
- {
- watcher.Changed -= watcher_Changed;
- watcher.EnableRaisingEvents = false;
- watcher.Dispose();
- }
-
- if (updateTimer != null)
- {
- updateTimer.Dispose();
- updateTimer = null;
- }
-
- FileSystemWatchers.Clear();
- affectedPaths.Clear();
- }
- }
-}
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.ScheduledTasks;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.IO
+{
+ /// <summary>
+ /// Class DirectoryWatchers
+ /// </summary>
+ public class DirectoryWatchers : IDisposable
+ {
+ /// <summary>
+ /// The file system watchers
+ /// </summary>
+ private ConcurrentBag<FileSystemWatcher> FileSystemWatchers = new ConcurrentBag<FileSystemWatcher>();
+ /// <summary>
+ /// The update timer
+ /// </summary>
+ private Timer updateTimer;
+ /// <summary>
+ /// The affected paths
+ /// </summary>
+ private readonly ConcurrentDictionary<string, string> affectedPaths = new ConcurrentDictionary<string, string>();
+
+ /// <summary>
+ /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications.
+ /// </summary>
+ private readonly ConcurrentDictionary<string,string> TempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// The timer lock
+ /// </summary>
+ private readonly object timerLock = new object();
+
+ /// <summary>
+ /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ public void TemporarilyIgnore(string path)
+ {
+ TempIgnoredPaths[path] = path;
+ }
+
+ /// <summary>
+ /// Removes the temp ignore.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ public void RemoveTempIgnore(string path)
+ {
+ string val;
+ TempIgnoredPaths.TryRemove(path, out val);
+ }
+
+ /// <summary>
+ /// Gets or sets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ private ILogger Logger { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DirectoryWatchers" /> class.
+ /// </summary>
+ public DirectoryWatchers()
+ {
+ Logger = LogManager.GetLogger(GetType().Name);
+ }
+
+ /// <summary>
+ /// Starts this instance.
+ /// </summary>
+ internal void Start()
+ {
+ Kernel.Instance.LibraryManager.LibraryChanged += Instance_LibraryChanged;
+
+ var pathsToWatch = new List<string> { Kernel.Instance.RootFolder.Path };
+
+ var paths = Kernel.Instance.RootFolder.Children.OfType<Folder>()
+ .SelectMany(f =>
+ {
+ try
+ {
+ // Accessing ResolveArgs could involve file system access
+ return f.ResolveArgs.PhysicalLocations;
+ }
+ catch (IOException)
+ {
+ return new string[] {};
+ }
+
+ })
+ .Where(Path.IsPathRooted);
+
+ foreach (var path in paths)
+ {
+ if (!ContainsParentFolder(pathsToWatch, path))
+ {
+ pathsToWatch.Add(path);
+ }
+ }
+
+ foreach (var path in pathsToWatch)
+ {
+ StartWatchingPath(path);
+ }
+ }
+
+ /// <summary>
+ /// Examine a list of strings assumed to be file paths to see if it contains a parent of
+ /// the provided path.
+ /// </summary>
+ /// <param name="lst">The LST.</param>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">path</exception>
+ private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ path = path.TrimEnd(Path.DirectorySeparatorChar);
+
+ return lst.Any(str =>
+ {
+ //this should be a little quicker than examining each actual parent folder...
+ var compare = str.TrimEnd(Path.DirectorySeparatorChar);
+
+ return (path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar));
+ });
+ }
+
+ /// <summary>
+ /// Starts the watching path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ private void StartWatchingPath(string path)
+ {
+ // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel
+ Task.Run(() =>
+ {
+ var newWatcher = new FileSystemWatcher(path, "*") { IncludeSubdirectories = true, InternalBufferSize = 32767 };
+
+ newWatcher.Created += watcher_Changed;
+ newWatcher.Deleted += watcher_Changed;
+ newWatcher.Renamed += watcher_Changed;
+ newWatcher.Changed += watcher_Changed;
+
+ newWatcher.Error += watcher_Error;
+
+ try
+ {
+ newWatcher.EnableRaisingEvents = true;
+ FileSystemWatchers.Add(newWatcher);
+
+ Logger.Info("Watching directory " + path);
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error watching path: {0}", ex, path);
+ }
+ catch (PlatformNotSupportedException ex)
+ {
+ Logger.ErrorException("Error watching path: {0}", ex, path);
+ }
+ });
+ }
+
+ /// <summary>
+ /// Stops the watching path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ private void StopWatchingPath(string path)
+ {
+ var watcher = FileSystemWatchers.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+
+ if (watcher != null)
+ {
+ DisposeWatcher(watcher);
+ }
+ }
+
+ /// <summary>
+ /// Disposes the watcher.
+ /// </summary>
+ /// <param name="watcher">The watcher.</param>
+ private void DisposeWatcher(FileSystemWatcher watcher)
+ {
+ Logger.Info("Stopping directory watching for path {0}", watcher.Path);
+
+ watcher.EnableRaisingEvents = false;
+ watcher.Dispose();
+
+ var watchers = FileSystemWatchers.ToList();
+
+ watchers.Remove(watcher);
+
+ FileSystemWatchers = new ConcurrentBag<FileSystemWatcher>(watchers);
+ }
+
+ /// <summary>
+ /// Handles the LibraryChanged event of the Kernel
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="Library.ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ void Instance_LibraryChanged(object sender, ChildrenChangedEventArgs e)
+ {
+ if (e.Folder is AggregateFolder && e.HasAddOrRemoveChange)
+ {
+ if (e.ItemsRemoved != null)
+ {
+ foreach (var item in e.ItemsRemoved.OfType<Folder>())
+ {
+ StopWatchingPath(item.Path);
+ }
+ }
+ if (e.ItemsAdded != null)
+ {
+ foreach (var item in e.ItemsAdded.OfType<Folder>())
+ {
+ StartWatchingPath(item.Path);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Handles the Error event of the watcher control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
+ async void watcher_Error(object sender, ErrorEventArgs e)
+ {
+ var ex = e.GetException();
+ var dw = (FileSystemWatcher) sender;
+
+ Logger.ErrorException("Error in Directory watcher for: "+dw.Path, ex);
+
+ if (ex.Message.Contains("network name is no longer available"))
+ {
+ //Network either dropped or, we are coming out of sleep and it hasn't reconnected yet - wait and retry
+ Logger.Warn("Network connection lost - will retry...");
+ var retries = 0;
+ var success = false;
+ while (!success && retries < 10)
+ {
+ await Task.Delay(500).ConfigureAwait(false);
+
+ try
+ {
+ dw.EnableRaisingEvents = false;
+ dw.EnableRaisingEvents = true;
+ success = true;
+ }
+ catch (IOException)
+ {
+ Logger.Warn("Network still unavailable...");
+ retries++;
+ }
+ }
+ if (!success)
+ {
+ Logger.Warn("Unable to access network. Giving up.");
+ DisposeWatcher(dw);
+ }
+
+ }
+ else
+ {
+ if (!ex.Message.Contains("BIOS command limit"))
+ {
+ Logger.Info("Attempting to re-start watcher.");
+
+ dw.EnableRaisingEvents = false;
+ dw.EnableRaisingEvents = true;
+ }
+
+ }
+ }
+
+ /// <summary>
+ /// Handles the Changed event of the watcher control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
+ void watcher_Changed(object sender, FileSystemEventArgs e)
+ {
+ if (e.ChangeType == WatcherChangeTypes.Created && e.Name == "New folder")
+ {
+ return;
+ }
+ if (TempIgnoredPaths.ContainsKey(e.FullPath))
+ {
+ Logger.Info("Watcher requested to ignore change to " + e.FullPath);
+ return;
+ }
+
+ Logger.Info("Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath);
+
+ //Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path
+ var affectedPath = e.FullPath;
+
+ affectedPaths.AddOrUpdate(affectedPath, affectedPath, (key, oldValue) => affectedPath);
+
+ lock (timerLock)
+ {
+ if (updateTimer == null)
+ {
+ updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
+ }
+ else
+ {
+ updateTimer.Change(TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Timers the stopped.
+ /// </summary>
+ /// <param name="stateInfo">The state info.</param>
+ private async void TimerStopped(object stateInfo)
+ {
+ lock (timerLock)
+ {
+ // Extend the timer as long as any of the paths are still being written to.
+ if (affectedPaths.Any(p => IsFileLocked(p.Key)))
+ {
+ Logger.Info("Timer extended.");
+ updateTimer.Change(TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
+ return;
+ }
+
+ Logger.Info("Timer stopped.");
+
+ updateTimer.Dispose();
+ updateTimer = null;
+ }
+
+ var paths = affectedPaths.Keys.ToList();
+ affectedPaths.Clear();
+
+ await ProcessPathChanges(paths).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Try and determine if a file is locked
+ /// This is not perfect, and is subject to race conditions, so I'd rather not make this a re-usable library method.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is file locked] [the specified path]; otherwise, <c>false</c>.</returns>
+ private bool IsFileLocked(string path)
+ {
+ try
+ {
+ var data = FileSystem.GetFileData(path);
+
+ if (!data.HasValue || data.Value.IsDirectory)
+ {
+ return false;
+ }
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+
+ FileStream stream = null;
+
+ try
+ {
+ stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
+ }
+ catch
+ {
+ //the file is unavailable because it is:
+ //still being written to
+ //or being processed by another thread
+ //or does not exist (has already been processed)
+ return true;
+ }
+ finally
+ {
+ if (stream != null)
+ stream.Close();
+ }
+
+ //file is not locked
+ return false;
+ }
+
+ /// <summary>
+ /// Processes the path changes.
+ /// </summary>
+ /// <param name="paths">The paths.</param>
+ /// <returns>Task.</returns>
+ private async Task ProcessPathChanges(List<string> paths)
+ {
+ var itemsToRefresh = paths.Select(Path.GetDirectoryName)
+ .Select(GetAffectedBaseItem)
+ .Where(item => item != null)
+ .Distinct()
+ .ToList();
+
+ foreach (var p in paths) Logger.Info(p + " reports change.");
+
+ // If the root folder changed, run the library task so the user can see it
+ if (itemsToRefresh.Any(i => i is AggregateFolder))
+ {
+ Kernel.Instance.TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+ return;
+ }
+
+ await Task.WhenAll(itemsToRefresh.Select(i => Task.Run(async () =>
+ {
+ Logger.Info(i.Name + " (" + i.Path + ") will be refreshed.");
+
+ try
+ {
+ await i.ChangedExternally().ConfigureAwait(false);
+ }
+ catch (IOException ex)
+ {
+ // For now swallow and log.
+ // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
+ // Should we remove it from it's parent?
+ Logger.ErrorException("Error refreshing {0}", ex, i.Name);
+ }
+
+ }))).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets the affected base item.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BaseItem.</returns>
+ private BaseItem GetAffectedBaseItem(string path)
+ {
+ BaseItem item = null;
+
+ while (item == null && !string.IsNullOrEmpty(path))
+ {
+ item = Kernel.Instance.RootFolder.FindByPath(path);
+
+ path = Path.GetDirectoryName(path);
+ }
+
+ if (item != null)
+ {
+ // If the item has been deleted find the first valid parent that still exists
+ while (!Directory.Exists(item.Path) && !File.Exists(item.Path))
+ {
+ item = item.Parent;
+
+ if (item == null)
+ {
+ break;
+ }
+ }
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Stops this instance.
+ /// </summary>
+ private void Stop()
+ {
+ Kernel.Instance.LibraryManager.LibraryChanged -= Instance_LibraryChanged;
+
+ FileSystemWatcher watcher;
+
+ while (FileSystemWatchers.TryTake(out watcher))
+ {
+ watcher.Changed -= watcher_Changed;
+ watcher.EnableRaisingEvents = false;
+ watcher.Dispose();
+ }
+
+ lock (timerLock)
+ {
+ if (updateTimer != null)
+ {
+ updateTimer.Dispose();
+ updateTimer = null;
+ }
+ }
+
+ affectedPaths.Clear();
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ Stop();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index 4ae2ee72f3..74d0a7e6fe 100644
--- a/MediaBrowser.Controller/IO/FileData.cs
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -1,251 +1,130 @@
-using MediaBrowser.Common.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Runtime.InteropServices;
-
-namespace MediaBrowser.Controller.IO
-{
- /// <summary>
- /// Provides low level File access that is much faster than the File/Directory api's
- /// </summary>
- public static class FileData
- {
- public const int MAX_PATH = 260;
- public const int MAX_ALTERNATE = 14;
- public static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
-
- /// <summary>
- /// Gets information about a path
- /// </summary>
- public static WIN32_FIND_DATA GetFileData(string path)
- {
- WIN32_FIND_DATA data;
- IntPtr handle = FindFirstFile(path, out data);
- bool getFilename = false;
-
- if (handle == INVALID_HANDLE_VALUE && !Path.HasExtension(path))
- {
- if (!path.EndsWith("*"))
- {
- Logger.LogInfo("Handle came back invalid for {0}. Since this is a directory we'll try appending \\*.", path);
-
- FindClose(handle);
-
- handle = FindFirstFile(Path.Combine(path, "*"), out data);
-
- getFilename = true;
- }
- }
-
- if (handle == IntPtr.Zero)
- {
- throw new IOException("FindFirstFile failed");
- }
-
- if (getFilename)
- {
- data.cFileName = Path.GetFileName(path);
- }
-
- FindClose(handle);
-
- data.Path = path;
- return data;
- }
-
- /// <summary>
- /// Gets all file system entries within a foler
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern)
- {
- return GetFileSystemEntries(path, searchPattern, true, true);
- }
-
- /// <summary>
- /// Gets all files within a folder
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetFiles(string path, string searchPattern)
- {
- return GetFileSystemEntries(path, searchPattern, true, false);
- }
-
- /// <summary>
- /// Gets all sub-directories within a folder
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetDirectories(string path, string searchPattern)
- {
- return GetFileSystemEntries(path, searchPattern, false, true);
- }
-
- /// <summary>
- /// Gets all file system entries within a foler
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern, bool includeFiles, bool includeDirectories)
- {
- string lpFileName = Path.Combine(path, searchPattern);
-
- WIN32_FIND_DATA lpFindFileData;
- var handle = FindFirstFile(lpFileName, out lpFindFileData);
-
- if (handle == IntPtr.Zero)
- {
- int hr = Marshal.GetLastWin32Error();
- if (hr != 2 && hr != 0x12)
- {
- throw new IOException("GetFileSystemEntries failed");
- }
- yield break;
- }
-
- if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
- {
- yield return lpFindFileData;
- }
-
- while (FindNextFile(handle, out lpFindFileData) != IntPtr.Zero)
- {
- if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
- {
- lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
- yield return lpFindFileData;
- }
- }
-
- FindClose(handle);
- }
-
- private static bool IncludeInOutput(string cFileName, FileAttributes attributes, bool includeFiles, bool includeDirectories)
- {
- if (cFileName.Equals(".", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
- if (cFileName.Equals("..", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (!includeFiles && !attributes.HasFlag(FileAttributes.Directory))
- {
- return false;
- }
-
- if (!includeDirectories && attributes.HasFlag(FileAttributes.Directory))
- {
- return false;
- }
-
- return true;
- }
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- private static extern IntPtr FindFirstFile(string fileName, out WIN32_FIND_DATA data);
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- private static extern IntPtr FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA data);
-
- [DllImport("kernel32")]
- private static extern bool FindClose(IntPtr hFindFile);
-
- private const char SpaceChar = ' ';
- private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
-
- /// <summary>
- /// Takes a filename and removes invalid characters
- /// </summary>
- public static string GetValidFilename(string filename)
- {
- foreach (char c in InvalidFileNameChars)
- {
- filename = filename.Replace(c, SpaceChar);
- }
-
- return filename;
- }
- }
-
- [StructLayout(LayoutKind.Sequential)]
- public struct FILETIME
- {
- public uint dwLowDateTime;
- public uint dwHighDateTime;
- }
-
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
- public struct WIN32_FIND_DATA
- {
- public FileAttributes dwFileAttributes;
- public FILETIME ftCreationTime;
- public FILETIME ftLastAccessTime;
- public FILETIME ftLastWriteTime;
- public int nFileSizeHigh;
- public int nFileSizeLow;
- public int dwReserved0;
- public int dwReserved1;
-
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_PATH)]
- public string cFileName;
-
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_ALTERNATE)]
- public string cAlternate;
-
- public bool IsHidden
- {
- get
- {
- return dwFileAttributes.HasFlag(FileAttributes.Hidden);
- }
- }
-
- public bool IsSystemFile
- {
- get
- {
- return dwFileAttributes.HasFlag(FileAttributes.System);
- }
- }
-
- public bool IsDirectory
- {
- get
- {
- return dwFileAttributes.HasFlag(FileAttributes.Directory);
- }
- }
-
- public DateTime CreationTimeUtc
- {
- get
- {
- return ParseFileTime(ftCreationTime);
- }
- }
-
- public DateTime LastAccessTimeUtc
- {
- get
- {
- return ParseFileTime(ftLastAccessTime);
- }
- }
-
- public DateTime LastWriteTimeUtc
- {
- get
- {
- return ParseFileTime(ftLastWriteTime);
- }
- }
-
- private DateTime ParseFileTime(FILETIME filetime)
- {
- long highBits = filetime.dwHighDateTime;
- highBits = highBits << 32;
- return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime);
- }
-
- public string Path { get; set; }
- }
-
-}
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace MediaBrowser.Controller.IO
+{
+ /// <summary>
+ /// Provides low level File access that is much faster than the File/Directory api's
+ /// </summary>
+ public static class FileData
+ {
+ /// <summary>
+ /// Gets all file system entries within a foler
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="searchPattern">The search pattern.</param>
+ /// <param name="includeFiles">if set to <c>true</c> [include files].</param>
+ /// <param name="includeDirectories">if set to <c>true</c> [include directories].</param>
+ /// <param name="flattenFolderDepth">The flatten folder depth.</param>
+ /// <param name="args">The args.</param>
+ /// <returns>Dictionary{System.StringWIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ /// <exception cref="System.IO.IOException">GetFileSystemEntries failed</exception>
+ public static Dictionary<string, WIN32_FIND_DATA> GetFilteredFileSystemEntries(string path, string searchPattern = "*", bool includeFiles = true, bool includeDirectories = true, int flattenFolderDepth = 0, ItemResolveArgs args = null)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ var lpFileName = Path.Combine(path, searchPattern);
+
+ WIN32_FIND_DATA lpFindFileData;
+ var handle = NativeMethods.FindFirstFileEx(lpFileName, FINDEX_INFO_LEVELS.FindExInfoBasic, out lpFindFileData,
+ FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, FindFirstFileExFlags.FIND_FIRST_EX_LARGE_FETCH);
+
+ if (handle == IntPtr.Zero)
+ {
+ int hr = Marshal.GetLastWin32Error();
+ if (hr != 2 && hr != 0x12)
+ {
+ throw new IOException("GetFileSystemEntries failed");
+ }
+ return new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ var dict = new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
+
+ if (FileSystem.IncludeInFindFileOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
+ {
+ if (!string.IsNullOrEmpty(lpFindFileData.cFileName))
+ {
+ lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
+
+ dict[lpFindFileData.Path] = lpFindFileData;
+ }
+ }
+
+ while (NativeMethods.FindNextFile(handle, out lpFindFileData) != IntPtr.Zero)
+ {
+ // This is the one circumstance where we can completely disregard a file
+ if (lpFindFileData.IsSystemFile)
+ {
+ continue;
+ }
+
+ // Filter out invalid entries
+ if (lpFindFileData.cFileName.Equals(".", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ if (lpFindFileData.cFileName.Equals("..", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
+
+ if (FileSystem.IsShortcut(lpFindFileData.Path))
+ {
+ var newPath = FileSystem.ResolveShortcut(lpFindFileData.Path);
+ if (string.IsNullOrWhiteSpace(newPath))
+ {
+ //invalid shortcut - could be old or target could just be unavailable
+ Logger.LogWarning("Encountered invalid shortuct: "+lpFindFileData.Path);
+ continue;
+ }
+ var data = FileSystem.GetFileData(newPath);
+
+ if (data.HasValue)
+ {
+ lpFindFileData = data.Value;
+
+ // Find out if the shortcut is pointing to a directory or file
+ if (lpFindFileData.IsDirectory)
+ {
+ // add to our physical locations
+ if (args != null)
+ {
+ args.AddAdditionalLocation(newPath);
+ }
+ }
+
+ dict[lpFindFileData.Path] = lpFindFileData;
+ }
+ }
+ else if (flattenFolderDepth > 0 && lpFindFileData.IsDirectory)
+ {
+ foreach (var child in GetFilteredFileSystemEntries(lpFindFileData.Path, flattenFolderDepth: flattenFolderDepth - 1))
+ {
+ dict[child.Key] = child.Value;
+ }
+ }
+ else
+ {
+ dict[lpFindFileData.Path] = lpFindFileData;
+ }
+ }
+
+ NativeMethods.FindClose(handle);
+ return dict;
+ }
+ }
+
+}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
deleted file mode 100644
index 732cf0803e..0000000000
--- a/MediaBrowser.Controller/IO/FileSystemHelper.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Controller.Library;
-
-namespace MediaBrowser.Controller.IO
-{
- public static class FileSystemHelper
- {
- /// <summary>
- /// Transforms shortcuts into their actual paths and filters out items that should be ignored
- /// </summary>
- public static ItemResolveEventArgs FilterChildFileSystemEntries(ItemResolveEventArgs args, bool flattenShortcuts)
- {
-
- List<WIN32_FIND_DATA> returnChildren = new List<WIN32_FIND_DATA>();
- List<WIN32_FIND_DATA> resolvedShortcuts = new List<WIN32_FIND_DATA>();
-
- foreach (var file in args.FileSystemChildren)
- {
- // If it's a shortcut, resolve it
- if (Shortcut.IsShortcut(file.Path))
- {
- string newPath = Shortcut.ResolveShortcut(file.Path);
- WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath);
-
- // Find out if the shortcut is pointing to a directory or file
- if (newPathData.IsDirectory)
- {
- // add to our physical locations
- args.AdditionalLocations.Add(newPath);
-
- // If we're flattening then get the shortcut's children
- if (flattenShortcuts)
- {
- returnChildren.Add(file);
- ItemResolveEventArgs newArgs = new ItemResolveEventArgs()
- {
- FileSystemChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray()
- };
-
- resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newArgs, false).FileSystemChildren);
- }
- else
- {
- returnChildren.Add(newPathData);
- }
- }
- else
- {
- returnChildren.Add(newPathData);
- }
- }
- else
- {
- //not a shortcut check to see if we should filter it out
- if (EntityResolutionHelper.ShouldResolvePath(file))
- {
- returnChildren.Add(file);
- }
- else
- {
- //filtered - see if it is one of our "indicator" folders and mark it now - no reason to search for it again
- args.IsBDFolder |= file.cFileName.Equals("bdmv", StringComparison.OrdinalIgnoreCase);
- args.IsDVDFolder |= file.cFileName.Equals("video_ts", StringComparison.OrdinalIgnoreCase);
- args.IsHDDVDFolder |= file.cFileName.Equals("hvdvd_ts", StringComparison.OrdinalIgnoreCase);
-
- //and check to see if it is a metadata folder and collect contents now if so
- if (IsMetadataFolder(file.cFileName))
- {
- args.MetadataFiles = Directory.GetFiles(Path.Combine(args.Path, "metadata"), "*", SearchOption.TopDirectoryOnly);
- }
- }
- }
- }
-
- if (resolvedShortcuts.Count > 0)
- {
- resolvedShortcuts.InsertRange(0, returnChildren);
- args.FileSystemChildren = resolvedShortcuts.ToArray();
- }
- else
- {
- args.FileSystemChildren = returnChildren.ToArray();
- }
- return args;
- }
-
- public static bool IsMetadataFolder(string path)
- {
- return path.TrimEnd('\\').EndsWith("metadata", StringComparison.OrdinalIgnoreCase);
- }
-
- public static bool IsVideoFile(string path)
- {
- string extension = System.IO.Path.GetExtension(path).ToLower();
-
- switch (extension)
- {
- case ".mkv":
- case ".m2ts":
- case ".iso":
- case ".ts":
- case ".rmvb":
- case ".mov":
- case ".avi":
- case ".mpg":
- case ".mpeg":
- case ".wmv":
- case ".mp4":
- case ".divx":
- case ".dvr-ms":
- case ".wtv":
- case ".ogm":
- case ".ogv":
- case ".asf":
- case ".m4v":
- case ".flv":
- case ".f4v":
- case ".3gp":
- return true;
-
- default:
- return false;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/IO/FileSystemManager.cs b/MediaBrowser.Controller/IO/FileSystemManager.cs
new file mode 100644
index 0000000000..e33983489b
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileSystemManager.cs
@@ -0,0 +1,112 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.IO
+{
+ /// <summary>
+ /// This class will manage our file system watching and modifications. Any process that needs to
+ /// modify the directories that the system is watching for changes should use the methods of
+ /// this class to do so. This way we can have the watchers correctly respond to only external changes.
+ /// </summary>
+ public class FileSystemManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// Gets or sets the directory watchers.
+ /// </summary>
+ /// <value>The directory watchers.</value>
+ private DirectoryWatchers DirectoryWatchers { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FileSystemManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public FileSystemManager(Kernel kernel)
+ : base(kernel)
+ {
+ DirectoryWatchers = new DirectoryWatchers();
+ }
+
+ /// <summary>
+ /// Start the directory watchers on our library folders
+ /// </summary>
+ public void StartWatchers()
+ {
+ DirectoryWatchers.Start();
+ }
+
+ /// <summary>
+ /// Saves to library filesystem.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="dataToSave">The data to save.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task SaveToLibraryFilesystem(BaseItem item, string path, Stream dataToSave, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+ if (dataToSave == null)
+ {
+ throw new ArgumentNullException();
+ }
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ //Tell the watchers to ignore
+ DirectoryWatchers.TemporarilyIgnore(path);
+
+ //Make the mod
+
+ dataToSave.Position = 0;
+
+ try
+ {
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await dataToSave.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+
+ dataToSave.Dispose();
+
+ // If this is ever used for something other than metadata we can add a file type param
+ item.ResolveArgs.AddMetadataFile(path);
+ }
+ }
+ finally
+ {
+ //Remove the ignore
+ DirectoryWatchers.RemoveTempIgnore(path);
+ }
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DirectoryWatchers.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/Shortcut.cs b/MediaBrowser.Controller/IO/Shortcut.cs
deleted file mode 100644
index e9ea21f17a..0000000000
--- a/MediaBrowser.Controller/IO/Shortcut.cs
+++ /dev/null
@@ -1,185 +0,0 @@
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace MediaBrowser.Controller.IO
-{
- /// <summary>
- /// Contains helpers to interact with shortcut files (.lnk)
- /// </summary>
- public static class Shortcut
- {
- #region Signitures were imported from http://pinvoke.net
- [Flags()]
- enum SLGP_FLAGS
- {
- /// <summary>Retrieves the standard short (8.3 format) file name</summary>
- SLGP_SHORTPATH = 0x1,
- /// <summary>Retrieves the Universal Naming Convention (UNC) path name of the file</summary>
- SLGP_UNCPRIORITY = 0x2,
- /// <summary>Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded</summary>
- SLGP_RAWPATH = 0x4
- }
-
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
- struct WIN32_FIND_DATAW
- {
- public uint dwFileAttributes;
- public long ftCreationTime;
- public long ftLastAccessTime;
- public long ftLastWriteTime;
- public uint nFileSizeHigh;
- public uint nFileSizeLow;
- public uint dwReserved0;
- public uint dwReserved1;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
- public string cFileName;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
- public string cAlternateFileName;
- }
-
- [Flags()]
-
- enum SLR_FLAGS
- {
- /// <summary>
- /// Do not display a dialog box if the link cannot be resolved. When SLR_NO_UI is set,
- /// the high-order word of fFlags can be set to a time-out value that specifies the
- /// maximum amount of time to be spent resolving the link. The function returns if the
- /// link cannot be resolved within the time-out duration. If the high-order word is set
- /// to zero, the time-out duration will be set to the default value of 3,000 milliseconds
- /// (3 seconds). To specify a value, set the high word of fFlags to the desired time-out
- /// duration, in milliseconds.
- /// </summary>
- SLR_NO_UI = 0x1,
- /// <summary>Obsolete and no longer used</summary>
- SLR_ANY_MATCH = 0x2,
- /// <summary>If the link object has changed, update its path and list of identifiers.
- /// If SLR_UPDATE is set, you do not need to call IPersistFile::IsDirty to determine
- /// whether or not the link object has changed.</summary>
- SLR_UPDATE = 0x4,
- /// <summary>Do not update the link information</summary>
- SLR_NOUPDATE = 0x8,
- /// <summary>Do not execute the search heuristics</summary>
- SLR_NOSEARCH = 0x10,
- /// <summary>Do not use distributed link tracking</summary>
- SLR_NOTRACK = 0x20,
- /// <summary>Disable distributed link tracking. By default, distributed link tracking tracks
- /// removable media across multiple devices based on the volume name. It also uses the
- /// Universal Naming Convention (UNC) path to track remote file systems whose drive letter
- /// has changed. Setting SLR_NOLINKINFO disables both types of tracking.</summary>
- SLR_NOLINKINFO = 0x40,
- /// <summary>Call the Microsoft Windows Installer</summary>
- SLR_INVOKE_MSI = 0x80
- }
-
-
- /// <summary>The IShellLink interface allows Shell links to be created, modified, and resolved</summary>
- [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
- interface IShellLinkW
- {
- /// <summary>Retrieves the path and file name of a Shell link object</summary>
- void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
- /// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
- void GetIDList(out IntPtr ppidl);
- /// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
- void SetIDList(IntPtr pidl);
- /// <summary>Retrieves the description string for a Shell link object</summary>
- void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
- /// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
- void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
- /// <summary>Retrieves the name of the working directory for a Shell link object</summary>
- void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
- /// <summary>Sets the name of the working directory for a Shell link object</summary>
- void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
- /// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
- void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
- /// <summary>Sets the command-line arguments for a Shell link object</summary>
- void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
- /// <summary>Retrieves the hot key for a Shell link object</summary>
- void GetHotkey(out short pwHotkey);
- /// <summary>Sets a hot key for a Shell link object</summary>
- void SetHotkey(short wHotkey);
- /// <summary>Retrieves the show command for a Shell link object</summary>
- void GetShowCmd(out int piShowCmd);
- /// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
- void SetShowCmd(int iShowCmd);
- /// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
- void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
- int cchIconPath, out int piIcon);
- /// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
- void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
- /// <summary>Sets the relative path to the Shell link object</summary>
- void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
- /// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
- void Resolve(IntPtr hwnd, SLR_FLAGS fFlags);
- /// <summary>Sets the path and file name of a Shell link object</summary>
- void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
-
- }
-
- [ComImport, Guid("0000010c-0000-0000-c000-000000000046"),
- InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- public interface IPersist
- {
- [PreserveSig]
- void GetClassID(out Guid pClassID);
- }
-
-
- [ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
- InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- public interface IPersistFile : IPersist
- {
- new void GetClassID(out Guid pClassID);
- [PreserveSig]
- int IsDirty();
-
- [PreserveSig]
- void Load([In, MarshalAs(UnmanagedType.LPWStr)]
- string pszFileName, uint dwMode);
-
- [PreserveSig]
- void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName,
- [In, MarshalAs(UnmanagedType.Bool)] bool remember);
-
- [PreserveSig]
- void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
-
- [PreserveSig]
- void GetCurFile([In, MarshalAs(UnmanagedType.LPWStr)] string ppszFileName);
- }
-
- const uint STGM_READ = 0;
- const int MAX_PATH = 260;
-
- // CLSID_ShellLink from ShlGuid.h
- [
- ComImport(),
- Guid("00021401-0000-0000-C000-000000000046")
- ]
- public class ShellLink
- {
- }
-
- #endregion
-
- public static string ResolveShortcut(string filename)
- {
- var link = new ShellLink();
- ((IPersistFile)link).Load(filename, STGM_READ);
- // TODO: if I can get hold of the hwnd call resolve first. This handles moved and renamed files.
- // ((IShellLinkW)link).Resolve(hwnd, 0)
- var sb = new StringBuilder(MAX_PATH);
- var data = new WIN32_FIND_DATAW();
- ((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0);
- return sb.ToString();
- }
-
- public static bool IsShortcut(string filename)
- {
- return filename != null ? Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase) : false;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Kernel.cs b/MediaBrowser.Controller/Kernel.cs
index 2430260ddc..aca7c4bd22 100644
--- a/MediaBrowser.Controller/Kernel.cs
+++ b/MediaBrowser.Controller/Kernel.cs
@@ -1,386 +1,599 @@
-using MediaBrowser.Common.Kernel;
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Controller.Weather;
-using MediaBrowser.Model.Authentication;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Progress;
-using MediaBrowser.Common.Extensions;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller
-{
- public class Kernel : BaseKernel<ServerConfiguration, ServerApplicationPaths>
- {
- #region Events
- /// <summary>
- /// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args.
- /// *** Will fire asynchronously. ***
- /// </summary>
- public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
- public void OnLibraryChanged(ChildrenChangedEventArgs args)
- {
- if (LibraryChanged != null)
- {
- Task.Run(() => LibraryChanged(this, args));
- }
- }
-
- #endregion
- public static Kernel Instance { get; private set; }
-
- public ItemController ItemController { get; private set; }
-
- public IEnumerable<User> Users { get; private set; }
- public Folder RootFolder { get; private set; }
-
- private DirectoryWatchers DirectoryWatchers { get; set; }
-
- private string MediaRootFolderPath
- {
- get
- {
- return ApplicationPaths.RootFolderPath;
- }
- }
-
- public override KernelContext KernelContext
- {
- get { return KernelContext.Server; }
- }
-
- /// <summary>
- /// Gets the list of currently registered weather prvoiders
- /// </summary>
- [ImportMany(typeof(BaseWeatherProvider))]
- public IEnumerable<BaseWeatherProvider> WeatherProviders { get; private set; }
-
- /// <summary>
- /// Gets the list of currently registered metadata prvoiders
- /// </summary>
- [ImportMany(typeof(BaseMetadataProvider))]
- private IEnumerable<BaseMetadataProvider> MetadataProvidersEnumerable { get; set; }
-
- /// <summary>
- /// Once MEF has loaded the resolvers, sort them by priority and store them in this array
- /// Given the sheer number of times they'll be iterated over it'll be faster to loop through an array
- /// </summary>
- private BaseMetadataProvider[] MetadataProviders { get; set; }
-
- /// <summary>
- /// Gets the list of currently registered entity resolvers
- /// </summary>
- [ImportMany(typeof(IBaseItemResolver))]
- private IEnumerable<IBaseItemResolver> EntityResolversEnumerable { get; set; }
-
- /// <summary>
- /// Once MEF has loaded the resolvers, sort them by priority and store them in this array
- /// Given the sheer number of times they'll be iterated over it'll be faster to loop through an array
- /// </summary>
- internal IBaseItemResolver[] EntityResolvers { get; private set; }
-
- /// <summary>
- /// Creates a kernel based on a Data path, which is akin to our current programdata path
- /// </summary>
- public Kernel()
- : base()
- {
- Instance = this;
- }
-
- /// <summary>
- /// Performs initializations that only occur once
- /// </summary>
- protected override void InitializeInternal(IProgress<TaskProgress> progress)
- {
- base.InitializeInternal(progress);
-
- ItemController = new ItemController();
- DirectoryWatchers = new DirectoryWatchers();
-
-
- ExtractFFMpeg();
- }
-
- /// <summary>
- /// Performs initializations that can be reloaded at anytime
- /// </summary>
- protected override async Task ReloadInternal(IProgress<TaskProgress> progress)
- {
- await base.ReloadInternal(progress).ConfigureAwait(false);
-
- ReportProgress(progress, "Loading Users");
- ReloadUsers();
-
- ReportProgress(progress, "Loading Media Library");
-
- await ReloadRoot(allowInternetProviders: false).ConfigureAwait(false);
-
- }
-
- /// <summary>
- /// Completely disposes the Kernel
- /// </summary>
- public override void Dispose()
- {
- base.Dispose();
-
- DirectoryWatchers.Stop();
-
- }
-
- protected override void OnComposablePartsLoaded()
- {
- // The base class will start up all the plugins
- base.OnComposablePartsLoaded();
-
- // Sort the resolvers by priority
- EntityResolvers = EntityResolversEnumerable.OrderBy(e => e.Priority).ToArray();
-
- // Sort the providers by priority
- MetadataProviders = MetadataProvidersEnumerable.OrderBy(e => e.Priority).ToArray();
- }
-
- public BaseItem ResolveItem(ItemResolveEventArgs args)
- {
- // Try first priority resolvers
- for (int i = 0; i < EntityResolvers.Length; i++)
- {
- var item = EntityResolvers[i].ResolvePath(args);
-
- if (item != null)
- {
- item.ResolveArgs = args;
- return item;
- }
- }
-
- return null;
- }
-
- private void ReloadUsers()
- {
- Users = GetAllUsers();
- }
-
- /// <summary>
- /// Reloads the root media folder
- /// </summary>
- public async Task ReloadRoot(bool allowInternetProviders = true)
- {
- if (!Directory.Exists(MediaRootFolderPath))
- {
- Directory.CreateDirectory(MediaRootFolderPath);
- }
-
- DirectoryWatchers.Stop();
-
- RootFolder = await ItemController.GetItem(MediaRootFolderPath, allowInternetProviders: allowInternetProviders).ConfigureAwait(false) as Folder;
- RootFolder.ChildrenChanged += RootFolder_ChildrenChanged;
-
- DirectoryWatchers.Start();
- }
-
- void RootFolder_ChildrenChanged(object sender, ChildrenChangedEventArgs e)
- {
- Logger.LogDebugInfo("Root Folder Children Changed. Added: " + e.ItemsAdded.Count + " Removed: " + e.ItemsRemoved.Count());
- //re-start the directory watchers
- DirectoryWatchers.Stop();
- DirectoryWatchers.Start();
- //Task.Delay(30000); //let's wait and see if more data gets filled in...
- var allChildren = RootFolder.RecursiveChildren;
- Logger.LogDebugInfo(string.Format("Loading complete. Movies: {0} Episodes: {1} Folders: {2}", allChildren.OfType<Entities.Movies.Movie>().Count(), allChildren.OfType<Entities.TV.Episode>().Count(), allChildren.Where(i => i is Folder && !(i is Series || i is Season)).Count()));
- //foreach (var child in allChildren)
- //{
- // Logger.LogDebugInfo("(" + child.GetType().Name + ") " + child.Name + " (" + child.Path + ")");
- //}
- }
-
- /// <summary>
- /// Gets the default user to use when EnableUserProfiles is false
- /// </summary>
- public User GetDefaultUser()
- {
- User user = Users.FirstOrDefault();
-
- return user;
- }
-
- /// <summary>
- /// Persists a User
- /// </summary>
- public void SaveUser(User user)
- {
-
- }
-
- /// <summary>
- /// Authenticates a User and returns a result indicating whether or not it succeeded
- /// </summary>
- public AuthenticationResult AuthenticateUser(User user, string password)
- {
- var result = new AuthenticationResult();
-
- // When EnableUserProfiles is false, only the default User can login
- if (!Configuration.EnableUserProfiles)
- {
- result.Success = user.Id == GetDefaultUser().Id;
- }
- else if (string.IsNullOrEmpty(user.Password))
- {
- result.Success = true;
- }
- else
- {
- password = password ?? string.Empty;
- result.Success = password.GetMD5().ToString().Equals(user.Password);
- }
-
- // Update LastActivityDate and LastLoginDate, then save
- if (result.Success)
- {
- user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
- SaveUser(user);
- }
-
- return result;
- }
-
- /// <summary>
- /// Finds a library item by Id
- /// </summary>
- public BaseItem GetItemById(Guid id)
- {
- if (id == Guid.Empty)
- {
- return RootFolder;
- }
-
- return RootFolder.FindItemById(id);
- }
-
- /// <summary>
- /// Gets all users within the system
- /// </summary>
- private IEnumerable<User> GetAllUsers()
- {
- var list = new List<User>();
-
- // Return a dummy user for now since all calls to get items requre a userId
- var user = new User { };
-
- user.Name = "Default User";
- user.Id = Guid.Parse("5d1cf7fce25943b790d140095457a42b");
- user.PrimaryImagePath = "D:\\Video\\TV\\Archer (2009)\\backdrop.jpg";
- list.Add(user);
-
- user = new User { };
- user.Name = "Abobader";
- user.Id = Guid.NewGuid();
- user.LastLoginDate = DateTime.UtcNow.AddDays(-1);
- user.LastActivityDate = DateTime.UtcNow.AddHours(-3);
- user.Password = ("1234").GetMD5().ToString();
- list.Add(user);
-
- user = new User { };
- user.Name = "Scottisafool";
- user.Id = Guid.NewGuid();
- list.Add(user);
-
- user = new User { };
- user.Name = "Redshirt";
- user.Id = Guid.NewGuid();
- list.Add(user);
-
- /*user = new User();
- user.Name = "Test User 4";
- user.Id = Guid.NewGuid();
- list.Add(user);
-
- user = new User();
- user.Name = "Test User 5";
- user.Id = Guid.NewGuid();
- list.Add(user);
-
- user = new User();
- user.Name = "Test User 6";
- user.Id = Guid.NewGuid();
- list.Add(user);*/
-
- return list;
- }
-
- /// <summary>
- /// Runs all metadata providers for an entity
- /// </summary>
- internal async Task ExecuteMetadataProviders(BaseEntity item, bool allowInternetProviders = true)
- {
- // Run them sequentially in order of priority
- for (int i = 0; i < MetadataProviders.Length; i++)
- {
- var provider = MetadataProviders[i];
-
- // Skip if internet providers are currently disabled
- if (provider.RequiresInternet && (!Configuration.EnableInternetProviders || !allowInternetProviders))
- {
- continue;
- }
-
- // Skip if the provider doesn't support the current item
- if (!provider.Supports(item))
- {
- continue;
- }
-
- try
- {
- await provider.FetchIfNeededAsync(item).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogException(ex);
- }
- }
- }
-
- private void ExtractFFMpeg()
- {
- ExtractFFMpeg(ApplicationPaths.FFMpegPath);
- ExtractFFMpeg(ApplicationPaths.FFProbePath);
- }
-
- /// <summary>
- /// Run these during Init.
- /// Can't run do this on-demand because there will be multiple workers accessing them at once and we'd have to lock them
- /// </summary>
- private void ExtractFFMpeg(string exe)
- {
- if (File.Exists(exe))
- {
- File.Delete(exe);
- }
-
- // Extract exe
- using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Controller.FFMpeg." + Path.GetFileName(exe)))
- {
- using (var fileStream = new FileStream(exe, FileMode.Create))
- {
- stream.CopyTo(fileStream);
- }
- }
- }
- }
-}
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Persistence.SQLite;
+using MediaBrowser.Controller.Playback;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.ScheduledTasks;
+using MediaBrowser.Controller.Updates;
+using MediaBrowser.Controller.Weather;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.System;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Class Kernel
+ /// </summary>
+ public class Kernel : BaseKernel<ServerConfiguration, ServerApplicationPaths>
+ {
+ /// <summary>
+ /// The MB admin URL
+ /// </summary>
+ public const string MBAdminUrl = "http://mb3admin.com/admin/";
+
+ /// <summary>
+ /// Gets the instance.
+ /// </summary>
+ /// <value>The instance.</value>
+ public static Kernel Instance { get; private set; }
+
+ /// <summary>
+ /// Gets the library manager.
+ /// </summary>
+ /// <value>The library manager.</value>
+ public LibraryManager LibraryManager { get; private set; }
+
+ /// <summary>
+ /// Gets the image manager.
+ /// </summary>
+ /// <value>The image manager.</value>
+ public ImageManager ImageManager { get; private set; }
+
+ /// <summary>
+ /// Gets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public UserManager UserManager { get; private set; }
+
+ /// <summary>
+ /// Gets the FFMPEG controller.
+ /// </summary>
+ /// <value>The FFMPEG controller.</value>
+ public FFMpegManager FFMpegManager { get; private set; }
+
+ /// <summary>
+ /// Gets the installation manager.
+ /// </summary>
+ /// <value>The installation manager.</value>
+ public InstallationManager InstallationManager { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the file system manager.
+ /// </summary>
+ /// <value>The file system manager.</value>
+ public FileSystemManager FileSystemManager { get; private set; }
+
+ /// <summary>
+ /// Gets the provider manager.
+ /// </summary>
+ /// <value>The provider manager.</value>
+ public ProviderManager ProviderManager { get; private set; }
+
+ /// <summary>
+ /// Gets the user data manager.
+ /// </summary>
+ /// <value>The user data manager.</value>
+ public UserDataManager UserDataManager { get; private set; }
+
+ /// <summary>
+ /// Gets the plug-in security manager.
+ /// </summary>
+ /// <value>The plug-in security manager.</value>
+ public PluginSecurityManager PluginSecurityManager { get; private set; }
+
+ /// <summary>
+ /// The _users
+ /// </summary>
+ private IEnumerable<User> _users;
+ /// <summary>
+ /// The _user lock
+ /// </summary>
+ private object _usersSyncLock = new object();
+ /// <summary>
+ /// The _users initialized
+ /// </summary>
+ private bool _usersInitialized;
+ /// <summary>
+ /// Gets the users.
+ /// </summary>
+ /// <value>The users.</value>
+ public IEnumerable<User> Users
+ {
+ get
+ {
+ // Call ToList to exhaust the stream because we'll be iterating over this multiple times
+ LazyInitializer.EnsureInitialized(ref _users, ref _usersInitialized, ref _usersSyncLock, UserManager.LoadUsers);
+ return _users;
+ }
+ internal set
+ {
+ _users = value;
+
+ if (value == null)
+ {
+ _usersInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// The _root folder
+ /// </summary>
+ private AggregateFolder _rootFolder;
+ /// <summary>
+ /// The _root folder sync lock
+ /// </summary>
+ private object _rootFolderSyncLock = new object();
+ /// <summary>
+ /// The _root folder initialized
+ /// </summary>
+ private bool _rootFolderInitialized;
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ public AggregateFolder RootFolder
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _rootFolder, ref _rootFolderInitialized, ref _rootFolderSyncLock, LibraryManager.CreateRootFolder);
+ return _rootFolder;
+ }
+ private set
+ {
+ _rootFolder = value;
+
+ if (value == null)
+ {
+ _rootFolderInitialized = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the kernel context.
+ /// </summary>
+ /// <value>The kernel context.</value>
+ public override KernelContext KernelContext
+ {
+ get { return KernelContext.Server; }
+ }
+
+ /// <summary>
+ /// Gets the list of plugin configuration pages
+ /// </summary>
+ /// <value>The configuration pages.</value>
+ [ImportMany(typeof(BaseConfigurationPage))]
+ public IEnumerable<BaseConfigurationPage> PluginConfigurationPages { get; private set; }
+
+ /// <summary>
+ /// Gets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ [ImportMany(typeof(BaseIntroProvider))]
+ public IEnumerable<BaseIntroProvider> IntroProviders { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently registered weather prvoiders
+ /// </summary>
+ /// <value>The weather providers.</value>
+ [ImportMany(typeof(BaseWeatherProvider))]
+ public IEnumerable<BaseWeatherProvider> WeatherProviders { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently registered metadata prvoiders
+ /// </summary>
+ /// <value>The metadata providers enumerable.</value>
+ [ImportMany(typeof(BaseMetadataProvider))]
+ public BaseMetadataProvider[] MetadataProviders { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently registered image processors
+ /// Image processors are specialized metadata providers that run after the normal ones
+ /// </summary>
+ /// <value>The image enhancers.</value>
+ [ImportMany(typeof(BaseImageEnhancer))]
+ public BaseImageEnhancer[] ImageEnhancers { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently registered entity resolvers
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ [ImportMany(typeof(IBaseItemResolver))]
+ internal IBaseItemResolver[] EntityResolvers { get; private set; }
+
+ /// <summary>
+ /// Gets the list of BasePluginFolders added by plugins
+ /// </summary>
+ /// <value>The plugin folders.</value>
+ [ImportMany(typeof(BasePluginFolder))]
+ internal IEnumerable<BasePluginFolder> PluginFolders { get; private set; }
+
+ /// <summary>
+ /// Gets the list of available user repositories
+ /// </summary>
+ /// <value>The user repositories.</value>
+ [ImportMany(typeof(IUserRepository))]
+ private IEnumerable<IUserRepository> UserRepositories { get; set; }
+
+ /// <summary>
+ /// Gets the active user repository
+ /// </summary>
+ /// <value>The user repository.</value>
+ public IUserRepository UserRepository { get; private set; }
+
+ /// <summary>
+ /// Gets the active user repository
+ /// </summary>
+ /// <value>The display preferences repository.</value>
+ public IDisplayPreferencesRepository DisplayPreferencesRepository { get; private set; }
+
+ /// <summary>
+ /// Gets the list of available item repositories
+ /// </summary>
+ /// <value>The item repositories.</value>
+ [ImportMany(typeof(IItemRepository))]
+ private IEnumerable<IItemRepository> ItemRepositories { get; set; }
+
+ /// <summary>
+ /// Gets the active item repository
+ /// </summary>
+ /// <value>The item repository.</value>
+ public IItemRepository ItemRepository { get; private set; }
+
+ /// <summary>
+ /// Gets the list of available item repositories
+ /// </summary>
+ /// <value>The user data repositories.</value>
+ [ImportMany(typeof(IUserDataRepository))]
+ private IEnumerable<IUserDataRepository> UserDataRepositories { get; set; }
+
+ /// <summary>
+ /// Gets the list of available DisplayPreferencesRepositories
+ /// </summary>
+ /// <value>The display preferences repositories.</value>
+ [ImportMany(typeof(IDisplayPreferencesRepository))]
+ private IEnumerable<IDisplayPreferencesRepository> DisplayPreferencesRepositories { get; set; }
+
+ /// <summary>
+ /// Gets the list of entity resolution ignore rules
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ [ImportMany(typeof(BaseResolutionIgnoreRule))]
+ internal IEnumerable<BaseResolutionIgnoreRule> EntityResolutionIgnoreRules { get; private set; }
+
+ /// <summary>
+ /// Gets the active user data repository
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataRepository UserDataRepository { get; private set; }
+
+ /// <summary>
+ /// Limits simultaneous access to various resources
+ /// </summary>
+ /// <value>The resource pools.</value>
+ public ResourcePool ResourcePools { get; set; }
+
+ /// <summary>
+ /// Gets the UDP server port number.
+ /// </summary>
+ /// <value>The UDP server port number.</value>
+ public override int UdpServerPortNumber
+ {
+ get { return 7359; }
+ }
+
+ /// <summary>
+ /// Creates a kernel based on a Data path, which is akin to our current programdata path
+ /// </summary>
+ public Kernel()
+ : base()
+ {
+ Instance = this;
+ }
+
+ /// <summary>
+ /// Performs initializations that can be reloaded at anytime
+ /// </summary>
+ /// <returns>Task.</returns>
+ protected override async Task ReloadInternal()
+ {
+ Logger.Info("Extracting tools");
+
+ // Reset these so that they can be lazy loaded again
+ Users = null;
+ RootFolder = null;
+
+ ReloadResourcePools();
+ InstallationManager = new InstallationManager(this);
+ LibraryManager = new LibraryManager(this);
+ UserManager = new UserManager(this);
+ FFMpegManager = new FFMpegManager(this);
+ ImageManager = new ImageManager(this);
+ ProviderManager = new ProviderManager(this);
+ UserDataManager = new UserDataManager(this);
+ PluginSecurityManager = new PluginSecurityManager(this);
+
+ await base.ReloadInternal().ConfigureAwait(false);
+
+ ReloadFileSystemManager();
+
+ await UserManager.RefreshUsersMetadata(CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DisposeResourcePools();
+
+ DisposeFileSystemManager();
+ }
+
+ base.Dispose(dispose);
+ }
+
+ /// <summary>
+ /// Disposes the resource pools.
+ /// </summary>
+ private void DisposeResourcePools()
+ {
+ if (ResourcePools != null)
+ {
+ ResourcePools.Dispose();
+ ResourcePools = null;
+ }
+ }
+
+ /// <summary>
+ /// Reloads the resource pools.
+ /// </summary>
+ private void ReloadResourcePools()
+ {
+ DisposeResourcePools();
+ ResourcePools = new ResourcePool();
+ }
+
+ /// <summary>
+ /// Called when [composable parts loaded].
+ /// </summary>
+ /// <returns>Task.</returns>
+ protected override async Task OnComposablePartsLoaded()
+ {
+ // The base class will start up all the plugins
+ await base.OnComposablePartsLoaded().ConfigureAwait(false);
+
+ // Get the current item repository
+ ItemRepository = GetRepository(ItemRepositories, Configuration.ItemRepository, SQLiteItemRepository.RepositoryName);
+ var itemRepoTask = ItemRepository.Initialize();
+
+ // Get the current user repository
+ UserRepository = GetRepository(UserRepositories, Configuration.UserRepository, SQLiteUserRepository.RepositoryName);
+ var userRepoTask = UserRepository.Initialize();
+
+ // Get the current item repository
+ UserDataRepository = GetRepository(UserDataRepositories, Configuration.UserDataRepository, SQLiteUserDataRepository.RepositoryName);
+ var userDataRepoTask = UserDataRepository.Initialize();
+
+ // Get the current display preferences repository
+ DisplayPreferencesRepository = GetRepository(DisplayPreferencesRepositories, Configuration.DisplayPreferencesRepository, SQLiteDisplayPreferencesRepository.RepositoryName);
+ var displayPreferencesRepoTask = DisplayPreferencesRepository.Initialize();
+
+ // Sort the resolvers by priority
+ EntityResolvers = EntityResolvers.OrderBy(e => e.Priority).ToArray();
+
+ // Sort the providers by priority
+ MetadataProviders = MetadataProviders.OrderBy(e => e.Priority).ToArray();
+
+ // Sort the image processors by priority
+ ImageEnhancers = ImageEnhancers.OrderBy(e => e.Priority).ToArray();
+
+ await Task.WhenAll(itemRepoTask, userRepoTask, userDataRepoTask, displayPreferencesRepoTask).ConfigureAwait(false);
+ }
+
+ protected override IEnumerable<Assembly> GetComposablePartAssemblies()
+ {
+ var runningDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
+
+ return base.GetComposablePartAssemblies().Concat(new[] {
+
+ Assembly.Load(File.ReadAllBytes(Path.Combine(runningDirectory, "MediaBrowser.Api.dll"))),
+ Assembly.Load(File.ReadAllBytes(Path.Combine(runningDirectory, "MediaBrowser.ApiInteraction.Javascript.dll"))),
+ Assembly.Load(File.ReadAllBytes(Path.Combine(runningDirectory, "MediaBrowser.WebDashboard.dll")))
+ });
+ }
+
+ /// <summary>
+ /// Gets a repository by name from a list, and returns the default if not found
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="repositories">The repositories.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="defaultName">The default name.</param>
+ /// <returns>``0.</returns>
+ private T GetRepository<T>(IEnumerable<T> repositories, string name, string defaultName)
+ where T : class, IRepository
+ {
+ var enumerable = repositories as T[] ?? repositories.ToArray();
+
+ return enumerable.FirstOrDefault(r => r.Name.Equals(name ?? defaultName, StringComparison.OrdinalIgnoreCase)) ??
+ enumerable.First(r => r.Name.Equals(defaultName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ /// <summary>
+ /// Disposes the file system manager.
+ /// </summary>
+ private void DisposeFileSystemManager()
+ {
+ if (FileSystemManager != null)
+ {
+ FileSystemManager.Dispose();
+ FileSystemManager = null;
+ }
+ }
+
+ /// <summary>
+ /// Reloads the file system manager.
+ /// </summary>
+ private void ReloadFileSystemManager()
+ {
+ DisposeFileSystemManager();
+
+ FileSystemManager = new FileSystemManager(this);
+ FileSystemManager.StartWatchers();
+ }
+
+ /// <summary>
+ /// Gets a User by Id
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>User.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public User GetUserById(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return Users.FirstOrDefault(u => u.Id == id);
+ }
+
+ /// <summary>
+ /// Finds a library item by Id and UserId.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public BaseItem GetItemById(Guid id, Guid userId)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ if (userId == Guid.Empty)
+ {
+ throw new ArgumentNullException("userId");
+ }
+
+ var user = GetUserById(userId);
+ var userRoot = user.RootFolder;
+
+ return userRoot.FindItemById(id, user);
+ }
+
+ /// <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");
+ }
+
+ return RootFolder.FindItemById(id, null);
+ }
+
+ /// <summary>
+ /// Completely overwrites the current configuration with a new copy
+ /// </summary>
+ /// <param name="config">The config.</param>
+ public void UpdateConfiguration(ServerConfiguration config)
+ {
+ var oldConfiguration = Configuration;
+
+ var reloadLogger = config.ShowLogWindow != oldConfiguration.ShowLogWindow;
+
+ // Figure out whether or not we should refresh people after the update is finished
+ var refreshPeopleAfterUpdate = !oldConfiguration.EnableInternetProviders && config.EnableInternetProviders;
+
+ // This is true if internet providers has just been turned on, or if People have just been removed from InternetProviderExcludeTypes
+ if (!refreshPeopleAfterUpdate)
+ {
+ var oldConfigurationFetchesPeopleImages = oldConfiguration.InternetProviderExcludeTypes == null || !oldConfiguration.InternetProviderExcludeTypes.Contains(typeof(Person).Name, StringComparer.OrdinalIgnoreCase);
+ var newConfigurationFetchesPeopleImages = config.InternetProviderExcludeTypes == null || !config.InternetProviderExcludeTypes.Contains(typeof(Person).Name, StringComparer.OrdinalIgnoreCase);
+
+ refreshPeopleAfterUpdate = newConfigurationFetchesPeopleImages && !oldConfigurationFetchesPeopleImages;
+ }
+
+ Configuration = config;
+ SaveConfiguration();
+
+ if (reloadLogger)
+ {
+ ReloadLogger();
+ }
+
+ TcpManager.OnApplicationConfigurationChanged(oldConfiguration, config);
+
+ // Validate currently executing providers, in the background
+ Task.Run(() =>
+ {
+ ProviderManager.ValidateCurrentlyRunningProviders();
+
+ // Any number of configuration settings could change the way the library is refreshed, so do that now
+ TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+
+ if (refreshPeopleAfterUpdate)
+ {
+ TaskManager.CancelIfRunningAndQueue<PeopleValidationTask>();
+ }
+ });
+ }
+
+ /// <summary>
+ /// Removes the plugin.
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ internal void RemovePlugin(IPlugin plugin)
+ {
+ var list = Plugins.ToList();
+ list.Remove(plugin);
+ Plugins = list;
+ }
+
+ /// <summary>
+ /// Gets the system info.
+ /// </summary>
+ /// <returns>SystemInfo.</returns>
+ public override SystemInfo GetSystemInfo()
+ {
+ var info = base.GetSystemInfo();
+
+ if (InstallationManager != null)
+ {
+ info.InProgressInstallations = InstallationManager.CurrentInstallations.Select(i => i.Item1).ToArray();
+ info.CompletedInstallations = InstallationManager.CompletedInstallations.ToArray();
+ }
+
+ return info;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
index 462fcc6d69..94f4c540ff 100644
--- a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
+++ b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
@@ -1,34 +1,137 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-
-namespace MediaBrowser.Controller.Library
-{
- public class ChildrenChangedEventArgs : EventArgs
- {
- public Folder Folder { get; set; }
- public List<BaseItem> ItemsAdded { get; set; }
- public IEnumerable<BaseItem> ItemsRemoved { get; set; }
-
- public ChildrenChangedEventArgs()
- {
- //initialize the list
- ItemsAdded = new List<BaseItem>();
- }
-
- /// <summary>
- /// Create the args and set the folder property
- /// </summary>
- /// <param name="folder"></param>
- public ChildrenChangedEventArgs(Folder folder)
- {
- //init the folder property
- this.Folder = folder;
- //init the list
- ItemsAdded = new List<BaseItem>();
- }
- }
-}
+using System.Collections.Concurrent;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class ChildrenChangedEventArgs
+ /// </summary>
+ public class ChildrenChangedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the folder.
+ /// </summary>
+ /// <value>The folder.</value>
+ public Folder Folder { get; set; }
+ /// <summary>
+ /// Gets or sets the items added.
+ /// </summary>
+ /// <value>The items added.</value>
+ public ConcurrentBag<BaseItem> ItemsAdded { get; set; }
+ /// <summary>
+ /// Gets or sets the items removed.
+ /// </summary>
+ /// <value>The items removed.</value>
+ public List<BaseItem> ItemsRemoved { get; set; }
+ /// <summary>
+ /// Gets or sets the items updated.
+ /// </summary>
+ /// <value>The items updated.</value>
+ public ConcurrentBag<BaseItem> ItemsUpdated { get; set; }
+
+ /// <summary>
+ /// Create the args and set the folder property
+ /// </summary>
+ /// <param name="folder">The folder.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public ChildrenChangedEventArgs(Folder folder)
+ {
+ if (folder == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ //init the folder property
+ Folder = folder;
+ //init the list
+ ItemsAdded = new ConcurrentBag<BaseItem>();
+ ItemsRemoved = new List<BaseItem>();
+ ItemsUpdated = new ConcurrentBag<BaseItem>();
+ }
+
+ /// <summary>
+ /// Adds the new item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddNewItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ ItemsAdded.Add(item);
+ }
+
+ /// <summary>
+ /// Adds the updated item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddUpdatedItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ ItemsUpdated.Add(item);
+ }
+
+ /// <summary>
+ /// Adds the removed item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddRemovedItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ ItemsRemoved.Add(item);
+ }
+
+ /// <summary>
+ /// Lists the has change.
+ /// </summary>
+ /// <param name="list">The list.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool ListHasChange(List<BaseItem> list)
+ {
+ return list != null && list.Count > 0;
+ }
+
+ /// <summary>
+ /// Lists the has change.
+ /// </summary>
+ /// <param name="list">The list.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool ListHasChange(ConcurrentBag<BaseItem> list)
+ {
+ return list != null && !list.IsEmpty;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has change.
+ /// </summary>
+ /// <value><c>true</c> if this instance has change; otherwise, <c>false</c>.</value>
+ public bool HasChange
+ {
+ get { return HasAddOrRemoveChange || ListHasChange(ItemsUpdated); }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has add or remove change.
+ /// </summary>
+ /// <value><c>true</c> if this instance has add or remove change; otherwise, <c>false</c>.</value>
+ public bool HasAddOrRemoveChange
+ {
+ get { return ListHasChange(ItemsAdded) || ListHasChange(ItemsRemoved); }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/DtoBuilder.cs b/MediaBrowser.Controller/Library/DtoBuilder.cs
new file mode 100644
index 0000000000..b16671e9e2
--- /dev/null
+++ b/MediaBrowser.Controller/Library/DtoBuilder.cs
@@ -0,0 +1,934 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Generates DTO's from domain entities
+ /// </summary>
+ public static class DtoBuilder
+ {
+ /// <summary>
+ /// The index folder delimeter
+ /// </summary>
+ const string IndexFolderDelimeter = "-index-";
+
+ /// <summary>
+ /// Gets the dto base item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="fields">The fields.</param>
+ /// <returns>Task{DtoBaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, List<ItemFields> fields)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (fields == null)
+ {
+ throw new ArgumentNullException("fields");
+ }
+
+ var dto = new DtoBaseItem();
+
+ var tasks = new List<Task>();
+
+ if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
+ {
+ try
+ {
+ tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
+ }
+ catch (Exception ex)
+ {
+ // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
+ Logger.LogException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
+ }
+ }
+
+ if (fields.Contains(ItemFields.Studios))
+ {
+ dto.Studios = item.Studios;
+ }
+
+ if (fields.Contains(ItemFields.People))
+ {
+ tasks.Add(AttachPeople(dto, item));
+ }
+
+ AttachBasicFields(dto, item, fields);
+
+ // Make sure all the tasks we kicked off have completed.
+ if (tasks.Count > 0)
+ {
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ return dto;
+ }
+
+ /// <summary>
+ /// Converts a BaseItem to a DTOBaseItem
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="fields">The fields.</param>
+ /// <returns>Task{DtoBaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, User user, List<ItemFields> fields)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+ if (fields == null)
+ {
+ throw new ArgumentNullException("fields");
+ }
+
+ var dto = new DtoBaseItem();
+
+ var tasks = new List<Task>();
+
+ if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
+ {
+ try
+ {
+ tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
+ }
+ catch (Exception ex)
+ {
+ // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
+ Logger.LogException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
+ }
+ }
+
+ if (fields.Contains(ItemFields.Studios))
+ {
+ dto.Studios = item.Studios;
+ }
+
+ if (fields.Contains(ItemFields.People))
+ {
+ tasks.Add(AttachPeople(dto, item));
+ }
+
+ AttachBasicFields(dto, item, fields);
+
+ AttachUserSpecificInfo(dto, item, user, fields);
+
+ // Make sure all the tasks we kicked off have completed.
+ if (tasks.Count > 0)
+ {
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ return dto;
+ }
+
+ /// <summary>
+ /// Attaches the user specific info.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="fields">The fields.</param>
+ private static void AttachUserSpecificInfo(DtoBaseItem dto, BaseItem item, User user, List<ItemFields> fields)
+ {
+ dto.IsNew = item.IsRecentlyAdded(user);
+
+ if (fields.Contains(ItemFields.UserData))
+ {
+ var userData = item.GetUserData(user, false);
+
+ if (userData != null)
+ {
+ dto.UserData = GetDtoUserItemData(userData);
+ }
+ }
+
+ if (item.IsFolder && fields.Contains(ItemFields.DisplayPreferences))
+ {
+ dto.DisplayPreferences = ((Folder)item).GetDisplayPrefs(user, false) ?? new DisplayPreferences { UserId = user.Id };
+ }
+
+ if (item.IsFolder)
+ {
+ if (fields.Contains(ItemFields.ItemCounts))
+ {
+ var folder = (Folder)item;
+
+ // Skip sorting since all we want is a count
+ dto.ChildCount = folder.GetChildren(user).Count();
+
+ SetSpecialCounts(folder, user, dto);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Attaches the primary image aspect ratio.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>Task.</returns>
+ private static async Task AttachPrimaryImageAspectRatio(DtoBaseItem dto, BaseItem item)
+ {
+ var path = item.PrimaryImagePath;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return;
+ }
+
+ var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(path);
+
+ // See if we can avoid a file system lookup by looking for the file in ResolveArgs
+ var dateModified = metaFileEntry == null ? File.GetLastWriteTimeUtc(path) : metaFileEntry.Value.LastWriteTimeUtc;
+
+ ImageSize size;
+
+ try
+ {
+ size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false);
+ }
+ catch (FileNotFoundException)
+ {
+ Logger.LogError("Image file does not exist: {0}", path);
+ return;
+ }
+
+ foreach (var enhancer in Kernel.Instance.ImageEnhancers
+ .Where(i => i.Supports(item, ImageType.Primary)))
+ {
+
+ size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size);
+ }
+
+ dto.PrimaryImageAspectRatio = size.Width / size.Height;
+ }
+
+ /// <summary>
+ /// Sets simple property values on a DTOBaseItem
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="fields">The fields.</param>
+ private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, List<ItemFields> fields)
+ {
+ if (fields.Contains(ItemFields.DateCreated))
+ {
+ dto.DateCreated = item.DateCreated;
+ }
+
+ if (fields.Contains(ItemFields.DisplayMediaType))
+ {
+ dto.DisplayMediaType = item.DisplayMediaType;
+ }
+
+ dto.AspectRatio = item.AspectRatio;
+
+ dto.BackdropImageTags = GetBackdropImageTags(item);
+
+ if (fields.Contains(ItemFields.Genres))
+ {
+ dto.Genres = item.Genres;
+ }
+
+ if (item.Images != null)
+ {
+ dto.ImageTags = new Dictionary<ImageType, Guid>();
+
+ foreach (var image in item.Images)
+ {
+ ImageType type;
+
+ if (Enum.TryParse(image.Key, true, out type))
+ {
+ dto.ImageTags[type] = Kernel.Instance.ImageManager.GetImageCacheTag(item, type, image.Value);
+ }
+ }
+ }
+
+ dto.Id = GetClientItemId(item);
+ dto.IndexNumber = item.IndexNumber;
+ dto.IsFolder = item.IsFolder;
+ dto.Language = item.Language;
+ dto.MediaType = item.MediaType;
+ dto.LocationType = item.LocationType;
+
+ var localTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count;
+
+ if (localTrailerCount > 0)
+ {
+ dto.LocalTrailerCount = localTrailerCount;
+ }
+
+ dto.Name = item.Name;
+ dto.OfficialRating = item.OfficialRating;
+
+ if (fields.Contains(ItemFields.Overview))
+ {
+ dto.Overview = item.Overview;
+ }
+
+ // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance
+ if (dto.BackdropImageTags.Count == 0)
+ {
+ var parentWithBackdrop = GetParentBackdropItem(item);
+
+ if (parentWithBackdrop != null)
+ {
+ dto.ParentBackdropItemId = GetClientItemId(parentWithBackdrop);
+ dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop);
+ }
+ }
+
+ if (item.Parent != null && fields.Contains(ItemFields.ParentId))
+ {
+ dto.ParentId = GetClientItemId(item.Parent);
+ }
+
+ dto.ParentIndexNumber = item.ParentIndexNumber;
+
+ // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance
+ if (!dto.HasLogo)
+ {
+ var parentWithLogo = GetParentLogoItem(item);
+
+ if (parentWithLogo != null)
+ {
+ dto.ParentLogoItemId = GetClientItemId(parentWithLogo);
+
+ dto.ParentLogoImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo));
+ }
+ }
+
+ if (fields.Contains(ItemFields.Path))
+ {
+ dto.Path = item.Path;
+ }
+
+ 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.Taglines))
+ {
+ dto.Taglines = item.Taglines;
+ }
+
+ if (fields.Contains(ItemFields.TrailerUrls))
+ {
+ dto.TrailerUrls = item.TrailerUrls;
+ }
+
+ dto.Type = item.GetType().Name;
+ dto.CommunityRating = item.CommunityRating;
+
+ if (item.IsFolder)
+ {
+ var folder = (Folder)item;
+
+ dto.IsRoot = folder.IsRoot;
+ dto.IsVirtualFolder = folder.IsVirtualFolder;
+
+ if (fields.Contains(ItemFields.IndexOptions))
+ {
+ dto.IndexOptions = folder.IndexByOptionStrings.ToArray();
+ }
+
+ if (fields.Contains(ItemFields.SortOptions))
+ {
+ dto.SortOptions = folder.SortByOptionStrings.ToArray();
+ }
+ }
+
+ // Add audio info
+ var audio = item as Audio;
+ if (audio != null)
+ {
+ if (fields.Contains(ItemFields.AudioInfo))
+ {
+ dto.Album = audio.Album;
+ dto.AlbumArtist = audio.AlbumArtist;
+ dto.Artist = audio.Artist;
+ }
+ }
+
+ // Add video info
+ var video = item as Video;
+ if (video != null)
+ {
+ dto.VideoType = video.VideoType;
+ dto.VideoFormat = video.VideoFormat;
+ dto.IsoType = video.IsoType;
+
+ if (fields.Contains(ItemFields.Chapters) && video.Chapters != null)
+ {
+ dto.Chapters = video.Chapters.Select(c => GetChapterInfoDto(c, item)).ToList();
+ }
+ }
+
+ if (fields.Contains(ItemFields.MediaStreams))
+ {
+ // Add VideoInfo
+ var iHasMediaStreams = item as IHasMediaStreams;
+
+ if (iHasMediaStreams != null)
+ {
+ dto.MediaStreams = iHasMediaStreams.MediaStreams;
+ }
+ }
+
+ // Add MovieInfo
+ var movie = item as Movie;
+
+ if (movie != null)
+ {
+ var specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count;
+
+ if (specialFeatureCount > 0)
+ {
+ dto.SpecialFeatureCount = specialFeatureCount;
+ }
+ }
+
+ if (fields.Contains(ItemFields.SeriesInfo))
+ {
+ // Add SeriesInfo
+ var series = item as Series;
+
+ if (series != null)
+ {
+ dto.AirDays = series.AirDays;
+ dto.AirTime = series.AirTime;
+ dto.Status = series.Status;
+ }
+
+ // Add EpisodeInfo
+ var episode = item as Episode;
+
+ if (episode != null)
+ {
+ series = item.FindParent<Series>();
+
+ dto.SeriesId = GetClientItemId(series);
+ dto.SeriesName = series.Name;
+ }
+
+ // Add SeasonInfo
+ var season = item as Season;
+
+ if (season != null)
+ {
+ series = item.FindParent<Series>();
+
+ dto.SeriesId = GetClientItemId(series);
+ dto.SeriesName = series.Name;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Since it can be slow to make all of these calculations independently, this method will provide a way to do them all at once
+ /// </summary>
+ /// <param name="folder">The folder.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="dto">The dto.</param>
+ private static void SetSpecialCounts(Folder folder, User user, DtoBaseItem dto)
+ {
+ var utcNow = DateTime.UtcNow;
+
+ var rcentlyAddedItemCount = 0;
+ var recursiveItemCount = 0;
+ var favoriteItemsCount = 0;
+ var recentlyAddedUnPlayedItemCount = 0;
+ var resumableItemCount = 0;
+ var recentlyPlayedItemCount = 0;
+
+ double totalPercentPlayed = 0;
+
+ // Loop through each recursive child
+ foreach (var child in folder.GetRecursiveChildren(user))
+ {
+ var userdata = child.GetUserData(user, false);
+
+ if (!child.IsFolder)
+ {
+ recursiveItemCount++;
+
+ // Check is recently added
+ if (child.IsRecentlyAdded(user))
+ {
+ rcentlyAddedItemCount++;
+
+ // Check recently added unplayed
+ if (userdata == null || userdata.PlayCount == 0)
+ {
+ recentlyAddedUnPlayedItemCount++;
+ }
+ }
+
+ // Incrememt totalPercentPlayed
+ if (userdata != null)
+ {
+ if (userdata.PlayCount > 0)
+ {
+ totalPercentPlayed += 100;
+ }
+ else if (userdata.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0)
+ {
+ double itemPercent = userdata.PlaybackPositionTicks;
+ itemPercent /= child.RunTimeTicks.Value;
+ totalPercentPlayed += itemPercent;
+ }
+ }
+ }
+
+ if (userdata != null)
+ {
+ if (userdata.IsFavorite)
+ {
+ favoriteItemsCount++;
+ }
+
+ if (userdata.PlaybackPositionTicks > 0)
+ {
+ resumableItemCount++;
+ }
+
+ if (userdata.LastPlayedDate.HasValue && (utcNow - userdata.LastPlayedDate.Value).TotalDays < Kernel.Instance.Configuration.RecentlyPlayedDays)
+ {
+ recentlyPlayedItemCount++;
+ }
+ }
+ }
+
+ dto.RecursiveItemCount = recursiveItemCount;
+ dto.RecentlyAddedItemCount = rcentlyAddedItemCount;
+ dto.RecentlyAddedUnPlayedItemCount = recentlyAddedUnPlayedItemCount;
+ dto.ResumableItemCount = resumableItemCount;
+ dto.FavoriteItemCount = favoriteItemsCount;
+ dto.RecentlyPlayedItemCount = recentlyPlayedItemCount;
+
+ if (recursiveItemCount > 0)
+ {
+ dto.PlayedPercentage = totalPercentPlayed / recursiveItemCount;
+ }
+ }
+
+ /// <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 static async Task AttachPeople(DtoBaseItem dto, BaseItem item)
+ {
+ if (item.People == null)
+ {
+ return;
+ }
+
+ // Attach People by transforming them into BaseItemPerson (DTO)
+ dto.People = new BaseItemPerson[item.People.Count];
+
+ var entities = await Task.WhenAll(item.People.Select(c =>
+
+ Task.Run(async () =>
+ {
+ try
+ {
+ return await Kernel.Instance.LibraryManager.GetPerson(c.Name).ConfigureAwait(false);
+ }
+ catch (IOException ex)
+ {
+ Logger.LogException("Error getting person {0}", ex, c.Name);
+ return null;
+ }
+ })
+
+ )).ConfigureAwait(false);
+
+ for (var i = 0; i < item.People.Count; i++)
+ {
+ var person = item.People[i];
+
+ var baseItemPerson = new BaseItemPerson
+ {
+ Name = person.Name,
+ Role = person.Role,
+ Type = person.Type
+ };
+
+ var ibnObject = entities[i];
+
+ if (ibnObject != null)
+ {
+ var primaryImagePath = ibnObject.PrimaryImagePath;
+
+ if (!string.IsNullOrEmpty(primaryImagePath))
+ {
+ baseItemPerson.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(ibnObject, ImageType.Primary, primaryImagePath);
+ }
+ }
+
+ dto.People[i] = baseItemPerson;
+ }
+ }
+
+ /// <summary>
+ /// If an item does not any backdrops, this can be used to find the first parent that does have one
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>BaseItem.</returns>
+ private static BaseItem GetParentBackdropItem(BaseItem item)
+ {
+ var parent = item.Parent;
+
+ while (parent != null)
+ {
+ if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Count > 0)
+ {
+ return parent;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// If an item does not have a logo, this can be used to find the first parent that does have one
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>BaseItem.</returns>
+ private static BaseItem GetParentLogoItem(BaseItem item)
+ {
+ var parent = item.Parent;
+
+ while (parent != null)
+ {
+ if (parent.HasImage(ImageType.Logo))
+ {
+ return parent;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the library update info.
+ /// </summary>
+ /// <param name="changeEvent">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ /// <returns>LibraryUpdateInfo.</returns>
+ internal static LibraryUpdateInfo GetLibraryUpdateInfo(ChildrenChangedEventArgs changeEvent)
+ {
+ return new LibraryUpdateInfo
+ {
+ Folder = GetBaseItemInfo(changeEvent.Folder),
+ ItemsAdded = changeEvent.ItemsAdded.Select(GetBaseItemInfo),
+ ItemsRemoved = changeEvent.ItemsRemoved.Select(i => i.Id),
+ ItemsUpdated = changeEvent.ItemsUpdated.Select(i => i.Id)
+ };
+ }
+
+ /// <summary>
+ /// Converts a UserItemData to a DTOUserItemData
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <returns>DtoUserItemData.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public static DtoUserItemData GetDtoUserItemData(UserItemData data)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new DtoUserItemData
+ {
+ IsFavorite = data.IsFavorite,
+ Likes = data.Likes,
+ PlaybackPositionTicks = data.PlaybackPositionTicks,
+ PlayCount = data.PlayCount,
+ Rating = data.Rating,
+ Played = data.Played
+ };
+ }
+
+ /// <summary>
+ /// Gets the chapter info dto.
+ /// </summary>
+ /// <param name="chapterInfo">The chapter info.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>ChapterInfoDto.</returns>
+ private static ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item)
+ {
+ var dto = new ChapterInfoDto
+ {
+ Name = chapterInfo.Name,
+ StartPositionTicks = chapterInfo.StartPositionTicks
+ };
+
+ if (!string.IsNullOrEmpty(chapterInfo.ImagePath))
+ {
+ dto.ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.ChapterImage, chapterInfo.ImagePath);
+ }
+
+ return dto;
+ }
+
+ /// <summary>
+ /// Converts a BaseItem to a BaseItemInfo
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>BaseItemInfo.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public static BaseItemInfo GetBaseItemInfo(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var info = new BaseItemInfo
+ {
+ Id = GetClientItemId(item),
+ Name = item.Name,
+ Type = item.GetType().Name,
+ IsFolder = item.IsFolder,
+ RunTimeTicks = item.RunTimeTicks
+ };
+
+ var imagePath = item.PrimaryImagePath;
+
+ if (!string.IsNullOrEmpty(imagePath))
+ {
+ info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath);
+ }
+
+ if (item.BackdropImagePaths != null && item.BackdropImagePaths.Count > 0)
+ {
+ imagePath = item.BackdropImagePaths[0];
+
+ if (!string.IsNullOrEmpty(imagePath))
+ {
+ info.BackdropImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, imagePath);
+ }
+ }
+
+ return info;
+ }
+
+ /// <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 static string GetClientItemId(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var indexFolder = item as IndexFolder;
+
+ if (indexFolder != null)
+ {
+ return GetClientItemId(indexFolder.Parent) + IndexFolderDelimeter + (indexFolder.IndexName ?? string.Empty) + IndexFolderDelimeter + indexFolder.Id;
+ }
+
+ return item.Id.ToString();
+ }
+
+ /// <summary>
+ /// Converts a User to a DTOUser
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>DtoUser.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public static DtoUser GetDtoUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ var dto = new DtoUser
+ {
+ Id = user.Id,
+ Name = user.Name,
+ HasPassword = !String.IsNullOrEmpty(user.Password),
+ LastActivityDate = user.LastActivityDate,
+ LastLoginDate = user.LastLoginDate,
+ Configuration = user.Configuration
+ };
+
+ var image = user.PrimaryImagePath;
+
+ if (!string.IsNullOrEmpty(image))
+ {
+ dto.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(user, ImageType.Primary, image);
+ }
+
+ return dto;
+ }
+
+ /// <summary>
+ /// Gets a BaseItem based upon it's client-side item id
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>BaseItem.</returns>
+ public static BaseItem GetItemByClientId(string id, Guid? userId = null)
+ {
+ var isIdEmpty = string.IsNullOrEmpty(id);
+
+ // If the item is an indexed folder we have to do a special routine to get it
+ var isIndexFolder = !isIdEmpty &&
+ id.IndexOf(IndexFolderDelimeter, StringComparison.OrdinalIgnoreCase) != -1;
+
+ if (isIndexFolder)
+ {
+ if (userId.HasValue)
+ {
+ return GetIndexFolder(id, userId.Value);
+ }
+ }
+
+ BaseItem item = null;
+
+ if (userId.HasValue)
+ {
+ item = isIdEmpty
+ ? Kernel.Instance.GetUserById(userId.Value).RootFolder
+ : Kernel.Instance.GetItemById(new Guid(id), userId.Value);
+ }
+ else if (!isIndexFolder)
+ {
+ item = Kernel.Instance.GetItemById(new Guid(id));
+ }
+
+ // If we still don't find it, look within individual user views
+ if (item == null && !userId.HasValue)
+ {
+ foreach (var user in Kernel.Instance.Users)
+ {
+ item = GetItemByClientId(id, user.Id);
+
+ if (item != null)
+ {
+ break;
+ }
+ }
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Finds an index folder based on an Id and userId
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>BaseItem.</returns>
+ private static BaseItem GetIndexFolder(string id, Guid userId)
+ {
+ var user = Kernel.Instance.GetUserById(userId);
+
+ var stringSeparators = new[] { IndexFolderDelimeter };
+
+ // Split using the delimeter
+ var values = id.Split(stringSeparators, StringSplitOptions.None).ToList();
+
+ // Get the top folder normally using the first id
+ var folder = GetItemByClientId(values[0], userId) as Folder;
+
+ values.RemoveAt(0);
+
+ // Get indexed folders using the remaining values in the id string
+ return GetIndexFolder(values, folder, user);
+ }
+
+ /// <summary>
+ /// Gets indexed folders based on a list of index names and folder id's
+ /// </summary>
+ /// <param name="values">The values.</param>
+ /// <param name="parentFolder">The parent folder.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>BaseItem.</returns>
+ private static BaseItem GetIndexFolder(List<string> values, Folder parentFolder, User user)
+ {
+ // The index name is first
+ var indexBy = values[0];
+
+ // The index folder id is next
+ var indexFolderId = new Guid(values[1]);
+
+ // Remove them from the lst
+ values.RemoveRange(0, 2);
+
+ // Get the IndexFolder
+ var indexFolder = parentFolder.GetChildren(user, indexBy).FirstOrDefault(i => i.Id == indexFolderId) as Folder;
+
+ // Nested index folder
+ if (values.Count > 0)
+ {
+ return GetIndexFolder(values, indexFolder, user);
+ }
+
+ return indexFolder;
+ }
+
+ /// <summary>
+ /// Gets the backdrop image tags.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>List{System.String}.</returns>
+ private static List<Guid> GetBackdropImageTags(BaseItem item)
+ {
+ if (item.BackdropImagePaths == null)
+ {
+ return new List<Guid>();
+ }
+
+ return item.BackdropImagePaths.Select(p => Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, p)).ToList();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ItemController.cs b/MediaBrowser.Controller/Library/ItemController.cs
deleted file mode 100644
index 54673e538d..0000000000
--- a/MediaBrowser.Controller/Library/ItemController.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Common.Extensions;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Library
-{
- public class ItemController
- {
-
- /// <summary>
- /// Resolves a path into a BaseItem
- /// </summary>
- public async Task<BaseItem> GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null, bool allowInternetProviders = true)
- {
- var args = new ItemResolveEventArgs
- {
- FileInfo = fileInfo ?? FileData.GetFileData(path),
- Parent = parent,
- Cancel = false,
- Path = path
- };
-
- // Gather child folder and files
- if (args.IsDirectory)
- {
- args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
-
- bool isVirtualFolder = parent != null && parent.IsRoot;
- args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
- }
- else
- {
- args.FileSystemChildren = new WIN32_FIND_DATA[] { };
- }
-
-
- // Check to see if we should resolve based on our contents
- if (!EntityResolutionHelper.ShouldResolvePathContents(args))
- {
- return null;
- }
-
- BaseItem item = Kernel.Instance.ResolveItem(args);
-
- return item;
- }
-
- /// <summary>
- /// Gets a Person
- /// </summary>
- public Task<Person> GetPerson(string name)
- {
- return GetImagesByNameItem<Person>(Kernel.Instance.ApplicationPaths.PeoplePath, name);
- }
-
- /// <summary>
- /// Gets a Studio
- /// </summary>
- public Task<Studio> GetStudio(string name)
- {
- return GetImagesByNameItem<Studio>(Kernel.Instance.ApplicationPaths.StudioPath, name);
- }
-
- /// <summary>
- /// Gets a Genre
- /// </summary>
- public Task<Genre> GetGenre(string name)
- {
- return GetImagesByNameItem<Genre>(Kernel.Instance.ApplicationPaths.GenrePath, name);
- }
-
- /// <summary>
- /// Gets a Year
- /// </summary>
- public Task<Year> GetYear(int value)
- {
- return GetImagesByNameItem<Year>(Kernel.Instance.ApplicationPaths.YearPath, value.ToString());
- }
-
- private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
-
- /// <summary>
- /// Generically retrieves an IBN item
- /// </summary>
- private Task<T> GetImagesByNameItem<T>(string path, string name)
- where T : BaseEntity, new()
- {
- name = FileData.GetValidFilename(name);
-
- path = Path.Combine(path, name);
-
- // Look for it in the cache, if it's not there, create it
- if (!ImagesByNameItemCache.ContainsKey(path))
- {
- ImagesByNameItemCache[path] = CreateImagesByNameItem<T>(path, name);
- }
-
- return ImagesByNameItemCache[path] as Task<T>;
- }
-
- /// <summary>
- /// Creates an IBN item based on a given path
- /// </summary>
- private async Task<T> CreateImagesByNameItem<T>(string path, string name)
- where T : BaseEntity, new()
- {
- var item = new T { };
-
- item.Name = name;
- item.Id = path.GetMD5();
-
- if (!Directory.Exists(path))
- {
- Directory.CreateDirectory(path);
- }
-
- item.DateCreated = Directory.GetCreationTimeUtc(path);
- item.DateModified = Directory.GetLastWriteTimeUtc(path);
-
- var args = new ItemResolveEventArgs { };
- args.FileInfo = FileData.GetFileData(path);
- args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
-
- await Kernel.Instance.ExecuteMetadataProviders(item).ConfigureAwait(false);
-
- return item;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
new file mode 100644
index 0000000000..c95300f745
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -0,0 +1,397 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// These are arguments relating to the file system that are collected once and then referred to
+ /// whenever needed. Primarily for entity resolution.
+ /// </summary>
+ public class ItemResolveArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets the file system children.
+ /// </summary>
+ /// <value>The file system children.</value>
+ public IEnumerable<WIN32_FIND_DATA> FileSystemChildren
+ {
+ get { return FileSystemDictionary.Values; }
+ }
+
+ /// <summary>
+ /// Gets or sets the file system dictionary.
+ /// </summary>
+ /// <value>The file system dictionary.</value>
+ public Dictionary<string, WIN32_FIND_DATA> FileSystemDictionary { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent.
+ /// </summary>
+ /// <value>The parent.</value>
+ public Folder Parent { get; set; }
+
+ /// <summary>
+ /// Gets or sets the file info.
+ /// </summary>
+ /// <value>The file info.</value>
+ public WIN32_FIND_DATA FileInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is directory.
+ /// </summary>
+ /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value>
+ public bool IsDirectory
+ {
+ get
+ {
+ return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory);
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden
+ {
+ get
+ {
+ return FileInfo.IsHidden;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is system file.
+ /// </summary>
+ /// <value><c>true</c> if this instance is system file; otherwise, <c>false</c>.</value>
+ public bool IsSystemFile
+ {
+ get
+ {
+ return FileInfo.IsSystemFile;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is vf.
+ /// </summary>
+ /// <value><c>true</c> if this instance is vf; otherwise, <c>false</c>.</value>
+ public bool IsVf
+ {
+ // we should be considered a virtual folder if we are a child of one of the children of the system root folder.
+ // this is a bit of a trick to determine that... the directory name of a sub-child of the root will start with
+ // the root but not be equal to it
+ get
+ {
+ if (!IsDirectory)
+ {
+ return false;
+ }
+
+ var parentDir = FileInfo.Path != null ? System.IO.Path.GetDirectoryName(FileInfo.Path) ?? string.Empty : string.Empty;
+
+ return (parentDir.Length > Kernel.Instance.ApplicationPaths.RootFolderPath.Length
+ && parentDir.StartsWith(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase));
+
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is physical root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
+ public bool IsPhysicalRoot
+ {
+ get
+ {
+ return IsDirectory && Path.Equals(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
+ public bool IsRoot
+ {
+ get
+ {
+ return Parent == null;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the additional locations.
+ /// </summary>
+ /// <value>The additional locations.</value>
+ private List<string> AdditionalLocations { get; set; }
+
+ /// <summary>
+ /// Adds the additional location.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddAdditionalLocation(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (AdditionalLocations == null)
+ {
+ AdditionalLocations = new List<string>();
+ }
+
+ AdditionalLocations.Add(path);
+ }
+
+ /// <summary>
+ /// Gets the physical locations.
+ /// </summary>
+ /// <value>The physical locations.</value>
+ public IEnumerable<string> PhysicalLocations
+ {
+ get
+ {
+ var paths = string.IsNullOrWhiteSpace(Path) ? new string[] {} : new[] {Path};
+ return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations);
+ }
+ }
+
+ /// <summary>
+ /// Store these to reduce disk access in Resolvers
+ /// </summary>
+ /// <value>The metadata file dictionary.</value>
+ private Dictionary<string, WIN32_FIND_DATA> MetadataFileDictionary { get; set; }
+
+ /// <summary>
+ /// Gets the metadata files.
+ /// </summary>
+ /// <value>The metadata files.</value>
+ public IEnumerable<WIN32_FIND_DATA> MetadataFiles
+ {
+ get
+ {
+ if (MetadataFileDictionary != null)
+ {
+ return MetadataFileDictionary.Values;
+ }
+
+ return new WIN32_FIND_DATA[] {};
+ }
+ }
+
+ /// <summary>
+ /// Adds the metadata file.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <exception cref="System.IO.FileNotFoundException"></exception>
+ public void AddMetadataFile(string path)
+ {
+ var file = FileSystem.GetFileData(path);
+
+ if (!file.HasValue)
+ {
+ throw new FileNotFoundException(path);
+ }
+
+ AddMetadataFile(file.Value);
+ }
+
+ /// <summary>
+ /// Adds the metadata file.
+ /// </summary>
+ /// <param name="fileInfo">The file info.</param>
+ public void AddMetadataFile(WIN32_FIND_DATA fileInfo)
+ {
+ AddMetadataFiles(new[] { fileInfo });
+ }
+
+ /// <summary>
+ /// Adds the metadata files.
+ /// </summary>
+ /// <param name="files">The files.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddMetadataFiles(IEnumerable<WIN32_FIND_DATA> files)
+ {
+ if (files == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (MetadataFileDictionary == null)
+ {
+ MetadataFileDictionary = new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
+ }
+ foreach (var file in files)
+ {
+ MetadataFileDictionary[file.cFileName] = file;
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the file system entry by.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public WIN32_FIND_DATA? GetFileSystemEntryByName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name));
+ }
+
+ /// <summary>
+ /// Gets the file system entry by path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public WIN32_FIND_DATA? GetFileSystemEntryByPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (FileSystemDictionary != null)
+ {
+ WIN32_FIND_DATA entry;
+
+ if (FileSystemDictionary.TryGetValue(path, out entry))
+ {
+ return entry;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the meta file by path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public WIN32_FIND_DATA? GetMetaFileByPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (MetadataFileDictionary != null)
+ {
+ WIN32_FIND_DATA entry;
+
+ if (MetadataFileDictionary.TryGetValue(System.IO.Path.GetFileName(path), out entry))
+ {
+ return entry;
+ }
+ }
+
+ return GetFileSystemEntryByPath(path);
+ }
+
+ /// <summary>
+ /// Gets the name of the meta file by.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public WIN32_FIND_DATA? GetMetaFileByName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (MetadataFileDictionary != null)
+ {
+ WIN32_FIND_DATA entry;
+
+ if (MetadataFileDictionary.TryGetValue(name, out entry))
+ {
+ return entry;
+ }
+ }
+
+ return GetFileSystemEntryByName(name);
+ }
+
+ /// <summary>
+ /// Determines whether [contains meta file by name] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [contains meta file by name] [the specified name]; otherwise, <c>false</c>.</returns>
+ public bool ContainsMetaFileByName(string name)
+ {
+ return GetMetaFileByName(name).HasValue;
+ }
+
+ /// <summary>
+ /// Determines whether [contains file system entry by name] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
+ public bool ContainsFileSystemEntryByName(string name)
+ {
+ return GetFileSystemEntryByName(name).HasValue;
+ }
+
+ #region Equality Overrides
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The object to compare with the current object.</param>
+ /// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return (Equals(obj as ItemResolveArgs));
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
+ public override int GetHashCode()
+ {
+ return Path.GetHashCode();
+ }
+
+ /// <summary>
+ /// Equalses the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected bool Equals(ItemResolveArgs args)
+ {
+ if (args != null)
+ {
+ if (args.Path == null && Path == null) return true;
+ return args.Path != null && args.Path.Equals(Path, StringComparison.OrdinalIgnoreCase);
+ }
+ return false;
+ }
+
+ #endregion
+ }
+
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs b/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs
deleted file mode 100644
index 32b8783dfd..0000000000
--- a/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.IO;
-using System.Collections.Generic;
-using System.Linq;
-using System;
-using System.IO;
-
-namespace MediaBrowser.Controller.Library
-{
- /// <summary>
- /// This is an EventArgs object used when resolving a Path into a BaseItem
- /// </summary>
- public class ItemResolveEventArgs : PreBeginResolveEventArgs
- {
- public WIN32_FIND_DATA[] FileSystemChildren { get; set; }
-
- protected List<string> _additionalLocations = new List<string>();
- public List<string> AdditionalLocations
- {
- get
- {
- return _additionalLocations;
- }
- set
- {
- _additionalLocations = value;
- }
- }
-
- public IEnumerable<string> PhysicalLocations
- {
- get
- {
- return (new List<string>() {this.Path}).Concat(AdditionalLocations);
- }
- }
-
- public bool IsBDFolder { get; set; }
- public bool IsDVDFolder { get; set; }
- public bool IsHDDVDFolder { get; set; }
-
- /// <summary>
- /// Store these to reduce disk access in Resolvers
- /// </summary>
- public string[] MetadataFiles { get; set; }
-
- public WIN32_FIND_DATA? GetFileSystemEntry(string path)
- {
- WIN32_FIND_DATA entry = FileSystemChildren.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
- return entry.cFileName != null ? (WIN32_FIND_DATA?)entry : null;
- }
-
- public bool ContainsFile(string name)
- {
- return FileSystemChildren.FirstOrDefault(f => f.cFileName.Equals(name, StringComparison.OrdinalIgnoreCase)).cFileName != null;
- }
-
- public bool ContainsFolder(string name)
- {
- return ContainsFile(name);
- }
- }
-
- /// <summary>
- /// This is an EventArgs object used before we begin resolving a Path into a BaseItem
- /// File system children have not been collected yet, but consuming events will
- /// have a chance to cancel resolution based on the Path, Parent and FileAttributes
- /// </summary>
- public class PreBeginResolveEventArgs : EventArgs
- {
- public Folder Parent { get; set; }
-
- public bool Cancel { get; set; }
-
- public WIN32_FIND_DATA FileInfo { get; set; }
-
- public string Path { get; set; }
-
- public bool IsDirectory
- {
- get
- {
- return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory);
- }
- }
-
- public bool IsHidden
- {
- get
- {
- return FileInfo.IsHidden;
- }
- }
-
- public bool IsSystemFile
- {
- get
- {
- return FileInfo.IsSystemFile;
- }
- }
-
- }
-}
diff --git a/MediaBrowser.Controller/Library/LibraryManager.cs b/MediaBrowser.Controller/Library/LibraryManager.cs
new file mode 100644
index 0000000000..7d3c764b2d
--- /dev/null
+++ b/MediaBrowser.Controller/Library/LibraryManager.cs
@@ -0,0 +1,511 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Tasks;
+using MoreLinq;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class LibraryManager
+ /// </summary>
+ public class LibraryManager : BaseManager<Kernel>
+ {
+ #region LibraryChanged Event
+ /// <summary>
+ /// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args.
+ /// *** Will fire asynchronously. ***
+ /// </summary>
+ public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
+
+ /// <summary>
+ /// Raises the <see cref="E:LibraryChanged" /> event.
+ /// </summary>
+ /// <param name="args">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ internal void OnLibraryChanged(ChildrenChangedEventArgs args)
+ {
+ EventHelper.QueueEventIfNotNull(LibraryChanged, this, args);
+
+ // Had to put this in a separate method to avoid an implicitly captured closure
+ SendLibraryChangedWebSocketMessage(args);
+ }
+
+ /// <summary>
+ /// Sends the library changed web socket message.
+ /// </summary>
+ /// <param name="args">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ private void SendLibraryChangedWebSocketMessage(ChildrenChangedEventArgs args)
+ {
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("LibraryChanged", () => DtoBuilder.GetLibraryUpdateInfo(args));
+ }
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public LibraryManager(Kernel kernel)
+ : base(kernel)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the item.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ public BaseItem ResolveItem(ItemResolveArgs args)
+ {
+ return Kernel.EntityResolvers.Select(r => r.ResolvePath(args)).FirstOrDefault(i => i != null);
+ }
+
+ /// <summary>
+ /// Resolves a path into a BaseItem
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="fileInfo">The file info.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public BaseItem GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ fileInfo = fileInfo ?? FileSystem.GetFileData(path);
+
+ if (!fileInfo.HasValue)
+ {
+ return null;
+ }
+
+ var args = new ItemResolveArgs
+ {
+ Parent = parent,
+ Path = path,
+ FileInfo = fileInfo.Value
+ };
+
+ // Return null if ignore rules deem that we should do so
+ if (Kernel.EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args)))
+ {
+ return null;
+ }
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ // When resolving the root, we need it's grandchildren (children of user views)
+ var flattenFolderDepth = args.IsPhysicalRoot ? 2 : 0;
+
+ args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, flattenFolderDepth: flattenFolderDepth, args: args);
+ }
+
+ // Check to see if we should resolve based on our contents
+ if (args.IsDirectory && !EntityResolutionHelper.ShouldResolvePathContents(args))
+ {
+ return null;
+ }
+
+ return ResolveItem(args);
+ }
+
+ /// <summary>
+ /// Resolves a set of files into a list of BaseItem
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="files">The files.</param>
+ /// <param name="parent">The parent.</param>
+ /// <returns>List{``0}.</returns>
+ public List<T> GetItems<T>(IEnumerable<WIN32_FIND_DATA> files, Folder parent)
+ where T : BaseItem
+ {
+ var list = new List<T>();
+
+ Parallel.ForEach(files, f =>
+ {
+ try
+ {
+ var item = GetItem(f.Path, parent, f) as T;
+
+ if (item != null)
+ {
+ lock (list)
+ {
+ list.Add(item);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error resolving path {0}", ex, f.Path);
+ }
+ });
+
+ return list;
+ }
+
+ /// <summary>
+ /// Creates the root media folder
+ /// </summary>
+ /// <returns>AggregateFolder.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot create the root folder until plugins have loaded</exception>
+ internal AggregateFolder CreateRootFolder()
+ {
+ if (Kernel.Plugins == null)
+ {
+ throw new InvalidOperationException("Cannot create the root folder until plugins have loaded");
+ }
+
+ var rootFolderPath = Kernel.ApplicationPaths.RootFolderPath;
+ var rootFolder = Kernel.ItemRepository.RetrieveItem(rootFolderPath.GetMBId(typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)GetItem(rootFolderPath);
+
+ // Add in the plug-in folders
+ foreach (var child in Kernel.PluginFolders)
+ {
+ rootFolder.AddVirtualChild(child);
+ }
+
+ return rootFolder;
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Person}.</returns>
+ public Task<Person> GetPerson(string name, bool allowSlowProviders = false)
+ {
+ return GetPerson(name, CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Person}.</returns>
+ private Task<Person> GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false)
+ {
+ return GetImagesByNameItem<Person>(Kernel.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Studio}.</returns>
+ public Task<Studio> GetStudio(string name, bool allowSlowProviders = false)
+ {
+ return GetImagesByNameItem<Studio>(Kernel.ApplicationPaths.StudioPath, name, CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Genre}.</returns>
+ public Task<Genre> GetGenre(string name, bool allowSlowProviders = false)
+ {
+ return GetImagesByNameItem<Genre>(Kernel.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// The us culture
+ /// </summary>
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Year}.</returns>
+ /// <exception cref="System.ArgumentOutOfRangeException"></exception>
+ public Task<Year> GetYear(int value, bool allowSlowProviders = false)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return GetImagesByNameItem<Year>(Kernel.ApplicationPaths.YearPath, value.ToString(UsCulture), CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// The images by name item cache
+ /// </summary>
+ private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Generically retrieves an IBN item
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="path">The path.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{``0}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ private Task<T> GetImagesByNameItem<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true)
+ where T : BaseItem, new()
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ var key = Path.Combine(path, FileSystem.GetValidFilename(name));
+
+ var obj = ImagesByNameItemCache.GetOrAdd(key, keyname => CreateImagesByNameItem<T>(path, name, cancellationToken, allowSlowProviders));
+
+ return obj as Task<T>;
+ }
+
+ /// <summary>
+ /// Creates an IBN item based on a given path
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="path">The path.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{``0}.</returns>
+ /// <exception cref="System.IO.IOException">Path not created: + path</exception>
+ private async Task<T> CreateImagesByNameItem<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true)
+ where T : BaseItem, new()
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Logger.Debug("Creating {0}: {1}", typeof(T).Name, name);
+
+ path = Path.Combine(path, FileSystem.GetValidFilename(name));
+
+ var fileInfo = FileSystem.GetFileData(path);
+
+ var isNew = false;
+
+ if (!fileInfo.HasValue)
+ {
+ Directory.CreateDirectory(path);
+ fileInfo = FileSystem.GetFileData(path);
+
+ if (!fileInfo.HasValue)
+ {
+ throw new IOException("Path not created: " + path);
+ }
+
+ isNew = true;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var id = path.GetMBId(typeof(T));
+
+ var item = Kernel.ItemRepository.RetrieveItem(id) as T;
+ if (item == null)
+ {
+ item = new T
+ {
+ Name = name,
+ Id = id,
+ DateCreated = fileInfo.Value.CreationTimeUtc,
+ DateModified = fileInfo.Value.LastWriteTimeUtc,
+ Path = path
+ };
+ isNew = true;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Set this now so we don't cause additional file system access during provider executions
+ item.ResetResolveArgs(fileInfo);
+
+ await item.RefreshMetadata(cancellationToken, isNew, allowSlowProviders: allowSlowProviders).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return item;
+ }
+
+ /// <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>
+ internal async Task ValidatePeople(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ // Clear the IBN cache
+ ImagesByNameItemCache.Clear();
+
+ const int maxTasks = 250;
+
+ var tasks = new List<Task>();
+
+ var includedPersonTypes = new[] { PersonType.Actor, PersonType.Director };
+
+ var people = Kernel.RootFolder.RecursiveChildren
+ .Where(c => c.People != null)
+ .SelectMany(c => c.People.Where(p => includedPersonTypes.Contains(p.Type)))
+ .DistinctBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ var numComplete = 0;
+
+ foreach (var person in people)
+ {
+ if (tasks.Count > maxTasks)
+ {
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ tasks.Clear();
+
+ // Safe cancellation point, when there are no pending tasks
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ // Avoid accessing the foreach variable within the closure
+ var currentPerson = person;
+
+ tasks.Add(Task.Run(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ await GetPerson(currentPerson.Name, cancellationToken, allowSlowProviders: true).ConfigureAwait(false);
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error validating IBN entry {0}", ex, currentPerson.Name);
+ }
+
+ // Update progress
+ lock (progress)
+ {
+ numComplete++;
+ double percent = numComplete;
+ percent /= people.Count;
+
+ progress.Report(new TaskProgress { PercentComplete = 100 * percent });
+ }
+ }));
+ }
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+
+ Logger.Info("People validation complete");
+ }
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ internal async Task ValidateMediaLibrary(IProgress<TaskProgress> progress, CancellationToken cancellationToken)
+ {
+ Logger.Info("Validating media library");
+
+ await Kernel.RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+ // Start by just validating the children of the root, but go no further
+ await Kernel.RootFolder.ValidateChildren(new Progress<TaskProgress> { }, cancellationToken, recursive: false);
+
+ // Validate only the collection folders for each user, just to make them available as quickly as possible
+ var userCollectionFolderTasks = Kernel.Users.AsParallel().Select(user => user.ValidateCollectionFolders(new Progress<TaskProgress> { }, cancellationToken));
+ await Task.WhenAll(userCollectionFolderTasks).ConfigureAwait(false);
+
+ // Now validate the entire media library
+ await Kernel.RootFolder.ValidateChildren(progress, cancellationToken, recursive: true).ConfigureAwait(false);
+
+ foreach (var user in Kernel.Users)
+ {
+ await user.ValidateMediaLibrary(new Progress<TaskProgress> { }, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Saves display preferences for a Folder
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="folder">The folder.</param>
+ /// <param name="data">The data.</param>
+ /// <returns>Task.</returns>
+ public Task SaveDisplayPreferencesForFolder(User user, Folder folder, DisplayPreferences data)
+ {
+ // Need to update all items with the same DisplayPrefsId
+ foreach (var child in Kernel.RootFolder.GetRecursiveChildren(user)
+ .OfType<Folder>()
+ .Where(i => i.DisplayPrefsId == folder.DisplayPrefsId))
+ {
+ child.AddOrUpdateDisplayPrefs(user, data);
+ }
+
+ return Kernel.DisplayPreferencesRepository.SaveDisplayPrefs(folder, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Gets the default view.
+ /// </summary>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ public IEnumerable<VirtualFolderInfo> GetDefaultVirtualFolders()
+ {
+ return GetView(Kernel.ApplicationPaths.DefaultUserViewsPath);
+ }
+
+ /// <summary>
+ /// Gets the view.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ public IEnumerable<VirtualFolderInfo> GetVirtualFolders(User user)
+ {
+ return GetView(user.RootFolderPath);
+ }
+
+ /// <summary>
+ /// Gets the view.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ private IEnumerable<VirtualFolderInfo> GetView(string path)
+ {
+ return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
+ .Select(dir => new VirtualFolderInfo
+ {
+ Name = Path.GetFileName(dir),
+ Locations = Directory.EnumerateFiles(dir, "*.lnk", SearchOption.TopDirectoryOnly).Select(FileSystem.ResolveShortcut).ToList()
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs
new file mode 100644
index 0000000000..4daa9d654c
--- /dev/null
+++ b/MediaBrowser.Controller/Library/Profiler.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Diagnostics;
+using MediaBrowser.Common.Logging;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class Profiler
+ /// </summary>
+ public class Profiler : IDisposable
+ {
+ /// <summary>
+ /// The name
+ /// </summary>
+ readonly string name;
+ /// <summary>
+ /// The stopwatch
+ /// </summary>
+ readonly Stopwatch stopwatch;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Profiler" /> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ public Profiler(string name)
+ {
+ this.name = name;
+
+ stopwatch = new Stopwatch();
+ stopwatch.Start();
+ }
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ stopwatch.Stop();
+ string message;
+ if (stopwatch.ElapsedMilliseconds > 300000)
+ {
+ message = string.Format("{0} took {1} minutes.",
+ name, ((float)stopwatch.ElapsedMilliseconds / 60000).ToString("F"));
+ }
+ else
+ {
+ message = string.Format("{0} took {1} seconds.",
+ name, ((float)stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000"));
+ }
+ Logger.LogInfo(message);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ResourcePool.cs b/MediaBrowser.Controller/Library/ResourcePool.cs
new file mode 100644
index 0000000000..3e4d532041
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ResourcePool.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// This is just a collection of semaphores to control the number of concurrent executions of various resources
+ /// </summary>
+ public class ResourcePool : IDisposable
+ {
+ /// <summary>
+ /// You tube
+ /// </summary>
+ public readonly SemaphoreSlim YouTube = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The trakt
+ /// </summary>
+ public readonly SemaphoreSlim Trakt = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The tv db
+ /// </summary>
+ public readonly SemaphoreSlim TvDb = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The movie db
+ /// </summary>
+ public readonly SemaphoreSlim MovieDb = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The fan art
+ /// </summary>
+ public readonly SemaphoreSlim FanArt = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The mb
+ /// </summary>
+ public readonly SemaphoreSlim Mb = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// Apple doesn't seem to like too many simulataneous requests.
+ /// </summary>
+ public readonly SemaphoreSlim AppleTrailerVideos = new SemaphoreSlim(1, 1);
+
+ /// <summary>
+ /// The apple trailer images
+ /// </summary>
+ public readonly SemaphoreSlim AppleTrailerImages = new SemaphoreSlim(1, 1);
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ YouTube.Dispose();
+ Trakt.Dispose();
+ TvDb.Dispose();
+ MovieDb.Dispose();
+ FanArt.Dispose();
+ Mb.Dispose();
+ AppleTrailerVideos.Dispose();
+ AppleTrailerImages.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/UserDataManager.cs b/MediaBrowser.Controller/Library/UserDataManager.cs
new file mode 100644
index 0000000000..dfa80483ec
--- /dev/null
+++ b/MediaBrowser.Controller/Library/UserDataManager.cs
@@ -0,0 +1,219 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Connectivity;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class UserDataManager
+ /// </summary>
+ public class UserDataManager : BaseManager<Kernel>
+ {
+ #region Events
+ /// <summary>
+ /// Occurs when [playback start].
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
+ /// <summary>
+ /// Occurs when [playback progress].
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+ /// <summary>
+ /// Occurs when [playback stopped].
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackStopped;
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserDataManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public UserDataManager(Kernel kernel)
+ : base(kernel)
+ {
+
+ }
+
+ /// <summary>
+ /// Used to report that playback has started for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void OnPlaybackStart(User user, BaseItem item, ClientType clientType, string deviceName)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item);
+
+ // Nothing to save here
+ // Fire events to inform plugins
+ EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs
+ {
+ Argument = item,
+ User = user
+ });
+ }
+
+ /// <summary>
+ /// Used to report playback progress for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="positionTicks">The position ticks.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item, positionTicks);
+
+ if (positionTicks.HasValue)
+ {
+ var data = item.GetUserData(user, true);
+
+ UpdatePlayState(item, data, positionTicks.Value, false);
+ await SaveUserDataForItem(user, item, data).ConfigureAwait(false);
+ }
+
+ EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs
+ {
+ Argument = item,
+ User = user,
+ PlaybackPositionTicks = positionTicks
+ });
+ }
+
+ /// <summary>
+ /// Used to report that playback has ended for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="positionTicks">The position ticks.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ Kernel.UserManager.RemoveNowPlayingItemId(user, clientType, deviceName, item);
+
+ var data = item.GetUserData(user, true);
+
+ if (positionTicks.HasValue)
+ {
+ UpdatePlayState(item, data, positionTicks.Value, true);
+ }
+ else
+ {
+ // If the client isn't able to report this, then we'll just have to make an assumption
+ data.PlayCount++;
+ data.Played = true;
+ }
+
+ await SaveUserDataForItem(user, item, data).ConfigureAwait(false);
+
+ EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs
+ {
+ Argument = item,
+ User = user,
+ PlaybackPositionTicks = positionTicks
+ });
+ }
+
+ /// <summary>
+ /// Updates playstate position for an item but does not save
+ /// </summary>
+ /// <param name="item">The item</param>
+ /// <param name="data">User data for the item</param>
+ /// <param name="positionTicks">The current playback position</param>
+ /// <param name="incrementPlayCount">Whether or not to increment playcount</param>
+ private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount)
+ {
+ // If a position has been reported, and if we know the duration
+ if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0)
+ {
+ var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100;
+
+ // Don't track in very beginning
+ if (pctIn < Kernel.Configuration.MinResumePct)
+ {
+ positionTicks = 0;
+ incrementPlayCount = false;
+ }
+
+ // If we're at the end, assume completed
+ else if (pctIn > Kernel.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value)
+ {
+ positionTicks = 0;
+ data.Played = true;
+ }
+
+ else
+ {
+ // Enforce MinResumeDuration
+ var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
+
+ if (durationSeconds < Kernel.Configuration.MinResumeDurationSeconds)
+ {
+ positionTicks = 0;
+ data.Played = true;
+ }
+ }
+ }
+
+ data.PlaybackPositionTicks = positionTicks;
+
+ if (incrementPlayCount)
+ {
+ data.PlayCount++;
+ data.LastPlayedDate = DateTime.UtcNow;
+ }
+ }
+
+ /// <summary>
+ /// Saves user data for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="data">The data.</param>
+ public Task SaveUserDataForItem(User user, BaseItem item, UserItemData data)
+ {
+ item.AddOrUpdateUserData(user, data);
+
+ return Kernel.UserDataRepository.SaveUserData(item, CancellationToken.None);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/UserManager.cs b/MediaBrowser.Controller/Library/UserManager.cs
new file mode 100644
index 0000000000..af3239657b
--- /dev/null
+++ b/MediaBrowser.Controller/Library/UserManager.cs
@@ -0,0 +1,395 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Connectivity;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class UserManager
+ /// </summary>
+ public class UserManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// The _active connections
+ /// </summary>
+ private readonly ConcurrentBag<ClientConnectionInfo> _activeConnections =
+ new ConcurrentBag<ClientConnectionInfo>();
+
+ /// <summary>
+ /// Gets all connections.
+ /// </summary>
+ /// <value>All connections.</value>
+ public IEnumerable<ClientConnectionInfo> AllConnections
+ {
+ get { return _activeConnections.Where(c => Kernel.GetUserById(c.UserId) != null).OrderByDescending(c => c.LastActivityDate); }
+ }
+
+ /// <summary>
+ /// Gets the active connections.
+ /// </summary>
+ /// <value>The active connections.</value>
+ public IEnumerable<ClientConnectionInfo> ActiveConnections
+ {
+ get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 10); }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public UserManager(Kernel kernel)
+ : base(kernel)
+ {
+ }
+
+ #region UserUpdated Event
+ /// <summary>
+ /// Occurs when [user updated].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<User>> UserUpdated;
+
+ /// <summary>
+ /// Called when [user updated].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ internal void OnUserUpdated(User user)
+ {
+ EventHelper.QueueEventIfNotNull(UserUpdated, this, new GenericEventArgs<User> { Argument = user });
+
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("UserUpdated", DtoBuilder.GetDtoUser(user));
+ }
+ #endregion
+
+ #region UserDeleted Event
+ /// <summary>
+ /// Occurs when [user deleted].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<User>> UserDeleted;
+ /// <summary>
+ /// Called when [user deleted].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ internal void OnUserDeleted(User user)
+ {
+ EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user });
+
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("UserDeleted", user.Id.ToString());
+ }
+ #endregion
+
+ /// <summary>
+ /// Authenticates a User and returns a result indicating whether or not it succeeded
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="password">The password.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public async Task<bool> AuthenticateUser(User user, string password)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ password = password ?? string.Empty;
+ var existingPassword = string.IsNullOrEmpty(user.Password) ? string.Empty.GetMD5().ToString() : user.Password;
+
+ var success = password.GetMD5().ToString().Equals(existingPassword);
+
+ // Update LastActivityDate and LastLoginDate, then save
+ if (success)
+ {
+ user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
+ await UpdateUser(user).ConfigureAwait(false);
+ }
+
+ Logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied"));
+
+ return success;
+ }
+
+ /// <summary>
+ /// Logs the user activity.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public Task LogUserActivity(User user, ClientType clientType, string deviceName)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ var activityDate = DateTime.UtcNow;
+
+ user.LastActivityDate = activityDate;
+
+ LogConnection(user.Id, clientType, deviceName, activityDate);
+
+ // Save this directly. No need to fire off all the events for this.
+ return Kernel.UserRepository.SaveUser(user, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Updates the now playing item id.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="currentPositionTicks">The current position ticks.</param>
+ public void UpdateNowPlayingItemId(User user, ClientType clientType, string deviceName, BaseItem item, long? currentPositionTicks = null)
+ {
+ var conn = GetConnection(user.Id, clientType, deviceName);
+
+ conn.NowPlayingPositionTicks = currentPositionTicks;
+ conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item);
+ }
+
+ /// <summary>
+ /// Removes the now playing item id.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="item">The item.</param>
+ public void RemoveNowPlayingItemId(User user, ClientType clientType, string deviceName, BaseItem item)
+ {
+ var conn = GetConnection(user.Id, clientType, deviceName);
+
+ if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString()))
+ {
+ conn.NowPlayingItem = null;
+ conn.NowPlayingPositionTicks = null;
+ }
+ }
+
+ /// <summary>
+ /// Logs the connection.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="lastActivityDate">The last activity date.</param>
+ private void LogConnection(Guid userId, ClientType clientType, string deviceName, DateTime lastActivityDate)
+ {
+ GetConnection(userId, clientType, deviceName).LastActivityDate = lastActivityDate;
+ }
+
+ /// <summary>
+ /// Gets the connection.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>ClientConnectionInfo.</returns>
+ private ClientConnectionInfo GetConnection(Guid userId, ClientType clientType, string deviceName)
+ {
+ var conn = _activeConnections.FirstOrDefault(c => c.UserId == userId && c.ClientType == clientType && string.Equals(deviceName, c.DeviceName, StringComparison.OrdinalIgnoreCase));
+
+ if (conn == null)
+ {
+ conn = new ClientConnectionInfo
+ {
+ UserId = userId,
+ ClientType = clientType,
+ DeviceName = deviceName
+ };
+
+ _activeConnections.Add(conn);
+ }
+
+ return conn;
+ }
+
+ /// <summary>
+ /// Loads the users from the repository
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ internal IEnumerable<User> LoadUsers()
+ {
+ var users = Kernel.UserRepository.RetrieveAllUsers().ToList();
+
+ // There always has to be at least one user.
+ if (users.Count == 0)
+ {
+ var name = Environment.UserName;
+
+ var user = InstantiateNewUser(name);
+
+ var task = Kernel.UserRepository.SaveUser(user, CancellationToken.None);
+
+ // Hate having to block threads
+ Task.WaitAll(task);
+
+ users.Add(user);
+ }
+
+ return users;
+ }
+
+ /// <summary>
+ /// Refreshes metadata for each user
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <returns>Task.</returns>
+ public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false)
+ {
+ var tasks = Kernel.Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList();
+
+ return Task.WhenAll(tasks);
+ }
+
+ /// <summary>
+ /// Renames the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="newName">The new name.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ public async Task RenameUser(User user, string newName)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ if (string.IsNullOrEmpty(newName))
+ {
+ throw new ArgumentNullException("newName");
+ }
+
+ if (Kernel.Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
+ {
+ throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName));
+ }
+
+ if (user.Name.Equals(newName, StringComparison.Ordinal))
+ {
+ throw new ArgumentException("The new and old names must be different.");
+ }
+
+ await user.Rename(newName);
+
+ OnUserUpdated(user);
+ }
+
+ /// <summary>
+ /// Updates the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ public async Task UpdateUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ if (user.Id == Guid.Empty || !Kernel.Users.Any(u => u.Id.Equals(user.Id)))
+ {
+ throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
+ }
+
+ user.DateModified = DateTime.UtcNow;
+
+ await Kernel.UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
+
+ OnUserUpdated(user);
+ }
+
+ /// <summary>
+ /// Creates the user.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>User.</returns>
+ /// <exception cref="System.ArgumentNullException">name</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ public async Task<User> CreateUser(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ if (Kernel.Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
+ {
+ throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
+ }
+
+ var user = InstantiateNewUser(name);
+
+ var list = Kernel.Users.ToList();
+ list.Add(user);
+ Kernel.Users = list;
+
+ await Kernel.UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
+
+ return user;
+ }
+
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ public async Task DeleteUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ if (Kernel.Users.FirstOrDefault(u => u.Id == user.Id) == null)
+ {
+ throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id));
+ }
+
+ if (Kernel.Users.Count() == 1)
+ {
+ throw new ArgumentException(string.Format("The user '{0}' be deleted because there must be at least one user in the system.", user.Name));
+ }
+
+ await Kernel.UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false);
+
+ OnUserDeleted(user);
+
+ // Force this to be lazy loaded again
+ Kernel.Users = null;
+ }
+
+ /// <summary>
+ /// Instantiates the new user.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>User.</returns>
+ private User InstantiateNewUser(string name)
+ {
+ return new User
+ {
+ Name = name,
+ Id = ("MBUser" + name).GetMD5(),
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/AURatingsDictionary.cs b/MediaBrowser.Controller/Localization/AURatingsDictionary.cs
new file mode 100644
index 0000000000..882302f103
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/AURatingsDictionary.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class AURatingsDictionary
+ /// </summary>
+ public class AURatingsDictionary : Dictionary<string, int>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AURatingsDictionary" /> class.
+ /// </summary>
+ public AURatingsDictionary()
+ {
+ Add("AU-G", 1);
+ Add("AU-PG", 5);
+ Add("AU-M", 6);
+ Add("AU-M15+", 7);
+ Add("AU-R18+", 9);
+ Add("AU-X18+", 10);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/BaseStrings.cs b/MediaBrowser.Controller/Localization/BaseStrings.cs
new file mode 100644
index 0000000000..c76df7c7c8
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/BaseStrings.cs
@@ -0,0 +1,290 @@
+using MediaBrowser.Common.Localization;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Localization
+{
+ [Export(typeof(LocalizedStringData))]
+ public class BaseStrings : LocalizedStringData
+ {
+ public BaseStrings()
+ {
+ ThisVersion = "1.0002";
+ Prefix = LocalizedStrings.BasePrefix;
+ }
+
+
+
+ //Config Panel
+ public string ConfigConfig = "Configuration";
+ public string VersionConfig = "Version";
+ public string MediaOptionsConfig = "Media Options";
+ public string ThemesConfig = "Theme Options";
+ public string ParentalControlConfig = "Parental Control";
+ public string ContinueConfig = "Continue";
+ public string ResetDefaultsConfig = "Reset Defaults";
+ public string ClearCacheConfig = "Clear Cache";
+ public string UnlockConfig = "Unlock";
+ public string GeneralConfig = "General";
+ public string EnableScreenSaverConfig = "Screen Saver";
+ public string SSTimeOutConfig = "Timeout (mins)";
+ public string TrackingConfig = "Tracking";
+ public string AssumeWatchedIfOlderThanConfig = "Assume Played If Older Than";
+ public string MetadataConfig = "Metadata";
+ public string EnableInternetProvidersConfig = "Allow Internet Providers";
+ public string UpdatesConfig = "Updates";
+ public string AutomaticUpdatesConfig = "Check For Updates";
+ public string LoggingConfig = "Logging";
+ public string BetaUpdatesConfig = "Beta Updates";
+ public string GlobalConfig = "Global";
+ public string EnableEHSConfig = "Enable EHS";
+ public string ShowClockConfig = "Show Clock";
+ public string DimUnselectedPostersConfig = "Dim Unselected Posters";
+ public string HideFocusFrameConfig = "Hide Focus Frame";
+ public string AlwaysShowDetailsConfig = "Always Show Details";
+ public string ExcludeRemoteContentInSearchesConfig = "Exclude Remote Content In Searches";
+ public string EnhancedMouseSupportConfig = "Enhanced Mouse Support";
+ public string ViewsConfig = "Views";
+ public string PosterGridSpacingConfig = "Poster Grid Spacing";
+ public string ThumbWidthSplitConfig = "Thumb Width Split";
+ public string BreadcrumbCountConfig = "Breadcrumb Count";
+ public string ShowFanArtonViewsConfig = "Show Fan Art on Views";
+ public string ShowInitialFolderBackgroundConfig = "Show Initial Folder Background";
+ public string ShowThemeBackgroundConfig = "Show Theme Background";
+ public string ShowHDOverlayonPostersConfig = "Show HD Overlay on Posters";
+ public string ShowIcononRemoteContentConfig = "Show Icon on Remote Content";
+ public string EnableAdvancedCmdsConfig = "Enable Advanced Commands";
+ public string MediaTrackingConfig = "Media Tracking";
+ public string RememberFolderIndexingConfig = "Remember Folder Indexing";
+ public string ShowUnwatchedCountConfig = "Show Unplayed Count";
+ public string WatchedIndicatoronFoldersConfig = "Played Indicator on Folders";
+ public string HighlightUnwatchedItemsConfig = "Highlight Unplayed Items";
+ public string WatchedIndicatoronVideosConfig = "Played Indicator on Items";
+ public string WatchedIndicatorinDetailViewConfig = "Played Indicator in Detail View";
+ public string DefaultToFirstUnwatchedItemConfig = "Default To First Unplayed Item";
+ public string GeneralBehaviorConfig = "General Behavior";
+ public string AllowNestedMovieFoldersConfig = "Allow Nested Movie Folders";
+ public string AutoEnterSingleFolderItemsConfig = "Auto Enter Single Folder Items";
+ public string MultipleFileBehaviorConfig = "Multiple File Behavior";
+ public string TreatMultipleFilesAsSingleMovieConfig = "Treat Multiple Files As Single Movie";
+ public string MultipleFileSizeLimitConfig = "Multiple File Size Limit";
+ public string MBThemeConfig = "Media Browser Theme";
+ public string VisualThemeConfig = "Visual Theme";
+ public string ColorSchemeConfig = "Color Scheme *";
+ public string FontSizeConfig = "Font Size *";
+ public string RequiresRestartConfig = "* Requires a restart to take effect.";
+ public string ThemeSettingsConfig = "Theme Specific Settings";
+ public string ShowConfigButtonConfig = "Show Config Button";
+ public string AlphaBlendingConfig = "Alpha Blending";
+ public string SecurityPINConfig = "Security PIN";
+ public string PCUnlockedTxtConfig = "Parental Controls are Temporarily Unlocked. You cannot change values unless you re-lock.";
+ public string RelockBtnConfig = "Re-Lock";
+ public string EnableParentalBlocksConfig = "Enable Parental Blocks";
+ public string MaxAllowedRatingConfig = "Max Allowed Rating ";
+ public string BlockUnratedContentConfig = "Block Unrated Content";
+ public string HideBlockedContentConfig = "Hide Blocked Content";
+ public string UnlockonPINEntryConfig = "Unlock on PIN Entry";
+ public string UnlockPeriodHoursConfig = "Unlock Period (Hours)";
+ public string EnterNewPINConfig = "Enter New PIN";
+ public string RandomizeBackdropConfig = "Randomize";
+ public string RotateBackdropConfig = "Rotate";
+ public string UpdateLibraryConfig = "Update Library";
+ public string BackdropSettingsConfig = "Backdrop Settings";
+ public string BackdropRotationIntervalConfig = "Rotation Time";
+ public string BackdropTransitionIntervalConfig = "Transition Time";
+ public string BackdropLoadDelayConfig = "Load Delay";
+ public string AutoScrollTextConfig = "Auto Scroll Overview";
+ public string SortYearsAscConfig = "Sort by Year in Ascending Order";
+ public string AutoValidateConfig = "Automatically Validate Items";
+ public string SaveLocalMetaConfig = "Save Locally";
+ public string HideEmptyFoldersConfig = "Hide Empty TV Folders";
+
+
+ //EHS
+ public string RecentlyWatchedEHS = "last played";
+ public string RecentlyAddedEHS = "last added";
+ public string RecentlyAddedUnwatchedEHS = "last added unplayed";
+ public string WatchedEHS = "Played";
+ public string AddedEHS = "Added";
+ public string UnwatchedEHS = "Unplayed";
+ public string AddedOnEHS = "Added on";
+ public string OnEHS = "on";
+ public string OfEHS = "of";
+ public string NoItemsEHS = "No Items To Show";
+ public string VariousEHS = "(various)";
+
+ //Context menu
+ public string CloseCMenu = "Close";
+ public string PlayMenuCMenu = "Play Menu";
+ public string ItemMenuCMenu = "Item Menu";
+ public string PlayAllCMenu = "Play All";
+ public string PlayAllFromHereCMenu = "Play All From Here";
+ public string ResumeCMenu = "Resume";
+ public string MarkUnwatchedCMenu = "Mark Unplayed";
+ public string MarkWatchedCMenu = "Mark Played";
+ public string ShufflePlayCMenu = "Shuffle Play";
+
+ //Media Detail Page
+ public string GeneralDetail = "General";
+ public string ActorsDetail = "Actors";
+ public string ArtistsDetail = "Artists";
+ public string PlayDetail = "Play";
+ public string ResumeDetail = "Resume";
+ public string RefreshDetail = "Refresh";
+ public string PlayTrailersDetail = "Trailer";
+ public string CacheDetail = "Cache 2 xml";
+ public string DeleteDetail = "Delete";
+ public string TMDBRatingDetail = "TMDb Rating";
+ public string OutOfDetail = "out of";
+ public string DirectorDetail = "Director";
+ public string ComposerDetail = "Composer";
+ public string HostDetail = "Host";
+ public string RuntimeDetail = "Runtime";
+ public string NextItemDetail = "Next";
+ public string PreviousItemDetail = "Previous";
+ public string FirstAiredDetail = "First aired";
+ public string LastPlayedDetail = "Last played";
+ public string TrackNumberDetail = "Track";
+
+ public string DirectedByDetail = "Directed By: ";
+ public string WrittenByDetail = "Written By: ";
+ public string ComposedByDetail = "Composed By: ";
+
+ //Display Prefs
+ public string ViewDispPref = "View";
+ public string ViewSearch = "Search";
+ public string CoverFlowDispPref = "Cover Flow";
+ public string DetailDispPref = "Detail";
+ public string PosterDispPref = "Poster";
+ public string ThumbDispPref = "Thumb";
+ public string ThumbStripDispPref = "Thumb Strip";
+ public string ShowLabelsDispPref = "Show Labels";
+ public string VerticalScrollDispPref = "Vertical Scroll";
+ public string UseBannersDispPref = "Use Banners";
+ public string UseCoverflowDispPref = "Use Coverflow Style";
+ public string ThumbSizeDispPref = "Thumb Size";
+ public string NameDispPref = "Name";
+ public string DateDispPref = "Date";
+ public string RatingDispPref = "User Rating";
+ public string OfficialRatingDispPref = "Rating";
+ public string RuntimeDispPref = "Runtime";
+ public string UnWatchedDispPref = "Unplayed";
+ public string YearDispPref = "Year";
+ public string NoneDispPref = "None";
+ public string PerformerDispPref = "Performer";
+ public string ActorDispPref = "Actor";
+ public string GenreDispPref = "Genre";
+ public string DirectorDispPref = "Director";
+ public string StudioDispPref = "Studio";
+
+ //Dialog boxes
+ //public string BrokenEnvironmentDial = "Application will now close due to broken MediaCenterEnvironment object, possibly due to 5 minutes of idle time and/or running with TVPack installed.";
+ //public string InitialConfigDial = "Initial configuration is complete, please restart Media Browser";
+ //public string DeleteMediaDial = "Are you sure you wish to delete this media item?";
+ //public string DeleteMediaCapDial = "Delete Confirmation";
+ //public string NotDeletedDial = "Item NOT Deleted.";
+ //public string NotDeletedCapDial = "Delete Cancelled by User";
+ //public string NotDelInvalidPathDial = "The selected media item cannot be deleted due to an invalid path. Or you may not have sufficient access rights to perform this command.";
+ //public string DelFailedDial = "Delete Failed";
+ //public string NotDelUnknownDial = "The selected media item cannot be deleted due to an unknown error.";
+ //public string NotDelTypeDial = "The selected media item cannot be deleted due to its Item-Type or you have not enabled this feature in the configuration file.";
+ //public string FirstTimeDial = "As this is the first time you have run Media Browser please setup the inital configuration";
+ //public string FirstTimeCapDial = "Configure";
+ //public string EntryPointErrorDial = "Media Browser could not launch directly into ";
+ //public string EntryPointErrorCapDial = "Entrypoint Error";
+ //public string CriticalErrorDial = "Media Browser encountered a critical error and had to shut down: ";
+ //public string CriticalErrorCapDial = "Critical Error";
+ //public string ClearCacheErrorDial = "An error occured during the clearing of the cache, you may wish to manually clear it from {0} before restarting Media Browser";
+ //public string RestartMBDial = "Please restart Media Browser";
+ //public string ClearCacheDial = "Are you sure you wish to clear the cache?\nThis will erase all cached and downloaded information and images.";
+ //public string ClearCacheCapDial = "Clear Cache";
+ //public string CacheClearedDial = "Cache Cleared";
+ //public string ResetConfigDial = "Are you sure you wish to reset all configuration to defaults?";
+ //public string ResetConfigCapDial = "Reset Configuration";
+ //public string ConfigResetDial = "Configuration Reset";
+ //public string UpdateMBDial = "Please visit www.mediabrowser.tv/download to install the new version.";
+ //public string UpdateMBCapDial = "Update Available";
+ //public string UpdateMBExtDial = "There is an update available for Media Browser. Please update Media Browser next time you are at your MediaCenter PC.";
+ //public string DLUpdateFailDial = "Media Browser will operate normally and prompt you again the next time you load it.";
+ //public string DLUpdateFailCapDial = "Update Download Failed";
+ //public string UpdateSuccessDial = "Media Browser must now exit to apply the update. It will restart automatically when it is done";
+ //public string UpdateSuccessCapDial = "Update Downloaded";
+ //public string CustomErrorDial = "Customisation Error";
+ //public string ConfigErrorDial = "Reset to default?";
+ //public string ConfigErrorCapDial = "Error in configuration file";
+ //public string ContentErrorDial = "There was a problem playing the content. Check location exists";
+ //public string ContentErrorCapDial = "Content Error";
+ //public string CannotMaximizeDial = "We can not maximize the window! This is a known bug with Windows 7 and TV Pack, you will have to restart Media Browser!";
+ //public string IncorrectPINDial = "Incorrect PIN Entered";
+ //public string ContentProtected = "Content Protected";
+ //public string CantChangePINDial = "Cannot Change PIN";
+ //public string LibraryUnlockedDial = "Library Temporarily Unlocked. Will Re-Lock in {0} Hour(s) or on Application Re-Start";
+ //public string LibraryUnlockedCapDial = "Unlock";
+ //public string PINChangedDial = "PIN Successfully Changed";
+ //public string PINChangedCapDial = "PIN Change";
+ //public string EnterPINToViewDial = "Please Enter PIN to View Protected Content";
+ //public string EnterPINToPlayDial = "Please Enter PIN to Play Protected Content";
+ //public string EnterCurrentPINDial = "Please Enter CURRENT PIN.";
+ //public string EnterNewPINDial = "Please Enter NEW PIN (exactly 4 digits).";
+ //public string EnterPINDial = "Please Enter PIN to Unlock Library";
+ //public string NoContentDial = "No Content that can be played in this context.";
+ //public string FontsMissingDial = "CustomFonts.mcml as been patched with missing values";
+ //public string StyleMissingDial = "{0} has been patched with missing values";
+ //public string ManualRefreshDial = "Library Update Started. Will proceed in the background.";
+ //public string ForcedRebuildDial = "Your library is currently being migrated by the service. The service will re-start when it is finished and you may then run Media Browser.";
+ //public string ForcedRebuildCapDial = "Library Migration";
+ //public string RefreshFailedDial = "The last service refresh process failed. Please run a manual refresh from the service.";
+ //public string RefreshFailedCapDial = "Service Refresh Failed";
+ //public string RebuildNecDial = "This version of Media Browser requires a re-build of your library. It has started automatically in the service. Some information may be incomplete until this process finishes.";
+ //public string MigrateNecDial = "This version of Media Browser requires a migration of your library. It has started automatically in the service. The service will restart when it is complete and you may then run Media Browser.";
+ //public string RebuildFailedDial = "There was an error attempting to tell the service to re-build your library. Please run the service and do a manual refresh with the cache clear options selected.";
+ //public string MigrateFailedDial = "There was an error attempting to tell the service to re-build your library. Please run the service and do a manual refresh with the cache clear options selected.";
+ //public string RefreshFolderDial = "Refresh all contents too?";
+ //public string RefreshFolderCapDial = "Refresh Folder";
+
+ //Generic
+ public string Restartstr = "Restart";
+ public string Errorstr = "Error";
+ public string Playstr = "Play";
+ public string MinutesStr = "mins"; //Minutes abbreviation
+ public string HoursStr = "hrs"; //Hours abbreviation
+ public string EndsStr = "Ends";
+ public string KBsStr = "Kbps"; //Kilobytes per second
+ public string FrameRateStr = "fps"; //Frames per second
+ public string AtStr = "at"; //x at y, e.g. 1920x1080 at 25 fps
+ public string Rated = "Rated";
+ public string Or = "Or ";
+ public string Lower = "Lower";
+ public string Higher = "Higher";
+ public string Search = "Search";
+ public string Cancel = "Cancel";
+ public string TitleContains = "Title Contains ";
+ public string Any = "Any";
+
+ //Search
+ public string IncludeNested = "Include Subfolders";
+ public string UnwatchedOnly = "Include Only Unwatched";
+ public string FilterByRated = "Filter by Rating";
+
+ //Profiler
+ public string WelcomeProf = "Welcome to Media Browser";
+ public string ProfilerTimeProf = "{1} took {2} seconds.";
+ public string RefreshProf = "Refresh";
+ public string SetWatchedProf = "Set Played {0}";
+ public string RefreshFolderProf = "Refresh Folder and all Contents of";
+ public string ClearWatchedProf = "Clear Played {0}";
+ public string FullRefreshProf = "Full Library Refresh";
+ public string FullValidationProf = "Full Library Validation";
+ public string FastRefreshProf = "Fast Metadata refresh";
+ public string SlowRefresh = "Slow Metadata refresh";
+ public string ImageRefresh = "Image refresh";
+ public string PluginUpdateProf = "An update is available for plug-in {0}";
+ public string NoPluginUpdateProf = "No Plugin Updates Currently Available.";
+ public string LibraryUnLockedProf = "Library Temporarily UnLocked. Will Re-Lock in {0} Hour(s)";
+ public string LibraryReLockedProf = "Library Re-Locked";
+
+ //Messages
+ public string FullRefreshMsg = "Updating Media Library...";
+ public string FullRefreshFinishedMsg = "Library update complete";
+
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/GBRatingsDictionary.cs b/MediaBrowser.Controller/Localization/GBRatingsDictionary.cs
new file mode 100644
index 0000000000..414abdd59d
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/GBRatingsDictionary.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class GBRatingsDictionary
+ /// </summary>
+ public class GBRatingsDictionary : Dictionary<string, int>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GBRatingsDictionary" /> class.
+ /// </summary>
+ public GBRatingsDictionary()
+ {
+ Add("GB-U", 1);
+ Add("GB-PG", 5);
+ Add("GB-12", 6);
+ Add("GB-12A", 7);
+ Add("GB-15", 8);
+ Add("GB-18", 9);
+ Add("GB-R18", 15);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/LocalizedStrings.cs b/MediaBrowser.Controller/Localization/LocalizedStrings.cs
new file mode 100644
index 0000000000..b7e66f0950
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/LocalizedStrings.cs
@@ -0,0 +1,155 @@
+using MediaBrowser.Common.Localization;
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Xml.Serialization;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class LocalizedStrings
+ /// </summary>
+ public class LocalizedStrings
+ {
+ /// <summary>
+ /// The base prefix
+ /// </summary>
+ public const string BasePrefix = "base-";
+ /// <summary>
+ /// The local strings
+ /// </summary>
+ protected ConcurrentDictionary<string, string> LocalStrings = new ConcurrentDictionary<string, string>();
+ /// <summary>
+ /// The _instance
+ /// </summary>
+ private static LocalizedStrings _instance;
+
+ /// <summary>
+ /// Gets the instance.
+ /// </summary>
+ /// <value>The instance.</value>
+ public static LocalizedStrings Instance { get { return _instance ?? (_instance = new LocalizedStrings()); } }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LocalizedStrings" /> class.
+ /// </summary>
+ public LocalizedStrings()
+ {
+ foreach (var stringObject in Kernel.Instance.StringFiles)
+ {
+ AddStringData(LoadFromFile(GetFileName(stringObject),stringObject.GetType()));
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the file.
+ /// </summary>
+ /// <param name="stringObject">The string object.</param>
+ /// <returns>System.String.</returns>
+ protected string GetFileName(LocalizedStringData stringObject)
+ {
+ var path = Kernel.Instance.ApplicationPaths.LocalizationPath;
+ var name = Path.Combine(path, stringObject.Prefix + "strings-" + CultureInfo.CurrentCulture + ".xml");
+ if (File.Exists(name))
+ {
+ return name;
+ }
+
+ name = Path.Combine(path, stringObject.Prefix + "strings-" + CultureInfo.CurrentCulture.Parent + ".xml");
+ if (File.Exists(name))
+ {
+ return name;
+ }
+
+ //just return default
+ return Path.Combine(path, stringObject.Prefix + "strings-en.xml");
+ }
+
+ /// <summary>
+ /// Loads from file.
+ /// </summary>
+ /// <param name="file">The file.</param>
+ /// <param name="t">The t.</param>
+ /// <returns>LocalizedStringData.</returns>
+ protected LocalizedStringData LoadFromFile(string file, Type t)
+ {
+ var xs = new XmlSerializer(t);
+ var strings = (LocalizedStringData)Activator.CreateInstance(t);
+ strings.FileName = file;
+ Logger.LogInfo("Using String Data from {0}", file);
+ if (File.Exists(file))
+ {
+ using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
+ {
+ strings = (LocalizedStringData)xs.Deserialize(fs);
+ }
+ }
+ else
+ {
+ strings.Save(); //brand new - save it
+ }
+
+ if (strings.ThisVersion != strings.Version && file.ToLower().Contains("-en.xml"))
+ {
+ //only re-save the english version as that is the one defined internally
+ strings = new BaseStrings {FileName = file};
+ strings.Save();
+ }
+ return strings;
+
+ }
+
+ /// <summary>
+ /// Adds the string data.
+ /// </summary>
+ /// <param name="stringData">The string data.</param>
+ public void AddStringData(object stringData )
+ {
+ //translate our object definition into a dictionary for lookups
+ // and a reverse dictionary so we can lookup keys by value
+ foreach (var field in stringData.GetType().GetFields().Where(f => f != null && f.FieldType == typeof(string)))
+ {
+ string value;
+
+ try
+ {
+ value = field.GetValue(stringData) as string;
+ }
+ catch (TargetException ex)
+ {
+ Logger.LogException("Error getting value for field: {0}", ex, field.Name);
+ continue;
+ }
+ catch (FieldAccessException ex)
+ {
+ Logger.LogException("Error getting value for field: {0}", ex, field.Name);
+ continue;
+ }
+ catch (NotSupportedException ex)
+ {
+ Logger.LogException("Error getting value for field: {0}", ex, field.Name);
+ continue;
+ }
+
+ LocalStrings.TryAdd(field.Name, value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the string.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <returns>System.String.</returns>
+ public string GetString(string key)
+ {
+ string value;
+
+ LocalStrings.TryGetValue(key, out value);
+ return value;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/NLRatingsDictionary.cs b/MediaBrowser.Controller/Localization/NLRatingsDictionary.cs
new file mode 100644
index 0000000000..7a20f50ba4
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/NLRatingsDictionary.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class NLRatingsDictionary
+ /// </summary>
+ public class NLRatingsDictionary : Dictionary<string, int>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NLRatingsDictionary" /> class.
+ /// </summary>
+ public NLRatingsDictionary()
+ {
+ Add("NL-AL", 1);
+ Add("NL-MG6", 2);
+ Add("NL-6", 3);
+ Add("NL-9", 5);
+ Add("NL-12", 6);
+ Add("NL-16", 8);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/Ratings.cs b/MediaBrowser.Controller/Localization/Ratings.cs
new file mode 100644
index 0000000000..946e25f536
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/Ratings.cs
@@ -0,0 +1,162 @@
+using MediaBrowser.Common.Extensions;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class Ratings
+ /// </summary>
+ public static class Ratings
+ {
+ /// <summary>
+ /// The ratings def
+ /// </summary>
+ private static RatingsDefinition ratingsDef;
+ /// <summary>
+ /// The _ratings dict
+ /// </summary>
+ private static Dictionary<string, int> _ratingsDict;
+ /// <summary>
+ /// Gets the ratings dict.
+ /// </summary>
+ /// <value>The ratings dict.</value>
+ public static Dictionary<string, int> RatingsDict
+ {
+ get { return _ratingsDict ?? (_ratingsDict = Initialize(false)); }
+ }
+ /// <summary>
+ /// The ratings strings
+ /// </summary>
+ private static readonly Dictionary<int, string> ratingsStrings = new Dictionary<int, string>();
+
+ /// <summary>
+ /// Initializes the specified block unrated.
+ /// </summary>
+ /// <param name="blockUnrated">if set to <c>true</c> [block unrated].</param>
+ /// <returns>Dictionary{System.StringSystem.Int32}.</returns>
+ public static Dictionary<string, int> Initialize(bool blockUnrated)
+ {
+ //build our ratings dictionary from the combined local one and us one
+ ratingsDef = new RatingsDefinition(Path.Combine(Kernel.Instance.ApplicationPaths.LocalizationPath, "Ratings-" + Kernel.Instance.Configuration.MetadataCountryCode+".txt"));
+ //global value of None
+ var dict = new Dictionary<string, int> {{"None", -1}};
+ foreach (var pair in ratingsDef.RatingsDict)
+ {
+ dict.TryAdd(pair.Key, pair.Value);
+ }
+ if (Kernel.Instance.Configuration.MetadataCountryCode.ToUpper() != "US")
+ {
+ foreach (var pair in new USRatingsDictionary())
+ {
+ dict.TryAdd(pair.Key, pair.Value);
+ }
+ }
+ //global values of CS
+ dict.TryAdd("CS", 1000);
+
+ dict.TryAdd("", blockUnrated ? 1000 : 0);
+
+ //and rating reverse lookup dictionary (non-redundant ones)
+ ratingsStrings.Clear();
+ var lastLevel = -10;
+ ratingsStrings.Add(-1,LocalizedStrings.Instance.GetString("Any"));
+ foreach (var pair in ratingsDef.RatingsDict.OrderBy(p => p.Value))
+ {
+ if (pair.Value > lastLevel)
+ {
+ lastLevel = pair.Value;
+ ratingsStrings.TryAdd(pair.Value, pair.Key);
+ }
+ }
+
+ ratingsStrings.TryAdd(999, "CS");
+
+ return dict;
+ }
+
+ /// <summary>
+ /// Switches the unrated.
+ /// </summary>
+ /// <param name="block">if set to <c>true</c> [block].</param>
+ public static void SwitchUnrated(bool block)
+ {
+ RatingsDict.Remove("");
+ RatingsDict.Add("", block ? 1000 : 0);
+ }
+
+ /// <summary>
+ /// Levels the specified rating STR.
+ /// </summary>
+ /// <param name="ratingStr">The rating STR.</param>
+ /// <returns>System.Int32.</returns>
+ public static int Level(string ratingStr)
+ {
+ if (ratingStr == null) ratingStr = "";
+ if (RatingsDict.ContainsKey(ratingStr))
+ return RatingsDict[ratingStr];
+
+ string stripped = StripCountry(ratingStr);
+ if (RatingsDict.ContainsKey(stripped))
+ return RatingsDict[stripped];
+
+ return RatingsDict[""]; //return "unknown" level
+ }
+
+ /// <summary>
+ /// Strips the country.
+ /// </summary>
+ /// <param name="rating">The rating.</param>
+ /// <returns>System.String.</returns>
+ private static string StripCountry(string rating)
+ {
+ int start = rating.IndexOf('-');
+ return start > 0 ? rating.Substring(start + 1) : rating;
+ }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <param name="level">The level.</param>
+ /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
+ public static string ToString(int level)
+ {
+ //return the closest one
+ while (level > 0)
+ {
+ if (ratingsStrings.ContainsKey(level))
+ return ratingsStrings[level];
+
+ level--;
+ }
+ return ratingsStrings.Values.FirstOrDefault(); //default to first one
+ }
+ /// <summary>
+ /// To the strings.
+ /// </summary>
+ /// <returns>List{System.String}.</returns>
+ public static List<string> ToStrings()
+ {
+ //return the whole list of ratings strings
+ return ratingsStrings.Values.ToList();
+ }
+
+ /// <summary>
+ /// To the values.
+ /// </summary>
+ /// <returns>List{System.Int32}.</returns>
+ public static List<int> ToValues()
+ {
+ //return the whole list of ratings values
+ return ratingsStrings.Keys.ToList();
+ }
+
+ //public Microsoft.MediaCenter.UI.Image RatingImage(string rating)
+ //{
+ // return Helper.GetMediaInfoImage("Rated_" + rating);
+ //}
+
+
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/RatingsDefinition.cs b/MediaBrowser.Controller/Localization/RatingsDefinition.cs
new file mode 100644
index 0000000000..65bd3bcbb4
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/RatingsDefinition.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MediaBrowser.Common.Logging;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class RatingsDefinition
+ /// </summary>
+ public class RatingsDefinition
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RatingsDefinition" /> class.
+ /// </summary>
+ /// <param name="file">The file.</param>
+ public RatingsDefinition(string file)
+ {
+ Logger.LogInfo("Loading Certification Ratings from file " + file);
+ this.file = file;
+ if (!Load())
+ {
+ Init(Kernel.Instance.Configuration.MetadataCountryCode.ToUpper());
+ }
+ }
+
+ /// <summary>
+ /// Inits the specified country.
+ /// </summary>
+ /// <param name="country">The country.</param>
+ protected void Init(string country)
+ {
+ //intitialze based on country
+ switch (country)
+ {
+ case "US":
+ RatingsDict = new USRatingsDictionary();
+ break;
+ case "GB":
+ RatingsDict = new GBRatingsDictionary();
+ break;
+ case "NL":
+ RatingsDict = new NLRatingsDictionary();
+ break;
+ case "AU":
+ RatingsDict = new AURatingsDictionary();
+ break;
+ default:
+ RatingsDict = new USRatingsDictionary();
+ break;
+ }
+ Save();
+ }
+
+ /// <summary>
+ /// The file
+ /// </summary>
+ readonly string file;
+
+ /// <summary>
+ /// Save to file
+ /// </summary>
+ public void Save()
+ {
+ // Use simple text serialization - no need for xml
+ using (var fs = new StreamWriter(file))
+ {
+ foreach (var pair in RatingsDict)
+ {
+ fs.WriteLine(pair.Key + "," + pair.Value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Load from file
+ /// </summary>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected bool Load()
+ {
+ // Read back in our simple serialized format
+ RatingsDict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
+ try
+ {
+ using (var fs = new StreamReader(file))
+ {
+ while (!fs.EndOfStream)
+ {
+ var line = fs.ReadLine() ?? "";
+ var values = line.Split(',');
+ if (values.Length == 2)
+ {
+
+ int value;
+
+ if (int.TryParse(values[1], out value))
+ {
+ RatingsDict[values[0].Trim()] = value;
+ }
+ else
+ {
+ Logger.LogError("Invalid line in ratings file " + file + "(" + line + ")");
+ }
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Couldn't load - probably just not there yet
+ return false;
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// The ratings dict
+ /// </summary>
+ public Dictionary<string, int> RatingsDict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
+
+ }
+}
diff --git a/MediaBrowser.Controller/Localization/USRatingsDictionary.cs b/MediaBrowser.Controller/Localization/USRatingsDictionary.cs
new file mode 100644
index 0000000000..2c7a69841c
--- /dev/null
+++ b/MediaBrowser.Controller/Localization/USRatingsDictionary.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Localization
+{
+ /// <summary>
+ /// Class USRatingsDictionary
+ /// </summary>
+ public class USRatingsDictionary : Dictionary<string,int>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="USRatingsDictionary" /> class.
+ /// </summary>
+ public USRatingsDictionary()
+ {
+ Add("G", 1);
+ Add("E", 1);
+ Add("EC", 1);
+ Add("TV-G", 1);
+ Add("TV-Y", 2);
+ Add("TV-Y7", 3);
+ Add("TV-Y7-FV", 4);
+ Add("PG", 5);
+ Add("TV-PG", 5);
+ Add("PG-13", 7);
+ Add("T", 7);
+ Add("TV-14", 8);
+ Add("R", 9);
+ Add("M", 9);
+ Add("TV-MA", 9);
+ Add("NC-17", 10);
+ Add("AO", 15);
+ Add("RP", 15);
+ Add("UR", 15);
+ Add("NR", 15);
+ Add("X", 15);
+ Add("XXX", 100);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index fc1e578e99..88e5ed551b 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -1,150 +1,279 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
- <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
- <PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
- <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid>
- <OutputType>Library</OutputType>
- <AppDesignerFolder>Properties</AppDesignerFolder>
- <RootNamespace>MediaBrowser.Controller</RootNamespace>
- <AssemblyName>MediaBrowser.Controller</AssemblyName>
- <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
- <FileAlignment>512</FileAlignment>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <DebugSymbols>true</DebugSymbols>
- <DebugType>full</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug\</OutputPath>
- <DefineConstants>DEBUG;TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <DebugType>pdbonly</DebugType>
- <Optimize>true</Optimize>
- <OutputPath>bin\Release\</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <ItemGroup>
- <Reference Include="protobuf-net">
- <HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
- </Reference>
- <Reference Include="System" />
- <Reference Include="System.ComponentModel.Composition" />
- <Reference Include="System.Core" />
- <Reference Include="System.Drawing" />
- <Reference Include="System.Net.Http" />
- <Reference Include="System.Net.Http.WebRequest" />
- <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
- <SpecificVersion>False</SpecificVersion>
- <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
- </Reference>
- <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
- <SpecificVersion>False</SpecificVersion>
- <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
- </Reference>
- <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
- <SpecificVersion>False</SpecificVersion>
- <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
- </Reference>
- <Reference Include="System.Runtime.Serialization" />
- <Reference Include="System.Xml.Linq" />
- <Reference Include="System.Data.DataSetExtensions" />
- <Reference Include="Microsoft.CSharp" />
- <Reference Include="System.Data" />
- <Reference Include="System.Xml" />
- </ItemGroup>
- <ItemGroup>
- <Compile Include="Drawing\DrawingUtils.cs" />
- <Compile Include="Drawing\ImageProcessor.cs" />
- <Compile Include="Entities\Audio.cs" />
- <Compile Include="Entities\BaseEntity.cs" />
- <Compile Include="Entities\BaseItem.cs" />
- <Compile Include="Entities\Folder.cs" />
- <Compile Include="Entities\Genre.cs" />
- <Compile Include="Entities\Movies\BoxSet.cs" />
- <Compile Include="Entities\Movies\Movie.cs" />
- <Compile Include="Entities\Person.cs" />
- <Compile Include="Entities\Studio.cs" />
- <Compile Include="Entities\TV\Episode.cs" />
- <Compile Include="Entities\TV\Season.cs" />
- <Compile Include="Entities\TV\Series.cs" />
- <Compile Include="Entities\User.cs" />
- <Compile Include="Entities\UserItemData.cs" />
- <Compile Include="Entities\Video.cs" />
- <Compile Include="Entities\Year.cs" />
- <Compile Include="IO\FileSystemHelper.cs" />
- <Compile Include="Library\ChildrenChangedEventArgs.cs" />
- <Compile Include="Providers\BaseProviderInfo.cs" />
- <Compile Include="Providers\Movies\MovieProviderFromXml.cs" />
- <Compile Include="Providers\Movies\MovieSpecialFeaturesProvider.cs" />
- <Compile Include="Providers\TV\EpisodeImageFromMediaLocationProvider.cs" />
- <Compile Include="Providers\TV\EpisodeProviderFromXml.cs" />
- <Compile Include="Providers\TV\EpisodeXmlParser.cs" />
- <Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
- <Compile Include="Providers\TV\SeriesXmlParser.cs" />
- <Compile Include="Resolvers\EntityResolutionHelper.cs" />
- <Compile Include="Resolvers\Movies\BoxSetResolver.cs" />
- <Compile Include="Resolvers\Movies\MovieResolver.cs" />
- <Compile Include="Resolvers\TV\EpisodeResolver.cs" />
- <Compile Include="Resolvers\TV\SeasonResolver.cs" />
- <Compile Include="Resolvers\TV\SeriesResolver.cs" />
- <Compile Include="Resolvers\TV\TVUtils.cs" />
- <Compile Include="ServerApplicationPaths.cs" />
- <Compile Include="Library\ItemResolveEventArgs.cs" />
- <Compile Include="FFMpeg\FFProbe.cs" />
- <Compile Include="FFMpeg\FFProbeResult.cs" />
- <Compile Include="IO\DirectoryWatchers.cs" />
- <Compile Include="IO\FileData.cs" />
- <Compile Include="IO\Shortcut.cs" />
- <Compile Include="Library\ItemController.cs" />
- <Compile Include="Kernel.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="Providers\BaseMetadataProvider.cs" />
- <Compile Include="Providers\AudioInfoProvider.cs" />
- <Compile Include="Providers\FolderProviderFromXml.cs" />
- <Compile Include="Providers\ImageFromMediaLocationProvider.cs" />
- <Compile Include="Providers\LocalTrailerProvider.cs" />
- <Compile Include="Providers\VideoInfoProvider.cs" />
- <Compile Include="Resolvers\AudioResolver.cs" />
- <Compile Include="Resolvers\BaseItemResolver.cs" />
- <Compile Include="Resolvers\FolderResolver.cs" />
- <Compile Include="Resolvers\VideoResolver.cs" />
- <Compile Include="Weather\BaseWeatherProvider.cs" />
- <Compile Include="Weather\WeatherProvider.cs" />
- <Compile Include="Providers\BaseItemXmlParser.cs" />
- <Compile Include="Xml\XmlExtensions.cs" />
- </ItemGroup>
- <ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
- <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
- <Name>MediaBrowser.Common</Name>
- </ProjectReference>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
- <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
- <Name>MediaBrowser.Model</Name>
- </ProjectReference>
- </ItemGroup>
- <ItemGroup>
- <None Include="packages.config" />
- </ItemGroup>
- <ItemGroup>
- <EmbeddedResource Include="FFMpeg\ffmpeg.exe" />
- </ItemGroup>
- <ItemGroup>
- <EmbeddedResource Include="FFMpeg\ffprobe.exe" />
- <Content Include="FFMpeg\readme.txt" />
- </ItemGroup>
- <Import Project="$(MSBuildToolsPath)\Microsoft.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.
- <Target Name="BeforeBuild">
- </Target>
- <Target Name="AfterBuild">
- </Target>
- -->
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.Controller</RootNamespace>
+ <AssemblyName>MediaBrowser.Controller</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
+ <RestorePackages>true</RestorePackages>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <RunPostBuildEvent>Always</RunPostBuildEvent>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>bin\x86\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <DebugType>full</DebugType>
+ <PlatformTarget>x86</PlatformTarget>
+ <ErrorReport>prompt</ErrorReport>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
+ <OutputPath>bin\x86\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <Optimize>true</Optimize>
+ <DebugType>pdbonly</DebugType>
+ <PlatformTarget>x86</PlatformTarget>
+ <ErrorReport>prompt</ErrorReport>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Ionic.Zip">
+ <HintPath>..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll</HintPath>
+ </Reference>
+ <Reference Include="Mediabrowser.PluginSecurity">
+ <HintPath>Plugins\Mediabrowser.PluginSecurity.dll</HintPath>
+ </Reference>
+ <Reference Include="MoreLinq">
+ <HintPath>..\packages\morelinq.1.0.15631-beta\lib\net35\MoreLinq.dll</HintPath>
+ </Reference>
+ <Reference Include="protobuf-net, Version=2.0.0.621, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\protobuf-net.2.0.0.621\lib\net40\protobuf-net.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.SQLite, Version=1.0.84.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\System.Data.SQLite.1.0.84.0\lib\net45\System.Data.SQLite.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Data.SQLite.Linq, Version=1.0.84.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\System.Data.SQLite.1.0.84.0\lib\net45\System.Data.SQLite.Linq.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Deployment" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Net" />
+ <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Core.2.0.21114\lib\Net45\System.Reactive.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Interfaces.2.0.21114\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Linq.2.0.21114\lib\Net45\System.Reactive.Linq.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Drawing\ImageManager.cs" />
+ <Compile Include="Entities\AggregateFolder.cs" />
+ <Compile Include="Entities\Audio\Audio.cs" />
+ <Compile Include="Entities\Audio\MusicAlbum.cs" />
+ <Compile Include="Entities\Audio\MusicArtist.cs" />
+ <Compile Include="Entities\BaseItem.cs" />
+ <Compile Include="Entities\BasePluginFolder.cs" />
+ <Compile Include="Entities\Folder.cs" />
+ <Compile Include="Entities\Genre.cs" />
+ <Compile Include="Entities\ICollectionFolder.cs" />
+ <Compile Include="Entities\IndexFolder.cs" />
+ <Compile Include="Entities\Movies\BoxSet.cs" />
+ <Compile Include="Entities\ISupportsSpecialFeatures.cs" />
+ <Compile Include="Entities\Movies\Movie.cs" />
+ <Compile Include="Entities\Person.cs" />
+ <Compile Include="Entities\PlaybackProgressEventArgs.cs" />
+ <Compile Include="Entities\Studio.cs" />
+ <Compile Include="Entities\Trailer.cs" />
+ <Compile Include="Entities\TV\Episode.cs" />
+ <Compile Include="Entities\TV\Season.cs" />
+ <Compile Include="Entities\TV\Series.cs" />
+ <Compile Include="Entities\User.cs" />
+ <Compile Include="Entities\UserItemData.cs" />
+ <Compile Include="Entities\UserRootFolder.cs" />
+ <Compile Include="Entities\Video.cs" />
+ <Compile Include="Entities\CollectionFolder.cs" />
+ <Compile Include="Entities\Year.cs" />
+ <Compile Include="IO\FileSystemManager.cs" />
+ <Compile Include="Library\ChildrenChangedEventArgs.cs" />
+ <Compile Include="Library\DtoBuilder.cs" />
+ <Compile Include="Library\Profiler.cs" />
+ <Compile Include="Library\UserDataManager.cs" />
+ <Compile Include="Library\UserManager.cs" />
+ <Compile Include="Localization\AURatingsDictionary.cs" />
+ <Compile Include="Localization\BaseStrings.cs" />
+ <Compile Include="Localization\GBRatingsDictionary.cs" />
+ <Compile Include="Localization\LocalizedStrings.cs" />
+ <Compile Include="Localization\NLRatingsDictionary.cs" />
+ <Compile Include="Localization\Ratings.cs" />
+ <Compile Include="Localization\RatingsDefinition.cs" />
+ <Compile Include="Localization\USRatingsDictionary.cs" />
+ <Compile Include="MediaInfo\BDInfoResult.cs" />
+ <Compile Include="MediaInfo\FFMpegManager.cs" />
+ <Compile Include="MediaInfo\FFProbeResult.cs" />
+ <Compile Include="Persistence\IDisplayPreferencesRepository.cs" />
+ <Compile Include="Persistence\IItemRepository.cs" />
+ <Compile Include="Persistence\IRepository.cs" />
+ <Compile Include="Persistence\IUserDataRepository.cs" />
+ <Compile Include="Persistence\IUserRepository.cs" />
+ <Compile Include="Persistence\SQLite\SQLiteDisplayPreferencesRepository.cs" />
+ <Compile Include="Persistence\SQLite\SQLiteExtensions.cs" />
+ <Compile Include="Persistence\SQLite\SQLiteItemRepository.cs" />
+ <Compile Include="Persistence\SQLite\SQLiteRepository.cs" />
+ <Compile Include="Persistence\SQLite\SQLiteUserDataRepository.cs" />
+ <Compile Include="Persistence\SQLite\SQLiteUserRepository.cs" />
+ <Compile Include="Persistence\TypeMapper.cs" />
+ <Compile Include="Playback\BaseIntroProvider.cs" />
+ <Compile Include="Plugins\BaseConfigurationPage.cs" />
+ <Compile Include="Plugins\PluginSecurityManager.cs" />
+ <Compile Include="Providers\BaseImageEnhancer.cs" />
+ <Compile Include="Providers\FanartBaseProvider.cs" />
+ <Compile Include="Providers\ImagesByNameProvider.cs" />
+ <Compile Include="Providers\MediaInfo\BaseFFMpegImageProvider.cs" />
+ <Compile Include="Providers\MediaInfo\BaseFFMpegProvider.cs" />
+ <Compile Include="Providers\MediaInfo\BDInfoProvider.cs" />
+ <Compile Include="Providers\MediaInfo\FFMpegAudioImageProvider.cs" />
+ <Compile Include="Providers\MediaInfo\BaseFFProbeProvider.cs" />
+ <Compile Include="Providers\BaseProviderInfo.cs" />
+ <Compile Include="Providers\Movies\FanArtMovieProvider.cs" />
+ <Compile Include="Providers\Movies\MovieDbProvider.cs" />
+ <Compile Include="Providers\Movies\MovieProviderFromJson.cs" />
+ <Compile Include="Providers\Movies\MovieProviderFromXml.cs" />
+ <Compile Include="Providers\Movies\PersonProviderFromJson.cs" />
+ <Compile Include="Providers\Movies\TmdbPersonProvider.cs" />
+ <Compile Include="Providers\ProviderManager.cs" />
+ <Compile Include="Providers\SortNameProvider.cs" />
+ <Compile Include="Providers\TV\EpisodeImageFromMediaLocationProvider.cs" />
+ <Compile Include="Providers\TV\EpisodeProviderFromXml.cs" />
+ <Compile Include="Providers\TV\EpisodeXmlParser.cs" />
+ <Compile Include="Providers\TV\FanArtTVProvider.cs" />
+ <Compile Include="Providers\TV\RemoteEpisodeProvider.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Providers\TV\RemoteSeasonProvider.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Providers\TV\RemoteSeriesProvider.cs" />
+ <Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
+ <Compile Include="Providers\TV\SeriesXmlParser.cs" />
+ <Compile Include="Providers\MediaInfo\FFMpegVideoImageProvider.cs" />
+ <Compile Include="Resolvers\Audio\MusicAlbumResolver.cs" />
+ <Compile Include="Resolvers\Audio\MusicArtistResolver.cs" />
+ <Compile Include="Resolvers\BaseResolutionIgnoreRule.cs" />
+ <Compile Include="Resolvers\CoreResolutionIgnoreRule.cs" />
+ <Compile Include="Resolvers\EntityResolutionHelper.cs" />
+ <Compile Include="Resolvers\LocalTrailerResolver.cs" />
+ <Compile Include="Resolvers\Movies\BoxSetResolver.cs" />
+ <Compile Include="Resolvers\Movies\MovieResolver.cs" />
+ <Compile Include="Resolvers\TV\EpisodeResolver.cs" />
+ <Compile Include="Resolvers\TV\SeasonResolver.cs" />
+ <Compile Include="Resolvers\TV\SeriesResolver.cs" />
+ <Compile Include="Resolvers\TV\TVUtils.cs" />
+ <Compile Include="Library\ResourcePool.cs" />
+ <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
+ <Compile Include="ScheduledTasks\ImageCleanupTask.cs" />
+ <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
+ <Compile Include="ScheduledTasks\PluginUpdateTask.cs" />
+ <Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" />
+ <Compile Include="ServerApplicationPaths.cs" />
+ <Compile Include="Library\ItemResolveArgs.cs" />
+ <Compile Include="IO\DirectoryWatchers.cs" />
+ <Compile Include="IO\FileData.cs" />
+ <Compile Include="Library\LibraryManager.cs" />
+ <Compile Include="Kernel.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Providers\BaseMetadataProvider.cs" />
+ <Compile Include="Providers\MediaInfo\FFProbeAudioInfoProvider.cs" />
+ <Compile Include="Providers\FolderProviderFromXml.cs" />
+ <Compile Include="Providers\ImageFromMediaLocationProvider.cs" />
+ <Compile Include="Providers\MediaInfo\FFProbeVideoInfoProvider.cs" />
+ <Compile Include="Resolvers\Audio\AudioResolver.cs" />
+ <Compile Include="Resolvers\BaseItemResolver.cs" />
+ <Compile Include="Resolvers\FolderResolver.cs" />
+ <Compile Include="Resolvers\VideoResolver.cs" />
+ <Compile Include="Sorting\BaseItemComparer.cs" />
+ <Compile Include="Sorting\SortOrder.cs" />
+ <Compile Include="Updates\InstallationManager.cs" />
+ <Compile Include="Weather\BaseWeatherProvider.cs" />
+ <Compile Include="Weather\WeatherProvider.cs" />
+ <Compile Include="Providers\BaseItemXmlParser.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\BDInfo\BDInfo.csproj">
+ <Project>{07b509c0-0c28-4f3f-8963-5263281f7e3d}</Project>
+ <Name>BDInfo</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="MediaInfo\ffmpeg20130209.zip" />
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="MediaInfo\readme.txt" />
+ <Content Include="x64\SQLite.Interop.dll">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Include="x86\SQLite.Interop.dll">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <PropertyGroup>
+ <PostBuildEvent>
+ </PostBuildEvent>
+ </PropertyGroup>
+ <PropertyGroup>
+ <PreBuildEvent>
+ </PreBuildEvent>
+ </PropertyGroup>
+ <Import Project="$(SolutionDir)\.nuget\nuget.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.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
</Project> \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaInfo/BDInfoResult.cs b/MediaBrowser.Controller/MediaInfo/BDInfoResult.cs
new file mode 100644
index 0000000000..08d1ce8a0b
--- /dev/null
+++ b/MediaBrowser.Controller/MediaInfo/BDInfoResult.cs
@@ -0,0 +1,41 @@
+using MediaBrowser.Model.Entities;
+using ProtoBuf;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.MediaInfo
+{
+ /// <summary>
+ /// Represents the result of BDInfo output
+ /// </summary>
+ [ProtoContract]
+ public class BDInfoResult
+ {
+ /// <summary>
+ /// Gets or sets the media streams.
+ /// </summary>
+ /// <value>The media streams.</value>
+ [ProtoMember(1)]
+ public List<MediaStream> MediaStreams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ [ProtoMember(2)]
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the files.
+ /// </summary>
+ /// <value>The files.</value>
+ [ProtoMember(3)]
+ public List<string> Files { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapters.
+ /// </summary>
+ /// <value>The chapters.</value>
+ [ProtoMember(4)]
+ public List<double> Chapters { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs
new file mode 100644
index 0000000000..ee7c87f901
--- /dev/null
+++ b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs
@@ -0,0 +1,1078 @@
+using Ionic.Zip;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.MediaInfo
+{
+ /// <summary>
+ /// Class FFMpegManager
+ /// </summary>
+ public class FFMpegManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// Holds the list of new items to generate chapter image for when the NewItemTimer expires
+ /// </summary>
+ private readonly List<Video> _newlyAddedItems = new List<Video>();
+
+ /// <summary>
+ /// The amount of time to wait before generating chapter images
+ /// </summary>
+ private const int NewItemDelay = 300000;
+
+ /// <summary>
+ /// The current new item timer
+ /// </summary>
+ /// <value>The new item timer.</value>
+ private Timer NewItemTimer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video image cache.
+ /// </summary>
+ /// <value>The video image cache.</value>
+ internal FileSystemRepository VideoImageCache { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image cache.
+ /// </summary>
+ /// <value>The image cache.</value>
+ internal FileSystemRepository AudioImageCache { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle cache.
+ /// </summary>
+ /// <value>The subtitle cache.</value>
+ internal FileSystemRepository SubtitleCache { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FFMpegManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public FFMpegManager(Kernel kernel)
+ : base(kernel)
+ {
+ // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes
+ SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX);
+
+ VideoImageCache = new FileSystemRepository(VideoImagesDataPath);
+ AudioImageCache = new FileSystemRepository(AudioImagesDataPath);
+ SubtitleCache = new FileSystemRepository(SubtitleCachePath);
+
+ Kernel.LibraryManager.LibraryChanged += LibraryManager_LibraryChanged;
+
+ Task.Run(() => VersionedDirectoryPath = GetVersionedDirectoryPath());
+ }
+
+ /// <summary>
+ /// Handles the LibraryChanged event of the LibraryManager control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ void LibraryManager_LibraryChanged(object sender, ChildrenChangedEventArgs e)
+ {
+ var videos = e.ItemsAdded.OfType<Video>().ToList();
+
+ // Use a timer to prevent lots of these notifications from showing in a short period of time
+ if (videos.Count > 0)
+ {
+ lock (_newlyAddedItems)
+ {
+ _newlyAddedItems.AddRange(videos);
+
+ if (NewItemTimer == null)
+ {
+ NewItemTimer = new Timer(NewItemTimerCallback, null, NewItemDelay, Timeout.Infinite);
+ }
+ else
+ {
+ NewItemTimer.Change(NewItemDelay, Timeout.Infinite);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Called when the new item timer expires
+ /// </summary>
+ /// <param name="state">The state.</param>
+ private async void NewItemTimerCallback(object state)
+ {
+ List<Video> newItems;
+
+ // Lock the list and release all resources
+ lock (_newlyAddedItems)
+ {
+ newItems = _newlyAddedItems.ToList();
+ _newlyAddedItems.Clear();
+
+ NewItemTimer.Dispose();
+ NewItemTimer = null;
+ }
+
+ // Limit the number of videos we generate images for
+ // The idea is to catch new items that are added here and there
+ // Mass image generation can be left to the scheduled task
+ foreach (var video in newItems.Where(c => c.Chapters != null).Take(3))
+ {
+ try
+ {
+ await PopulateChapterImages(video, CancellationToken.None, true, true).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error creating chapter images for {0}", ex, video.Name);
+ }
+ }
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ if (NewItemTimer != null)
+ {
+ NewItemTimer.Dispose();
+ }
+
+ SetErrorMode(ErrorModes.SYSTEM_DEFAULT);
+
+ Kernel.LibraryManager.LibraryChanged -= LibraryManager_LibraryChanged;
+
+ AudioImageCache.Dispose();
+ VideoImageCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+
+ /// <summary>
+ /// The FF probe resource pool count
+ /// </summary>
+ private const int FFProbeResourcePoolCount = 4;
+ /// <summary>
+ /// The audio image resource pool count
+ /// </summary>
+ private const int AudioImageResourcePoolCount = 4;
+ /// <summary>
+ /// The video image resource pool count
+ /// </summary>
+ private const int VideoImageResourcePoolCount = 2;
+
+ /// <summary>
+ /// The FF probe resource pool
+ /// </summary>
+ private readonly SemaphoreSlim FFProbeResourcePool = new SemaphoreSlim(FFProbeResourcePoolCount, FFProbeResourcePoolCount);
+ /// <summary>
+ /// The audio image resource pool
+ /// </summary>
+ private readonly SemaphoreSlim AudioImageResourcePool = new SemaphoreSlim(AudioImageResourcePoolCount, AudioImageResourcePoolCount);
+ /// <summary>
+ /// The video image resource pool
+ /// </summary>
+ private readonly SemaphoreSlim VideoImageResourcePool = new SemaphoreSlim(VideoImageResourcePoolCount, VideoImageResourcePoolCount);
+
+ /// <summary>
+ /// Gets or sets the versioned directory path.
+ /// </summary>
+ /// <value>The versioned directory path.</value>
+ private string VersionedDirectoryPath { get; set; }
+
+ /// <summary>
+ /// Gets the FFMPEG version.
+ /// </summary>
+ /// <value>The FFMPEG version.</value>
+ public string FFMpegVersion
+ {
+ get { return Path.GetFileNameWithoutExtension(VersionedDirectoryPath); }
+ }
+
+ /// <summary>
+ /// The _ FF MPEG path
+ /// </summary>
+ private string _FFMpegPath;
+ /// <summary>
+ /// Gets the path to ffmpeg.exe
+ /// </summary>
+ /// <value>The FF MPEG path.</value>
+ public string FFMpegPath
+ {
+ get
+ {
+ return _FFMpegPath ?? (_FFMpegPath = Path.Combine(VersionedDirectoryPath, "ffmpeg.exe"));
+ }
+ }
+
+ /// <summary>
+ /// The _ FF probe path
+ /// </summary>
+ private string _FFProbePath;
+ /// <summary>
+ /// Gets the path to ffprobe.exe
+ /// </summary>
+ /// <value>The FF probe path.</value>
+ public string FFProbePath
+ {
+ get
+ {
+ return _FFProbePath ?? (_FFProbePath = Path.Combine(VersionedDirectoryPath, "ffprobe.exe"));
+ }
+ }
+
+ /// <summary>
+ /// The _video images data path
+ /// </summary>
+ private string _videoImagesDataPath;
+ /// <summary>
+ /// Gets the video images data path.
+ /// </summary>
+ /// <value>The video images data path.</value>
+ public string VideoImagesDataPath
+ {
+ get
+ {
+ if (_videoImagesDataPath == null)
+ {
+ _videoImagesDataPath = Path.Combine(Kernel.ApplicationPaths.DataPath, "ffmpeg-video-images");
+
+ if (!Directory.Exists(_videoImagesDataPath))
+ {
+ Directory.CreateDirectory(_videoImagesDataPath);
+ }
+ }
+
+ return _videoImagesDataPath;
+ }
+ }
+
+ /// <summary>
+ /// The _audio images data path
+ /// </summary>
+ private string _audioImagesDataPath;
+ /// <summary>
+ /// Gets the audio images data path.
+ /// </summary>
+ /// <value>The audio images data path.</value>
+ public string AudioImagesDataPath
+ {
+ get
+ {
+ if (_audioImagesDataPath == null)
+ {
+ _audioImagesDataPath = Path.Combine(Kernel.ApplicationPaths.DataPath, "ffmpeg-audio-images");
+
+ if (!Directory.Exists(_audioImagesDataPath))
+ {
+ Directory.CreateDirectory(_audioImagesDataPath);
+ }
+ }
+
+ return _audioImagesDataPath;
+ }
+ }
+
+ /// <summary>
+ /// The _subtitle cache path
+ /// </summary>
+ private string _subtitleCachePath;
+ /// <summary>
+ /// Gets the subtitle cache path.
+ /// </summary>
+ /// <value>The subtitle cache path.</value>
+ public string SubtitleCachePath
+ {
+ get
+ {
+ if (_subtitleCachePath == null)
+ {
+ _subtitleCachePath = Path.Combine(Kernel.ApplicationPaths.CachePath, "ffmpeg-subtitles");
+
+ if (!Directory.Exists(_subtitleCachePath))
+ {
+ Directory.CreateDirectory(_subtitleCachePath);
+ }
+ }
+
+ return _subtitleCachePath;
+ }
+ }
+
+ /// <summary>
+ /// Gets the versioned directory path.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetVersionedDirectoryPath()
+ {
+ var assembly = GetType().Assembly;
+
+ const string prefix = "MediaBrowser.Controller.MediaInfo.";
+ const string srch = prefix + "ffmpeg";
+
+ var resource = assembly.GetManifestResourceNames().First(r => r.StartsWith(srch));
+
+ var filename = resource.Substring(resource.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) + prefix.Length);
+
+ var versionedDirectoryPath = Path.Combine(Kernel.ApplicationPaths.MediaToolsPath, Path.GetFileNameWithoutExtension(filename));
+
+ if (!Directory.Exists(versionedDirectoryPath))
+ {
+ Directory.CreateDirectory(versionedDirectoryPath);
+ }
+
+ ExtractTools(assembly, resource, versionedDirectoryPath);
+
+ return versionedDirectoryPath;
+ }
+
+ /// <summary>
+ /// Extracts the tools.
+ /// </summary>
+ /// <param name="assembly">The assembly.</param>
+ /// <param name="zipFileResourcePath">The zip file resource path.</param>
+ /// <param name="targetPath">The target path.</param>
+ private void ExtractTools(Assembly assembly, string zipFileResourcePath, string targetPath)
+ {
+ using (var resourceStream = assembly.GetManifestResourceStream(zipFileResourcePath))
+ {
+ using (var zipFile = ZipFile.Read(resourceStream))
+ {
+ zipFile.ExtractAll(targetPath, ExtractExistingFileAction.DoNotOverwrite);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the probe size argument.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ public string GetProbeSizeArgument(BaseItem item)
+ {
+ var video = item as Video;
+
+ return video != null ? GetProbeSizeArgument(video.VideoType, video.IsoType) : string.Empty;
+ }
+
+ /// <summary>
+ /// Gets the probe size argument.
+ /// </summary>
+ /// <param name="videoType">Type of the video.</param>
+ /// <param name="isoType">Type of the iso.</param>
+ /// <returns>System.String.</returns>
+ public string GetProbeSizeArgument(VideoType videoType, IsoType? isoType)
+ {
+ if (videoType == VideoType.Dvd || (isoType.HasValue && isoType.Value == IsoType.Dvd))
+ {
+ return "-probesize 1G -analyzeduration 200M";
+ }
+
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Runs FFProbe against a BaseItem
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cache">The cache.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{FFProbeResult}.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public Task<FFProbeResult> RunFFProbe(BaseItem item, string inputPath, DateTime lastDateModified, FileSystemRepository cache, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(inputPath))
+ {
+ throw new ArgumentNullException("inputPath");
+ }
+
+ if (cache == null)
+ {
+ throw new ArgumentNullException("cache");
+ }
+
+ // Put the ffmpeg version into the cache name so that it's unique per-version
+ // We don't want to try and deserialize data based on an old version, which could potentially fail
+ var resourceName = item.Id + "_" + lastDateModified.Ticks + "_" + FFMpegVersion;
+
+ // Forumulate the cache file path
+ var cacheFilePath = cache.GetResourcePath(resourceName, ".pb");
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Avoid File.Exists by just trying to deserialize
+ try
+ {
+ return Task.FromResult(Kernel.ProtobufSerializer.DeserializeFromFile<FFProbeResult>(cacheFilePath));
+ }
+ catch (FileNotFoundException)
+ {
+ var extractChapters = false;
+ var video = item as Video;
+ var probeSizeArgument = string.Empty;
+
+ if (video != null)
+ {
+ extractChapters = true;
+ probeSizeArgument = GetProbeSizeArgument(video.VideoType, video.IsoType);
+ }
+
+ return RunFFProbeInternal(inputPath, extractChapters, cacheFilePath, probeSizeArgument, cancellationToken);
+ }
+ }
+
+ /// <summary>
+ /// Runs FFProbe against a BaseItem
+ /// </summary>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
+ /// <param name="cacheFile">The cache file.</param>
+ /// <param name="probeSizeArgument">The probe size argument.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{FFProbeResult}.</returns>
+ /// <exception cref="System.ApplicationException"></exception>
+ private async Task<FFProbeResult> RunFFProbeInternal(string inputPath, bool extractChapters, string cacheFile, string probeSizeArgument, CancellationToken cancellationToken)
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+
+ // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ FileName = FFProbePath,
+ Arguments = string.Format("{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format", probeSizeArgument, inputPath).Trim(),
+
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+
+ EnableRaisingEvents = true
+ };
+
+ Logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Exited += ProcessExited;
+
+ await FFProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ FFProbeResult result;
+ string standardError = null;
+
+ try
+ {
+ process.Start();
+
+ Task<string> standardErrorReadTask = null;
+
+ // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+ if (extractChapters)
+ {
+ standardErrorReadTask = process.StandardError.ReadToEndAsync();
+ }
+ else
+ {
+ process.BeginErrorReadLine();
+ }
+
+ result = JsonSerializer.DeserializeFromStream<FFProbeResult>(process.StandardOutput.BaseStream);
+
+ if (extractChapters)
+ {
+ standardError = await standardErrorReadTask.ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ // Hate having to do this
+ try
+ {
+ process.Kill();
+ }
+ catch (InvalidOperationException ex1)
+ {
+ Logger.ErrorException("Error killing ffprobe", ex1);
+ }
+ catch (Win32Exception ex1)
+ {
+ Logger.ErrorException("Error killing ffprobe", ex1);
+ }
+
+ throw;
+ }
+ finally
+ {
+ FFProbeResourcePool.Release();
+ }
+
+ if (result == null)
+ {
+ throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (extractChapters && !string.IsNullOrEmpty(standardError))
+ {
+ AddChapters(result, standardError);
+ }
+
+ Kernel.ProtobufSerializer.SerializeToFile(result, cacheFile);
+
+ return result;
+ }
+
+ /// <summary>
+ /// Adds the chapters.
+ /// </summary>
+ /// <param name="result">The result.</param>
+ /// <param name="standardError">The standard error.</param>
+ private void AddChapters(FFProbeResult result, string standardError)
+ {
+ var lines = standardError.Split('\n').Select(l => l.TrimStart());
+
+ var chapters = new List<ChapterInfo> { };
+
+ ChapterInfo lastChapter = null;
+
+ foreach (var line in lines)
+ {
+ if (line.StartsWith("Chapter", StringComparison.OrdinalIgnoreCase))
+ {
+ // Example:
+ // Chapter #0.2: start 400.534, end 4565.435
+ const string srch = "start ";
+ var start = line.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+
+ if (start == -1)
+ {
+ continue;
+ }
+
+ var subString = line.Substring(start + srch.Length);
+ subString = subString.Substring(0, subString.IndexOf(','));
+
+ double seconds;
+
+ if (double.TryParse(subString, out seconds))
+ {
+ lastChapter = new ChapterInfo
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(seconds).Ticks
+ };
+
+ chapters.Add(lastChapter);
+ }
+ }
+
+ else if (line.StartsWith("title", StringComparison.OrdinalIgnoreCase))
+ {
+ if (lastChapter != null && string.IsNullOrEmpty(lastChapter.Name))
+ {
+ var index = line.IndexOf(':');
+
+ if (index != -1)
+ {
+ lastChapter.Name = line.Substring(index + 1).Trim().TrimEnd('\r');
+ }
+ }
+ }
+ }
+
+ result.Chapters = chapters;
+ }
+
+ /// <summary>
+ /// The first chapter ticks
+ /// </summary>
+ private static long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
+
+ /// <summary>
+ /// Extracts the chapter images.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="extractImages">if set to <c>true</c> [extract images].</param>
+ /// <param name="saveItem">if set to <c>true</c> [save item].</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task PopulateChapterImages(Video video, CancellationToken cancellationToken, bool extractImages, bool saveItem)
+ {
+ if (video.Chapters == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var changesMade = false;
+
+ foreach (var chapter in video.Chapters)
+ {
+ var filename = video.Id + "_" + video.DateModified.Ticks + "_" + chapter.StartPositionTicks;
+
+ var path = VideoImageCache.GetResourcePath(filename, ".jpg");
+
+ if (!VideoImageCache.ContainsFilePath(path))
+ {
+ if (extractImages)
+ {
+ // Disable for now on folder rips
+ if (video.VideoType != VideoType.VideoFile)
+ {
+ continue;
+ }
+
+ // Add some time for the first chapter to make sure we don't end up with a black image
+ var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
+
+ var success = await ExtractImage(GetInputArgument(video), time, path, cancellationToken).ConfigureAwait(false);
+
+ if (success)
+ {
+ chapter.ImagePath = path;
+ changesMade = true;
+ }
+ }
+ }
+ else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
+ {
+ chapter.ImagePath = path;
+ changesMade = true;
+ }
+ }
+
+ if (saveItem && changesMade)
+ {
+ await Kernel.ItemRepository.SaveItem(video, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Extracts an image from an Audio file and returns a Task whose result indicates whether it was successful or not
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException">input</exception>
+ public async Task<bool> ExtractImage(Audio input, string outputPath, CancellationToken cancellationToken)
+ {
+ if (input == null)
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException("outputPath");
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = string.Format("-i {0} -threads 0 -v quiet -f image2 \"{1}\"", GetInputArgument(input), outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ await AudioImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ await process.RunAsync().ConfigureAwait(false);
+
+ AudioImageResourcePool.Release();
+
+ var exitCode = process.ExitCode;
+
+ process.Dispose();
+
+ if (exitCode != -1 && File.Exists(outputPath))
+ {
+ return true;
+ }
+
+ Logger.Error("ffmpeg audio image extraction failed for {0}", input.Path);
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [is subtitle cached] [the specified input].
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+ /// <param name="outputExtension">The output extension.</param>
+ /// <returns><c>true</c> if [is subtitle cached] [the specified input]; otherwise, <c>false</c>.</returns>
+ public bool IsSubtitleCached(Video input, int subtitleStreamIndex, string outputExtension)
+ {
+ return SubtitleCache.ContainsFilePath(GetSubtitleCachePath(input, subtitleStreamIndex, outputExtension));
+ }
+
+ /// <summary>
+ /// Gets the subtitle cache path.
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+ /// <param name="outputExtension">The output extension.</param>
+ /// <returns>System.String.</returns>
+ public string GetSubtitleCachePath(Video input, int subtitleStreamIndex, string outputExtension)
+ {
+ return SubtitleCache.GetResourcePath(input.Id + "_" + subtitleStreamIndex + "_" + input.DateModified.Ticks, outputExtension);
+ }
+
+ /// <summary>
+ /// Extracts the text subtitle.
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException">input</exception>
+ public async Task<bool> ExtractTextSubtitle(Video input, int subtitleStreamIndex, string outputPath, CancellationToken cancellationToken)
+ {
+ if (input == null)
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", GetInputArgument(input), subtitleStreamIndex, outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ Logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ await AudioImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ await process.RunAsync().ConfigureAwait(false);
+
+ AudioImageResourcePool.Release();
+
+ var exitCode = process.ExitCode;
+
+ process.Dispose();
+
+ if (exitCode != -1 && File.Exists(outputPath))
+ {
+ return true;
+ }
+
+ Logger.Error("ffmpeg subtitle extraction failed for {0}", input.Path);
+ return false;
+ }
+
+ /// <summary>
+ /// Converts the text subtitle.
+ /// </summary>
+ /// <param name="mediaStream">The media stream.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException">mediaStream</exception>
+ /// <exception cref="System.ArgumentException">The given MediaStream is not an external subtitle stream</exception>
+ public async Task<bool> ConvertTextSubtitle(MediaStream mediaStream, string outputPath, CancellationToken cancellationToken)
+ {
+ if (mediaStream == null)
+ {
+ throw new ArgumentNullException("mediaStream");
+ }
+
+ if (!mediaStream.IsExternal || string.IsNullOrEmpty(mediaStream.Path))
+ {
+ throw new ArgumentException("The given MediaStream is not an external subtitle stream");
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = string.Format("-i \"{0}\" \"{1}\"", mediaStream.Path, outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ Logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ await AudioImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ await process.RunAsync().ConfigureAwait(false);
+
+ AudioImageResourcePool.Release();
+
+ var exitCode = process.ExitCode;
+
+ process.Dispose();
+
+ if (exitCode != -1 && File.Exists(outputPath))
+ {
+ return true;
+ }
+
+ Logger.Error("ffmpeg subtitle conversion failed for {0}", mediaStream.Path);
+ return false;
+ }
+
+ /// <summary>
+ /// Extracts an image from a Video and returns a Task whose result indicates whether it was successful or not
+ /// </summary>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="offset">The offset.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException">video</exception>
+ public async Task<bool> ExtractImage(string inputPath, TimeSpan offset, string outputPath, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(inputPath))
+ {
+ throw new ArgumentNullException("inputPath");
+ }
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException("outputPath");
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = string.Format("-ss {0} -i {1} -threads 0 -v quiet -t 1 -f image2 \"{2}\"", Convert.ToInt32(offset.TotalSeconds), inputPath, outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ await VideoImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ process.Start();
+
+ var ranToCompletion = process.WaitForExit(10000);
+
+ if (!ranToCompletion)
+ {
+ try
+ {
+ Logger.Info("Killing ffmpeg process");
+
+ process.Kill();
+ process.WaitForExit(1000);
+ }
+ catch (Win32Exception ex)
+ {
+ Logger.ErrorException("Error killing process", ex);
+ }
+ catch (InvalidOperationException ex)
+ {
+ Logger.ErrorException("Error killing process", ex);
+ }
+ catch (NotSupportedException ex)
+ {
+ Logger.ErrorException("Error killing process", ex);
+ }
+ }
+
+ VideoImageResourcePool.Release();
+
+ var exitCode = ranToCompletion ? process.ExitCode : -1;
+
+ process.Dispose();
+
+ if (exitCode == -1)
+ {
+ if (File.Exists(outputPath))
+ {
+ try
+ {
+ Logger.Info("Deleting extracted image due to failure: ", outputPath);
+ File.Delete(outputPath);
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error deleting extracted image {0}", ex, outputPath);
+ }
+ }
+ }
+ else
+ {
+ if (File.Exists(outputPath))
+ {
+ return true;
+ }
+ }
+
+ Logger.Error("ffmpeg video image extraction failed for {0}", inputPath);
+ return false;
+ }
+
+ /// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ public string GetInputArgument(BaseItem item)
+ {
+ var video = item as Video;
+
+ if (video != null)
+ {
+ if (video.VideoType == VideoType.BluRay)
+ {
+ return GetBlurayInputArgument(video.Path);
+ }
+
+ if (video.VideoType == VideoType.Dvd)
+ {
+ return GetDvdInputArgument(video.GetPlayableStreamFiles());
+ }
+ }
+
+ return string.Format("file:\"{0}\"", item.Path);
+ }
+
+ /// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="mount">The mount.</param>
+ /// <returns>System.String.</returns>
+ public string GetInputArgument(Video item, IIsoMount mount)
+ {
+ if (item.VideoType == VideoType.Iso && item.IsoType.HasValue)
+ {
+ if (item.IsoType.Value == IsoType.BluRay)
+ {
+ return GetBlurayInputArgument(mount.MountedPath);
+ }
+ if (item.IsoType.Value == IsoType.Dvd)
+ {
+ return GetDvdInputArgument(item.GetPlayableStreamFiles(mount.MountedPath));
+ }
+ }
+
+ return GetInputArgument(item);
+ }
+
+ /// <summary>
+ /// Gets the bluray input argument.
+ /// </summary>
+ /// <param name="blurayRoot">The bluray root.</param>
+ /// <returns>System.String.</returns>
+ public string GetBlurayInputArgument(string blurayRoot)
+ {
+ return string.Format("bluray:\"{0}\"", blurayRoot);
+ }
+
+ /// <summary>
+ /// Gets the DVD input argument.
+ /// </summary>
+ /// <param name="playableStreamFiles">The playable stream files.</param>
+ /// <returns>System.String.</returns>
+ public string GetDvdInputArgument(IEnumerable<string> playableStreamFiles)
+ {
+ // Get all streams
+ var streamFilePaths = (playableStreamFiles ?? new string[] { }).ToArray();
+
+ // If there's more than one we'll need to use the concat command
+ if (streamFilePaths.Length > 1)
+ {
+ var files = string.Join("|", streamFilePaths);
+
+ return string.Format("concat:\"{0}\"", files);
+ }
+
+ // Determine the input path for video files
+ return string.Format("file:\"{0}\"", streamFilePaths[0]);
+ }
+
+ /// <summary>
+ /// Processes the exited.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ void ProcessExited(object sender, EventArgs e)
+ {
+ ((Process)sender).Dispose();
+ }
+
+ /// <summary>
+ /// Sets the error mode.
+ /// </summary>
+ /// <param name="uMode">The u mode.</param>
+ /// <returns>ErrorModes.</returns>
+ [DllImport("kernel32.dll")]
+ static extern ErrorModes SetErrorMode(ErrorModes uMode);
+
+ /// <summary>
+ /// Enum ErrorModes
+ /// </summary>
+ [Flags]
+ public enum ErrorModes : uint
+ {
+ /// <summary>
+ /// The SYSTE m_ DEFAULT
+ /// </summary>
+ SYSTEM_DEFAULT = 0x0,
+ /// <summary>
+ /// The SE m_ FAILCRITICALERRORS
+ /// </summary>
+ SEM_FAILCRITICALERRORS = 0x0001,
+ /// <summary>
+ /// The SE m_ NOALIGNMENTFAULTEXCEPT
+ /// </summary>
+ SEM_NOALIGNMENTFAULTEXCEPT = 0x0004,
+ /// <summary>
+ /// The SE m_ NOGPFAULTERRORBOX
+ /// </summary>
+ SEM_NOGPFAULTERRORBOX = 0x0002,
+ /// <summary>
+ /// The SE m_ NOOPENFILEERRORBOX
+ /// </summary>
+ SEM_NOOPENFILEERRORBOX = 0x8000
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaInfo/FFProbeResult.cs b/MediaBrowser.Controller/MediaInfo/FFProbeResult.cs
new file mode 100644
index 0000000000..06b4ff87d7
--- /dev/null
+++ b/MediaBrowser.Controller/MediaInfo/FFProbeResult.cs
@@ -0,0 +1,354 @@
+using MediaBrowser.Model.Entities;
+using ProtoBuf;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.MediaInfo
+{
+ /// <summary>
+ /// Provides a class that we can use to deserialize the ffprobe json output
+ /// Sample output:
+ /// http://stackoverflow.com/questions/7708373/get-ffmpeg-information-in-friendly-way
+ /// </summary>
+ [ProtoContract]
+ public class FFProbeResult
+ {
+ /// <summary>
+ /// Gets or sets the streams.
+ /// </summary>
+ /// <value>The streams.</value>
+ [ProtoMember(1)]
+ public FFProbeMediaStreamInfo[] streams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the format.
+ /// </summary>
+ /// <value>The format.</value>
+ [ProtoMember(2)]
+ public FFProbeMediaFormatInfo format { get; set; }
+
+ [ProtoMember(3)]
+ public List<ChapterInfo> Chapters { get; set; }
+ }
+
+ /// <summary>
+ /// Represents a stream within the output
+ /// </summary>
+ [ProtoContract]
+ public class FFProbeMediaStreamInfo
+ {
+ /// <summary>
+ /// Gets or sets the index.
+ /// </summary>
+ /// <value>The index.</value>
+ [ProtoMember(1)]
+ public int index { get; set; }
+
+ /// <summary>
+ /// Gets or sets the profile.
+ /// </summary>
+ /// <value>The profile.</value>
+ [ProtoMember(2)]
+ public string profile { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec_name.
+ /// </summary>
+ /// <value>The codec_name.</value>
+ [ProtoMember(3)]
+ public string codec_name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec_long_name.
+ /// </summary>
+ /// <value>The codec_long_name.</value>
+ [ProtoMember(4)]
+ public string codec_long_name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec_type.
+ /// </summary>
+ /// <value>The codec_type.</value>
+ [ProtoMember(5)]
+ public string codec_type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sample_rate.
+ /// </summary>
+ /// <value>The sample_rate.</value>
+ [ProtoMember(6)]
+ public string sample_rate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channels.
+ /// </summary>
+ /// <value>The channels.</value>
+ [ProtoMember(7)]
+ public int channels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the avg_frame_rate.
+ /// </summary>
+ /// <value>The avg_frame_rate.</value>
+ [ProtoMember(8)]
+ public string avg_frame_rate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the duration.
+ /// </summary>
+ /// <value>The duration.</value>
+ [ProtoMember(9)]
+ public string duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the bit_rate.
+ /// </summary>
+ /// <value>The bit_rate.</value>
+ [ProtoMember(10)]
+ public string bit_rate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ [ProtoMember(11)]
+ public int width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ [ProtoMember(12)]
+ public int height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display_aspect_ratio.
+ /// </summary>
+ /// <value>The display_aspect_ratio.</value>
+ [ProtoMember(13)]
+ public string display_aspect_ratio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tags.
+ /// </summary>
+ /// <value>The tags.</value>
+ [ProtoMember(14)]
+ public Dictionary<string, string> tags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the bits_per_sample.
+ /// </summary>
+ /// <value>The bits_per_sample.</value>
+ [ProtoMember(17)]
+ public int bits_per_sample { get; set; }
+
+ /// <summary>
+ /// Gets or sets the r_frame_rate.
+ /// </summary>
+ /// <value>The r_frame_rate.</value>
+ [ProtoMember(18)]
+ public string r_frame_rate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the has_b_frames.
+ /// </summary>
+ /// <value>The has_b_frames.</value>
+ [ProtoMember(19)]
+ public int has_b_frames { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sample_aspect_ratio.
+ /// </summary>
+ /// <value>The sample_aspect_ratio.</value>
+ [ProtoMember(20)]
+ public string sample_aspect_ratio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the pix_fmt.
+ /// </summary>
+ /// <value>The pix_fmt.</value>
+ [ProtoMember(21)]
+ public string pix_fmt { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level.
+ /// </summary>
+ /// <value>The level.</value>
+ [ProtoMember(22)]
+ public int level { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time_base.
+ /// </summary>
+ /// <value>The time_base.</value>
+ [ProtoMember(23)]
+ public string time_base { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start_time.
+ /// </summary>
+ /// <value>The start_time.</value>
+ [ProtoMember(24)]
+ public string start_time { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec_time_base.
+ /// </summary>
+ /// <value>The codec_time_base.</value>
+ [ProtoMember(25)]
+ public string codec_time_base { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec_tag.
+ /// </summary>
+ /// <value>The codec_tag.</value>
+ [ProtoMember(26)]
+ public string codec_tag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec_tag_string.
+ /// </summary>
+ /// <value>The codec_tag_string.</value>
+ [ProtoMember(27)]
+ public string codec_tag_string { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sample_fmt.
+ /// </summary>
+ /// <value>The sample_fmt.</value>
+ [ProtoMember(28)]
+ public string sample_fmt { get; set; }
+
+ /// <summary>
+ /// Gets or sets the dmix_mode.
+ /// </summary>
+ /// <value>The dmix_mode.</value>
+ [ProtoMember(29)]
+ public string dmix_mode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start_pts.
+ /// </summary>
+ /// <value>The start_pts.</value>
+ [ProtoMember(30)]
+ public string start_pts { get; set; }
+
+ /// <summary>
+ /// Gets or sets the is_avc.
+ /// </summary>
+ /// <value>The is_avc.</value>
+ [ProtoMember(31)]
+ public string is_avc { get; set; }
+
+ /// <summary>
+ /// Gets or sets the nal_length_size.
+ /// </summary>
+ /// <value>The nal_length_size.</value>
+ [ProtoMember(32)]
+ public string nal_length_size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ltrt_cmixlev.
+ /// </summary>
+ /// <value>The ltrt_cmixlev.</value>
+ [ProtoMember(33)]
+ public string ltrt_cmixlev { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ltrt_surmixlev.
+ /// </summary>
+ /// <value>The ltrt_surmixlev.</value>
+ [ProtoMember(34)]
+ public string ltrt_surmixlev { get; set; }
+
+ /// <summary>
+ /// Gets or sets the loro_cmixlev.
+ /// </summary>
+ /// <value>The loro_cmixlev.</value>
+ [ProtoMember(35)]
+ public string loro_cmixlev { get; set; }
+
+ /// <summary>
+ /// Gets or sets the loro_surmixlev.
+ /// </summary>
+ /// <value>The loro_surmixlev.</value>
+ [ProtoMember(36)]
+ public string loro_surmixlev { get; set; }
+
+ /// <summary>
+ /// Gets or sets the disposition.
+ /// </summary>
+ /// <value>The disposition.</value>
+ [ProtoMember(37)]
+ public Dictionary<string, string> disposition { get; set; }
+ }
+
+ /// <summary>
+ /// Class MediaFormat
+ /// </summary>
+ [ProtoContract]
+ public class FFProbeMediaFormatInfo
+ {
+ /// <summary>
+ /// Gets or sets the filename.
+ /// </summary>
+ /// <value>The filename.</value>
+ [ProtoMember(1)]
+ public string filename { get; set; }
+
+ /// <summary>
+ /// Gets or sets the nb_streams.
+ /// </summary>
+ /// <value>The nb_streams.</value>
+ [ProtoMember(2)]
+ public int nb_streams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the format_name.
+ /// </summary>
+ /// <value>The format_name.</value>
+ [ProtoMember(3)]
+ public string format_name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the format_long_name.
+ /// </summary>
+ /// <value>The format_long_name.</value>
+ [ProtoMember(4)]
+ public string format_long_name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start_time.
+ /// </summary>
+ /// <value>The start_time.</value>
+ [ProtoMember(5)]
+ public string start_time { get; set; }
+
+ /// <summary>
+ /// Gets or sets the duration.
+ /// </summary>
+ /// <value>The duration.</value>
+ [ProtoMember(6)]
+ public string duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ /// <value>The size.</value>
+ [ProtoMember(7)]
+ public string size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the bit_rate.
+ /// </summary>
+ /// <value>The bit_rate.</value>
+ [ProtoMember(8)]
+ public string bit_rate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tags.
+ /// </summary>
+ /// <value>The tags.</value>
+ [ProtoMember(9)]
+ public Dictionary<string, string> tags { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaInfo/ffmpeg20130209.zip.REMOVED.git-id b/MediaBrowser.Controller/MediaInfo/ffmpeg20130209.zip.REMOVED.git-id
new file mode 100644
index 0000000000..307afc51cb
--- /dev/null
+++ b/MediaBrowser.Controller/MediaInfo/ffmpeg20130209.zip.REMOVED.git-id
@@ -0,0 +1 @@
+985770c0d2633a13719be2e5cf19554262415f62 \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaInfo/readme.txt b/MediaBrowser.Controller/MediaInfo/readme.txt
new file mode 100644
index 0000000000..b32dd9aec0
--- /dev/null
+++ b/MediaBrowser.Controller/MediaInfo/readme.txt
@@ -0,0 +1,5 @@
+This is the 32-bit static build of ffmpeg, located at:
+
+http://ffmpeg.zeranoe.com/builds/
+
+The zip file contains both ffmpeg and ffprobe, and is suffixed with the date of the build. \ No newline at end of file
diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
new file mode 100644
index 0000000000..b820f54a8d
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Interface IDisplayPreferencesRepository
+ /// </summary>
+ public interface IDisplayPreferencesRepository : IRepository
+ {
+ /// <summary>
+ /// Saves display preferences for an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveDisplayPrefs(Folder item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets display preferences for an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{DisplayPreferences}.</returns>
+ IEnumerable<DisplayPreferences> RetrieveDisplayPrefs(Folder item);
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
new file mode 100644
index 0000000000..c7e51e38c3
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides an interface to implement an Item repository
+ /// </summary>
+ public interface IItemRepository : IRepository
+ {
+ /// <summary>
+ /// Saves an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveItem(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets an item
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem RetrieveItem(Guid id);
+
+ /// <summary>
+ /// Gets children of a given Folder
+ /// </summary>
+ /// <param name="parent">The parent.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ IEnumerable<BaseItem> RetrieveChildren(Folder parent);
+
+ /// <summary>
+ /// Saves children of a given Folder
+ /// </summary>
+ /// <param name="parentId">The parent id.</param>
+ /// <param name="children">The children.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveChildren(Guid parentId, IEnumerable<BaseItem> children, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IRepository.cs b/MediaBrowser.Controller/Persistence/IRepository.cs
new file mode 100644
index 0000000000..2d051aa82c
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IRepository.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides a base interface for all the repository interfaces
+ /// </summary>
+ public interface IRepository : IDisposable
+ {
+ /// <summary>
+ /// Opens the connection to the repository
+ /// </summary>
+ /// <returns>Task.</returns>
+ Task Initialize();
+
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
new file mode 100644
index 0000000000..115bd411ab
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -0,0 +1,28 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides an interface to implement a UserData repository
+ /// </summary>
+ public interface IUserDataRepository : IRepository
+ {
+ /// <summary>
+ /// Saves user data for an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveUserData(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets user data for an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{UserItemData}.</returns>
+ IEnumerable<UserItemData> RetrieveUserData(BaseItem item);
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IUserRepository.cs b/MediaBrowser.Controller/Persistence/IUserRepository.cs
new file mode 100644
index 0000000000..80961a369a
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IUserRepository.cs
@@ -0,0 +1,35 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides an interface to implement a User repository
+ /// </summary>
+ public interface IUserRepository : IRepository
+ {
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteUser(User user, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Saves the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveUser(User user, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Retrieves all users.
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ IEnumerable<User> RetrieveAllUsers();
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/SQLite/SQLiteDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/SQLite/SQLiteDisplayPreferencesRepository.cs
new file mode 100644
index 0000000000..db1535b345
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/SQLite/SQLiteDisplayPreferencesRepository.cs
@@ -0,0 +1,139 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Data;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence.SQLite
+{
+ /// <summary>
+ /// Class SQLiteDisplayPreferencesRepository
+ /// </summary>
+ [Export(typeof(IDisplayPreferencesRepository))]
+ class SQLiteDisplayPreferencesRepository : SqliteRepository, IDisplayPreferencesRepository
+ {
+ /// <summary>
+ /// The repository name
+ /// </summary>
+ public const string RepositoryName = "SQLite";
+
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get
+ {
+ return RepositoryName;
+ }
+ }
+
+ /// <summary>
+ /// Opens the connection to the database
+ /// </summary>
+ /// <returns>Task.</returns>
+ public async Task Initialize()
+ {
+ var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "displaypreferences.db");
+
+ await ConnectToDB(dbFile).ConfigureAwait(false);
+
+ string[] queries = {
+
+ "create table if not exists display_prefs (item_id GUID, user_id GUID, data BLOB)",
+ "create unique index if not exists idx_display_prefs on display_prefs (item_id, user_id)",
+ "create table if not exists schema_version (table_name primary key, version)",
+ //pragmas
+ "pragma temp_store = memory"
+ };
+
+ RunQueries(queries);
+ }
+
+ /// <summary>
+ /// Save the display preferences associated with an item in the repo
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public Task SaveDisplayPrefs(Folder item, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return Task.Run(() =>
+ {
+ var cmd = connection.CreateCommand();
+
+ cmd.CommandText = "delete from display_prefs where item_id = @guid";
+ cmd.AddParam("@guid", item.DisplayPrefsId);
+
+ QueueCommand(cmd);
+
+ if (item.DisplayPrefs != null)
+ {
+ foreach (var data in item.DisplayPrefs)
+ {
+ cmd = connection.CreateCommand();
+ cmd.CommandText = "insert into display_prefs (item_id, user_id, data) values (@1, @2, @3)";
+ cmd.AddParam("@1", item.DisplayPrefsId);
+ cmd.AddParam("@2", data.UserId);
+
+ cmd.AddParam("@3", Kernel.Instance.ProtobufSerializer.SerializeToBytes(data));
+
+ QueueCommand(cmd);
+ }
+ }
+ });
+ }
+
+ /// <summary>
+ /// Gets display preferences for an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{DisplayPreferences}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public IEnumerable<DisplayPreferences> RetrieveDisplayPrefs(Folder item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "select data from display_prefs where item_id = @guid";
+ var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+ guidParam.Value = item.DisplayPrefsId;
+
+ using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
+ {
+ while (reader.Read())
+ {
+ using (var stream = GetStream(reader, 0))
+ {
+ var data = Kernel.Instance.ProtobufSerializer.DeserializeFromStream<DisplayPreferences>(stream);
+ if (data != null)
+ {
+ yield return data;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/SQLite/SQLiteExtensions.cs b/MediaBrowser.Controller/Persistence/SQLite/SQLiteExtensions.cs
new file mode 100644
index 0000000000..f1ed774924
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/SQLite/SQLiteExtensions.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Data;
+using System.Data.SQLite;
+
+namespace MediaBrowser.Controller.Persistence.SQLite
+{
+ /// <summary>
+ /// Class SQLiteExtensions
+ /// </summary>
+ static class SQLiteExtensions
+ {
+ /// <summary>
+ /// Adds the param.
+ /// </summary>
+ /// <param name="cmd">The CMD.</param>
+ /// <param name="param">The param.</param>
+ /// <returns>SQLiteParameter.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param)
+ {
+ if (string.IsNullOrEmpty(param))
+ {
+ throw new ArgumentNullException();
+ }
+
+ var sqliteParam = new SQLiteParameter(param);
+ cmd.Parameters.Add(sqliteParam);
+ return sqliteParam;
+ }
+
+ /// <summary>
+ /// Adds the param.
+ /// </summary>
+ /// <param name="cmd">The CMD.</param>
+ /// <param name="param">The param.</param>
+ /// <param name="data">The data.</param>
+ /// <returns>SQLiteParameter.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param, object data)
+ {
+ if (string.IsNullOrEmpty(param))
+ {
+ throw new ArgumentNullException();
+ }
+
+ var sqliteParam = AddParam(cmd, param);
+ sqliteParam.Value = data;
+ return sqliteParam;
+ }
+
+ /// <summary>
+ /// Determines whether the specified conn is open.
+ /// </summary>
+ /// <param name="conn">The conn.</param>
+ /// <returns><c>true</c> if the specified conn is open; otherwise, <c>false</c>.</returns>
+ public static bool IsOpen(this SQLiteConnection conn)
+ {
+ return conn.State == ConnectionState.Open;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/SQLite/SQLiteItemRepository.cs b/MediaBrowser.Controller/Persistence/SQLite/SQLiteItemRepository.cs
new file mode 100644
index 0000000000..08527f9c1f
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/SQLite/SQLiteItemRepository.cs
@@ -0,0 +1,268 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Data;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence.SQLite
+{
+ /// <summary>
+ /// Class SQLiteItemRepository
+ /// </summary>
+ [Export(typeof(IItemRepository))]
+ public class SQLiteItemRepository : SqliteRepository, IItemRepository
+ {
+ /// <summary>
+ /// The _type mapper
+ /// </summary>
+ private readonly TypeMapper _typeMapper = new TypeMapper();
+
+ /// <summary>
+ /// The repository name
+ /// </summary>
+ public const string RepositoryName = "SQLite";
+
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get
+ {
+ return RepositoryName;
+ }
+ }
+
+ /// <summary>
+ /// Opens the connection to the database
+ /// </summary>
+ /// <returns>Task.</returns>
+ public async Task Initialize()
+ {
+ var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "library.db");
+
+ await ConnectToDB(dbFile).ConfigureAwait(false);
+
+ string[] queries = {
+
+ "create table if not exists items (guid GUID primary key, obj_type, data BLOB)",
+ "create index if not exists idx_items on items(guid)",
+ "create table if not exists children (guid GUID, child GUID)",
+ "create unique index if not exists idx_children on children(guid, child)",
+ "create table if not exists schema_version (table_name primary key, version)",
+ //triggers
+ TriggerSql,
+ //pragmas
+ "pragma temp_store = memory"
+ };
+
+ RunQueries(queries);
+ }
+
+ //cascade delete triggers
+ /// <summary>
+ /// The trigger SQL
+ /// </summary>
+ protected string TriggerSql =
+ @"CREATE TRIGGER if not exists delete_item
+ AFTER DELETE
+ ON items
+ FOR EACH ROW
+ BEGIN
+ DELETE FROM children WHERE children.guid = old.child;
+ DELETE FROM children WHERE children.child = old.child;
+ END";
+
+ /// <summary>
+ /// Save a standard item in the repo
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public Task SaveItem(BaseItem item, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return Task.Run(() =>
+ {
+ var serialized = JsonSerializer.SerializeToBytes(item);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "replace into items (guid, obj_type, data) values (@1, @2, @3)";
+ cmd.AddParam("@1", item.Id);
+ cmd.AddParam("@2", item.GetType().FullName);
+ cmd.AddParam("@3", serialized);
+ QueueCommand(cmd);
+ });
+ }
+
+ /// <summary>
+ /// Retrieve a standard item from the repo
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentException"></exception>
+ public BaseItem RetrieveItem(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentException();
+ }
+
+ return RetrieveItemInternal(id);
+ }
+
+ /// <summary>
+ /// Internal retrieve from items or users table
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentException"></exception>
+ protected BaseItem RetrieveItemInternal(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentException();
+ }
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "select obj_type,data from items where guid = @guid";
+ var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+ guidParam.Value = id;
+
+ using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
+ {
+ if (reader.Read())
+ {
+ var type = reader.GetString(0);
+ using (var stream = GetStream(reader, 1))
+ {
+ var itemType = _typeMapper.GetType(type);
+
+ if (itemType == null)
+ {
+ Logger.Error("Cannot find type {0}. Probably belongs to plug-in that is no longer loaded.", type);
+ return null;
+ }
+
+ var item = JsonSerializer.DeserializeFromStream(stream, itemType);
+ return item as BaseItem;
+ }
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Retrieve all the children of the given folder
+ /// </summary>
+ /// <param name="parent">The parent.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public IEnumerable<BaseItem> RetrieveChildren(Folder parent)
+ {
+ if (parent == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "select obj_type,data from items where guid in (select child from children where guid = @guid)";
+ var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+ guidParam.Value = parent.Id;
+
+ using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
+ {
+ while (reader.Read())
+ {
+ var type = reader.GetString(0);
+
+ using (var stream = GetStream(reader, 1))
+ {
+ var itemType = _typeMapper.GetType(type);
+ if (itemType == null)
+ {
+ Logger.Error("Cannot find type {0}. Probably belongs to plug-in that is no longer loaded.",type);
+ continue;
+ }
+ var item = JsonSerializer.DeserializeFromStream(stream, itemType) as BaseItem;
+ if (item != null)
+ {
+ item.Parent = parent;
+ yield return item;
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Save references to all the children for the given folder
+ /// (Doesn't actually save the child entities)
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="children">The children.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public Task SaveChildren(Guid id, IEnumerable<BaseItem> children, CancellationToken cancellationToken)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ if (children == null)
+ {
+ throw new ArgumentNullException("children");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return Task.Run(() =>
+ {
+ var cmd = connection.CreateCommand();
+
+ cmd.CommandText = "delete from children where guid = @guid";
+ cmd.AddParam("@guid", id);
+
+ QueueCommand(cmd);
+
+ foreach (var child in children)
+ {
+ var guid = child.Id;
+ cmd = connection.CreateCommand();
+ cmd.AddParam("@guid", id);
+ cmd.CommandText = "replace into children (guid, child) values (@guid, @child)";
+ var childParam = cmd.Parameters.Add("@child", DbType.Guid);
+
+ childParam.Value = guid;
+ QueueCommand(cmd);
+ }
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/SQLite/SQLiteRepository.cs b/MediaBrowser.Controller/Persistence/SQLite/SQLiteRepository.cs
new file mode 100644
index 0000000000..5cf57541b9
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/SQLite/SQLiteRepository.cs
@@ -0,0 +1,301 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Data;
+using System.Data.Common;
+using System.Data.SQLite;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence.SQLite
+{
+ /// <summary>
+ /// Class SqliteRepository
+ /// </summary>
+ public abstract class SqliteRepository : IDisposable
+ {
+ /// <summary>
+ /// The db file name
+ /// </summary>
+ protected string dbFileName;
+ /// <summary>
+ /// The connection
+ /// </summary>
+ protected SQLiteConnection connection;
+ /// <summary>
+ /// The delayed commands
+ /// </summary>
+ protected ConcurrentQueue<SQLiteCommand> delayedCommands = new ConcurrentQueue<SQLiteCommand>();
+ /// <summary>
+ /// The flush interval
+ /// </summary>
+ private const int FlushInterval = 5000;
+
+ /// <summary>
+ /// The flush timer
+ /// </summary>
+ private Timer FlushTimer;
+
+ protected ILogger Logger { get; private set; }
+
+ /// <summary>
+ /// Connects to DB.
+ /// </summary>
+ /// <param name="dbPath">The db path.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ protected async Task ConnectToDB(string dbPath)
+ {
+ if (string.IsNullOrEmpty(dbPath))
+ {
+ throw new ArgumentNullException("dbPath");
+ }
+
+ Logger = LogManager.GetLogger(GetType().Name);
+
+ dbFileName = dbPath;
+ var connectionstr = new SQLiteConnectionStringBuilder
+ {
+ PageSize = 4096,
+ CacheSize = 40960,
+ SyncMode = SynchronizationModes.Off,
+ DataSource = dbPath,
+ JournalMode = SQLiteJournalModeEnum.Memory
+ };
+
+ connection = new SQLiteConnection(connectionstr.ConnectionString);
+
+ await connection.OpenAsync().ConfigureAwait(false);
+
+ // Run once
+ FlushTimer = new Timer(Flush, null, TimeSpan.FromMilliseconds(FlushInterval), TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Runs the queries.
+ /// </summary>
+ /// <param name="queries">The queries.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ protected void RunQueries(string[] queries)
+ {
+ if (queries == null)
+ {
+ throw new ArgumentNullException("queries");
+ }
+
+ using (var tran = connection.BeginTransaction())
+ {
+ try
+ {
+ var cmd = connection.CreateCommand();
+
+ foreach (var query in queries)
+ {
+ cmd.Transaction = tran;
+ cmd.CommandText = query;
+ cmd.ExecuteNonQuery();
+ }
+
+ tran.Commit();
+ }
+ catch (Exception e)
+ {
+ Logger.ErrorException("Error running queries", e);
+ tran.Rollback();
+ throw;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ Logger.Info("Disposing " + GetType().Name);
+
+ try
+ {
+ // If we're not already flushing, do it now
+ if (!IsFlushing)
+ {
+ Flush(null);
+ }
+
+ // Don't dispose in the middle of a flush
+ while (IsFlushing)
+ {
+ Thread.Sleep(50);
+ }
+
+ if (FlushTimer != null)
+ {
+ FlushTimer.Dispose();
+ FlushTimer = null;
+ }
+
+ if (connection.IsOpen())
+ {
+ connection.Close();
+ }
+
+ connection.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error disposing database", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Queues the command.
+ /// </summary>
+ /// <param name="cmd">The CMD.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ protected void QueueCommand(SQLiteCommand cmd)
+ {
+ if (cmd == null)
+ {
+ throw new ArgumentNullException("cmd");
+ }
+
+ delayedCommands.Enqueue(cmd);
+ }
+
+ /// <summary>
+ /// The is flushing
+ /// </summary>
+ private bool IsFlushing;
+
+ /// <summary>
+ /// Flushes the specified sender.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ private void Flush(object sender)
+ {
+ // Cannot call Count on a ConcurrentQueue since it's an O(n) operation
+ // Use IsEmpty instead
+ if (delayedCommands.IsEmpty)
+ {
+ FlushTimer.Change(TimeSpan.FromMilliseconds(FlushInterval), TimeSpan.FromMilliseconds(-1));
+ return;
+ }
+
+ if (IsFlushing)
+ {
+ return;
+ }
+
+ IsFlushing = true;
+ var numCommands = 0;
+
+ using (var tran = connection.BeginTransaction())
+ {
+ try
+ {
+ while (!delayedCommands.IsEmpty)
+ {
+ SQLiteCommand command;
+
+ delayedCommands.TryDequeue(out command);
+
+ command.Connection = connection;
+ command.Transaction = tran;
+
+ command.ExecuteNonQuery();
+ numCommands++;
+ }
+
+ tran.Commit();
+ }
+ catch (Exception e)
+ {
+ Logger.ErrorException("Failed to commit transaction.", e);
+ tran.Rollback();
+ }
+ }
+
+ Logger.Info("SQL Delayed writer executed " + numCommands + " commands");
+
+ FlushTimer.Change(TimeSpan.FromMilliseconds(FlushInterval), TimeSpan.FromMilliseconds(-1));
+ IsFlushing = false;
+ }
+
+ /// <summary>
+ /// Executes the command.
+ /// </summary>
+ /// <param name="cmd">The CMD.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task ExecuteCommand(DbCommand cmd)
+ {
+ if (cmd == null)
+ {
+ throw new ArgumentNullException("cmd");
+ }
+
+ using (var tran = connection.BeginTransaction())
+ {
+ try
+ {
+ cmd.Connection = connection;
+ cmd.Transaction = tran;
+
+ await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
+
+ tran.Commit();
+ }
+ catch (Exception e)
+ {
+ Logger.ErrorException("Failed to commit transaction.", e);
+ tran.Rollback();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a stream from a DataReader at a given ordinal
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="ordinal">The ordinal.</param>
+ /// <returns>Stream.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ protected static Stream GetStream(IDataReader reader, int ordinal)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException("reader");
+ }
+
+ var memoryStream = new MemoryStream();
+ var num = 0L;
+ var array = new byte[4096];
+ long bytes;
+ do
+ {
+ bytes = reader.GetBytes(ordinal, num, array, 0, array.Length);
+ memoryStream.Write(array, 0, (int)bytes);
+ num += bytes;
+ }
+ while (bytes > 0L);
+ memoryStream.Position = 0;
+ return memoryStream;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/SQLite/SQLiteUserDataRepository.cs b/MediaBrowser.Controller/Persistence/SQLite/SQLiteUserDataRepository.cs
new file mode 100644
index 0000000000..a027e84751
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/SQLite/SQLiteUserDataRepository.cs
@@ -0,0 +1,138 @@
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Data;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence.SQLite
+{
+ /// <summary>
+ /// Class SQLiteUserDataRepository
+ /// </summary>
+ [Export(typeof(IUserDataRepository))]
+ public class SQLiteUserDataRepository : SqliteRepository, IUserDataRepository
+ {
+ /// <summary>
+ /// The repository name
+ /// </summary>
+ public const string RepositoryName = "SQLite";
+
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get
+ {
+ return RepositoryName;
+ }
+ }
+
+ /// <summary>
+ /// Opens the connection to the database
+ /// </summary>
+ /// <returns>Task.</returns>
+ public async Task Initialize()
+ {
+ var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "userdata.db");
+
+ await ConnectToDB(dbFile).ConfigureAwait(false);
+
+ string[] queries = {
+
+ "create table if not exists user_data (item_id GUID, user_id GUID, data BLOB)",
+ "create unique index if not exists idx_user_data on user_data (item_id, user_id)",
+ "create table if not exists schema_version (table_name primary key, version)",
+ //pragmas
+ "pragma temp_store = memory"
+ };
+
+ RunQueries(queries);
+ }
+
+ /// <summary>
+ /// Save the user specific data associated with an item in the repo
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public Task SaveUserData(BaseItem item, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var cmd = connection.CreateCommand();
+
+ cmd.CommandText = "delete from user_data where item_id = @guid";
+ cmd.AddParam("@guid", item.UserDataId);
+
+ QueueCommand(cmd);
+
+ if (item.UserData != null)
+ {
+ foreach (var data in item.UserData)
+ {
+ cmd = connection.CreateCommand();
+ cmd.CommandText = "insert into user_data (item_id, user_id, data) values (@1, @2, @3)";
+ cmd.AddParam("@1", item.UserDataId);
+ cmd.AddParam("@2", data.UserId);
+
+ cmd.AddParam("@3", Kernel.Instance.ProtobufSerializer.SerializeToBytes(data));
+
+ QueueCommand(cmd);
+ }
+ }
+ });
+ }
+
+ /// <summary>
+ /// Gets user data for an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{UserItemData}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public IEnumerable<UserItemData> RetrieveUserData(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "select data from user_data where item_id = @guid";
+ var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+ guidParam.Value = item.UserDataId;
+
+ using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
+ {
+ while (reader.Read())
+ {
+ using (var stream = GetStream(reader, 0))
+ {
+ var data = Kernel.Instance.ProtobufSerializer.DeserializeFromStream<UserItemData>(stream);
+ if (data != null)
+ {
+ yield return data;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/SQLite/SQLiteUserRepository.cs b/MediaBrowser.Controller/Persistence/SQLite/SQLiteUserRepository.cs
new file mode 100644
index 0000000000..9fe5e5624f
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/SQLite/SQLiteUserRepository.cs
@@ -0,0 +1,147 @@
+using System.Threading;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Data;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Persistence.SQLite
+{
+ /// <summary>
+ /// Class SQLiteUserRepository
+ /// </summary>
+ [Export(typeof(IUserRepository))]
+ public class SQLiteUserRepository : SqliteRepository, IUserRepository
+ {
+ /// <summary>
+ /// The repository name
+ /// </summary>
+ public const string RepositoryName = "SQLite";
+
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get
+ {
+ return RepositoryName;
+ }
+ }
+
+ /// <summary>
+ /// Opens the connection to the database
+ /// </summary>
+ /// <returns>Task.</returns>
+ public async Task Initialize()
+ {
+ var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "users.db");
+
+ await ConnectToDB(dbFile).ConfigureAwait(false);
+
+ string[] queries = {
+
+ "create table if not exists users (guid GUID primary key, data BLOB)",
+ "create index if not exists idx_users on users(guid)",
+ "create table if not exists schema_version (table_name primary key, version)",
+ //pragmas
+ "pragma temp_store = memory"
+ };
+
+ RunQueries(queries);
+ }
+
+ /// <summary>
+ /// Save a user in the repo
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public Task SaveUser(User user, CancellationToken cancellationToken)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var serialized = JsonSerializer.SerializeToBytes(user);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "replace into users (guid, data) values (@1, @2)";
+ cmd.AddParam("@1", user.Id);
+ cmd.AddParam("@2", serialized);
+ QueueCommand(cmd);
+ });
+ }
+
+ /// <summary>
+ /// Retrieve all users from the database
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ public IEnumerable<User> RetrieveAllUsers()
+ {
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "select data from users";
+
+ using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
+ {
+ while (reader.Read())
+ {
+ using (var stream = GetStream(reader, 0))
+ {
+ var user = JsonSerializer.DeserializeFromStream<User>(stream);
+ yield return user;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public Task DeleteUser(User user, CancellationToken cancellationToken)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var cmd = connection.CreateCommand();
+ cmd.CommandText = "delete from users where guid=@guid";
+ var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+ guidParam.Value = user.Id;
+
+ return ExecuteCommand(cmd);
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/TypeMapper.cs b/MediaBrowser.Controller/Persistence/TypeMapper.cs
new file mode 100644
index 0000000000..2b9ec9e5ec
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/TypeMapper.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Class TypeMapper
+ /// </summary>
+ public class TypeMapper
+ {
+ /// <summary>
+ /// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types
+ /// </summary>
+ private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
+
+ /// <summary>
+ /// Gets the type.
+ /// </summary>
+ /// <param name="typeName">Name of the type.</param>
+ /// <returns>Type.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public Type GetType(string typeName)
+ {
+ if (string.IsNullOrEmpty(typeName))
+ {
+ throw new ArgumentNullException();
+ }
+
+ return _typeMap.GetOrAdd(typeName, LookupType);
+ }
+
+ /// <summary>
+ /// Lookups the type.
+ /// </summary>
+ /// <param name="typeName">Name of the type.</param>
+ /// <returns>Type.</returns>
+ private Type LookupType(string typeName)
+ {
+ return AppDomain
+ .CurrentDomain
+ .GetAssemblies()
+ .Select(a => a.GetType(typeName, false))
+ .FirstOrDefault(t => t != null);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Playback/BaseIntroProvider.cs b/MediaBrowser.Controller/Playback/BaseIntroProvider.cs
new file mode 100644
index 0000000000..6e90baf3f3
--- /dev/null
+++ b/MediaBrowser.Controller/Playback/BaseIntroProvider.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Playback
+{
+ /// <summary>
+ /// Class BaseIntroProvider
+ /// </summary>
+ public abstract class BaseIntroProvider
+ {
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ public abstract IEnumerable<string> GetIntros(BaseItem item, User user);
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/BaseConfigurationPage.cs b/MediaBrowser.Controller/Plugins/BaseConfigurationPage.cs
new file mode 100644
index 0000000000..dc9405840a
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/BaseConfigurationPage.cs
@@ -0,0 +1,81 @@
+using MediaBrowser.Common.Plugins;
+using System.IO;
+
+namespace MediaBrowser.Controller.Plugins
+{
+ /// <summary>
+ /// Class BaseConfigurationPage
+ /// </summary>
+ public abstract class BaseConfigurationPage
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public abstract string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public virtual string Description
+ {
+ get { return string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets the type of the configuration page.
+ /// </summary>
+ /// <value>The type of the configuration page.</value>
+ public virtual ConfigurationPageType ConfigurationPageType
+ {
+ get { return ConfigurationPageType.PluginConfiguration; }
+ }
+
+ /// <summary>
+ /// Gets the HTML stream from manifest resource.
+ /// </summary>
+ /// <param name="resource">The resource.</param>
+ /// <returns>Stream.</returns>
+ protected Stream GetHtmlStreamFromManifestResource(string resource)
+ {
+ return GetType().Assembly.GetManifestResourceStream(resource);
+ }
+
+ /// <summary>
+ /// Gets the HTML stream.
+ /// </summary>
+ /// <returns>Stream.</returns>
+ public abstract Stream GetHtmlStream();
+
+ /// <summary>
+ /// Gets the name of the plugin.
+ /// </summary>
+ /// <value>The name of the plugin.</value>
+ public virtual string OwnerPluginName
+ {
+ get { return GetOwnerPlugin().Name; }
+ }
+
+ /// <summary>
+ /// Gets the owner plugin.
+ /// </summary>
+ /// <returns>BasePlugin.</returns>
+ public abstract IPlugin GetOwnerPlugin();
+ }
+
+ /// <summary>
+ /// Enum ConfigurationPageType
+ /// </summary>
+ public enum ConfigurationPageType
+ {
+ /// <summary>
+ /// The plugin configuration
+ /// </summary>
+ PluginConfiguration,
+ /// <summary>
+ /// The none
+ /// </summary>
+ None
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/PluginSecurityManager.cs b/MediaBrowser.Controller/Plugins/PluginSecurityManager.cs
new file mode 100644
index 0000000000..341e3582bb
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/PluginSecurityManager.cs
@@ -0,0 +1,65 @@
+using Mediabrowser.Model.Entities;
+using Mediabrowser.PluginSecurity;
+using MediaBrowser.Common.Kernel;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Plugins
+{
+ public class PluginSecurityManager : BaseManager<Kernel>
+ {
+ private bool? _isMBSupporter;
+ private bool _isMBSupporterInitialized;
+ private object _isMBSupporterSyncLock = new object();
+
+ public bool IsMBSupporter
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _isMBSupporter, ref _isMBSupporterInitialized, ref _isMBSupporterSyncLock, () => GetRegistrationStatus("MBSupporter").Result.IsRegistered);
+ return _isMBSupporter.Value;
+ }
+ }
+
+ public PluginSecurityManager(Kernel kernel) : base(kernel)
+ {
+ }
+
+ public async Task<MBRegistrationRecord> GetRegistrationStatus(string feature, string mb2Equivalent = null)
+ {
+ return await MBRegistration.GetRegistrationStatus(feature, mb2Equivalent).ConfigureAwait(false);
+ }
+
+ public string SupporterKey
+ {
+ get { return MBRegistration.SupporterKey; }
+ set {
+ if (value != MBRegistration.SupporterKey)
+ {
+ MBRegistration.SupporterKey = value;
+ // Clear this so it will re-evaluate
+ ResetSupporterInfo();
+ // And we'll need to restart to re-evaluate the status of plug-ins
+ Kernel.NotifyPendingRestart();
+
+ }
+ }
+ }
+
+ public string LegacyKey
+ {
+ get { return MBRegistration.LegacyKey; }
+ set {
+ MBRegistration.LegacyKey = value;
+ // And we'll need to restart to re-evaluate the status of plug-ins
+ Kernel.NotifyPendingRestart();
+ }
+ }
+
+ private void ResetSupporterInfo()
+ {
+ _isMBSupporter = null;
+ _isMBSupporterInitialized = false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
index 63cc65d7aa..f48500878d 100644
--- a/MediaBrowser.Controller/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
@@ -1,35 +1,34 @@
-using System.Reflection;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.Controller")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("MediaBrowser.Controller")]
-[assembly: AssemblyCopyright("Copyright © 2012")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("bc09905a-04ed-497d-b39b-27593401e715")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Controller")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Controller")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("bc09905a-04ed-497d-b39b-27593401e715")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("2.9.*")]
diff --git a/MediaBrowser.Controller/Providers/AudioInfoProvider.cs b/MediaBrowser.Controller/Providers/AudioInfoProvider.cs
deleted file mode 100644
index 302902646e..0000000000
--- a/MediaBrowser.Controller/Providers/AudioInfoProvider.cs
+++ /dev/null
@@ -1,262 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.FFMpeg;
-using MediaBrowser.Controller.Library;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- [Export(typeof(BaseMetadataProvider))]
- public class AudioInfoProvider : BaseMediaInfoProvider<Audio>
- {
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- protected override string CacheDirectory
- {
- get { return Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory; }
- }
-
- protected override void Fetch(Audio audio, FFProbeResult data)
- {
- MediaStream stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
-
- audio.Channels = stream.channels;
-
- if (!string.IsNullOrEmpty(stream.sample_rate))
- {
- audio.SampleRate = int.Parse(stream.sample_rate);
- }
-
- string bitrate = stream.bit_rate;
- string duration = stream.duration;
-
- if (string.IsNullOrEmpty(bitrate))
- {
- bitrate = data.format.bit_rate;
- }
-
- if (string.IsNullOrEmpty(duration))
- {
- duration = data.format.duration;
- }
-
- if (!string.IsNullOrEmpty(bitrate))
- {
- audio.BitRate = int.Parse(bitrate);
- }
-
- if (!string.IsNullOrEmpty(duration))
- {
- audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
- }
-
- if (data.format.tags != null)
- {
- FetchDataFromTags(audio, data.format.tags);
- }
- }
-
- private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
- {
- string title = GetDictionaryValue(tags, "title");
-
- if (!string.IsNullOrEmpty(title))
- {
- audio.Name = title;
- }
-
- string composer = GetDictionaryValue(tags, "composer");
-
- if (!string.IsNullOrEmpty(composer))
- {
- audio.AddPerson(new PersonInfo { Name = composer, Type = "Composer" });
- }
-
- audio.Album = GetDictionaryValue(tags, "album");
- audio.Artist = GetDictionaryValue(tags, "artist");
- audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
-
- audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
- audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
-
- audio.Language = GetDictionaryValue(tags, "language");
-
- audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
-
- audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
-
- FetchGenres(audio, tags);
-
- FetchStudios(audio, tags, "organization");
- FetchStudios(audio, tags, "ensemble");
- FetchStudios(audio, tags, "publisher");
- }
-
- private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
- {
- string val = GetDictionaryValue(tags, tagName);
-
- if (!string.IsNullOrEmpty(val))
- {
- var list = audio.Studios ?? new List<string>();
- list.AddRange(val.Split('/'));
- audio.Studios = list;
- }
- }
-
- private void FetchGenres(Audio audio, Dictionary<string, string> tags)
- {
- string val = GetDictionaryValue(tags, "genre");
-
- if (!string.IsNullOrEmpty(val))
- {
- var list = audio.Genres ?? new List<string>();
- list.AddRange(val.Split('/'));
- audio.Genres = list;
- }
- }
-
- private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
- {
- string disc = GetDictionaryValue(tags, "disc");
-
- if (!string.IsNullOrEmpty(disc))
- {
- disc = disc.Split('/')[0];
-
- int num;
-
- if (int.TryParse(disc, out num))
- {
- return num;
- }
- }
-
- return null;
- }
- }
-
- public abstract class BaseMediaInfoProvider<T> : BaseMetadataProvider
- where T : BaseItem
- {
- protected abstract string CacheDirectory { get; }
-
- public override bool Supports(BaseEntity item)
- {
- return item is T;
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() =>
- {
- /*T myItem = item as T;
-
- if (CanSkipFFProbe(myItem))
- {
- return;
- }
-
- FFProbeResult result = FFProbe.Run(myItem, CacheDirectory);
-
- if (result == null)
- {
- Logger.LogInfo("Null FFProbeResult for {0} {1}", item.Id, item.Name);
- return;
- }
-
- if (result.format != null && result.format.tags != null)
- {
- result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
- }
-
- if (result.streams != null)
- {
- foreach (MediaStream stream in result.streams)
- {
- if (stream.tags != null)
- {
- stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
- }
- }
- }
-
- Fetch(myItem, result);*/
- });
- }
-
- protected abstract void Fetch(T item, FFProbeResult result);
-
- protected virtual bool CanSkipFFProbe(T item)
- {
- return false;
- }
-
- protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
- {
- if (tags == null)
- {
- return null;
- }
-
- if (!tags.ContainsKey(key))
- {
- return null;
- }
-
- return tags[key];
- }
-
- protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
- {
- string val = GetDictionaryValue(tags, key);
-
- if (!string.IsNullOrEmpty(val))
- {
- int i;
-
- if (int.TryParse(val, out i))
- {
- return i;
- }
- }
-
- return null;
- }
-
- protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
- {
- string val = GetDictionaryValue(tags, key);
-
- if (!string.IsNullOrEmpty(val))
- {
- DateTime i;
-
- if (DateTime.TryParse(val, out i))
- {
- return i.ToUniversalTime();
- }
- }
-
- return null;
- }
-
- private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
- {
- var newDict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- foreach (string key in dict.Keys)
- {
- newDict[key] = dict[key];
- }
-
- return newDict;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Providers/BaseImageEnhancer.cs b/MediaBrowser.Controller/Providers/BaseImageEnhancer.cs
new file mode 100644
index 0000000000..bd60003beb
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BaseImageEnhancer.cs
@@ -0,0 +1,113 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Drawing;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class BaseImageEnhancer
+ /// </summary>
+ public abstract class BaseImageEnhancer : IDisposable
+ {
+ /// <summary>
+ /// Return true only if the given image for the given item will be enhanced by this enhancer.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
+ public abstract bool Supports(BaseItem item, ImageType imageType);
+
+ /// <summary>
+ /// Gets the priority or order in which this enhancer should be run.
+ /// </summary>
+ /// <value>The priority.</value>
+ public abstract MetadataProviderPriority Priority { get; }
+
+ /// <summary>
+ /// Return the date of the last configuration change affecting the provided baseitem and image type
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns>Date of last config change</returns>
+ public virtual DateTime LastConfigurationChange(BaseItem item, ImageType imageType)
+ {
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ }
+
+ /// <summary>
+ /// Gets the size of the enhanced image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <param name="originalImageSize">Size of the original image.</param>
+ /// <returns>ImageSize.</returns>
+ public virtual ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize)
+ {
+ return originalImageSize;
+ }
+
+ /// <summary>
+ /// Enhances the supplied image and returns it
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="originalImage">The original image.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{System.Drawing.Image}.</returns>
+ protected abstract Task<Image> EnhanceImageAsyncInternal(BaseItem item, Image originalImage, ImageType imageType, int imageIndex);
+
+ /// <summary>
+ /// Enhances the image async.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="originalImage">The original image.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{Image}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task<Image> EnhanceImageAsync(BaseItem item, Image originalImage, ImageType imageType, int imageIndex)
+ {
+ if (item == null || originalImage == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var typeName = GetType().Name;
+
+ Logger.LogDebugInfo("Running {0} for {1}", typeName, item.Path ?? item.Name ?? "--Unknown--");
+
+ try
+ {
+ return await EnhanceImageAsyncInternal(item, originalImage, imageType, imageIndex).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException("{0} failed enhancing {1}", ex, typeName, item.Name);
+
+ throw;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
index 38afb2b524..0869b25bc4 100644
--- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
+++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
@@ -1,724 +1,608 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Xml;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Common.Logging;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides a base class for parsing metadata xml
- /// </summary>
- public class BaseItemXmlParser<T>
- where T : BaseItem, new()
- {
- /// <summary>
- /// Fetches metadata for an item from one xml file
- /// </summary>
- public void Fetch(T item, string metadataFile)
- {
- // Use XmlReader for best performance
- using (XmlReader reader = XmlReader.Create(metadataFile))
- {
- reader.MoveToContent();
-
- // Loop through each element
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- }
- }
- }
-
- /// <summary>
- /// Fetches metadata from one Xml Element
- /// </summary>
- protected virtual void FetchDataFromXmlNode(XmlReader reader, T item)
- {
- switch (reader.Name)
- {
- // DateCreated
- case "Added":
- DateTime added;
- if (DateTime.TryParse(reader.ReadElementContentAsString() ?? string.Empty, out added))
- {
- item.DateCreated = added.ToUniversalTime();
- }
- break;
-
- // DisplayMediaType
- case "Type":
- {
- item.DisplayMediaType = reader.ReadElementContentAsString();
-
- switch (item.DisplayMediaType.ToLower())
- {
- case "blu-ray":
- item.DisplayMediaType = VideoType.BluRay.ToString();
- break;
- case "dvd":
- item.DisplayMediaType = VideoType.Dvd.ToString();
- break;
- case "":
- item.DisplayMediaType = null;
- break;
- }
-
- break;
- }
-
- // TODO: Do we still need this?
- case "banner":
- item.BannerImagePath = reader.ReadElementContentAsString();
- break;
-
- case "LocalTitle":
- item.Name = reader.ReadElementContentAsString();
- break;
-
- case "SortTitle":
- item.SortName = reader.ReadElementContentAsString();
- break;
-
- case "Overview":
- case "Description":
- item.Overview = reader.ReadElementContentAsString();
- break;
-
- case "TagLine":
- {
- var list = item.Taglines ?? new List<string>();
- var tagline = reader.ReadElementContentAsString();
-
- if (!list.Contains(tagline))
- {
- list.Add(tagline);
- }
-
- item.Taglines = list;
- break;
- }
-
- case "TagLines":
- {
- FetchFromTaglinesNode(reader.ReadSubtree(), item);
- break;
- }
-
- case "ContentRating":
- case "MPAARating":
- item.OfficialRating = reader.ReadElementContentAsString();
- break;
-
- case "CustomRating":
- item.CustomRating = reader.ReadElementContentAsString();
- break;
-
- case "CustomPin":
- item.CustomPin = reader.ReadElementContentAsString();
- break;
-
- case "Runtime":
- case "RunningTime":
- {
- string text = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(text))
- {
- int runtime;
- if (int.TryParse(text.Split(' ')[0], out runtime))
- {
- item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
- }
- break;
- }
-
- case "Genre":
- {
- var list = item.Genres ?? new List<string>();
- list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
-
- item.Genres = list;
- break;
- }
-
- case "AspectRatio":
- item.AspectRatio = reader.ReadElementContentAsString();
- break;
-
- case "Network":
- {
- var list = item.Studios ?? new List<string>();
- list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
-
- item.Studios = list;
- break;
- }
-
- case "Director":
- {
- foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Director" }))
- {
- item.AddPerson(p);
- }
- break;
- }
- case "Writer":
- {
- foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Writer" }))
- {
- item.AddPerson(p);
- }
- break;
- }
-
- case "Actors":
- case "GuestStars":
- {
- foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Actor" }))
- {
- item.AddPerson(p);
- }
- break;
- }
-
- case "Trailer":
- item.TrailerUrl = reader.ReadElementContentAsString();
- break;
-
- case "ProductionYear":
- {
- string val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- int ProductionYear;
- if (int.TryParse(val, out ProductionYear) && ProductionYear > 1850)
- {
- item.ProductionYear = ProductionYear;
- }
- }
-
- break;
- }
-
- case "Rating":
- case "IMDBrating":
-
- string rating = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(rating))
- {
- float val;
-
- if (float.TryParse(rating, out val))
- {
- item.CommunityRating = val;
- }
- }
- break;
-
- case "FirstAired":
- {
- string firstAired = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(firstAired))
- {
- DateTime airDate;
-
- if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
- {
- item.PremiereDate = airDate.ToUniversalTime();
- item.ProductionYear = airDate.Year;
- }
- }
-
- break;
- }
-
- case "TMDbId":
- string tmdb = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(tmdb))
- {
- item.SetProviderId(MetadataProviders.Tmdb, tmdb);
- }
- break;
-
- case "TVcomId":
- string TVcomId = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(TVcomId))
- {
- item.SetProviderId(MetadataProviders.Tvcom, TVcomId);
- }
- break;
-
- case "IMDB_ID":
- case "IMDB":
- case "IMDbId":
- string IMDbId = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(IMDbId))
- {
- item.SetProviderId(MetadataProviders.Imdb, IMDbId);
- }
- break;
-
- case "Genres":
- FetchFromGenresNode(reader.ReadSubtree(), item);
- break;
-
- case "Persons":
- FetchDataFromPersonsNode(reader.ReadSubtree(), item);
- break;
-
- case "ParentalRating":
- FetchFromParentalRatingNode(reader.ReadSubtree(), item);
- break;
-
- case "Studios":
- FetchFromStudiosNode(reader.ReadSubtree(), item);
- break;
-
- case "MediaInfo":
- {
- var video = item as Video;
-
- if (video != null)
- {
- FetchMediaInfo(reader.ReadSubtree(), video);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
-
- private void FetchMediaInfo(XmlReader reader, Video item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Audio":
- {
- AudioStream stream = FetchMediaInfoAudio(reader.ReadSubtree());
-
- List<AudioStream> streams = item.AudioStreams ?? new List<AudioStream>();
- streams.Add(stream);
- item.AudioStreams = streams;
-
- break;
- }
-
- case "Video":
- FetchMediaInfoVideo(reader.ReadSubtree(), item);
- break;
-
- case "Subtitle":
- {
- SubtitleStream stream = FetchMediaInfoSubtitles(reader.ReadSubtree());
-
- List<SubtitleStream> streams = item.Subtitles ?? new List<SubtitleStream>();
- streams.Add(stream);
- item.Subtitles = streams;
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private AudioStream FetchMediaInfoAudio(XmlReader reader)
- {
- var stream = new AudioStream();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Default":
- stream.IsDefault = reader.ReadElementContentAsString() == "True";
- break;
-
- case "SamplingRate":
- stream.SampleRate = reader.ReadIntSafe();
- break;
-
- case "BitRate":
- stream.BitRate = reader.ReadIntSafe();
- break;
-
- case "Channels":
- stream.Channels = reader.ReadIntSafe();
- break;
-
- case "Language":
- stream.Language = reader.ReadElementContentAsString();
- break;
-
- case "Codec":
- stream.Codec = reader.ReadElementContentAsString();
- break;
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- return stream;
- }
-
- private void FetchMediaInfoVideo(XmlReader reader, Video item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Width":
- item.Width = reader.ReadIntSafe();
- break;
-
- case "Height":
- item.Height = reader.ReadIntSafe();
- break;
-
- case "BitRate":
- item.BitRate = reader.ReadIntSafe();
- break;
-
- case "FrameRate":
- item.FrameRate = reader.ReadFloatSafe();
- break;
-
- case "ScanType":
- item.ScanType = reader.ReadElementContentAsString();
- break;
-
- case "Duration":
- item.RunTimeTicks = TimeSpan.FromMinutes(reader.ReadIntSafe()).Ticks;
- break;
-
- case "DurationSeconds":
- int seconds = reader.ReadIntSafe();
- if (seconds > 0)
- {
- item.RunTimeTicks = TimeSpan.FromSeconds(seconds).Ticks;
- }
- break;
-
- case "Codec":
- {
- string videoCodec = reader.ReadElementContentAsString();
-
- switch (videoCodec.ToLower())
- {
- case "sorenson h.263":
- item.Codec = "Sorenson H263";
- break;
- case "h.262":
- item.Codec = "MPEG-2 Video";
- break;
- case "h.264":
- item.Codec = "AVC";
- break;
- default:
- item.Codec = videoCodec;
- break;
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private SubtitleStream FetchMediaInfoSubtitles(XmlReader reader)
- {
- var stream = new SubtitleStream();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Language":
- stream.Language = reader.ReadElementContentAsString();
- break;
-
- case "Default":
- stream.IsDefault = reader.ReadElementContentAsString() == "True";
- break;
-
- case "Forced":
- stream.IsForced = reader.ReadElementContentAsString() == "True";
- break;
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- return stream;
- }
-
- private void FetchFromTaglinesNode(XmlReader reader, T item)
- {
- var list = item.Taglines ?? new List<string>();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Tagline":
- {
- string val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val) && !list.Contains(val))
- {
- list.Add(val);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- item.Taglines = list;
- }
-
- private void FetchFromGenresNode(XmlReader reader, T item)
- {
- var list = item.Genres ?? new List<string>();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Genre":
- {
- string genre = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(genre))
- {
- list.Add(genre);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- item.Genres = list;
- }
-
- private void FetchDataFromPersonsNode(XmlReader reader, T item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Person":
- {
- item.AddPerson(GetPersonFromXmlNode(reader.ReadSubtree()));
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private void FetchFromStudiosNode(XmlReader reader, T item)
- {
- var list = item.Studios ?? new List<string>();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Studio":
- {
- string studio = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(studio))
- {
- list.Add(studio);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- item.Studios = list;
- }
-
- private void FetchFromParentalRatingNode(XmlReader reader, T item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Value":
- {
- string ratingString = reader.ReadElementContentAsString();
-
- int rating = 7;
-
- if (!string.IsNullOrWhiteSpace(ratingString))
- {
- int.TryParse(ratingString, out rating);
- }
-
- switch (rating)
- {
- case -1:
- item.OfficialRating = "NR";
- break;
- case 0:
- item.OfficialRating = "UR";
- break;
- case 1:
- item.OfficialRating = "G";
- break;
- case 3:
- item.OfficialRating = "PG";
- break;
- case 4:
- item.OfficialRating = "PG-13";
- break;
- case 5:
- item.OfficialRating = "NC-17";
- break;
- case 6:
- item.OfficialRating = "R";
- break;
- default:
- break;
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private PersonInfo GetPersonFromXmlNode(XmlReader reader)
- {
- var person = new PersonInfo();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Name":
- person.Name = reader.ReadElementContentAsString();
- break;
-
- case "Type":
- person.Type = reader.ReadElementContentAsString();
- break;
-
- case "Role":
- person.Overview = reader.ReadElementContentAsString();
- break;
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- return person;
- }
-
- protected IEnumerable<string> GetSplitValues(string value, char deliminator)
- {
- value = (value ?? string.Empty).Trim(deliminator);
-
- return string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(deliminator);
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides a base class for parsing metadata xml
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class BaseItemXmlParser<T>
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Fetches metadata for an item from one xml file
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="metadataFile">The metadata file.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void Fetch(T item, string metadataFile, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (string.IsNullOrEmpty(metadataFile))
+ {
+ throw new ArgumentNullException();
+ }
+
+ // Use XmlReader for best performance
+ using (var reader = XmlReader.Create(metadataFile))
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata from one Xml Element
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ protected virtual void FetchDataFromXmlNode(XmlReader reader, T item)
+ {
+ switch (reader.Name)
+ {
+ // DateCreated
+ case "Added":
+ DateTime added;
+ if (DateTime.TryParse(reader.ReadElementContentAsString() ?? string.Empty, out added))
+ {
+ item.DateCreated = added.ToUniversalTime();
+ }
+ break;
+
+ case "LocalTitle":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Type":
+ {
+ var type = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(type) && !type.Equals("none",StringComparison.OrdinalIgnoreCase))
+ {
+ item.DisplayMediaType = type;
+ }
+
+ break;
+ }
+ case "SortTitle":
+ item.SortName = reader.ReadElementContentAsString();
+ break;
+
+ case "Overview":
+ case "Description":
+ item.Overview = reader.ReadElementContentAsString();
+ break;
+
+ case "TagLine":
+ {
+ var tagline = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(tagline))
+ {
+ item.AddTagline(tagline);
+ }
+
+ break;
+ }
+
+ case "TagLines":
+ {
+ FetchFromTaglinesNode(reader.ReadSubtree(), item);
+ break;
+ }
+
+ case "ContentRating":
+ case "certification":
+ case "MPAARating":
+ {
+ var rating = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(rating))
+ {
+ item.OfficialRating = rating;
+ }
+ break;
+ }
+
+ case "CustomRating":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.CustomRating = val;
+ }
+ break;
+ }
+
+ case "Runtime":
+ case "RunningTime":
+ {
+ var text = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ int runtime;
+ if (int.TryParse(text.Split(' ')[0], out runtime))
+ {
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
+ }
+ }
+ break;
+ }
+
+ case "Genre":
+ {
+ foreach (var name in SplitNames(reader.ReadElementContentAsString()))
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ continue;
+ }
+ item.AddGenre(name);
+ }
+ break;
+ }
+
+ case "AspectRatio":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AspectRatio = val;
+ }
+ break;
+ }
+
+ case "Network":
+ {
+ foreach (var name in SplitNames(reader.ReadElementContentAsString()))
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ continue;
+ }
+ item.AddStudio(name);
+ }
+ break;
+ }
+
+ case "Director":
+ {
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v, Type = PersonType.Director }))
+ {
+ if (string.IsNullOrWhiteSpace(p.Name))
+ {
+ continue;
+ }
+ item.AddPerson(p);
+ }
+ break;
+ }
+ case "Writer":
+ {
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v, Type = PersonType.Writer }))
+ {
+ if (string.IsNullOrWhiteSpace(p.Name))
+ {
+ continue;
+ }
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Actors":
+ case "GuestStars":
+ {
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Actor }))
+ {
+ if (string.IsNullOrWhiteSpace(p.Name))
+ {
+ continue;
+ }
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Trailer":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AddTrailerUrl(val);
+ }
+ break;
+ }
+
+ case "ProductionYear":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int ProductionYear;
+ if (int.TryParse(val, out ProductionYear) && ProductionYear > 1850)
+ {
+ item.ProductionYear = ProductionYear;
+ }
+ }
+
+ break;
+ }
+
+ case "Rating":
+ case "IMDBrating":
+ {
+
+ var rating = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(rating))
+ {
+ float val;
+
+ if (float.TryParse(rating, out val))
+ {
+ item.CommunityRating = val;
+ }
+ }
+ break;
+ }
+
+ case "FirstAired":
+ {
+ var firstAired = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(firstAired))
+ {
+ DateTime airDate;
+
+ if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+ {
+ item.PremiereDate = airDate.ToUniversalTime();
+ item.ProductionYear = airDate.Year;
+ }
+ }
+
+ break;
+ }
+
+ case "TMDbId":
+ var tmdb = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(tmdb))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, tmdb);
+ }
+ break;
+
+ case "TVcomId":
+ var TVcomId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(TVcomId))
+ {
+ item.SetProviderId(MetadataProviders.Tvcom, TVcomId);
+ }
+ break;
+
+ case "IMDB_ID":
+ case "IMDB":
+ case "IMDbId":
+ var IMDbId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(IMDbId))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, IMDbId);
+ }
+ break;
+
+ case "Genres":
+ FetchFromGenresNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Persons":
+ FetchDataFromPersonsNode(reader.ReadSubtree(), item);
+ break;
+
+ case "ParentalRating":
+ FetchFromParentalRatingNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Studios":
+ FetchFromStudiosNode(reader.ReadSubtree(), item);
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Fetches from taglines node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromTaglinesNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Tagline":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AddTagline(val);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches from genres node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromGenresNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Genre":
+ {
+ var genre = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(genre))
+ {
+ item.AddGenre(genre);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches the data from persons node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchDataFromPersonsNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Person":
+ {
+ item.AddPeople(GetPersonsFromXmlNode(reader.ReadSubtree()));
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches from studios node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromStudiosNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Studio":
+ {
+ var studio = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(studio))
+ {
+ item.AddStudio(studio);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches from parental rating node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromParentalRatingNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Value":
+ {
+ var ratingString = reader.ReadElementContentAsString();
+
+ int rating = 7;
+
+ if (!string.IsNullOrWhiteSpace(ratingString))
+ {
+ int.TryParse(ratingString, out rating);
+ }
+
+ switch (rating)
+ {
+ case -1:
+ item.OfficialRating = "NR";
+ break;
+ case 0:
+ item.OfficialRating = "UR";
+ break;
+ case 1:
+ item.OfficialRating = "G";
+ break;
+ case 3:
+ item.OfficialRating = "PG";
+ break;
+ case 4:
+ item.OfficialRating = "PG-13";
+ break;
+ case 5:
+ item.OfficialRating = "NC-17";
+ break;
+ case 6:
+ item.OfficialRating = "R";
+ break;
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the persons from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>IEnumerable{PersonInfo}.</returns>
+ private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
+ {
+ var names = new List<string>();
+ var type = string.Empty;
+ var role = string.Empty;
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Name":
+ names.AddRange(SplitNames(reader.ReadElementContentAsString()));
+ break;
+
+ case "Type":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ type = val;
+ }
+ break;
+ }
+
+ case "Role":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ role = val;
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return names.Select(n => new PersonInfo { Name = n, Role = role, Type = type });
+ }
+
+ /// <summary>
+ /// Used to split names of comma or pipe delimeted genres and people
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ private IEnumerable<string> SplitNames(string value)
+ {
+ value = value ?? string.Empty;
+
+ // Only split by comma if there is no pipe in the string
+ // We have to be careful to not split names like Matthew, Jr.
+ var separator = value.IndexOf('|') == -1 ? ',' : '|';
+
+ value = value.Trim().Trim(separator);
+
+ return string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(separator, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
index 50004be442..3916c0f541 100644
--- a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
+++ b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
@@ -1,104 +1,408 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Common.Extensions;
-using System.Threading.Tasks;
-using System;
-
-namespace MediaBrowser.Controller.Providers
-{
- public abstract class BaseMetadataProvider
- {
- protected Guid _id;
- public virtual Guid Id
- {
- get
- {
- if (_id == null) _id = this.GetType().FullName.GetMD5();
- return _id;
- }
- }
-
- public abstract bool Supports(BaseEntity item);
-
- public virtual bool RequiresInternet
- {
- get
- {
- return false;
- }
- }
-
- /// <summary>
- /// Returns the last refresh time of this provider for this item. Providers that care should
- /// call SetLastRefreshed to update this value.
- /// </summary>
- /// <param name="item"></param>
- /// <returns></returns>
- protected virtual DateTime LastRefreshed(BaseEntity item)
- {
- return (item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo())).LastRefreshed;
- }
-
- /// <summary>
- /// Sets the persisted last refresh date on the item for this provider.
- /// </summary>
- /// <param name="item"></param>
- /// <param name="value"></param>
- protected virtual void SetLastRefreshed(BaseEntity item, DateTime value)
- {
- var data = item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo());
- data.LastRefreshed = value;
- item.ProviderData[this.Id] = data;
- }
-
- /// <summary>
- /// Returns whether or not this provider should be re-fetched. Default functionality can
- /// compare a provided date with a last refresh time. This can be overridden for more complex
- /// determinations.
- /// </summary>
- /// <returns></returns>
- public virtual bool NeedsRefresh(BaseEntity item)
- {
- return CompareDate(item) > LastRefreshed(item);
- }
-
- /// <summary>
- /// Override this to return the date that should be compared to the last refresh date
- /// to determine if this provider should be re-fetched.
- /// </summary>
- protected virtual DateTime CompareDate(BaseEntity item)
- {
- return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
- }
-
- public virtual Task FetchIfNeededAsync(BaseEntity item)
- {
- if (this.NeedsRefresh(item))
- return FetchAsync(item, item.ResolveArgs);
- else
- return new Task(() => { });
- }
-
- public abstract Task FetchAsync(BaseEntity item, ItemResolveEventArgs args);
-
- public abstract MetadataProviderPriority Priority { get; }
- }
-
- /// <summary>
- /// Determines when a provider should execute, relative to others
- /// </summary>
- public enum MetadataProviderPriority
- {
- // Run this provider at the beginning
- First = 1,
-
- // Run this provider after all first priority providers
- Second = 2,
-
- // Run this provider after all second priority providers
- Third = 3,
-
- // Run this provider last
- Last = 4
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class BaseMetadataProvider
+ /// </summary>
+ public abstract class BaseMetadataProvider : IDisposable
+ {
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ protected ILogger Logger { get; private set; }
+
+ // Cache these since they will be used a lot
+ /// <summary>
+ /// The false task result
+ /// </summary>
+ protected static readonly Task<bool> FalseTaskResult = Task.FromResult(false);
+ /// <summary>
+ /// The true task result
+ /// </summary>
+ protected static readonly Task<bool> TrueTaskResult = Task.FromResult(true);
+
+ /// <summary>
+ /// The _id
+ /// </summary>
+ protected Guid _id;
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public virtual Guid Id
+ {
+ get
+ {
+ if (_id == Guid.Empty) _id = GetType().FullName.GetMD5();
+ return _id;
+ }
+ }
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public abstract bool Supports(BaseItem item);
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public virtual bool RequiresInternet
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Gets the provider version.
+ /// </summary>
+ /// <value>The provider version.</value>
+ protected virtual string ProviderVersion
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [refresh on version change].
+ /// </summary>
+ /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
+ protected virtual bool RefreshOnVersionChange
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines if this provider is relatively slow and, therefore, should be skipped
+ /// in certain instances. Default is whether or not it requires internet. Can be overridden
+ /// for explicit designation.
+ /// </summary>
+ /// <value><c>true</c> if this instance is slow; otherwise, <c>false</c>.</value>
+ public virtual bool IsSlow
+ {
+ get { return RequiresInternet; }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class.
+ /// </summary>
+ protected BaseMetadataProvider()
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes this instance.
+ /// </summary>
+ protected virtual void Initialize()
+ {
+ Logger = LogManager.GetLogger(GetType().Name);
+ }
+
+ /// <summary>
+ /// Sets the persisted last refresh date on the item for this provider.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="providerVersion">The provider version.</param>
+ /// <param name="status">The status.</param>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ protected virtual void SetLastRefreshed(BaseItem item, DateTime value, string providerVersion, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var data = item.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id });
+ data.LastRefreshed = value;
+ data.LastRefreshStatus = status;
+ data.ProviderVersion = providerVersion;
+
+ // Save the file system stamp for future comparisons
+ if (RefreshOnFileSystemStampChange)
+ {
+ data.FileSystemStamp = GetCurrentFileSystemStamp(item);
+ }
+
+ item.ProviderData[Id] = data;
+ }
+
+ /// <summary>
+ /// Sets the last refreshed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="status">The status.</param>
+ protected virtual void SetLastRefreshed(BaseItem item, DateTime value, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
+ {
+ SetLastRefreshed(item, value, ProviderVersion, status);
+ }
+
+ /// <summary>
+ /// Returns whether or not this provider should be re-fetched. Default functionality can
+ /// compare a provided date with a last refresh time. This can be overridden for more complex
+ /// determinations.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public bool NeedsRefresh(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var providerInfo = item.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo());
+
+ return NeedsRefreshInternal(item, providerInfo);
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ protected virtual bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (providerInfo == null)
+ {
+ throw new ArgumentNullException("providerInfo");
+ }
+
+ if (CompareDate(item) > providerInfo.LastRefreshed)
+ {
+ return true;
+ }
+
+ if (RefreshOnFileSystemStampChange && HasFileSystemStampChanged(item, providerInfo))
+ {
+ return true;
+ }
+
+ if (RefreshOnVersionChange && !string.Equals(ProviderVersion, providerInfo.ProviderVersion))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines if the item's file system stamp has changed from the last time the provider refreshed
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if [has file system stamp changed] [the specified item]; otherwise, <c>false</c>.</returns>
+ protected bool HasFileSystemStampChanged(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ return GetCurrentFileSystemStamp(item) != providerInfo.FileSystemStamp;
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected virtual DateTime CompareDate(BaseItem item)
+ {
+ return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Logger.Info("Running for {0}", item.Path ?? item.Name ?? "--Unknown--");
+
+ // This provides the ability to cancel just this one provider
+ var innerCancellationTokenSource = new CancellationTokenSource();
+
+ Kernel.Instance.ProviderManager.OnProviderRefreshBeginning(this, item, innerCancellationTokenSource);
+
+ try
+ {
+ var task = FetchAsyncInternal(item, force, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token);
+
+ await task.ConfigureAwait(false);
+
+ if (task.IsFaulted)
+ {
+ // Log the AggregateException
+ if (task.Exception != null)
+ {
+ Logger.ErrorException("AggregateException:", task.Exception);
+ }
+
+ return false;
+ }
+
+ return task.Result;
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.Info("{0} cancelled for {1}", GetType().Name, item.Name);
+
+ // If the outer cancellation token is the one that caused the cancellation, throw it
+ if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken)
+ {
+ throw;
+ }
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("failed refreshing {0}", ex, item.Name);
+
+ SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.Failure);
+ return true;
+ }
+ finally
+ {
+ innerCancellationTokenSource.Dispose();
+
+ Kernel.Instance.ProviderManager.OnProviderRefreshCompleted(this, item);
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected abstract Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public abstract MetadataProviderPriority Priority { get; }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ }
+
+ /// <summary>
+ /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
+ /// </summary>
+ /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
+ protected virtual bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines if the parent's file system stamp should be used for comparison
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected virtual bool UseParentFileSystemStamp(BaseItem item)
+ {
+ // True when the current item is just a file
+ return !item.ResolveArgs.IsDirectory;
+ }
+
+ /// <summary>
+ /// Gets the item's current file system stamp
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>Guid.</returns>
+ private Guid GetCurrentFileSystemStamp(BaseItem item)
+ {
+ if (UseParentFileSystemStamp(item) && item.Parent != null)
+ {
+ return item.Parent.FileSystemStamp;
+ }
+
+ return item.FileSystemStamp;
+ }
+ }
+
+ /// <summary>
+ /// Determines when a provider should execute, relative to others
+ /// </summary>
+ public enum MetadataProviderPriority
+ {
+ // Run this provider at the beginning
+ /// <summary>
+ /// The first
+ /// </summary>
+ First = 1,
+
+ // Run this provider after all first priority providers
+ /// <summary>
+ /// The second
+ /// </summary>
+ Second = 2,
+
+ // Run this provider after all second priority providers
+ /// <summary>
+ /// The third
+ /// </summary>
+ Third = 3,
+
+ // Run this provider last
+ /// <summary>
+ /// The last
+ /// </summary>
+ Last = 4
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseProviderInfo.cs b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
index 1538b2262f..877ba5c4bf 100644
--- a/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
+++ b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
@@ -1,15 +1,60 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- public class BaseProviderInfo
- {
- public Guid ProviderId { get; set; }
- public DateTime LastRefreshed { get; set; }
-
- }
-}
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class BaseProviderInfo
+ /// </summary>
+ public class BaseProviderInfo
+ {
+ /// <summary>
+ /// Gets or sets the provider id.
+ /// </summary>
+ /// <value>The provider id.</value>
+ public Guid ProviderId { get; set; }
+ /// <summary>
+ /// Gets or sets the last refreshed.
+ /// </summary>
+ /// <value>The last refreshed.</value>
+ public DateTime LastRefreshed { get; set; }
+ /// <summary>
+ /// Gets or sets the file system stamp.
+ /// </summary>
+ /// <value>The file system stamp.</value>
+ public Guid FileSystemStamp { get; set; }
+ /// <summary>
+ /// Gets or sets the last refresh status.
+ /// </summary>
+ /// <value>The last refresh status.</value>
+ public ProviderRefreshStatus LastRefreshStatus { get; set; }
+ /// <summary>
+ /// Gets or sets the provider version.
+ /// </summary>
+ /// <value>The provider version.</value>
+ public string ProviderVersion { get; set; }
+ /// <summary>
+ /// Gets or sets the data hash.
+ /// </summary>
+ /// <value>The data hash.</value>
+ public Guid DataHash { get; set; }
+ }
+
+ /// <summary>
+ /// Enum ProviderRefreshStatus
+ /// </summary>
+ public enum ProviderRefreshStatus
+ {
+ /// <summary>
+ /// The success
+ /// </summary>
+ Success,
+ /// <summary>
+ /// The failure
+ /// </summary>
+ Failure,
+ /// <summary>
+ /// The completed with errors
+ /// </summary>
+ CompletedWithErrors
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/FanartBaseProvider.cs b/MediaBrowser.Controller/Providers/FanartBaseProvider.cs
new file mode 100644
index 0000000000..3063f3c9e1
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/FanartBaseProvider.cs
@@ -0,0 +1,84 @@
+using MediaBrowser.Controller.Entities;
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class FanartBaseProvider
+ /// </summary>
+ public abstract class FanartBaseProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// The LOG o_ FILE
+ /// </summary>
+ protected const string LOGO_FILE = "logo.png";
+ /// <summary>
+ /// The AR t_ FILE
+ /// </summary>
+ protected const string ART_FILE = "clearart.png";
+ /// <summary>
+ /// The THUM b_ FILE
+ /// </summary>
+ protected const string THUMB_FILE = "thumb.jpg";
+ /// <summary>
+ /// The DIS c_ FILE
+ /// </summary>
+ protected const string DISC_FILE = "disc.png";
+ /// <summary>
+ /// The BANNE r_ FILE
+ /// </summary>
+ protected const string BANNER_FILE = "banner.png";
+
+ /// <summary>
+ /// The API key
+ /// </summary>
+ protected const string APIKey = "5c6b04c68e904cfed1e6cbc9a9e683d4";
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item.DontFetchMeta) return false;
+
+ return DateTime.UtcNow > (providerInfo.LastRefreshed.AddDays(Kernel.Instance.Configuration.MetadataRefreshDays))
+ && ShouldFetch(item, providerInfo);
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Third; }
+ }
+
+ /// <summary>
+ /// Shoulds the fetch.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected virtual bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ return false;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
index b7d9b71893..110502bc1f 100644
--- a/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
@@ -1,38 +1,83 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides metadata for Folders and all subclasses by parsing folder.xml
- /// </summary>
- [Export(typeof(BaseMetadataProvider))]
- public class FolderProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Folder;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFile("folder.xml"))
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- new BaseItemXmlParser<Folder>().Fetch(item as Folder, Path.Combine(args.Path, "folder.xml"));
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides metadata for Folders and all subclasses by parsing folder.xml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FolderProviderFromXml : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Folder && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ var entry = item.MetaLocation != null ? item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "folder.xml")) : null;
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "folder.xml"));
+
+ if (metadataFile.HasValue)
+ {
+ var path = metadataFile.Value.Path;
+ new BaseItemXmlParser<Folder>().Fetch((Folder)item, path, cancellationToken);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
index d6fd26d1c4..9858764069 100644
--- a/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
+++ b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
@@ -1,128 +1,231 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
- /// </summary>
- [Export(typeof(BaseMetadataProvider))]
- public class ImageFromMediaLocationProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return true;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.IsDirectory)
- {
- var baseItem = item as BaseItem;
-
- if (baseItem != null)
- {
- return Task.Run(() => PopulateBaseItemImages(baseItem, args));
- }
-
- return Task.Run(() => PopulateImages(item, args));
- }
-
- return Task.FromResult<object>(null);
- }
-
- /// <summary>
- /// Fills in image paths based on files win the folder
- /// </summary>
- private void PopulateImages(BaseEntity item, ItemResolveEventArgs args)
- {
- for (int i = 0; i < args.FileSystemChildren.Length; i++)
- {
- var file = args.FileSystemChildren[i];
-
- string filePath = file.Path;
-
- string ext = Path.GetExtension(filePath);
-
- // Only support png and jpg files
- if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- string name = Path.GetFileNameWithoutExtension(filePath);
-
- if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
- {
- item.PrimaryImagePath = filePath;
- }
- }
- }
-
- /// <summary>
- /// Fills in image paths based on files win the folder
- /// </summary>
- private void PopulateBaseItemImages(BaseItem item, ItemResolveEventArgs args)
- {
- var backdropFiles = new List<string>();
-
- for (int i = 0; i < args.FileSystemChildren.Length; i++)
- {
- var file = args.FileSystemChildren[i];
-
- string filePath = file.Path;
-
- string ext = Path.GetExtension(filePath);
-
- // Only support png and jpg files
- if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- string name = Path.GetFileNameWithoutExtension(filePath);
-
- if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
- {
- item.PrimaryImagePath = filePath;
- }
- else if (name.StartsWith("backdrop", StringComparison.OrdinalIgnoreCase))
- {
- backdropFiles.Add(filePath);
- }
- if (name.Equals("logo", StringComparison.OrdinalIgnoreCase))
- {
- item.LogoImagePath = filePath;
- }
- if (name.Equals("banner", StringComparison.OrdinalIgnoreCase))
- {
- item.BannerImagePath = filePath;
- }
- if (name.Equals("clearart", StringComparison.OrdinalIgnoreCase))
- {
- item.ArtImagePath = filePath;
- }
- if (name.Equals("thumb", StringComparison.OrdinalIgnoreCase))
- {
- item.ThumbnailImagePath = filePath;
- }
- }
-
- if (backdropFiles.Count > 0)
- {
- item.BackdropImagePaths = backdropFiles;
- }
- }
-
- }
-}
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class ImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item.ResolveArgs.IsDirectory && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <summary>
+ /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
+ /// </summary>
+ /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Make sure current image paths still exist
+ ValidateImages(item);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Make sure current backdrop paths still exist
+ ValidateBackdrops(item);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ PopulateBaseItemImages(item);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Validates that images within the item are still on the file system
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void ValidateImages(BaseItem item)
+ {
+ if (item.Images == null)
+ {
+ return;
+ }
+
+ // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+ var deletedKeys = item.Images.Keys.Where(image =>
+ {
+ var path = item.Images[image];
+
+ return IsInSameDirectory(item, path) && !item.ResolveArgs.GetMetaFileByPath(path).HasValue;
+ }).ToList();
+
+ // Now remove them from the dictionary
+ foreach(var key in deletedKeys)
+ {
+ item.Images.Remove(key);
+ }
+ }
+
+ /// <summary>
+ /// Validates that backdrops within the item are still on the file system
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void ValidateBackdrops(BaseItem item)
+ {
+ if (item.BackdropImagePaths == null)
+ {
+ return;
+ }
+
+ // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+ var deletedImages = item.BackdropImagePaths.Where(path => IsInSameDirectory(item, path) && !item.ResolveArgs.GetMetaFileByPath(path).HasValue).ToList();
+
+ // Now remove them from the dictionary
+ foreach (var path in deletedImages)
+ {
+ item.BackdropImagePaths.Remove(path);
+ }
+ }
+
+ /// <summary>
+ /// Determines whether [is in same directory] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is in same directory] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool IsInSameDirectory(BaseItem item, string path)
+ {
+ return string.Equals(Path.GetDirectoryName(path), item.Path, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="filenameWithoutExtension">The filename without extension.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ protected virtual WIN32_FIND_DATA? GetImage(BaseItem item, string filenameWithoutExtension)
+ {
+ return item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.ResolveArgs.Path, filenameWithoutExtension + ".png")) ?? item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.ResolveArgs.Path, filenameWithoutExtension + ".jpg"));
+ }
+
+ /// <summary>
+ /// Fills in image paths based on files win the folder
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void PopulateBaseItemImages(BaseItem item)
+ {
+ var backdropFiles = new List<string>();
+
+ // Primary Image
+ var image = GetImage(item, "folder");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Primary, image.Value.Path);
+ }
+
+ // Logo Image
+ image = GetImage(item, "logo");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Logo, image.Value.Path);
+ }
+
+ // Banner Image
+ image = GetImage(item, "banner");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Banner, image.Value.Path);
+ }
+
+ // Clearart
+ image = GetImage(item, "clearart");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Art, image.Value.Path);
+ }
+
+ // Thumbnail Image
+ image = GetImage(item, "thumb");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Thumb, image.Value.Path);
+ }
+
+ // Backdrop Image
+ image = GetImage(item, "backdrop");
+
+ if (image.HasValue)
+ {
+ backdropFiles.Add(image.Value.Path);
+ }
+
+ var unfound = 0;
+ for (var i = 1; i <= 20; i++)
+ {
+ // Backdrop Image
+ image = GetImage(item, "backdrop" + i);
+
+ if (image.HasValue)
+ {
+ backdropFiles.Add(image.Value.Path);
+ }
+ else
+ {
+ unfound++;
+
+ if (unfound >= 3)
+ {
+ break;
+ }
+ }
+ }
+
+ if (backdropFiles.Count > 0)
+ {
+ item.BackdropImagePaths = backdropFiles;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ImagesByNameProvider.cs b/MediaBrowser.Controller/Providers/ImagesByNameProvider.cs
new file mode 100644
index 0000000000..114176e2c9
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ImagesByNameProvider.cs
@@ -0,0 +1,103 @@
+using System.Globalization;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides images for generic types by looking for standard images in the IBN
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class ImagesByNameProvider : ImageFromMediaLocationProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ //only run for these generic types since we are expensive in file i/o
+ return item is IndexFolder || item is BasePluginFolder;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get
+ {
+ return MetadataProviderPriority.Last;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [refresh on file system stamp change].
+ /// </summary>
+ /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ // If the IBN location exists return the last modified date of any file in it
+ var location = GetLocation(item);
+ return Directory.Exists(location) ? FileSystem.GetFiles(location).Select(f => f.CreationTimeUtc > f.LastWriteTimeUtc ? f.CreationTimeUtc : f.LastWriteTimeUtc).Max() : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// The us culture
+ /// </summary>
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets the location.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ protected string GetLocation(BaseItem item)
+ {
+ var invalid = Path.GetInvalidFileNameChars();
+
+ var name = item.Name ?? string.Empty;
+ name = invalid.Aggregate(name, (current, c) => current.Replace(c.ToString(UsCulture), string.Empty));
+
+ return Path.Combine(Kernel.Instance.ApplicationPaths.GeneralPath, name);
+ }
+
+ /// <summary>
+ /// Gets the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="filenameWithoutExtension">The filename without extension.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ protected override WIN32_FIND_DATA? GetImage(BaseItem item, string filenameWithoutExtension)
+ {
+ var location = GetLocation(item);
+
+ var result = FileSystem.GetFileData(Path.Combine(location, filenameWithoutExtension + ".png"));
+ if (!result.HasValue)
+ result = FileSystem.GetFileData(Path.Combine(location, filenameWithoutExtension + ".jpg"));
+
+ return result;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs b/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs
deleted file mode 100644
index 8823da6911..0000000000
--- a/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides local trailers by checking the trailers subfolder
- /// </summary>
- [Export(typeof(BaseMetadataProvider))]
- public class LocalTrailerProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is BaseItem;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFolder("trailers"))
- {
- var items = new List<Video>();
-
- foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "trailers"), "*"))
- {
- var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
-
- if (video != null)
- {
- items.Add(video);
- }
- }
-
- (item as BaseItem).LocalTrailers = items;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs
new file mode 100644
index 0000000000..6214cb0dae
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs
@@ -0,0 +1,265 @@
+using BDInfo;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Extracts dvd information using VgtMpeg
+ /// </summary>
+ internal static class BDInfoProvider
+ {
+ internal static void FetchBdInfo(BaseItem item, string inputPath, FileSystemRepository bdInfoCache, CancellationToken cancellationToken)
+ {
+ var video = (Video)item;
+
+ // Get the path to the cache file
+ var cacheName = item.Id + "_" + item.DateModified.Ticks;
+
+ var cacheFile = bdInfoCache.GetResourcePath(cacheName, ".pb");
+
+ BDInfoResult result;
+
+ try
+ {
+ result = Kernel.Instance.ProtobufSerializer.DeserializeFromFile<BDInfoResult>(cacheFile);
+ }
+ catch (FileNotFoundException)
+ {
+ result = GetBDInfo(inputPath);
+
+ Kernel.Instance.ProtobufSerializer.SerializeToFile(result, cacheFile);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ int? currentHeight = null;
+ int? currentWidth = null;
+ int? currentBitRate = null;
+
+ var videoStream = video.MediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+
+ // Grab the values that ffprobe recorded
+ if (videoStream != null)
+ {
+ currentBitRate = videoStream.BitRate;
+ currentWidth = videoStream.Width;
+ currentHeight = videoStream.Height;
+ }
+
+ // Fill video properties from the BDInfo result
+ Fetch(video, inputPath, result);
+
+ videoStream = video.MediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+
+ // Use the ffprobe values if these are empty
+ if (videoStream != null)
+ {
+ videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
+ videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
+ videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified num is empty.
+ /// </summary>
+ /// <param name="num">The num.</param>
+ /// <returns><c>true</c> if the specified num is empty; otherwise, <c>false</c>.</returns>
+ private static bool IsEmpty(int? num)
+ {
+ return !num.HasValue || num.Value == 0;
+ }
+
+ /// <summary>
+ /// Fills video properties from the VideoStream of the largest playlist
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="stream">The stream.</param>
+ private static void Fetch(Video video, string inputPath, BDInfoResult stream)
+ {
+ // Check all input for null/empty/zero
+
+ video.MediaStreams = stream.MediaStreams;
+
+ if (stream.RunTimeTicks.HasValue && stream.RunTimeTicks.Value > 0)
+ {
+ video.RunTimeTicks = stream.RunTimeTicks;
+ }
+
+ video.PlayableStreamFileNames = stream.Files.ToList();
+
+ if (stream.Chapters != null)
+ {
+ video.Chapters = stream.Chapters.Select(c => new ChapterInfo
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(c).Ticks
+
+ }).ToList();
+ }
+ }
+
+ /// <summary>
+ /// Gets information about the longest playlist on a bdrom
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>VideoStream.</returns>
+ private static BDInfoResult GetBDInfo(string path)
+ {
+ var bdrom = new BDROM(path);
+
+ bdrom.Scan();
+
+ // Get the longest playlist
+ var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
+
+ var outputStream = new BDInfoResult
+ {
+ MediaStreams = new List<MediaStream>()
+ };
+
+ if (playlist == null)
+ {
+ return outputStream;
+ }
+
+ outputStream.Chapters = playlist.Chapters;
+
+ outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
+
+ var mediaStreams = new List<MediaStream> {};
+
+ foreach (var stream in playlist.SortedStreams)
+ {
+ var videoStream = stream as TSVideoStream;
+
+ if (videoStream != null)
+ {
+ AddVideoStream(mediaStreams, videoStream);
+ continue;
+ }
+
+ var audioStream = stream as TSAudioStream;
+
+ if (audioStream != null)
+ {
+ AddAudioStream(mediaStreams, audioStream);
+ continue;
+ }
+
+ var textStream = stream as TSTextStream;
+
+ if (textStream != null)
+ {
+ AddSubtitleStream(mediaStreams, textStream);
+ continue;
+ }
+
+ var graphicsStream = stream as TSGraphicsStream;
+
+ if (graphicsStream != null)
+ {
+ AddSubtitleStream(mediaStreams, graphicsStream);
+ }
+ }
+
+ outputStream.MediaStreams = mediaStreams;
+
+ if (playlist.StreamClips != null && playlist.StreamClips.Any())
+ {
+ // Get the files in the playlist
+ outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToList();
+ }
+
+ return outputStream;
+ }
+
+ /// <summary>
+ /// Adds the video stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="videoStream">The video stream.</param>
+ private static void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
+ {
+ var mediaStream = new MediaStream
+ {
+ BitRate = Convert.ToInt32(videoStream.BitRate),
+ Width = videoStream.Width,
+ Height = videoStream.Height,
+ Codec = videoStream.CodecShortName,
+ ScanType = videoStream.IsInterlaced ? "interlaced" : "progressive",
+ Type = MediaStreamType.Video,
+ Index = streams.Count
+ };
+
+ if (videoStream.FrameRateDenominator > 0)
+ {
+ float frameRateEnumerator = videoStream.FrameRateEnumerator;
+ float frameRateDenominator = videoStream.FrameRateDenominator;
+
+ mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
+ }
+
+ streams.Add(mediaStream);
+ }
+
+ /// <summary>
+ /// Adds the audio stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="audioStream">The audio stream.</param>
+ private static void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
+ {
+ streams.Add(new MediaStream
+ {
+ BitRate = Convert.ToInt32(audioStream.BitRate),
+ Codec = audioStream.CodecShortName,
+ Language = audioStream.LanguageCode,
+ Channels = audioStream.ChannelCount,
+ SampleRate = audioStream.SampleRate,
+ Type = MediaStreamType.Audio,
+ Index = streams.Count
+ });
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private static void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private static void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs
new file mode 100644
index 0000000000..95b70044a4
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ public abstract class BaseFFMpegImageProvider<T> : BaseFFMpegProvider<T>
+ where T : BaseItem
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Last; }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs
new file mode 100644
index 0000000000..605c03414f
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Class BaseFFMpegProvider
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseFFMpegProvider<T> : BaseMetadataProvider
+ where T : BaseItem
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item.LocationType == LocationType.FileSystem && item is T;
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ return item.DateModified;
+ }
+
+ /// <summary>
+ /// The null mount task result
+ /// </summary>
+ protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
+
+ /// <summary>
+ /// Gets the provider version.
+ /// </summary>
+ /// <value>The provider version.</value>
+ protected override string ProviderVersion
+ {
+ get
+ {
+ return Kernel.Instance.FFMpegManager.FFMpegVersion;
+ }
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ // If the last run wasn't successful, try again when there's a new version of ffmpeg
+ if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success)
+ {
+ if (!string.Equals(ProviderVersion, providerInfo.ProviderVersion))
+ {
+ return true;
+ }
+ }
+
+ return base.NeedsRefreshInternal(item, providerInfo);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs
new file mode 100644
index 0000000000..bb2b82819c
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs
@@ -0,0 +1,358 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Provides a base class for extracting media information through ffprobe
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseFFProbeProvider<T> : BaseFFMpegProvider<T>
+ where T : BaseItem
+ {
+ /// <summary>
+ /// Gets or sets the FF probe cache.
+ /// </summary>
+ /// <value>The FF probe cache.</value>
+ protected FileSystemRepository FFProbeCache { get; set; }
+
+ /// <summary>
+ /// Initializes this instance.
+ /// </summary>
+ protected override void Initialize()
+ {
+ base.Initialize();
+ FFProbeCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, CacheDirectoryName));
+ }
+
+ /// <summary>
+ /// Gets the name of the cache directory.
+ /// </summary>
+ /// <value>The name of the cache directory.</value>
+ protected virtual string CacheDirectoryName
+ {
+ get
+ {
+ return "ffmpeg-video-info";
+ }
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ // Give this second priority
+ // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ var myItem = (T)item;
+
+ var isoMount = await MountIsoIfNeeded(myItem, cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ OnPreFetch(myItem, isoMount);
+
+ var inputPath = isoMount == null ?
+ Kernel.Instance.FFMpegManager.GetInputArgument(myItem) :
+ Kernel.Instance.FFMpegManager.GetInputArgument((Video)item, isoMount);
+
+ var result = await Kernel.Instance.FFMpegManager.RunFFProbe(item, inputPath, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ NormalizeFFProbeResult(result);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Fetch(myItem, cancellationToken, result, isoMount).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ }
+ finally
+ {
+ if (isoMount != null)
+ {
+ isoMount.Dispose();
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [refresh on version change].
+ /// </summary>
+ /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
+ protected override bool RefreshOnVersionChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Mounts the iso if needed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ protected virtual Task<IIsoMount> MountIsoIfNeeded(T item, CancellationToken cancellationToken)
+ {
+ return NullMountTaskResult;
+ }
+
+ /// <summary>
+ /// Called when [pre fetch].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="mount">The mount.</param>
+ protected virtual void OnPreFetch(T item, IIsoMount mount)
+ {
+
+ }
+
+ /// <summary>
+ /// Normalizes the FF probe result.
+ /// </summary>
+ /// <param name="result">The result.</param>
+ private void NormalizeFFProbeResult(FFProbeResult result)
+ {
+ if (result.format != null && result.format.tags != null)
+ {
+ result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
+ }
+
+ if (result.streams != null)
+ {
+ // Convert all dictionaries to case insensitive
+ foreach (var stream in result.streams)
+ {
+ if (stream.tags != null)
+ {
+ stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
+ }
+
+ if (stream.disposition != null)
+ {
+ stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Subclasses must set item values using this
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="result">The result.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>Task.</returns>
+ protected abstract Task Fetch(T item, CancellationToken cancellationToken, FFProbeResult result, IIsoMount isoMount);
+
+ /// <summary>
+ /// Converts ffprobe stream info to our MediaStream class
+ /// </summary>
+ /// <param name="streamInfo">The stream info.</param>
+ /// <param name="formatInfo">The format info.</param>
+ /// <returns>MediaStream.</returns>
+ protected MediaStream GetMediaStream(FFProbeMediaStreamInfo streamInfo, FFProbeMediaFormatInfo formatInfo)
+ {
+ var stream = new MediaStream
+ {
+ Codec = streamInfo.codec_name,
+ Language = GetDictionaryValue(streamInfo.tags, "language"),
+ Profile = streamInfo.profile,
+ Index = streamInfo.index
+ };
+
+ if (streamInfo.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Type = MediaStreamType.Audio;
+
+ stream.Channels = streamInfo.channels;
+
+ if (!string.IsNullOrEmpty(streamInfo.sample_rate))
+ {
+ stream.SampleRate = int.Parse(streamInfo.sample_rate);
+ }
+ }
+ else if (streamInfo.codec_type.Equals("subtitle", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Type = MediaStreamType.Subtitle;
+ }
+ else
+ {
+ stream.Type = MediaStreamType.Video;
+
+ stream.Width = streamInfo.width;
+ stream.Height = streamInfo.height;
+ stream.AspectRatio = streamInfo.display_aspect_ratio;
+
+ stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
+ stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
+ }
+
+ // Get stream bitrate
+ if (stream.Type != MediaStreamType.Subtitle)
+ {
+ if (!string.IsNullOrEmpty(streamInfo.bit_rate))
+ {
+ stream.BitRate = int.Parse(streamInfo.bit_rate);
+ }
+ else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate))
+ {
+ // If the stream info doesn't have a bitrate get the value from the media format info
+ stream.BitRate = int.Parse(formatInfo.bit_rate);
+ }
+ }
+
+ if (streamInfo.disposition != null)
+ {
+ var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
+ var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
+
+ stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
+
+ stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return stream;
+ }
+
+ /// <summary>
+ /// Gets a frame rate from a string value in ffprobe output
+ /// This could be a number or in the format of 2997/125.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>System.Nullable{System.Single}.</returns>
+ private float? GetFrameRate(string value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ var parts = value.Split('/');
+
+ if (parts.Length == 2)
+ {
+ return float.Parse(parts[0]) / float.Parse(parts[1]);
+ }
+ return float.Parse(parts[0]);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a string from an FFProbeResult tags dictionary
+ /// </summary>
+ /// <param name="tags">The tags.</param>
+ /// <param name="key">The key.</param>
+ /// <returns>System.String.</returns>
+ protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
+ {
+ if (tags == null)
+ {
+ return null;
+ }
+
+ string val;
+
+ tags.TryGetValue(key, out val);
+ return val;
+ }
+
+ /// <summary>
+ /// Gets an int from an FFProbeResult tags dictionary
+ /// </summary>
+ /// <param name="tags">The tags.</param>
+ /// <param name="key">The key.</param>
+ /// <returns>System.Nullable{System.Int32}.</returns>
+ protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
+ {
+ var val = GetDictionaryValue(tags, key);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ int i;
+
+ if (int.TryParse(val, out i))
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a DateTime from an FFProbeResult tags dictionary
+ /// </summary>
+ /// <param name="tags">The tags.</param>
+ /// <param name="key">The key.</param>
+ /// <returns>System.Nullable{DateTime}.</returns>
+ protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
+ {
+ var val = GetDictionaryValue(tags, key);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ DateTime i;
+
+ if (DateTime.TryParse(val, out i))
+ {
+ return i.ToUniversalTime();
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Converts a dictionary to case insensitive
+ /// </summary>
+ /// <param name="dict">The dict.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
+ {
+ return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ FFProbeCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs
new file mode 100644
index 0000000000..523192d4e3
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs
@@ -0,0 +1,84 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses ffmpeg to create video images
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFMpegAudioImageProvider : BaseFFMpegImageProvider<Audio>
+ {
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ var audio = (Audio)item;
+
+ if (string.IsNullOrEmpty(audio.PrimaryImagePath))
+ {
+ // First try to use the parent's image
+ audio.PrimaryImagePath = audio.ResolveArgs.Parent.PrimaryImagePath;
+
+ // If it's still empty see if there's an embedded image
+ if (string.IsNullOrEmpty(audio.PrimaryImagePath))
+ {
+ if (audio.MediaStreams != null && audio.MediaStreams.Any(s => s.Type == MediaStreamType.Video))
+ {
+ var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
+
+ var path = Kernel.Instance.FFMpegManager.AudioImageCache.GetResourcePath(filename, ".jpg");
+
+ if (!Kernel.Instance.FFMpegManager.AudioImageCache.ContainsFilePath(path))
+ {
+ return ExtractImage(audio, path, cancellationToken);
+ }
+
+ // Image is already in the cache
+ audio.PrimaryImagePath = path;
+ }
+
+ }
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Extracts the image.
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> ExtractImage(Audio audio, string path, CancellationToken cancellationToken)
+ {
+ var success = await Kernel.Instance.FFMpegManager.ExtractImage(audio, path, cancellationToken).ConfigureAwait(false);
+
+ if (success)
+ {
+ audio.PrimaryImagePath = path;
+ SetLastRefreshed(audio, DateTime.UtcNow);
+ }
+ else
+ {
+ SetLastRefreshed(audio, DateTime.UtcNow, ProviderRefreshStatus.Failure);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs
new file mode 100644
index 0000000000..2f617b5b18
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs
@@ -0,0 +1,137 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses ffmpeg to create video images
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFMpegVideoImageProvider : BaseFFMpegImageProvider<Video>
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ if (item.LocationType != LocationType.FileSystem)
+ {
+ return false;
+ }
+
+ var video = item as Video;
+
+ if (video != null)
+ {
+ if (video.VideoType == VideoType.Iso && video.IsoType.HasValue && Kernel.Instance.IsoManager.CanMount(item.Path))
+ {
+ return true;
+ }
+
+ // We can only extract images from folder rips if we know the largest stream path
+ return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(item.PrimaryImagePath))
+ {
+ var video = (Video)item;
+
+ var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
+
+ var path = Kernel.Instance.FFMpegManager.VideoImageCache.GetResourcePath(filename, ".jpg");
+
+ if (!Kernel.Instance.FFMpegManager.VideoImageCache.ContainsFilePath(path))
+ {
+ return ExtractImage(video, path, cancellationToken);
+ }
+
+ // Image is already in the cache
+ item.PrimaryImagePath = path;
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Mounts the iso if needed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
+ {
+ if (item.VideoType == VideoType.Iso)
+ {
+ return Kernel.Instance.IsoManager.Mount(item.Path, cancellationToken);
+ }
+
+ return NullMountTaskResult;
+ }
+
+ /// <summary>
+ /// Extracts the image.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> ExtractImage(Video video, string path, CancellationToken cancellationToken)
+ {
+ var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
+ // Always use 10 seconds for dvd because our duration could be out of whack
+ var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue && video.RunTimeTicks.Value > 0
+ ? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1))
+ : TimeSpan.FromSeconds(10);
+
+ var inputPath = isoMount == null ?
+ Kernel.Instance.FFMpegManager.GetInputArgument(video) :
+ Kernel.Instance.FFMpegManager.GetInputArgument(video, isoMount);
+
+ var success = await Kernel.Instance.FFMpegManager.ExtractImage(inputPath, imageOffset, path, cancellationToken).ConfigureAwait(false);
+
+ if (success)
+ {
+ video.PrimaryImagePath = path;
+ SetLastRefreshed(video, DateTime.UtcNow);
+ }
+ else
+ {
+ SetLastRefreshed(video, DateTime.UtcNow, ProviderRefreshStatus.Failure);
+ }
+ }
+ finally
+ {
+ if (isoMount != null)
+ {
+ isoMount.Dispose();
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs
new file mode 100644
index 0000000000..d8fd76805b
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs
@@ -0,0 +1,208 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Extracts audio information using ffprobe
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFProbeAudioInfoProvider : BaseFFProbeProvider<Audio>
+ {
+ /// <summary>
+ /// Gets the name of the cache directory.
+ /// </summary>
+ /// <value>The name of the cache directory.</value>
+ protected override string CacheDirectoryName
+ {
+ get
+ {
+ return "ffmpeg-audio-info";
+ }
+ }
+
+ /// <summary>
+ /// Fetches the specified audio.
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>Task.</returns>
+ protected override Task Fetch(Audio audio, CancellationToken cancellationToken, FFProbeResult data, IIsoMount isoMount)
+ {
+ return Task.Run(() =>
+ {
+ if (data.streams == null)
+ {
+ Logger.Error("Audio item has no streams: " + audio.Path);
+ return;
+ }
+
+ audio.MediaStreams = data.streams.Select(s => GetMediaStream(s, data.format)).ToList();
+
+ // Get the first audio stream
+ var stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
+
+ // Get duration from stream properties
+ var duration = stream.duration;
+
+ // If it's not there go into format properties
+ if (string.IsNullOrEmpty(duration))
+ {
+ duration = data.format.duration;
+ }
+
+ // If we got something, parse it
+ if (!string.IsNullOrEmpty(duration))
+ {
+ audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
+ }
+
+ if (data.format.tags != null)
+ {
+ FetchDataFromTags(audio, data.format.tags);
+ }
+ });
+ }
+
+ /// <summary>
+ /// Fetches data from the tags dictionary
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="tags">The tags.</param>
+ private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
+ {
+ var title = GetDictionaryValue(tags, "title");
+
+ // Only set Name if title was found in the dictionary
+ if (!string.IsNullOrEmpty(title))
+ {
+ audio.Name = title;
+ }
+
+ var composer = GetDictionaryValue(tags, "composer");
+
+ if (!string.IsNullOrWhiteSpace(composer))
+ {
+ // Only use the comma as a delimeter if there are no slashes or pipes.
+ // We want to be careful not to split names that have commas in them
+ var delimeter = composer.IndexOf('/') == -1 && composer.IndexOf('|') == -1 ? new[] { ',' } : new[] { '/', '|' };
+
+ foreach (var person in composer.Split(delimeter, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var name = person.Trim();
+
+ if (!string.IsNullOrEmpty(name))
+ {
+ audio.AddPerson(new PersonInfo { Name = name, Type = PersonType.Composer });
+ }
+ }
+ }
+
+ audio.Album = GetDictionaryValue(tags, "album");
+ audio.Artist = GetDictionaryValue(tags, "artist");
+
+ if (!string.IsNullOrWhiteSpace(audio.Artist))
+ {
+ // Add to people too
+ audio.AddPerson(new PersonInfo {Name = audio.Artist, Type = PersonType.MusicArtist});
+ }
+
+ // Several different forms of albumartist
+ audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
+
+ // Track number
+ audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
+
+ // Disc number
+ audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
+
+ audio.Language = GetDictionaryValue(tags, "language");
+
+ audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
+
+ // Several different forms of retaildate
+ audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
+
+ // If we don't have a ProductionYear try and get it from PremiereDate
+ if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
+ {
+ audio.ProductionYear = audio.PremiereDate.Value.Year;
+ }
+
+ FetchGenres(audio, tags);
+
+ // There's several values in tags may or may not be present
+ FetchStudios(audio, tags, "organization");
+ FetchStudios(audio, tags, "ensemble");
+ FetchStudios(audio, tags, "publisher");
+ }
+
+ /// <summary>
+ /// Gets the studios from the tags collection
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="tags">The tags.</param>
+ /// <param name="tagName">Name of the tag.</param>
+ private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
+ {
+ var val = GetDictionaryValue(tags, tagName);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ audio.AddStudios(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ }
+
+ /// <summary>
+ /// Gets the genres from the tags collection
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="tags">The tags.</param>
+ private void FetchGenres(Audio audio, Dictionary<string, string> tags)
+ {
+ var val = GetDictionaryValue(tags, "genre");
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ audio.AddGenres(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ }
+
+ /// <summary>
+ /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
+ /// </summary>
+ /// <param name="tags">The tags.</param>
+ /// <returns>System.Nullable{System.Int32}.</returns>
+ private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
+ {
+ var disc = GetDictionaryValue(tags, "disc");
+
+ if (!string.IsNullOrEmpty(disc))
+ {
+ disc = disc.Split('/')[0];
+
+ int num;
+
+ if (int.TryParse(disc, out num))
+ {
+ return num;
+ }
+ }
+
+ return null;
+ }
+ }
+
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs
new file mode 100644
index 0000000000..5092429e84
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs
@@ -0,0 +1,291 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Extracts video information using ffprobe
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFProbeVideoInfoProvider : BaseFFProbeProvider<Video>
+ {
+ /// <summary>
+ /// Gets or sets the bd info cache.
+ /// </summary>
+ /// <value>The bd info cache.</value>
+ private FileSystemRepository BdInfoCache { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FFProbeVideoInfoProvider" /> class.
+ /// </summary>
+ public FFProbeVideoInfoProvider()
+ : base()
+ {
+ BdInfoCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, "bdinfo"));
+ }
+
+ /// <summary>
+ /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
+ /// </summary>
+ /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Supports video files and dvd structures
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ var video = item as Video;
+
+ if (video != null)
+ {
+ if (video.VideoType == VideoType.Iso)
+ {
+ return Kernel.Instance.IsoManager.CanMount(item.Path);
+ }
+
+ return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Called when [pre fetch].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="mount">The mount.</param>
+ protected override void OnPreFetch(Video item, IIsoMount mount)
+ {
+ if (item.VideoType == VideoType.Iso)
+ {
+ item.IsoType = DetermineIsoType(mount);
+ }
+
+ if (item.VideoType == VideoType.Dvd || (item.IsoType.HasValue && item.IsoType == IsoType.Dvd))
+ {
+ PopulateDvdStreamFiles(item, mount);
+ }
+
+ base.OnPreFetch(item, mount);
+ }
+
+ /// <summary>
+ /// Mounts the iso if needed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ protected override Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
+ {
+ if (item.VideoType == VideoType.Iso)
+ {
+ return Kernel.Instance.IsoManager.Mount(item.Path, cancellationToken);
+ }
+
+ return base.MountIsoIfNeeded(item, cancellationToken);
+ }
+
+ /// <summary>
+ /// Determines the type of the iso.
+ /// </summary>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>System.Nullable{IsoType}.</returns>
+ private IsoType? DetermineIsoType(IIsoMount isoMount)
+ {
+ var folders = Directory.EnumerateDirectories(isoMount.MountedPath).Select(Path.GetFileName).ToList();
+
+ if (folders.Contains("video_ts", StringComparer.OrdinalIgnoreCase))
+ {
+ return IsoType.Dvd;
+ }
+ if (folders.Contains("bdmv", StringComparer.OrdinalIgnoreCase))
+ {
+ return IsoType.BluRay;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Finds vob files and populates the dvd stream file properties
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ private void PopulateDvdStreamFiles(Video video, IIsoMount isoMount)
+ {
+ // min size 300 mb
+ const long minPlayableSize = 314572800;
+
+ var root = isoMount != null ? isoMount.MountedPath : video.Path;
+
+ // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
+ // Once we reach a file that is at least the minimum, return all subsequent ones
+ video.PlayableStreamFileNames = Directory.EnumerateFiles(root, "*.vob", SearchOption.AllDirectories).SkipWhile(f => new FileInfo(f).Length < minPlayableSize).Select(Path.GetFileName).ToList();
+ }
+
+ /// <summary>
+ /// Fetches the specified video.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>Task.</returns>
+ protected override Task Fetch(Video video, CancellationToken cancellationToken, FFProbeResult data, IIsoMount isoMount)
+ {
+ return Task.Run(() =>
+ {
+ if (data.format != null)
+ {
+ // For dvd's this may not always be accurate, so don't set the runtime if the item already has one
+ var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
+
+ if (needToSetRuntime && !string.IsNullOrEmpty(data.format.duration))
+ {
+ video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration)).Ticks;
+ }
+ }
+
+ if (data.streams != null)
+ {
+ video.MediaStreams = data.streams.Select(s => GetMediaStream(s, data.format)).ToList();
+ }
+
+ if (data.Chapters != null)
+ {
+ video.Chapters = data.Chapters;
+ }
+
+ if (video.Chapters == null || video.Chapters.Count == 0)
+ {
+ AddDummyChapters(video);
+ }
+
+ if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
+ {
+ var inputPath = isoMount != null ? isoMount.MountedPath : video.Path;
+ BDInfoProvider.FetchBdInfo(video, inputPath, BdInfoCache, cancellationToken);
+ }
+
+ AddExternalSubtitles(video);
+ });
+ }
+
+ /// <summary>
+ /// Adds the external subtitles.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ private void AddExternalSubtitles(Video video)
+ {
+ var useParent = (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) && !(video is Movie);
+
+ if (useParent && video.Parent == null)
+ {
+ return;
+ }
+
+ var fileSystemChildren = useParent
+ ? video.Parent.ResolveArgs.FileSystemChildren
+ : video.ResolveArgs.FileSystemChildren;
+
+ var startIndex = video.MediaStreams == null ? 0 : video.MediaStreams.Count;
+ var streams = new List<MediaStream>();
+
+ foreach (var file in fileSystemChildren.Where(f => !f.IsDirectory))
+ {
+ var extension = Path.GetExtension(file.Path);
+
+ if (string.Equals(extension, ".srt", StringComparison.OrdinalIgnoreCase))
+ {
+ streams.Add(new MediaStream
+ {
+ Index = startIndex,
+ Type = MediaStreamType.Subtitle,
+ IsExternal = true,
+ Path = file.Path,
+ Codec = "srt"
+ });
+
+ startIndex++;
+ }
+ }
+
+ if (video.MediaStreams == null)
+ {
+ video.MediaStreams = new List<MediaStream>();
+ }
+ video.MediaStreams.AddRange(streams);
+ }
+
+ /// <summary>
+ /// The dummy chapter duration
+ /// </summary>
+ private static readonly long DummyChapterDuration = TimeSpan.FromMinutes(10).Ticks;
+
+ /// <summary>
+ /// Adds the dummy chapters.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ private void AddDummyChapters(Video video)
+ {
+ var runtime = video.RunTimeTicks ?? 0;
+
+ if (runtime < DummyChapterDuration)
+ {
+ return;
+ }
+
+ long currentChapterTicks = 0;
+ var index = 1;
+
+ var chapters = new List<ChapterInfo> { };
+
+ while (currentChapterTicks < runtime)
+ {
+ chapters.Add(new ChapterInfo
+ {
+ Name = "Chapter " + index,
+ StartPositionTicks = currentChapterTicks
+ });
+
+ index++;
+ currentChapterTicks += DummyChapterDuration;
+ }
+
+ video.Chapters = chapters;
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ BdInfoCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs b/MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs
new file mode 100644
index 0000000000..38e5475230
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs
@@ -0,0 +1,220 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class FanArtMovieProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class FanArtMovieProvider : FanartBaseProvider
+ {
+ /// <summary>
+ /// The fan art base URL
+ /// </summary>
+ protected string FanArtBaseUrl = "http://api.fanart.tv/webservice/movie/{0}/{1}/xml/all/1/1";
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Movie || item is BoxSet;
+ }
+
+ /// <summary>
+ /// Shoulds the fetch.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ var baseItem = item;
+ if (item.Path == null || item.DontFetchMeta || string.IsNullOrEmpty(baseItem.GetProviderId(MetadataProviders.Tmdb))) return false; //nothing to do
+ var artExists = item.ResolveArgs.ContainsMetaFileByName(ART_FILE);
+ var logoExists = item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE);
+ var discExists = item.ResolveArgs.ContainsMetaFileByName(DISC_FILE);
+
+ return (!artExists && Kernel.Instance.Configuration.DownloadMovieArt)
+ || (!logoExists && Kernel.Instance.Configuration.DownloadMovieLogo)
+ || (!discExists && Kernel.Instance.Configuration.DownloadMovieDisc);
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var movie = item;
+ if (ShouldFetch(movie, movie.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id })))
+ {
+ var language = Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower();
+ var url = string.Format(FanArtBaseUrl, APIKey, movie.GetProviderId(MetadataProviders.Tmdb));
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var xml = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(xml);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (doc.HasChildNodes)
+ {
+ string path;
+ var hd = Kernel.Instance.Configuration.DownloadHDFanArt ? "hd" : "";
+ if (Kernel.Instance.Configuration.DownloadMovieLogo && !item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE))
+ {
+ var node =
+ doc.SelectSingleNode("//fanart/movie/movielogos/" + hd + "movielogo[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/movielogos/movielogo[@lang = \"" + language + "\"]/@url");
+ if (node == null && language != "en")
+ {
+ //maybe just couldn't find language - try just first one
+ node = doc.SelectSingleNode("//fanart/movie/movielogos/" + hd + "movielogo/@url");
+ }
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearLogo for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Logo, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, LOGO_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieArt && !item.ResolveArgs.ContainsMetaFileByName(ART_FILE))
+ {
+ var node =
+ doc.SelectSingleNode("//fanart/movie/moviearts/" + hd + "movieart[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviearts/" + hd + "movieart/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviearts/movieart[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviearts/movieart/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearArt for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Art, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, ART_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieDisc && !item.ResolveArgs.ContainsMetaFileByName(DISC_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/movie/moviediscs/moviedisc[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviediscs/moviedisc/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting DiscArt for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Disc, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, DISC_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieBanner && !item.ResolveArgs.ContainsMetaFileByName(BANNER_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/movie/moviebanners/moviebanner[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviebanners/moviebanner/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting Banner for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Banner, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, BANNER_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieThumb && !item.ResolveArgs.ContainsMetaFileByName(THUMB_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/movie/moviethumbs/moviethumb[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviethumbs/moviethumb/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting Banner for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Thumb, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, THUMB_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+ SetLastRefreshed(movie, DateTime.UtcNow);
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs
new file mode 100644
index 0000000000..2319e5cfa4
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs
@@ -0,0 +1,1607 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ class MovieDbProviderException : ApplicationException
+ {
+ public MovieDbProviderException(string msg) : base(msg)
+ {
+ }
+
+ }
+ /// <summary>
+ /// Class MovieDbProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieDbProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Movie || item is BoxSet;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// If we save locally, refresh if they delete something
+ /// </summary>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return Kernel.Instance.Configuration.SaveLocalMeta;
+ }
+ }
+
+ /// <summary>
+ /// The _TMDB settings task
+ /// </summary>
+ private Task<TmdbSettingsResult> _tmdbSettingsTask;
+ /// <summary>
+ /// The _TMDB settings task initialized
+ /// </summary>
+ private bool _tmdbSettingsTaskInitialized;
+ /// <summary>
+ /// The _TMDB settings task sync lock
+ /// </summary>
+ private object _tmdbSettingsTaskSyncLock = new object();
+
+ /// <summary>
+ /// Gets the TMDB settings.
+ /// </summary>
+ /// <value>The TMDB settings.</value>
+ public Task<TmdbSettingsResult> TmdbSettings
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _tmdbSettingsTask, ref _tmdbSettingsTaskInitialized, ref _tmdbSettingsTaskSyncLock, GetTmdbSettings);
+ return _tmdbSettingsTask;
+ }
+ }
+
+ /// <summary>
+ /// Gets the TMDB settings.
+ /// </summary>
+ /// <returns>Task{TmdbSettingsResult}.</returns>
+ private static async Task<TmdbSettingsResult> GetTmdbSettings()
+ {
+ try
+ {
+ using (var json = await Kernel.Instance.HttpManager.Get(String.Format(TmdbConfigUrl, ApiKey), Kernel.Instance.ResourcePools.MovieDb, CancellationToken.None).ConfigureAwait(false))
+ {
+ return JsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
+ }
+ }
+ catch (HttpException e)
+ {
+ return new TmdbSettingsResult
+ {
+ images = new TmdbImageSettings
+ {
+ backdrop_sizes =
+ new List<string>
+ {
+ "w380",
+ "w780",
+ "w1280",
+ "original"
+ },
+ poster_sizes =
+ new List<string>
+ {
+ "w92",
+ "w154",
+ "w185",
+ "w342",
+ "w500",
+ "original"
+ },
+ profile_sizes =
+ new List<string>
+ {
+ "w45",
+ "w185",
+ "h632",
+ "original"
+ },
+ base_url = "http://cf2.imgobject.com/t/p/"
+
+ }
+ };
+ }
+ }
+
+ /// <summary>
+ /// The json provider
+ /// </summary>
+ protected MovieProviderFromJson JsonProvider;
+ /// <summary>
+ /// Sets the persisted last refresh date on the item for this provider.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="status">The status.</param>
+ protected override void SetLastRefreshed(BaseItem item, DateTime value, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
+ {
+ base.SetLastRefreshed(item, value, status);
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ //in addition to ours, we need to set the last refreshed time for the local data provider
+ //so it won't see the new files we download and process them all over again
+ if (JsonProvider == null) JsonProvider = new MovieProviderFromJson();
+ var data = item.ProviderData.GetValueOrDefault(JsonProvider.Id, new BaseProviderInfo { ProviderId = JsonProvider.Id });
+ data.LastRefreshed = value;
+ item.ProviderData[JsonProvider.Id] = data;
+ }
+ }
+
+ private const string TmdbConfigUrl = "http://api.themoviedb.org/3/configuration?api_key={0}";
+ private const string Search3 = @"http://api.themoviedb.org/3/search/movie?api_key={1}&query={0}&language={2}";
+ private const string AltTitleSearch = @"http://api.themoviedb.org/3/movie/{0}/alternative_titles?api_key={1}&country={2}";
+ private const string GetInfo3 = @"http://api.themoviedb.org/3/{3}/{0}?api_key={1}&language={2}";
+ private const string CastInfo = @"http://api.themoviedb.org/3/movie/{0}/casts?api_key={1}";
+ private const string ReleaseInfo = @"http://api.themoviedb.org/3/movie/{0}/releases?api_key={1}";
+ private const string GetImages = @"http://api.themoviedb.org/3/{2}/{0}/images?api_key={1}";
+ public static string ApiKey = "f6bd687ffa63cd282b6ff2c6877f2669";
+
+ static readonly Regex[] NameMatches = new[] {
+ new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year
+ new Regex(@"(?<name>.*)") // last resort matches the whole string as the name
+ };
+
+ public const string LOCAL_META_FILE_NAME = "MBMovie.json";
+ public const string ALT_META_FILE_NAME = "movie.xml";
+ protected string ItemType = "movie";
+
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item.DontFetchMeta) return false;
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta && HasFileSystemStampChanged(item, providerInfo))
+ {
+ //If they deleted something from file system, chances are, this item was mis-identified the first time
+ item.SetProviderId(MetadataProviders.Tmdb, null);
+ Logger.Debug("MovieProvider reports file system stamp change...");
+ return true;
+
+ }
+
+ if (providerInfo.LastRefreshStatus == ProviderRefreshStatus.CompletedWithErrors)
+ {
+ Logger.Debug("MovieProvider for {0} - last attempt had errors. Will try again.", item.Path);
+ return true;
+ }
+
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ {
+ return false;
+ }
+
+ if (DateTime.Today.Subtract(item.DateCreated).TotalDays > 180 && downloadDate != DateTime.MinValue)
+ return false; // don't trigger a refresh data for item that are more than 6 months old and have been refreshed before
+
+ if (DateTime.Today.Subtract(downloadDate).TotalDays < Kernel.Instance.Configuration.MetadataRefreshDays) // only refresh every n days
+ return false;
+
+ if (HasAltMeta(item))
+ return false; //never refresh if has meta from other source
+
+
+
+ Logger.Debug("MovieDbProvider - " + item.Name + " needs refresh. Download date: " + downloadDate + " item created date: " + item.DateCreated + " Check for Update age: " + Kernel.Instance.Configuration.MetadataRefreshDays);
+ return true;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ if (HasAltMeta(item))
+ {
+ Logger.Info("MovieDbProvider - Not fetching because 3rd party meta exists for " + item.Name);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ if (item.DontFetchMeta)
+ {
+ Logger.Info("MovieDbProvider - Not fetching because requested to ignore " + item.Name);
+ return false;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!Kernel.Instance.Configuration.SaveLocalMeta || !HasLocalMeta(item) || (force && !HasLocalMeta(item)))
+ {
+ try
+ {
+ await FetchMovieData(item, cancellationToken).ConfigureAwait(false);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ }
+ catch (MovieDbProviderException e)
+ {
+ SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.CompletedWithErrors);
+ }
+
+ return true;
+ }
+ Logger.Debug("MovieDBProvider not fetching because local meta exists for " + item.Name);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(BaseItem item)
+ {
+ //need at least the xml and folder.jpg/png or a movie.xml put in by someone else
+ return item.ResolveArgs.ContainsMetaFileByName(LOCAL_META_FILE_NAME);
+ }
+
+ /// <summary>
+ /// Determines whether [has alt meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has alt meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasAltMeta(BaseItem item)
+ {
+ return item.ResolveArgs.ContainsMetaFileByName(ALT_META_FILE_NAME);
+ }
+
+ /// <summary>
+ /// Fetches the movie data.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>Task.</returns>
+ private async Task FetchMovieData(BaseItem item, CancellationToken cancellationToken)
+ {
+ string id = item.GetProviderId(MetadataProviders.Tmdb) ?? await FindId(item, item.ProductionYear, cancellationToken).ConfigureAwait(false);
+ if (id != null)
+ {
+ Logger.Debug("MovieDbProvider - getting movie info with id: " + id);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await FetchMovieData(item, id, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ Logger.Info("MovieDBProvider could not find " + item.Name + ". Check name on themoviedb.org.");
+ }
+ }
+
+ /// <summary>
+ /// Parses the name.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="justName">Name of the just.</param>
+ /// <param name="year">The year.</param>
+ protected void ParseName(string name, out string justName, out int? year)
+ {
+ justName = null;
+ year = null;
+ foreach (var re in NameMatches)
+ {
+ Match m = re.Match(name);
+ if (m.Success)
+ {
+ justName = m.Groups["name"].Value.Trim();
+ string y = m.Groups["year"] != null ? m.Groups["year"].Value : null;
+ int temp;
+ year = Int32.TryParse(y, out temp) ? temp : (int?)null;
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Finds the id.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="productionYear">The production year.</param>
+ /// <returns>Task{System.String}.</returns>
+ public async Task<string> FindId(BaseItem item, int? productionYear, CancellationToken cancellationToken)
+ {
+ string justName = item.Path != null ? item.Path.Substring(item.Path.LastIndexOf(Path.DirectorySeparatorChar)) : string.Empty;
+ var id = justName.GetAttributeValue("tmdbid");
+ if (id != null)
+ {
+ Logger.Debug("Using tmdb id specified in path.");
+ return id;
+ }
+
+ int? year;
+ string name = item.Name;
+ ParseName(name, out name, out year);
+
+ if (year == null && productionYear != null)
+ {
+ year = productionYear;
+ }
+
+ Logger.Info("MovieDbProvider: Finding id for movie: " + name);
+ string language = Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower();
+
+ //if we are a boxset - look at our first child
+ var boxset = item as BoxSet;
+ if (boxset != null)
+ {
+ if (!boxset.Children.IsEmpty)
+ {
+ var firstChild = boxset.Children.First();
+ Logger.Debug("MovieDbProvider - Attempting to find boxset ID from: " + firstChild.Name);
+ string childName;
+ int? childYear;
+ ParseName(firstChild.Name, out childName, out childYear);
+ id = await GetBoxsetIdFromMovie(childName, childYear, language, cancellationToken).ConfigureAwait(false);
+ if (id != null)
+ {
+ Logger.Info("MovieDbProvider - Found Boxset ID: " + id);
+ }
+ }
+
+ return id;
+ }
+ //nope - search for it
+ id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+ if (id == null)
+ {
+ //try in english if wasn't before
+ if (language != "en")
+ {
+ id = await AttemptFindId(name, year, "en", cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // try with dot and _ turned to space
+ name = name.Replace(",", " ");
+ name = name.Replace(".", " ");
+ name = name.Replace(" ", " ");
+ name = name.Replace("_", " ");
+ name = name.Replace("-", "");
+ id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+ if (id == null && language != "en")
+ {
+ //one more time, in english
+ id = await AttemptFindId(name, year, "en", cancellationToken).ConfigureAwait(false);
+
+ }
+ if (id == null)
+ {
+ //last resort - try using the actual folder name
+ id = await AttemptFindId(Path.GetFileName(item.ResolveArgs.Path), year, "en", cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ return id;
+ }
+
+ /// <summary>
+ /// Attempts the find id.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="year">The year.</param>
+ /// <param name="language">The language.</param>
+ /// <returns>Task{System.String}.</returns>
+ public virtual async Task<string> AttemptFindId(string name, int? year, string language, CancellationToken cancellationToken)
+ {
+ string url3 = string.Format(Search3, UrlEncode(name), ApiKey, language);
+ TmdbMovieSearchResults searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url3, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<TmdbMovieSearchResults>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ if (searchResult == null || searchResult.results.Count == 0)
+ {
+ //try replacing numbers
+ foreach (var pair in ReplaceStartNumbers)
+ {
+ if (name.StartsWith(pair.Key))
+ {
+ name = name.Remove(0, pair.Key.Length);
+ name = pair.Value + name;
+ }
+ }
+ foreach (var pair in ReplaceEndNumbers)
+ {
+ if (name.EndsWith(pair.Key))
+ {
+ name = name.Remove(name.IndexOf(pair.Key), pair.Key.Length);
+ name = name + pair.Value;
+ }
+ }
+ Logger.Info("MovieDBProvider - No results. Trying replacement numbers: " + name);
+ url3 = string.Format(Search3, UrlEncode(name), ApiKey, language);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url3, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<TmdbMovieSearchResults>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+ if (searchResult != null)
+ {
+ string compName = GetComparableName(name, Logger);
+ foreach (var possible in searchResult.results)
+ {
+ string matchedName = null;
+ string id = possible.id.ToString();
+ string n = possible.title;
+ if (GetComparableName(n, Logger) == compName)
+ {
+ matchedName = n;
+ }
+ else
+ {
+ n = possible.original_title;
+ if (GetComparableName(n, Logger) == compName)
+ {
+ matchedName = n;
+ }
+ }
+
+ Logger.Debug("MovieDbProvider - " + compName + " didn't match " + n);
+ //if main title matches we don't have to look for alternatives
+ if (matchedName == null)
+ {
+ //that title didn't match - look for alternatives
+ url3 = string.Format(AltTitleSearch, id, ApiKey, Kernel.Instance.Configuration.MetadataCountryCode);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url3, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ var response = JsonSerializer.DeserializeFromStream<TmdbAltTitleResults>(json);
+
+ if (response != null && response.titles != null)
+ {
+ foreach (var title in response.titles)
+ {
+ var t = GetComparableName(title.title, Logger);
+ if (t == compName)
+ {
+ Logger.Debug("MovieDbProvider - " + compName +
+ " matched " + t);
+ matchedName = t;
+ break;
+ }
+ Logger.Debug("MovieDbProvider - " + compName +
+ " did not match " + t);
+ }
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+
+ if (matchedName != null)
+ {
+ Logger.Debug("Match " + matchedName + " for " + name);
+ if (year != null)
+ {
+ DateTime r;
+
+ if (DateTime.TryParse(possible.release_date, out r))
+ {
+ if (Math.Abs(r.Year - year.Value) > 1) // allow a 1 year tolerance on release date
+ {
+ Logger.Debug("Result " + matchedName + " released on " + r + " did not match year " + year);
+ continue;
+ }
+ }
+ }
+ //matched name and year
+ return id;
+ }
+
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// URLs the encode.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.String.</returns>
+ private static string UrlEncode(string name)
+ {
+ return WebUtility.UrlEncode(name);
+ }
+
+ /// <summary>
+ /// Gets the boxset id from movie.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="year">The year.</param>
+ /// <param name="language">The language.</param>
+ /// <returns>Task{System.String}.</returns>
+ protected async Task<string> GetBoxsetIdFromMovie(string name, int? year, string language, CancellationToken cancellationToken)
+ {
+ string id = null;
+ string childId = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+ if (childId != null)
+ {
+ string url = string.Format(GetInfo3, childId, ApiKey, language, ItemType);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ var movieResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
+
+ if (movieResult != null && movieResult.belongs_to_collection != null)
+ {
+ id = movieResult.belongs_to_collection.id.ToString();
+ }
+ else
+ {
+ Logger.Error("Unable to obtain boxset id.");
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+ return id;
+ }
+
+ /// <summary>
+ /// Fetches the movie data.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task.</returns>
+ protected async Task FetchMovieData(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (String.IsNullOrEmpty(id))
+ {
+ Logger.Info("MoviedbProvider: Ignoring " + item.Name + " because ID forced blank.");
+ return;
+ }
+ if (item.GetProviderId(MetadataProviders.Tmdb) == null) item.SetProviderId(MetadataProviders.Tmdb, id);
+ var mainTask = FetchMainResult(item, id, cancellationToken);
+ var castTask = FetchCastInfo(item, id, cancellationToken);
+ var releaseTask = FetchReleaseInfo(item, id, cancellationToken);
+ var imageTask = FetchImageInfo(item, id, cancellationToken);
+
+ await Task.WhenAll(mainTask, castTask, releaseTask).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var mainResult = mainTask.Result;
+ if (mainResult == null) return;
+
+ if (castTask.Result != null)
+ {
+ mainResult.cast = castTask.Result.cast;
+ mainResult.crew = castTask.Result.crew;
+ }
+
+ if (releaseTask.Result != null)
+ {
+ mainResult.countries = releaseTask.Result.countries;
+ }
+
+ ProcessMainInfo(item, mainResult);
+
+ await Task.WhenAll(imageTask).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (imageTask.Result != null)
+ {
+ await ProcessImages(item, imageTask.Result, cancellationToken).ConfigureAwait(false);
+ }
+
+ //and save locally
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ var ms = new MemoryStream();
+ JsonSerializer.SerializeToStream(mainResult, ms);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(item, Path.Combine(item.MetaLocation, LOCAL_META_FILE_NAME), ms, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Fetches the main result.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{CompleteMovieData}.</returns>
+ protected async Task<CompleteMovieData> FetchMainResult(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ ItemType = item is BoxSet ? "collection" : "movie";
+ string url = string.Format(GetInfo3, id, ApiKey, Kernel.Instance.Configuration.PreferredMetadataLanguage, ItemType);
+ CompleteMovieData mainResult = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (var json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ mainResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
+ }
+ }
+ catch (HttpException e)
+ {
+ if (e.IsTimedOut)
+ {
+ Logger.ErrorException("MovieDbProvider timed out attempting to retrieve main info for {0}", e, item.Path);
+ throw new MovieDbProviderException("Timed out on main info");
+ }
+ if (e.StatusCode == HttpStatusCode.NotFound)
+ {
+ Logger.ErrorException("MovieDbProvider not found error attempting to retrieve main info for {0}", e, item.Path);
+ throw new MovieDbProviderException("Not Found");
+ }
+
+ throw;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (mainResult != null && string.IsNullOrEmpty(mainResult.overview))
+ {
+ if (Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower() != "en")
+ {
+ Logger.Info("MovieDbProvider couldn't find meta for language " + Kernel.Instance.Configuration.PreferredMetadataLanguage + ". Trying English...");
+ url = string.Format(GetInfo3, id, ApiKey, "en", ItemType);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ mainResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (String.IsNullOrEmpty(mainResult.overview))
+ {
+ Logger.Error("MovieDbProvider - Unable to find information for " + item.Name + " (id:" + id + ")");
+ return null;
+ }
+ }
+ }
+ return mainResult;
+ }
+
+ /// <summary>
+ /// Fetches the cast info.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{TmdbCastResult}.</returns>
+ protected async Task<TmdbCastResult> FetchCastInfo(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ //get cast and crew info
+ var url = string.Format(CastInfo, id, ApiKey, ItemType);
+ TmdbCastResult cast = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ cast = JsonSerializer.DeserializeFromStream<TmdbCastResult>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ return cast;
+ }
+
+ /// <summary>
+ /// Fetches the release info.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{TmdbReleasesResult}.</returns>
+ protected async Task<TmdbReleasesResult> FetchReleaseInfo(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ var url = string.Format(ReleaseInfo, id, ApiKey);
+ TmdbReleasesResult releases = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ releases = JsonSerializer.DeserializeFromStream<TmdbReleasesResult>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ return releases;
+ }
+
+ /// <summary>
+ /// Fetches the image info.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{TmdbImages}.</returns>
+ protected async Task<TmdbImages> FetchImageInfo(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ //fetch images
+ var url = string.Format(GetImages, id, ApiKey, ItemType);
+ TmdbImages images = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ images = JsonSerializer.DeserializeFromStream<TmdbImages>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ return images;
+ }
+
+ /// <summary>
+ /// Processes the main info.
+ /// </summary>
+ /// <param name="movie">The movie.</param>
+ /// <param name="movieData">The movie data.</param>
+ protected virtual void ProcessMainInfo(BaseItem movie, CompleteMovieData movieData)
+ {
+ if (movie != null && movieData != null)
+ {
+
+ movie.Name = movieData.title ?? movieData.original_title ?? movie.Name;
+ movie.Overview = movieData.overview;
+ movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
+ if (!string.IsNullOrEmpty(movieData.tagline)) movie.AddTagline(movieData.tagline);
+ movie.SetProviderId(MetadataProviders.Imdb, movieData.imdb_id);
+ float rating;
+ string voteAvg = movieData.vote_average.ToString();
+ string cultureStr = Kernel.Instance.Configuration.PreferredMetadataLanguage + "-" + Kernel.Instance.Configuration.MetadataCountryCode;
+ CultureInfo culture;
+ try
+ {
+ culture = new CultureInfo(cultureStr);
+ }
+ catch
+ {
+ culture = CultureInfo.CurrentCulture; //default to windows settings if other was invalid
+ }
+ Logger.Debug("Culture for numeric conversion is: " + culture.Name);
+ if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, culture, out rating))
+ movie.CommunityRating = rating;
+
+ //release date and certification are retrieved based on configured country and we fall back on US if not there
+ if (movieData.countries != null)
+ {
+ var ourRelease = movieData.countries.FirstOrDefault(c => c.iso_3166_1.Equals(Kernel.Instance.Configuration.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)) ?? new Country();
+ var usRelease = movieData.countries.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase)) ?? new Country();
+
+ movie.OfficialRating = ourRelease.certification ?? usRelease.certification;
+ if (ourRelease.release_date > new DateTime(1900, 1, 1))
+ {
+ movie.PremiereDate = ourRelease.release_date;
+ movie.ProductionYear = ourRelease.release_date.Year;
+ }
+ else
+ {
+ movie.PremiereDate = usRelease.release_date;
+ movie.ProductionYear = usRelease.release_date.Year;
+ }
+ }
+ else
+ {
+ //no specific country release info at all
+ movie.PremiereDate = movieData.release_date;
+ movie.ProductionYear = movieData.release_date.Year;
+ }
+
+ //if that didn't find a rating and we are a boxset, use the one from our first child
+ if (movie.OfficialRating == null && movie is BoxSet)
+ {
+ var boxset = movie as BoxSet;
+ Logger.Info("MovieDbProvider - Using rating of first child of boxset...");
+ boxset.OfficialRating = !boxset.Children.IsEmpty ? boxset.Children.First().OfficialRating : null;
+ }
+
+ if (movie.RunTimeTicks == null && movieData.runtime > 0)
+ movie.RunTimeTicks = TimeSpan.FromMinutes(movieData.runtime).Ticks;
+
+ //studios
+ if (movieData.production_companies != null)
+ {
+ //always clear so they don't double up
+ movie.AddStudios(movieData.production_companies.Select(c => c.name));
+ }
+
+ //genres
+ if (movieData.genres != null)
+ {
+ movie.AddGenres(movieData.genres.Select(g => g.name));
+ }
+
+ //Actors, Directors, Writers - all in People
+ //actors come from cast
+ if (movieData.cast != null)
+ {
+ foreach (var actor in movieData.cast.OrderBy(a => a.order)) movie.AddPerson(new PersonInfo { Name = actor.name, Role = actor.character, Type = PersonType.Actor });
+ }
+ //and the rest from crew
+ if (movieData.crew != null)
+ {
+ foreach (var person in movieData.crew) movie.AddPerson(new PersonInfo { Name = person.name, Role = person.job, Type = person.department });
+ }
+
+
+ }
+
+ }
+
+ /// <summary>
+ /// Processes the images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="images">The images.</param>
+ /// <returns>Task.</returns>
+ protected virtual async Task ProcessImages(BaseItem item, TmdbImages images, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // poster
+ if (images.posters != null && images.posters.Count > 0 && (Kernel.Instance.Configuration.RefreshItemImages || !item.HasLocalImage("folder")))
+ {
+ var tmdbSettings = await TmdbSettings.ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.base_url + Kernel.Instance.Configuration.TmdbFetchedPosterSize;
+ // get highest rated poster for our language
+
+ var postersSortedByVote = images.posters.OrderByDescending(i => i.vote_average);
+
+ var poster = postersSortedByVote.FirstOrDefault(p => p.iso_639_1 != null && p.iso_639_1.Equals(Kernel.Instance.Configuration.PreferredMetadataLanguage, StringComparison.OrdinalIgnoreCase));
+ if (poster == null && !Kernel.Instance.Configuration.PreferredMetadataLanguage.Equals("en"))
+ {
+ // couldn't find our specific language, find english (if that wasn't our language)
+ poster = postersSortedByVote.FirstOrDefault(p => p.iso_639_1 != null && p.iso_639_1.Equals("en", StringComparison.OrdinalIgnoreCase));
+ }
+ if (poster == null)
+ {
+ //still couldn't find it - try highest rated null one
+ poster = postersSortedByVote.FirstOrDefault(p => p.iso_639_1 == null);
+ }
+ if (poster == null)
+ {
+ //finally - just get the highest rated one
+ poster = postersSortedByVote.FirstOrDefault();
+ }
+ if (poster != null)
+ {
+ try
+ {
+ item.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + poster.file_path, "folder" + Path.GetExtension(poster.file_path), Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // backdrops
+ if (images.backdrops != null && images.backdrops.Count > 0)
+ {
+ item.BackdropImagePaths = new List<string>();
+
+ var tmdbSettings = await TmdbSettings.ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.base_url + Kernel.Instance.Configuration.TmdbFetchedBackdropSize;
+ //backdrops should be in order of rating. get first n ones
+ var numToFetch = Math.Min(Kernel.Instance.Configuration.MaxBackdrops, images.backdrops.Count);
+ for (var i = 0; i < numToFetch; i++)
+ {
+ var bdName = "backdrop" + (i == 0 ? "" : i.ToString());
+
+ if (Kernel.Instance.Configuration.RefreshItemImages || !item.HasLocalImage(bdName))
+ {
+ try
+ {
+ item.BackdropImagePaths.Add(await Kernel.Instance.ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + images.backdrops[i].file_path, bdName + Path.GetExtension(images.backdrops[i].file_path), Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// The remove
+ /// </summary>
+ const string remove = "\"'!`?";
+ // "Face/Off" support.
+ /// <summary>
+ /// The spacers
+ /// </summary>
+ const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
+ /// <summary>
+ /// The replace start numbers
+ /// </summary>
+ static readonly Dictionary<string, string> ReplaceStartNumbers = new Dictionary<string, string> {
+ {"1 ","one "},
+ {"2 ","two "},
+ {"3 ","three "},
+ {"4 ","four "},
+ {"5 ","five "},
+ {"6 ","six "},
+ {"7 ","seven "},
+ {"8 ","eight "},
+ {"9 ","nine "},
+ {"10 ","ten "},
+ {"11 ","eleven "},
+ {"12 ","twelve "},
+ {"13 ","thirteen "},
+ {"100 ","one hundred "},
+ {"101 ","one hundred one "}
+ };
+
+ /// <summary>
+ /// The replace end numbers
+ /// </summary>
+ static readonly Dictionary<string, string> ReplaceEndNumbers = new Dictionary<string, string> {
+ {" 1"," i"},
+ {" 2"," ii"},
+ {" 3"," iii"},
+ {" 4"," iv"},
+ {" 5"," v"},
+ {" 6"," vi"},
+ {" 7"," vii"},
+ {" 8"," viii"},
+ {" 9"," ix"},
+ {" 10"," x"}
+ };
+
+ /// <summary>
+ /// Gets the name of the comparable.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="logger">The logger.</param>
+ /// <returns>System.String.</returns>
+ internal static string GetComparableName(string name, ILogger logger)
+ {
+ name = name.ToLower();
+ name = name.Replace("á", "a");
+ name = name.Replace("é", "e");
+ name = name.Replace("í", "i");
+ name = name.Replace("ó", "o");
+ name = name.Replace("ú", "u");
+ name = name.Replace("ü", "u");
+ name = name.Replace("ñ", "n");
+ foreach (var pair in ReplaceStartNumbers)
+ {
+ if (name.StartsWith(pair.Key))
+ {
+ name = name.Remove(0, pair.Key.Length);
+ name = pair.Value + name;
+ logger.Info("MovieDbProvider - Replaced Start Numbers: " + name);
+ }
+ }
+ foreach (var pair in ReplaceEndNumbers)
+ {
+ if (name.EndsWith(pair.Key))
+ {
+ name = name.Remove(name.IndexOf(pair.Key), pair.Key.Length);
+ name = name + pair.Value;
+ logger.Info("MovieDbProvider - Replaced End Numbers: " + name);
+ }
+ }
+ name = name.Normalize(NormalizationForm.FormKD);
+ var sb = new StringBuilder();
+ foreach (var c in name)
+ {
+ if ((int)c >= 0x2B0 && (int)c <= 0x0333)
+ {
+ // skip char modifier and diacritics
+ }
+ else if (remove.IndexOf(c) > -1)
+ {
+ // skip chars we are removing
+ }
+ else if (spacers.IndexOf(c) > -1)
+ {
+ sb.Append(" ");
+ }
+ else if (c == '&')
+ {
+ sb.Append(" and ");
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ name = sb.ToString();
+ name = name.Replace(", the", "");
+ name = name.Replace(" the ", " ");
+ name = name.Replace("the ", "");
+
+ string prev_name;
+ do
+ {
+ prev_name = name;
+ name = name.Replace(" ", " ");
+ } while (name.Length != prev_name.Length);
+
+ return name.Trim();
+ }
+
+ #region Result Objects
+
+
+ /// <summary>
+ /// Class TmdbMovieSearchResult
+ /// </summary>
+ protected class TmdbMovieSearchResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool adult { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrop_path.
+ /// </summary>
+ /// <value>The backdrop_path.</value>
+ public string backdrop_path { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the original_title.
+ /// </summary>
+ /// <value>The original_title.</value>
+ public string original_title { get; set; }
+ /// <summary>
+ /// Gets or sets the release_date.
+ /// </summary>
+ /// <value>The release_date.</value>
+ public string release_date { get; set; }
+ /// <summary>
+ /// Gets or sets the poster_path.
+ /// </summary>
+ /// <value>The poster_path.</value>
+ public string poster_path { get; set; }
+ /// <summary>
+ /// Gets or sets the popularity.
+ /// </summary>
+ /// <value>The popularity.</value>
+ public double popularity { get; set; }
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string title { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_average.
+ /// </summary>
+ /// <value>The vote_average.</value>
+ public double vote_average { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_count.
+ /// </summary>
+ /// <value>The vote_count.</value>
+ public int vote_count { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbMovieSearchResults
+ /// </summary>
+ protected class TmdbMovieSearchResults
+ {
+ /// <summary>
+ /// Gets or sets the page.
+ /// </summary>
+ /// <value>The page.</value>
+ public int page { get; set; }
+ /// <summary>
+ /// Gets or sets the results.
+ /// </summary>
+ /// <value>The results.</value>
+ public List<TmdbMovieSearchResult> results { get; set; }
+ /// <summary>
+ /// Gets or sets the total_pages.
+ /// </summary>
+ /// <value>The total_pages.</value>
+ public int total_pages { get; set; }
+ /// <summary>
+ /// Gets or sets the total_results.
+ /// </summary>
+ /// <value>The total_results.</value>
+ public int total_results { get; set; }
+ }
+
+ /// <summary>
+ /// Class BelongsToCollection
+ /// </summary>
+ protected class BelongsToCollection
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the poster_path.
+ /// </summary>
+ /// <value>The poster_path.</value>
+ public string poster_path { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrop_path.
+ /// </summary>
+ /// <value>The backdrop_path.</value>
+ public string backdrop_path { get; set; }
+ }
+
+ /// <summary>
+ /// Class Genre
+ /// </summary>
+ protected class Genre
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ }
+
+ /// <summary>
+ /// Class ProductionCompany
+ /// </summary>
+ protected class ProductionCompany
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ }
+
+ /// <summary>
+ /// Class ProductionCountry
+ /// </summary>
+ protected class ProductionCountry
+ {
+ /// <summary>
+ /// Gets or sets the iso_3166_1.
+ /// </summary>
+ /// <value>The iso_3166_1.</value>
+ public string iso_3166_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ }
+
+ /// <summary>
+ /// Class SpokenLanguage
+ /// </summary>
+ protected class SpokenLanguage
+ {
+ /// <summary>
+ /// Gets or sets the iso_639_1.
+ /// </summary>
+ /// <value>The iso_639_1.</value>
+ public string iso_639_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ }
+
+ /// <summary>
+ /// Class Cast
+ /// </summary>
+ protected class Cast
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the character.
+ /// </summary>
+ /// <value>The character.</value>
+ public string character { get; set; }
+ /// <summary>
+ /// Gets or sets the order.
+ /// </summary>
+ /// <value>The order.</value>
+ public int order { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_path.
+ /// </summary>
+ /// <value>The profile_path.</value>
+ public string profile_path { get; set; }
+ }
+
+ /// <summary>
+ /// Class Crew
+ /// </summary>
+ protected class Crew
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the department.
+ /// </summary>
+ /// <value>The department.</value>
+ public string department { get; set; }
+ /// <summary>
+ /// Gets or sets the job.
+ /// </summary>
+ /// <value>The job.</value>
+ public string job { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_path.
+ /// </summary>
+ /// <value>The profile_path.</value>
+ public object profile_path { get; set; }
+ }
+
+ /// <summary>
+ /// Class Country
+ /// </summary>
+ protected class Country
+ {
+ /// <summary>
+ /// Gets or sets the iso_3166_1.
+ /// </summary>
+ /// <value>The iso_3166_1.</value>
+ public string iso_3166_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the certification.
+ /// </summary>
+ /// <value>The certification.</value>
+ public string certification { get; set; }
+ /// <summary>
+ /// Gets or sets the release_date.
+ /// </summary>
+ /// <value>The release_date.</value>
+ public DateTime release_date { get; set; }
+ }
+
+ //protected class TmdbMovieResult
+ //{
+ // public bool adult { get; set; }
+ // public string backdrop_path { get; set; }
+ // public int belongs_to_collection { get; set; }
+ // public int budget { get; set; }
+ // public List<Genre> genres { get; set; }
+ // public string homepage { get; set; }
+ // public int id { get; set; }
+ // public string imdb_id { get; set; }
+ // public string original_title { get; set; }
+ // public string overview { get; set; }
+ // public double popularity { get; set; }
+ // public string poster_path { get; set; }
+ // public List<ProductionCompany> production_companies { get; set; }
+ // public List<ProductionCountry> production_countries { get; set; }
+ // public string release_date { get; set; }
+ // public int revenue { get; set; }
+ // public int runtime { get; set; }
+ // public List<SpokenLanguage> spoken_languages { get; set; }
+ // public string tagline { get; set; }
+ // public string title { get; set; }
+ // public double vote_average { get; set; }
+ // public int vote_count { get; set; }
+ //}
+
+ /// <summary>
+ /// Class TmdbTitle
+ /// </summary>
+ protected class TmdbTitle
+ {
+ /// <summary>
+ /// Gets or sets the iso_3166_1.
+ /// </summary>
+ /// <value>The iso_3166_1.</value>
+ public string iso_3166_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string title { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbAltTitleResults
+ /// </summary>
+ protected class TmdbAltTitleResults
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the titles.
+ /// </summary>
+ /// <value>The titles.</value>
+ public List<TmdbTitle> titles { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbCastResult
+ /// </summary>
+ protected class TmdbCastResult
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the cast.
+ /// </summary>
+ /// <value>The cast.</value>
+ public List<Cast> cast { get; set; }
+ /// <summary>
+ /// Gets or sets the crew.
+ /// </summary>
+ /// <value>The crew.</value>
+ public List<Crew> crew { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbReleasesResult
+ /// </summary>
+ protected class TmdbReleasesResult
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the countries.
+ /// </summary>
+ /// <value>The countries.</value>
+ public List<Country> countries { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbImage
+ /// </summary>
+ protected class TmdbImage
+ {
+ /// <summary>
+ /// Gets or sets the file_path.
+ /// </summary>
+ /// <value>The file_path.</value>
+ public string file_path { get; set; }
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int width { get; set; }
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int height { get; set; }
+ /// <summary>
+ /// Gets or sets the iso_639_1.
+ /// </summary>
+ /// <value>The iso_639_1.</value>
+ public string iso_639_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the aspect_ratio.
+ /// </summary>
+ /// <value>The aspect_ratio.</value>
+ public double aspect_ratio { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_average.
+ /// </summary>
+ /// <value>The vote_average.</value>
+ public double vote_average { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_count.
+ /// </summary>
+ /// <value>The vote_count.</value>
+ public int vote_count { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbImages
+ /// </summary>
+ protected class TmdbImages
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrops.
+ /// </summary>
+ /// <value>The backdrops.</value>
+ public List<TmdbImage> backdrops { get; set; }
+ /// <summary>
+ /// Gets or sets the posters.
+ /// </summary>
+ /// <value>The posters.</value>
+ public List<TmdbImage> posters { get; set; }
+ }
+
+ /// <summary>
+ /// Class CompleteMovieData
+ /// </summary>
+ protected class CompleteMovieData
+ {
+ public bool adult { get; set; }
+ public string backdrop_path { get; set; }
+ public BelongsToCollection belongs_to_collection { get; set; }
+ public int budget { get; set; }
+ public List<Genre> genres { get; set; }
+ public string homepage { get; set; }
+ public int id { get; set; }
+ public string imdb_id { get; set; }
+ public string original_title { get; set; }
+ public string overview { get; set; }
+ public double popularity { get; set; }
+ public string poster_path { get; set; }
+ public List<ProductionCompany> production_companies { get; set; }
+ public List<ProductionCountry> production_countries { get; set; }
+ public DateTime release_date { get; set; }
+ public int revenue { get; set; }
+ public int runtime { get; set; }
+ public List<SpokenLanguage> spoken_languages { get; set; }
+ public string tagline { get; set; }
+ public string title { get; set; }
+ public double vote_average { get; set; }
+ public int vote_count { get; set; }
+ public List<Country> countries { get; set; }
+ public List<Cast> cast { get; set; }
+ public List<Crew> crew { get; set; }
+ }
+
+ public class TmdbImageSettings
+ {
+ public List<string> backdrop_sizes { get; set; }
+ public string base_url { get; set; }
+ public List<string> poster_sizes { get; set; }
+ public List<string> profile_sizes { get; set; }
+ }
+
+ public class TmdbSettingsResult
+ {
+ public TmdbImageSettings images { get; set; }
+ }
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs
new file mode 100644
index 0000000000..ad5f6626be
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class MovieProviderFromJson
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieProviderFromJson : MovieDbProvider
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, LOCAL_META_FILE_NAME));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item.ResolveArgs.ContainsMetaFileByName(ALT_META_FILE_NAME))
+ {
+ return false; // don't read our file if 3rd party data exists
+ }
+
+ if (!item.ResolveArgs.ContainsMetaFileByName(LOCAL_META_FILE_NAME))
+ {
+ return false; // nothing to read
+ }
+
+ // Need to re-override to jump over intermediate implementation
+ return CompareDate(item) > providerInfo.LastRefreshed;
+ }
+
+ /// <summary>
+ /// Fetches the async.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ // Since we don't have anything truly async, and since deserializing can be expensive, create a task to force parallelism
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, LOCAL_META_FILE_NAME));
+ if (entry.HasValue)
+ {
+ // read in our saved meta and pass to processing function
+ var movieData = JsonSerializer.DeserializeFromFile<CompleteMovieData>(entry.Value.Path);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ProcessMainInfo(item, movieData);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
index 7ef53d546a..b87c71df3d 100644
--- a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
@@ -1,43 +1,91 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-using System;
-
-namespace MediaBrowser.Controller.Providers.Movies
-{
- [Export(typeof(BaseMetadataProvider))]
- public class MovieProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Movie;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- protected override DateTime CompareDate(BaseEntity item)
- {
- var entry = item.ResolveArgs.GetFileSystemEntry(Path.Combine(item.Path, "movie.xml"));
- return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFile("movie.xml"))
- {
- new BaseItemXmlParser<Movie>().Fetch(item as Movie, Path.Combine(args.Path, "movie.xml"));
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class MovieProviderFromXml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieProviderFromXml : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Movie || item is BoxSet;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "movie.xml"));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "movie.xml"));
+
+ if (metadataFile.HasValue)
+ {
+ var path = metadataFile.Value.Path;
+ var boxset = item as BoxSet;
+ if (boxset != null)
+ {
+ new BaseItemXmlParser<BoxSet>().Fetch(boxset, path, cancellationToken);
+ }
+ else
+ {
+ new BaseItemXmlParser<Movie>().Fetch((Movie)item, path, cancellationToken);
+ }
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs
deleted file mode 100644
index b6b856d292..0000000000
--- a/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.Movies
-{
- [Export(typeof(BaseMetadataProvider))]
- public class MovieSpecialFeaturesProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Movie;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFolder("specials"))
- {
- var items = new List<Video>();
-
- foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "specials"), "*"))
- {
- var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
-
- if (video != null)
- {
- items.Add(video);
- }
- }
-
- (item as Movie).SpecialFeatures = items;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs b/MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs
new file mode 100644
index 0000000000..19a707be30
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs
@@ -0,0 +1,113 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class PersonProviderFromJson
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class PersonProviderFromJson : TmdbPersonProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Person;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ // Need to re-override to jump over intermediate implementation
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (!item.ResolveArgs.ContainsMetaFileByName(MetaFileName))
+ {
+ return false;
+ }
+
+ return CompareDate(item) > providerInfo.LastRefreshed;
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation,MetaFileName));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get
+ {
+ return MetadataProviderPriority.Third;
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ var personInfo = JsonSerializer.DeserializeFromFile<PersonResult>(Path.Combine(item.MetaLocation, MetaFileName));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ProcessInfo((Person)item, personInfo);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ catch (FileNotFoundException)
+ {
+ // This is okay - just means we force refreshed and there isn't a json file
+ return false;
+ }
+
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs b/MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs
new file mode 100644
index 0000000000..4cdfc58940
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs
@@ -0,0 +1,465 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class TmdbPersonProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class TmdbPersonProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// The meta file name
+ /// </summary>
+ protected const string MetaFileName = "MBPerson.json";
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Person;
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ //we fetch if either info or image needed and haven't already tried recently
+ return (string.IsNullOrEmpty(item.PrimaryImagePath) || !item.ResolveArgs.ContainsMetaFileByName(MetaFileName))
+ && DateTime.Today.Subtract(providerInfo.LastRefreshed).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var person = (Person)item;
+ var tasks = new List<Task>();
+
+ var id = person.GetProviderId(MetadataProviders.Tmdb);
+
+ // We don't already have an Id, need to fetch it
+ if (string.IsNullOrEmpty(id))
+ {
+ id = await GetTmdbId(item, cancellationToken).ConfigureAwait(false);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ //get info only if not already saved
+ if (!item.ResolveArgs.ContainsMetaFileByName(MetaFileName))
+ {
+ tasks.Add(FetchInfo(person, id, cancellationToken));
+ }
+
+ //get image only if not already there
+ if (string.IsNullOrEmpty(item.PrimaryImagePath))
+ {
+ tasks.Add(FetchImages(person, id, cancellationToken));
+ }
+
+ //and wait for them to complete
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+ else
+ {
+ Logger.Debug("TmdbPersonProvider Unable to obtain id for " + item.Name);
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets the TMDB id.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> GetTmdbId(BaseItem person, CancellationToken cancellationToken)
+ {
+ string url = string.Format(@"http://api.themoviedb.org/3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(person.Name), MovieDbProvider.ApiKey);
+ PersonSearchResults searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<PersonSearchResults>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ return searchResult != null && searchResult.Total_Results > 0 ? searchResult.Results[0].Id.ToString() : null;
+ }
+
+ /// <summary>
+ /// Fetches the info.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="id">The id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchInfo(Person person, string id, CancellationToken cancellationToken)
+ {
+ string url = string.Format(@"http://api.themoviedb.org/3/person/{1}?api_key={0}", MovieDbProvider.ApiKey, id);
+ PersonResult searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ if (json != null)
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<PersonResult>(json);
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (searchResult != null && searchResult.Biography != null)
+ {
+ ProcessInfo(person, searchResult);
+
+ //save locally
+ var memoryStream = new MemoryStream();
+
+ JsonSerializer.SerializeToStream(searchResult, memoryStream);
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(person, Path.Combine(person.MetaLocation, MetaFileName), memoryStream, cancellationToken);
+
+ Logger.Debug("TmdbPersonProvider downloaded and saved information for {0}", person.Name);
+ }
+ }
+
+ /// <summary>
+ /// Processes the info.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="searchResult">The search result.</param>
+ protected void ProcessInfo(Person person, PersonResult searchResult)
+ {
+ person.Overview = searchResult.Biography;
+
+ DateTime date;
+
+ if (DateTime.TryParseExact(searchResult.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+ {
+ person.PremiereDate = date;
+ }
+
+ person.SetProviderId(MetadataProviders.Tmdb, searchResult.Id.ToString());
+ }
+
+ /// <summary>
+ /// Fetches the images.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="id">The id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchImages(Person person, string id, CancellationToken cancellationToken)
+ {
+ string url = string.Format(@"http://api.themoviedb.org/3/person/{1}/images?api_key={0}", MovieDbProvider.ApiKey, id);
+
+ PersonImages searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ if (json != null)
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<PersonImages>(json);
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (searchResult != null && searchResult.Profiles.Count > 0)
+ {
+ //get our language
+ var profile =
+ searchResult.Profiles.FirstOrDefault(
+ p =>
+ !string.IsNullOrEmpty(p.Iso_639_1) &&
+ p.Iso_639_1.Equals(Kernel.Instance.Configuration.PreferredMetadataLanguage,
+ StringComparison.OrdinalIgnoreCase));
+ if (profile == null)
+ {
+ //didn't find our language - try first null one
+ profile =
+ searchResult.Profiles.FirstOrDefault(
+ p =>
+ !string.IsNullOrEmpty(p.Iso_639_1) &&
+ p.Iso_639_1.Equals(Kernel.Instance.Configuration.PreferredMetadataLanguage,
+ StringComparison.OrdinalIgnoreCase));
+
+ }
+ if (profile == null)
+ {
+ //still nothing - just get first one
+ profile = searchResult.Profiles[0];
+ }
+ if (profile != null)
+ {
+ var tmdbSettings = await Kernel.Instance.MetadataProviders.OfType<MovieDbProvider>().First().TmdbSettings.ConfigureAwait(false);
+
+ var img = await DownloadAndSaveImage(person, tmdbSettings.images.base_url + Kernel.Instance.Configuration.TmdbFetchedProfileSize + profile.File_Path,
+ "folder" + Path.GetExtension(profile.File_Path), cancellationToken).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(img))
+ {
+ person.PrimaryImagePath = img;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Downloads the and save image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="source">The source.</param>
+ /// <param name="targetName">Name of the target.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, CancellationToken cancellationToken)
+ {
+ if (source == null) return null;
+
+ //download and save locally (if not already there)
+ var localPath = Path.Combine(item.MetaLocation, targetName);
+ if (!item.ResolveArgs.ContainsMetaFileByName(targetName))
+ {
+ using (var sourceStream = await Kernel.Instance.HttpManager.FetchToMemoryStream(source, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(item, localPath, sourceStream, cancellationToken).ConfigureAwait(false);
+
+ Logger.Debug("TmdbPersonProvider downloaded and saved image for {0}", item.Name);
+ }
+ }
+ return localPath;
+ }
+
+ #region Result Objects
+ /// <summary>
+ /// Class PersonSearchResult
+ /// </summary>
+ public class PersonSearchResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool Adult { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_ path.
+ /// </summary>
+ /// <value>The profile_ path.</value>
+ public string Profile_Path { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonSearchResults
+ /// </summary>
+ public class PersonSearchResults
+ {
+ /// <summary>
+ /// Gets or sets the page.
+ /// </summary>
+ /// <value>The page.</value>
+ public int Page { get; set; }
+ /// <summary>
+ /// Gets or sets the results.
+ /// </summary>
+ /// <value>The results.</value>
+ public List<PersonSearchResult> Results { get; set; }
+ /// <summary>
+ /// Gets or sets the total_ pages.
+ /// </summary>
+ /// <value>The total_ pages.</value>
+ public int Total_Pages { get; set; }
+ /// <summary>
+ /// Gets or sets the total_ results.
+ /// </summary>
+ /// <value>The total_ results.</value>
+ public int Total_Results { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonResult
+ /// </summary>
+ public class PersonResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PersonResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool Adult { get; set; }
+ /// <summary>
+ /// Gets or sets the also_ known_ as.
+ /// </summary>
+ /// <value>The also_ known_ as.</value>
+ public List<object> Also_Known_As { get; set; }
+ /// <summary>
+ /// Gets or sets the biography.
+ /// </summary>
+ /// <value>The biography.</value>
+ public string Biography { get; set; }
+ /// <summary>
+ /// Gets or sets the birthday.
+ /// </summary>
+ /// <value>The birthday.</value>
+ public string Birthday { get; set; }
+ /// <summary>
+ /// Gets or sets the deathday.
+ /// </summary>
+ /// <value>The deathday.</value>
+ public string Deathday { get; set; }
+ /// <summary>
+ /// Gets or sets the homepage.
+ /// </summary>
+ /// <value>The homepage.</value>
+ public string Homepage { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the place_ of_ birth.
+ /// </summary>
+ /// <value>The place_ of_ birth.</value>
+ public string Place_Of_Birth { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_ path.
+ /// </summary>
+ /// <value>The profile_ path.</value>
+ public string Profile_Path { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonProfile
+ /// </summary>
+ public class PersonProfile
+ {
+ /// <summary>
+ /// Gets or sets the aspect_ ratio.
+ /// </summary>
+ /// <value>The aspect_ ratio.</value>
+ public double Aspect_Ratio { get; set; }
+ /// <summary>
+ /// Gets or sets the file_ path.
+ /// </summary>
+ /// <value>The file_ path.</value>
+ public string File_Path { get; set; }
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int Height { get; set; }
+ /// <summary>
+ /// Gets or sets the iso_639_1.
+ /// </summary>
+ /// <value>The iso_639_1.</value>
+ public string Iso_639_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int Width { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonImages
+ /// </summary>
+ public class PersonImages
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the profiles.
+ /// </summary>
+ /// <value>The profiles.</value>
+ public List<PersonProfile> Profiles { get; set; }
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ProviderManager.cs b/MediaBrowser.Controller/Providers/ProviderManager.cs
new file mode 100644
index 0000000000..0d5d497e8f
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ProviderManager.cs
@@ -0,0 +1,332 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class ProviderManager
+ /// </summary>
+ public class ProviderManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// The remote image cache
+ /// </summary>
+ private readonly FileSystemRepository _remoteImageCache;
+
+ /// <summary>
+ /// The currently running metadata providers
+ /// </summary>
+ private readonly ConcurrentDictionary<string, Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource>> _currentlyRunningProviders =
+ new ConcurrentDictionary<string, Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource>>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public ProviderManager(Kernel kernel)
+ : base(kernel)
+ {
+ _remoteImageCache = new FileSystemRepository(ImagesDataPath);
+ }
+
+ /// <summary>
+ /// The _images data path
+ /// </summary>
+ private string _imagesDataPath;
+ /// <summary>
+ /// Gets the images data path.
+ /// </summary>
+ /// <value>The images data path.</value>
+ public string ImagesDataPath
+ {
+ get
+ {
+ if (_imagesDataPath == null)
+ {
+ _imagesDataPath = Path.Combine(Kernel.ApplicationPaths.DataPath, "remote-images");
+
+ if (!Directory.Exists(_imagesDataPath))
+ {
+ Directory.CreateDirectory(_imagesDataPath);
+ }
+ }
+
+ return _imagesDataPath;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the supported providers key.
+ /// </summary>
+ /// <value>The supported providers key.</value>
+ private Guid SupportedProvidersKey { get; set; }
+
+ /// <summary>
+ /// Runs all metadata providers for an entity, and returns true or false indicating if at least one was refreshed and requires persistence
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ internal async Task<bool> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true)
+ {
+ // Allow providers of the same priority to execute in parallel
+ MetadataProviderPriority? currentPriority = null;
+ var currentTasks = new List<Task<bool>>();
+
+ var result = false;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Determine if supported providers have changed
+ var supportedProviders = Kernel.MetadataProviders.Where(p => p.Supports(item)).ToList();
+
+ BaseProviderInfo supportedProvidersInfo;
+
+ if (SupportedProvidersKey == Guid.Empty)
+ {
+ SupportedProvidersKey = "SupportedProviders".GetMD5();
+ }
+
+ var supportedProvidersHash = string.Join("+", supportedProviders.Select(i => i.GetType().Name)).GetMD5();
+ bool providersChanged;
+
+ item.ProviderData.TryGetValue(SupportedProvidersKey, out supportedProvidersInfo);
+ if (supportedProvidersInfo == null)
+ {
+ // First time
+ supportedProvidersInfo = new BaseProviderInfo { ProviderId = SupportedProvidersKey, FileSystemStamp = supportedProvidersHash };
+ providersChanged = force = true;
+ }
+ else
+ {
+ // Force refresh if the supported providers have changed
+ providersChanged = force = force || supportedProvidersInfo.FileSystemStamp != supportedProvidersHash;
+ }
+
+ // If providers have changed, clear provider info and update the supported providers hash
+ if (providersChanged)
+ {
+ Logger.Debug("Providers changed for {0}. Clearing and forcing refresh.", item.Name);
+ item.ProviderData.Clear();
+ supportedProvidersInfo.FileSystemStamp = supportedProvidersHash;
+ }
+
+ if (force) item.ClearMetaValues();
+
+ // Run the normal providers sequentially in order of priority
+ foreach (var provider in supportedProviders)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Skip if internet providers are currently disabled
+ if (provider.RequiresInternet && !Kernel.Configuration.EnableInternetProviders)
+ {
+ continue;
+ }
+
+ // Skip if is slow and we aren't allowing slow ones
+ if (provider.IsSlow && !allowSlowProviders)
+ {
+ continue;
+ }
+
+ // Skip if internet provider and this type is not allowed
+ if (provider.RequiresInternet && Kernel.Configuration.EnableInternetProviders && Kernel.Configuration.InternetProviderExcludeTypes.Contains(item.GetType().Name, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // When a new priority is reached, await the ones that are currently running and clear the list
+ if (currentPriority.HasValue && currentPriority.Value != provider.Priority && currentTasks.Count > 0)
+ {
+ var results = await Task.WhenAll(currentTasks).ConfigureAwait(false);
+ result |= results.Contains(true);
+
+ currentTasks.Clear();
+ }
+
+ // Put this check below the await because the needs refresh of the next tier of providers may depend on the previous ones running
+ // This is the case for the fan art provider which depends on the movie and tv providers having run before them
+ if (!force && !provider.NeedsRefresh(item))
+ {
+ continue;
+ }
+
+ currentTasks.Add(provider.FetchAsync(item, force, cancellationToken));
+ currentPriority = provider.Priority;
+ }
+
+ if (currentTasks.Count > 0)
+ {
+ var results = await Task.WhenAll(currentTasks).ConfigureAwait(false);
+ result |= results.Contains(true);
+ }
+
+ if (providersChanged)
+ {
+ item.ProviderData[SupportedProvidersKey] = supportedProvidersInfo;
+ }
+
+ return result || providersChanged;
+ }
+
+ /// <summary>
+ /// Notifies the kernal that a provider has begun refreshing
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
+ internal void OnProviderRefreshBeginning(BaseMetadataProvider provider, BaseItem item, CancellationTokenSource cancellationTokenSource)
+ {
+ var key = item.Id + provider.GetType().Name;
+
+ Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource> current;
+
+ if (_currentlyRunningProviders.TryGetValue(key, out current))
+ {
+ try
+ {
+ current.Item3.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+
+ }
+ }
+
+ var tuple = new Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource>(provider, item, cancellationTokenSource);
+
+ _currentlyRunningProviders.AddOrUpdate(key, tuple, (k, v) => tuple);
+ }
+
+ /// <summary>
+ /// Notifies the kernal that a provider has completed refreshing
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="item">The item.</param>
+ internal void OnProviderRefreshCompleted(BaseMetadataProvider provider, BaseItem item)
+ {
+ var key = item.Id + provider.GetType().Name;
+
+ Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource> current;
+
+ if (_currentlyRunningProviders.TryRemove(key, out current))
+ {
+ current.Item3.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Validates the currently running providers and cancels any that should not be run due to configuration changes
+ /// </summary>
+ internal void ValidateCurrentlyRunningProviders()
+ {
+ Logger.Info("Validing currently running providers");
+
+ var enableInternetProviders = Kernel.Configuration.EnableInternetProviders;
+ var internetProviderExcludeTypes = Kernel.Configuration.InternetProviderExcludeTypes;
+
+ foreach (var tuple in _currentlyRunningProviders.Values
+ .Where(p => p.Item1.RequiresInternet && (!enableInternetProviders || internetProviderExcludeTypes.Contains(p.Item2.GetType().Name, StringComparer.OrdinalIgnoreCase)))
+ .ToList())
+ {
+ tuple.Item3.Cancel();
+ }
+ }
+
+ /// <summary>
+ /// Downloads the and save image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="source">The source.</param>
+ /// <param name="targetName">Name of the target.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (string.IsNullOrEmpty(source))
+ {
+ throw new ArgumentNullException("source");
+ }
+ if (string.IsNullOrEmpty(targetName))
+ {
+ throw new ArgumentNullException("targetName");
+ }
+ if (resourcePool == null)
+ {
+ throw new ArgumentNullException("resourcePool");
+ }
+
+ //download and save locally
+ var localPath = Kernel.Configuration.SaveLocalMeta ?
+ Path.Combine(item.MetaLocation, targetName) :
+ _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Path.ToLower(), targetName);
+
+ var img = await Kernel.HttpManager.FetchToMemoryStream(source, resourcePool, cancellationToken).ConfigureAwait(false);
+
+ if (Kernel.Configuration.SaveLocalMeta) // queue to media directories
+ {
+ await Kernel.FileSystemManager.SaveToLibraryFilesystem(item, localPath, img, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // we can write directly here because it won't affect the watchers
+
+ try
+ {
+ using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await img.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ Logger.ErrorException("Error downloading and saving image " + localPath, e);
+ throw;
+ }
+ finally
+ {
+ img.Dispose();
+ }
+
+ }
+ return localPath;
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ _remoteImageCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/SortNameProvider.cs b/MediaBrowser.Controller/Providers/SortNameProvider.cs
new file mode 100644
index 0000000000..071732f3e3
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/SortNameProvider.cs
@@ -0,0 +1,129 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class SortNameProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class SortNameProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return true;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Last; }
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ return !string.IsNullOrEmpty(item.Name) && string.IsNullOrEmpty(item.SortName);
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return SetSortName(item, cancellationToken) ? TrueTaskResult : FalseTaskResult;
+ }
+
+ /// <summary>
+ /// Sets the name of the sort.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected bool SetSortName(BaseItem item, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(item.SortName)) return false; //let the earlier provider win
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (item is Episode)
+ {
+ //special handling for TV episodes season and episode number
+ item.SortName = (item.ParentIndexNumber != null ? item.ParentIndexNumber.Value.ToString("000-") : "")
+ + (item.IndexNumber != null ? item.IndexNumber.Value.ToString("0000 - ") : "") + item.Name;
+
+ }
+ else if (item is Season)
+ {
+ //sort seasons by season number - numerically
+ item.SortName = item.IndexNumber != null ? item.IndexNumber.Value.ToString("0000") : item.Name;
+ }
+ else if (item is Audio)
+ {
+ //sort tracks by production year and index no so they will sort in order if in a multi-album list
+ item.SortName = (item.ProductionYear != null ? item.ProductionYear.Value.ToString("000-") : "")
+ + (item.IndexNumber != null ? item.IndexNumber.Value.ToString("0000 - ") : "") + item.Name;
+ }
+ else if (item is MusicAlbum)
+ {
+ //sort albums by year
+ item.SortName = item.ProductionYear != null ? item.ProductionYear.Value.ToString("0000") : item.Name;
+ }
+ else
+ {
+ if (item.Name == null) return false; //some items may not have name filled in properly
+
+ var sortable = item.Name.Trim().ToLower();
+ sortable = Kernel.Instance.Configuration.SortRemoveCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), string.Empty));
+
+ sortable = Kernel.Instance.Configuration.SortReplaceCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), " "));
+
+ foreach (var search in Kernel.Instance.Configuration.SortRemoveWords)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var searchLower = search.ToLower();
+ // Remove from beginning if a space follows
+ if (sortable.StartsWith(searchLower + " "))
+ {
+ sortable = sortable.Remove(0, searchLower.Length + 1);
+ }
+ // Remove from middle if surrounded by spaces
+ sortable = sortable.Replace(" " + searchLower + " ", " ");
+
+ // Remove from end if followed by a space
+ if (sortable.EndsWith(" " + searchLower))
+ {
+ sortable = sortable.Remove(sortable.Length - (searchLower.Length + 1));
+ }
+ }
+ item.SortName = sortable;
+ }
+
+ return true;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
index 0b9cf85ebe..a493ce746f 100644
--- a/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
@@ -1,67 +1,127 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- [Export(typeof(BaseMetadataProvider))]
- public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Episode;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- return Task.Run(() =>
- {
- var episode = item as Episode;
-
- string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
-
- string episodeFileName = Path.GetFileName(episode.Path);
-
- var season = args.Parent as Season;
-
- SetPrimaryImagePath(episode, season, metadataFolder, episodeFileName);
- });
- }
-
- private void SetPrimaryImagePath(Episode item, Season season, string metadataFolder, string episodeFileName)
- {
- // Look for the image file in the metadata folder, and if found, set PrimaryImagePath
- var imageFiles = new string[] {
- Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".jpg")),
- Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".png"))
- };
-
- string image;
-
- if (season == null)
- {
- // Epsiode directly in Series folder. Gotta do this the slow way
- image = imageFiles.FirstOrDefault(f => File.Exists(f));
- }
- else
- {
- image = imageFiles.FirstOrDefault(f => season.ContainsMetadataFile(f));
- }
-
- // If we found something, set PrimaryImagePath
- if (!string.IsNullOrEmpty(image))
- {
- item.PrimaryImagePath = image;
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class EpisodeImageFromMediaLocationProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Episode && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <summary>
+ /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
+ /// </summary>
+ /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var episode = (Episode)item;
+
+ var episodeFileName = Path.GetFileName(episode.Path);
+
+ var parent = item.ResolveArgs.Parent;
+
+ ValidateImage(episode, item.MetaLocation);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SetPrimaryImagePath(episode, parent, item.MetaLocation, episodeFileName);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Validates the primary image path still exists
+ /// </summary>
+ /// <param name="episode">The episode.</param>
+ /// <param name="metadataFolderPath">The metadata folder path.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private void ValidateImage(Episode episode, string metadataFolderPath)
+ {
+ var path = episode.PrimaryImagePath;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return;
+ }
+
+ // Only validate images in the season/metadata folder
+ if (!string.Equals(Path.GetDirectoryName(path), metadataFolderPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ if (!episode.Parent.ResolveArgs.GetMetaFileByPath(path).HasValue)
+ {
+ episode.PrimaryImagePath = null;
+ }
+ }
+
+ /// <summary>
+ /// Sets the primary image path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="metadataFolder">The metadata folder.</param>
+ /// <param name="episodeFileName">Name of the episode file.</param>
+ private void SetPrimaryImagePath(Episode item, Folder parent, string metadataFolder, string episodeFileName)
+ {
+ // Look for the image file in the metadata folder, and if found, set PrimaryImagePath
+ var imageFiles = new[] {
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".jpg")),
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".png"))
+ };
+
+ var file = parent.ResolveArgs.GetMetaFileByPath(imageFiles[0]) ??
+ parent.ResolveArgs.GetMetaFileByPath(imageFiles[1]);
+
+ if (file.HasValue)
+ {
+ item.PrimaryImagePath = file.Value.Path;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
index f3c19a7048..df46b7167f 100644
--- a/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
@@ -1,59 +1,122 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- [Export(typeof(BaseMetadataProvider))]
- public class EpisodeProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Episode;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
-
- string metadataFile = Path.Combine(metadataFolder, Path.ChangeExtension(Path.GetFileName(args.Path), ".xml"));
-
- FetchMetadata(item as Episode, args.Parent as Season, metadataFile);
- }
-
- private void FetchMetadata(Episode item, Season season, string metadataFile)
- {
- if (season == null)
- {
- // Episode directly in Series folder
- // Need to validate it the slow way
- if (!File.Exists(metadataFile))
- {
- return;
- }
- }
- else
- {
- if (!season.ContainsMetadataFile(metadataFile))
- {
- return;
- }
- }
-
- new EpisodeXmlParser().Fetch(item, metadataFile);
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class EpisodeProviderFromXml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeProviderFromXml : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Episode && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml"));
+
+ var file = item.ResolveArgs.Parent.ResolveArgs.GetMetaFileByPath(metadataFile);
+
+ if (!file.HasValue)
+ {
+ return base.CompareDate(item);
+ }
+
+ return file.Value.LastWriteTimeUtc;
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml"));
+
+ var episode = (Episode)item;
+
+ if (!FetchMetadata(episode, item.ResolveArgs.Parent, metadataFile, cancellationToken))
+ {
+ // Don't set last refreshed if we didn't do anything
+ return false;
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ /// <summary>
+ /// Fetches the metadata.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="metadataFile">The metadata file.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool FetchMetadata(Episode item, Folder parent, string metadataFile, CancellationToken cancellationToken)
+ {
+ var file = parent.ResolveArgs.GetMetaFileByPath(metadataFile);
+
+ if (!file.HasValue)
+ {
+ return false;
+ }
+
+ new EpisodeXmlParser().Fetch(item, metadataFile, cancellationToken);
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
index 1cb604a51c..7133d87451 100644
--- a/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
@@ -1,60 +1,104 @@
-using MediaBrowser.Controller.Entities.TV;
-using System.IO;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- public class EpisodeXmlParser : BaseItemXmlParser<Episode>
- {
- protected override void FetchDataFromXmlNode(XmlReader reader, Episode item)
- {
- switch (reader.Name)
- {
- case "filename":
- {
- string filename = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(filename))
- {
- // Strip off everything but the filename. Some metadata tools like MetaBrowser v1.0 will have an 'episodes' prefix
- // even though it's actually using the metadata folder.
- filename = Path.GetFileName(filename);
-
- string seasonFolder = Path.GetDirectoryName(item.Path);
- item.PrimaryImagePath = Path.Combine(seasonFolder, "metadata", filename);
- }
- break;
- }
- case "SeasonNumber":
- {
- string number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- item.ParentIndexNumber = int.Parse(number);
- }
- break;
- }
-
- case "EpisodeNumber":
- {
- string number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- item.IndexNumber = int.Parse(number);
- }
- break;
- }
-
- case "EpisodeName":
- item.Name = reader.ReadElementContentAsString();
- break;
-
- default:
- base.FetchDataFromXmlNode(reader, item);
- break;
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities.TV;
+using System.IO;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class EpisodeXmlParser
+ /// </summary>
+ public class EpisodeXmlParser : BaseItemXmlParser<Episode>
+ {
+ /// <summary>
+ /// Fetches the data from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ protected override void FetchDataFromXmlNode(XmlReader reader, Episode item)
+ {
+ switch (reader.Name)
+ {
+ case "Episode":
+ //MB generated metadata is within an "Episode" node
+ using (var subTree = reader.ReadSubtree())
+ {
+ subTree.MoveToContent();
+
+ // Loop through each element
+ while (subTree.Read())
+ {
+ if (subTree.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(subTree, item);
+ }
+ }
+
+ }
+ break;
+
+ case "filename":
+ {
+ string filename = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(filename))
+ {
+ // Strip off everything but the filename. Some metadata tools like MetaBrowser v1.0 will have an 'episodes' prefix
+ // even though it's actually using the metadata folder.
+ filename = Path.GetFileName(filename);
+
+ string seasonFolder = Path.GetDirectoryName(item.Path);
+ item.PrimaryImagePath = Path.Combine(seasonFolder, "metadata", filename);
+ }
+ break;
+ }
+ case "SeasonNumber":
+ {
+ var number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ int num;
+
+ if (int.TryParse(number, out num))
+ {
+ item.ParentIndexNumber = num;
+ }
+ }
+ break;
+ }
+
+ case "EpisodeNumber":
+ {
+ var number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ int num;
+
+ if (int.TryParse(number, out num))
+ {
+ item.IndexNumber = num;
+ }
+ }
+ break;
+ }
+
+ case "EpisodeName":
+ {
+ var name = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ item.Name = name;
+ }
+ break;
+ }
+
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs b/MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs
new file mode 100644
index 0000000000..2640e04825
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs
@@ -0,0 +1,140 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ class FanArtTVProvider : FanartBaseProvider
+ {
+ protected string FanArtBaseUrl = "http://api.fanart.tv/webservice/series/{0}/{1}/xml/all/1/1";
+
+ public override bool Supports(BaseItem item)
+ {
+ return item is Series;
+ }
+
+ protected override bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item.DontFetchMeta || string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb))) return false; //nothing to do
+ var artExists = item.ResolveArgs.ContainsMetaFileByName(ART_FILE);
+ var logoExists = item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE);
+ var thumbExists = item.ResolveArgs.ContainsMetaFileByName(THUMB_FILE);
+
+
+ return (!artExists && Kernel.Instance.Configuration.DownloadTVArt)
+ || (!logoExists && Kernel.Instance.Configuration.DownloadTVLogo)
+ || (!thumbExists && Kernel.Instance.Configuration.DownloadTVThumb);
+ }
+
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var series = (Series)item;
+ if (ShouldFetch(series, series.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id })))
+ {
+ string language = Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower();
+ string url = string.Format(FanArtBaseUrl, APIKey, series.GetProviderId(MetadataProviders.Tvdb));
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var xml = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(xml);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (doc.HasChildNodes)
+ {
+ string path;
+ if (Kernel.Instance.Configuration.DownloadTVLogo && !series.ResolveArgs.ContainsMetaFileByName(LOGO_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/series/clearlogos/clearlogo[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/series/clearlogos/clearlogo/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearLogo for " + series.Name);
+ try
+ {
+ series.SetImage(ImageType.Logo, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, path, LOGO_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadTVArt && !series.ResolveArgs.ContainsMetaFileByName(ART_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/series/cleararts/clearart[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/series/cleararts/clearart/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearArt for " + series.Name);
+ try
+ {
+ series.SetImage(ImageType.Art, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, path, ART_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadTVThumb && !series.ResolveArgs.ContainsMetaFileByName(THUMB_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/series/tvthumbs/tvthumb[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/series/tvthumbs/tvthumb/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ThumbArt for " + series.Name);
+ try
+ {
+ series.SetImage(ImageType.Disc, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, path, THUMB_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+ SetLastRefreshed(series, DateTime.UtcNow);
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs
new file mode 100644
index 0000000000..dbe744ed40
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs
@@ -0,0 +1,288 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+
+ /// <summary>
+ /// Class RemoteEpisodeProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class RemoteEpisodeProvider : BaseMetadataProvider
+ {
+
+ /// <summary>
+ /// The episode query
+ /// </summary>
+ private const string episodeQuery = "http://www.thetvdb.com/api/{0}/series/{1}/default/{2}/{3}/{4}.xml";
+ /// <summary>
+ /// The abs episode query
+ /// </summary>
+ private const string absEpisodeQuery = "http://www.thetvdb.com/api/{0}/series/{1}/absolute/{2}/{3}.xml";
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Episode;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get { return true; }
+ }
+
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ bool fetch = false;
+ var episode = (Episode)item;
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ {
+ return false;
+ }
+
+ if (!item.DontFetchMeta && !HasLocalMeta(episode))
+ {
+ fetch = Kernel.Instance.Configuration.MetadataRefreshDays != -1 &&
+ DateTime.Today.Subtract(downloadDate).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays;
+ }
+
+ return fetch;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var episode = (Episode)item;
+ if (!item.DontFetchMeta && !HasLocalMeta(episode))
+ {
+ var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+ if (seriesId != null)
+ {
+ await FetchEpisodeData(episode, seriesId, cancellationToken).ConfigureAwait(false);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ Logger.Info("Episode provider cannot determine Series Id for " + item.Path);
+ return false;
+ }
+ Logger.Info("Episode provider not fetching because local meta exists or requested to ignore: " + item.Name);
+ return false;
+ }
+
+
+ /// <summary>
+ /// Fetches the episode data.
+ /// </summary>
+ /// <param name="episode">The episode.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> FetchEpisodeData(Episode episode, string seriesId, CancellationToken cancellationToken)
+ {
+
+ string name = episode.Name;
+ string location = episode.Path;
+
+ Logger.Debug("TvDbProvider: Fetching episode data for: " + name);
+ string epNum = TVUtils.EpisodeNumberFromFile(location, episode.Season != null);
+
+ if (epNum == null)
+ {
+ Logger.Warn("TvDbProvider: Could not determine episode number for: " + episode.Path);
+ return false;
+ }
+
+ var episodeNumber = Int32.Parse(epNum);
+
+ episode.IndexNumber = episodeNumber;
+ var usingAbsoluteData = false;
+
+ if (string.IsNullOrEmpty(seriesId)) return false;
+
+ var seasonNumber = "";
+ if (episode.Parent is Season)
+ {
+ seasonNumber = episode.Parent.IndexNumber.ToString();
+ }
+
+ if (string.IsNullOrEmpty(seasonNumber))
+ seasonNumber = TVUtils.SeasonNumberFromEpisodeFile(location); // try and extract the season number from the file name for S1E1, 1x04 etc.
+
+ if (!string.IsNullOrEmpty(seasonNumber))
+ {
+ seasonNumber = seasonNumber.TrimStart('0');
+
+ if (string.IsNullOrEmpty(seasonNumber))
+ {
+ seasonNumber = "0"; // Specials
+ }
+
+ var url = string.Format(episodeQuery, TVUtils.TVDBApiKey, seriesId, seasonNumber, episodeNumber, Kernel.Instance.Configuration.PreferredMetadataLanguage);
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var result = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(result);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ //episode does not exist under this season, try absolute numbering.
+ //still assuming it's numbered as 1x01
+ //this is basicly just for anime.
+ if (!doc.HasChildNodes && Int32.Parse(seasonNumber) == 1)
+ {
+ url = string.Format(absEpisodeQuery, TVUtils.TVDBApiKey, seriesId, episodeNumber, Kernel.Instance.Configuration.PreferredMetadataLanguage);
+
+ try
+ {
+ using (var result = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ if (result != null) doc.Load(result);
+ usingAbsoluteData = true;
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+
+ if (doc.HasChildNodes)
+ {
+ var p = doc.SafeGetString("//filename");
+ if (p != null)
+ {
+ if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
+
+ try
+ {
+ episode.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), Kernel.Instance.ResourcePools.TvDb, cancellationToken);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+
+ episode.Overview = doc.SafeGetString("//Overview");
+ if (usingAbsoluteData)
+ episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1);
+ if (episode.IndexNumber < 0)
+ episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber");
+
+ episode.Name = episode.IndexNumber.Value.ToString("000") + " - " + doc.SafeGetString("//EpisodeName");
+ episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10);
+ var firstAired = doc.SafeGetString("//FirstAired");
+ DateTime airDate;
+ if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+ {
+ episode.PremiereDate = airDate.ToUniversalTime();
+ episode.ProductionYear = airDate.Year;
+ }
+
+ var actors = doc.SafeGetString("//GuestStars");
+ if (actors != null)
+ {
+ episode.AddPeople(actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = "Actor", Name = str }));
+ }
+
+
+ var directors = doc.SafeGetString("//Director");
+ if (directors != null)
+ {
+ episode.AddPeople(directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = "Director", Name = str }));
+ }
+
+
+ var writers = doc.SafeGetString("//Writer");
+ if (writers != null)
+ {
+ episode.AddPeople(writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = "Writer", Name = str }));
+ }
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
+ var ms = new MemoryStream();
+ doc.Save(ms);
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false);
+ }
+
+ return true;
+ }
+
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified episode].
+ /// </summary>
+ /// <param name="episode">The episode.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified episode]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(Episode episode)
+ {
+ return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml"));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs
new file mode 100644
index 0000000000..0d6cc41b8d
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs
@@ -0,0 +1,287 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class RemoteSeasonProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class RemoteSeasonProvider : BaseMetadataProvider
+ {
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Season;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ bool fetch = false;
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ return false;
+
+ if (!HasLocalMeta(item))
+ {
+ fetch = Kernel.Instance.Configuration.MetadataRefreshDays != -1 &&
+ DateTime.UtcNow.Subtract(downloadDate).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays;
+ }
+
+ return fetch;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var season = (Season)item;
+
+ if (!HasLocalMeta(item))
+ {
+ var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+ if (seriesId != null)
+ {
+ await FetchSeasonData(season, seriesId, cancellationToken).ConfigureAwait(false);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ Logger.Info("Season provider unable to obtain series id for {0}", item.Path);
+ }
+ else
+ {
+ Logger.Info("Season provider not fetching because local meta exists: " + season.Name);
+ }
+ return false;
+ }
+
+
+ /// <summary>
+ /// Fetches the season data.
+ /// </summary>
+ /// <param name="season">The season.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> FetchSeasonData(Season season, string seriesId, CancellationToken cancellationToken)
+ {
+ string name = season.Name;
+
+ Logger.Debug("TvDbProvider: Fetching season data: " + name);
+ var seasonNumber = TVUtils.GetSeasonNumberFromPath(season.Path) ?? -1;
+
+ season.IndexNumber = seasonNumber;
+
+ if (seasonNumber == 0)
+ {
+ season.Name = "Specials";
+ }
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+ if ((season.PrimaryImagePath == null) || (!season.HasImage(ImageType.Banner)) || (season.BackdropImagePaths == null))
+ {
+ var images = new XmlDocument();
+ var url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TVDBApiKey + "/series/{0}/banners.xml", seriesId);
+
+ try
+ {
+ using (var imgs = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ images.Load(imgs);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (images.HasChildNodes)
+ {
+ if (Kernel.Instance.Configuration.RefreshItemImages || !season.HasLocalImage("folder"))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+
+ try
+ {
+ if (n != null)
+ season.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ if (Kernel.Instance.Configuration.DownloadTVSeasonBanner && (Kernel.Instance.Configuration.RefreshItemImages || !season.HasLocalImage("banner")))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ var bannerImagePath =
+ await
+ Kernel.Instance.ProviderManager.DownloadAndSaveImage(season,
+ TVUtils.BannerUrl + n.InnerText,
+ "banner" +
+ Path.GetExtension(n.InnerText),
+ Kernel.Instance.ResourcePools.TvDb, cancellationToken).
+ ConfigureAwait(false);
+
+ season.SetImage(ImageType.Banner, bannerImagePath);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+
+ if (Kernel.Instance.Configuration.DownloadTVSeasonBackdrops && (Kernel.Instance.Configuration.RefreshItemImages || !season.HasLocalImage("backdrop")))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List<string>();
+ season.BackdropImagePaths.Add(await Kernel.Instance.ProviderManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ else if (!Kernel.Instance.Configuration.SaveLocalMeta) //if saving local - season will inherit from series
+ {
+ // not necessarily accurate but will give a different bit of art to each season
+ var lst = images.SelectNodes("//Banner[BannerType='fanart']");
+ if (lst != null && lst.Count > 0)
+ {
+ var num = seasonNumber % lst.Count;
+ n = lst[num];
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ if (season.BackdropImagePaths == null)
+ season.BackdropImagePaths = new List<string>();
+
+ try
+ {
+ season.BackdropImagePaths.Add(
+ await
+ Kernel.Instance.ProviderManager.DownloadAndSaveImage(season,
+ TVUtils.BannerUrl +
+ n.InnerText,
+ "backdrop" +
+ Path.GetExtension(
+ n.InnerText),
+ Kernel.Instance.ResourcePools.TvDb, cancellationToken)
+ .ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(BaseItem item)
+ {
+ //just folder.jpg/png
+ return (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") ||
+ item.ResolveArgs.ContainsMetaFileByName("folder.png"));
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs
new file mode 100644
index 0000000000..901d390407
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs
@@ -0,0 +1,545 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class RemoteSeriesProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class RemoteSeriesProvider : BaseMetadataProvider
+ {
+
+ /// <summary>
+ /// The root URL
+ /// </summary>
+ private const string rootUrl = "http://www.thetvdb.com/api/";
+ /// <summary>
+ /// The series query
+ /// </summary>
+ private const string seriesQuery = "GetSeries.php?seriesname={0}";
+ /// <summary>
+ /// The series get
+ /// </summary>
+ private const string seriesGet = "http://www.thetvdb.com/api/{0}/series/{1}/{2}.xml";
+ /// <summary>
+ /// The get actors
+ /// </summary>
+ private const string getActors = "http://www.thetvdb.com/api/{0}/series/{1}/actors.xml";
+
+ /// <summary>
+ /// The LOCA l_ MET a_ FIL e_ NAME
+ /// </summary>
+ protected const string LOCAL_META_FILE_NAME = "Series.xml";
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Series;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public override bool RequiresInternet
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Needses the refresh internal.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ {
+ return false;
+ }
+
+ if (item.DontFetchMeta) return false;
+
+ return !HasLocalMeta(item) && (Kernel.Instance.Configuration.MetadataRefreshDays != -1 &&
+ DateTime.UtcNow.Subtract(downloadDate).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays);
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var series = (Series)item;
+ if (!item.DontFetchMeta && !HasLocalMeta(series))
+ {
+ var path = item.Path ?? "";
+ var seriesId = Path.GetFileName(path).GetAttributeValue("tvdbid") ?? await GetSeriesId(series, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+ series.SetProviderId(MetadataProviders.Tvdb, seriesId);
+ if (!HasCompleteMetadata(series))
+ {
+ await FetchSeriesData(series, seriesId, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ Logger.Info("Series provider not fetching because local meta exists or requested to ignore: " + item.Name);
+ return false;
+
+ }
+
+ /// <summary>
+ /// Fetches the series data.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken)
+ {
+ var success = false;
+
+ var name = series.Name;
+ Logger.Debug("TvDbProvider: Fetching series data: " + name);
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+
+ string url = string.Format(seriesGet, TVUtils.TVDBApiKey, seriesId, Kernel.Instance.Configuration.PreferredMetadataLanguage);
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var xml = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(xml);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (doc.HasChildNodes)
+ {
+ //kick off the actor and image fetch simultaneously
+ var actorTask = FetchActors(series, seriesId, doc, cancellationToken);
+ var imageTask = FetchImages(series, seriesId, cancellationToken);
+
+ success = true;
+
+ series.Name = doc.SafeGetString("//SeriesName");
+ series.Overview = doc.SafeGetString("//Overview");
+ series.CommunityRating = doc.SafeGetSingle("//Rating", 0, 10);
+ series.AirDays = TVUtils.GetAirDays(doc.SafeGetString("//Airs_DayOfWeek"));
+ series.AirTime = doc.SafeGetString("//Airs_Time");
+
+ string n = doc.SafeGetString("//banner");
+ if (!string.IsNullOrWhiteSpace(n))
+ {
+ series.SetImage(ImageType.Banner, TVUtils.BannerUrl + n);
+ }
+
+ string s = doc.SafeGetString("//Network");
+ if (!string.IsNullOrWhiteSpace(s))
+ series.AddStudios(new List<string>(s.Trim().Split('|')));
+
+ series.OfficialRating = doc.SafeGetString("//ContentRating");
+
+ string g = doc.SafeGetString("//Genre");
+
+ if (g != null)
+ {
+ string[] genres = g.Trim('|').Split('|');
+ if (g.Length > 0)
+ {
+ series.AddGenres(genres);
+ }
+ }
+
+ //wait for other tasks
+ await Task.WhenAll(actorTask, imageTask).ConfigureAwait(false);
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ var ms = new MemoryStream();
+ doc.Save(ms);
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LOCAL_META_FILE_NAME), ms, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+
+
+ return success;
+ }
+
+ /// <summary>
+ /// Fetches the actors.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="doc">The doc.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchActors(Series series, string seriesId, XmlDocument doc, CancellationToken cancellationToken)
+ {
+ string urlActors = string.Format(getActors, TVUtils.TVDBApiKey, seriesId);
+ var docActors = new XmlDocument();
+
+ try
+ {
+ using (var actors = await Kernel.Instance.HttpManager.Get(urlActors, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ docActors.Load(actors);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (docActors.HasChildNodes)
+ {
+ XmlNode actorsNode = null;
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ //add to the main doc for saving
+ var seriesNode = doc.SelectSingleNode("//Series");
+ if (seriesNode != null)
+ {
+ actorsNode = doc.CreateNode(XmlNodeType.Element, "Persons", null);
+ seriesNode.AppendChild(actorsNode);
+ }
+ }
+
+ var xmlNodeList = docActors.SelectNodes("Actors/Actor");
+ if (xmlNodeList != null)
+ foreach (XmlNode p in xmlNodeList)
+ {
+ string actorName = p.SafeGetString("Name");
+ string actorRole = p.SafeGetString("Role");
+ if (!string.IsNullOrWhiteSpace(actorName))
+ {
+ series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName, Role = actorRole });
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta && actorsNode != null)
+ {
+ //create in main doc
+ var personNode = doc.CreateNode(XmlNodeType.Element, "Person", null);
+ foreach (XmlNode subNode in p.ChildNodes)
+ personNode.AppendChild(doc.ImportNode(subNode, true));
+ //need to add the type
+ var typeNode = doc.CreateNode(XmlNodeType.Element, "Type", null);
+ typeNode.InnerText = "Actor";
+ personNode.AppendChild(typeNode);
+ actorsNode.AppendChild(personNode);
+ }
+
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches the images.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchImages(Series series, string seriesId, CancellationToken cancellationToken)
+ {
+ if ((!string.IsNullOrEmpty(seriesId)) && ((series.PrimaryImagePath == null) || (series.BackdropImagePaths == null)))
+ {
+ string url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TVDBApiKey + "/series/{0}/banners.xml", seriesId);
+ var images = new XmlDocument();
+
+ try
+ {
+ using (var imgs = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ images.Load(imgs);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (images.HasChildNodes)
+ {
+ if (Kernel.Instance.Configuration.RefreshItemImages || !series.HasLocalImage("folder"))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='poster']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ series.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+
+ if (Kernel.Instance.Configuration.DownloadTVBanner && (Kernel.Instance.Configuration.RefreshItemImages || !series.HasLocalImage("banner")))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='series']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ var bannerImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken);
+
+ series.SetImage(ImageType.Banner, bannerImagePath);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+
+ var bdNo = 0;
+ var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']");
+ if (xmlNodeList != null)
+ foreach (XmlNode b in xmlNodeList)
+ {
+ series.BackdropImagePaths = new List<string>();
+ var p = b.SelectSingleNode("./BannerPath");
+ if (p != null)
+ {
+ var bdName = "backdrop" + (bdNo > 0 ? bdNo.ToString() : "");
+ if (Kernel.Instance.Configuration.RefreshItemImages || !series.HasLocalImage(bdName))
+ {
+ try
+ {
+ series.BackdropImagePaths.Add(await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ bdNo++;
+ if (bdNo >= Kernel.Instance.Configuration.MaxBackdrops) break;
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Determines whether [has complete metadata] [the specified series].
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <returns><c>true</c> if [has complete metadata] [the specified series]; otherwise, <c>false</c>.</returns>
+ private bool HasCompleteMetadata(Series series)
+ {
+ return (series.HasImage(ImageType.Banner)) && (series.CommunityRating != null)
+ && (series.Overview != null) && (series.Name != null) && (series.People != null)
+ && (series.Genres != null) && (series.OfficialRating != null);
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(BaseItem item)
+ {
+ //need at least the xml and folder.jpg/png
+ return item.ResolveArgs.ContainsMetaFileByName(LOCAL_META_FILE_NAME) && (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") ||
+ item.ResolveArgs.ContainsMetaFileByName("folder.png"));
+ }
+
+ /// <summary>
+ /// Gets the series id.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> GetSeriesId(BaseItem item, CancellationToken cancellationToken)
+ {
+ var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
+ if (string.IsNullOrEmpty(seriesId))
+ {
+ seriesId = await FindSeries(item.Name, cancellationToken).ConfigureAwait(false);
+ }
+ return seriesId;
+ }
+
+
+ /// <summary>
+ /// Finds the series.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ public async Task<string> FindSeries(string name, CancellationToken cancellationToken)
+ {
+
+ //nope - search for it
+ string url = string.Format(rootUrl + seriesQuery, WebUtility.UrlEncode(name));
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var results = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(results);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (doc.HasChildNodes)
+ {
+ XmlNodeList nodes = doc.SelectNodes("//Series");
+ string comparableName = GetComparableName(name);
+ if (nodes != null)
+ foreach (XmlNode node in nodes)
+ {
+ var n = node.SelectSingleNode("./SeriesName");
+ if (n != null && GetComparableName(n.InnerText) == comparableName)
+ {
+ n = node.SelectSingleNode("./seriesid");
+ if (n != null)
+ return n.InnerText;
+ }
+ else
+ {
+ if (n != null)
+ Logger.Info("TVDb Provider - " + n.InnerText + " did not match " + comparableName);
+ }
+ }
+ }
+
+ Logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
+ return null;
+ }
+
+ /// <summary>
+ /// The remove
+ /// </summary>
+ const string remove = "\"'!`?";
+ /// <summary>
+ /// The spacers
+ /// </summary>
+ const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
+
+ /// <summary>
+ /// Gets the name of the comparable.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.String.</returns>
+ internal static string GetComparableName(string name)
+ {
+ name = name.ToLower();
+ name = name.Normalize(NormalizationForm.FormKD);
+ var sb = new StringBuilder();
+ foreach (var c in name)
+ {
+ if ((int)c >= 0x2B0 && (int)c <= 0x0333)
+ {
+ // skip char modifier and diacritics
+ }
+ else if (remove.IndexOf(c) > -1)
+ {
+ // skip chars we are removing
+ }
+ else if (spacers.IndexOf(c) > -1)
+ {
+ sb.Append(" ");
+ }
+ else if (c == '&')
+ {
+ sb.Append(" and ");
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ name = sb.ToString();
+ name = name.Replace(", the", "");
+ name = name.Replace("the ", " ");
+ name = name.Replace(" the ", " ");
+
+ string prevName;
+ do
+ {
+ prevName = name;
+ name = name.Replace(" ", " ");
+ } while (name.Length != prevName.Length);
+
+ return name.Trim();
+ }
+
+
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
index 76d7e7ac11..728ac0549c 100644
--- a/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
@@ -1,36 +1,86 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- [Export(typeof(BaseMetadataProvider))]
- public class SeriesProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Series;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFile("series.xml"))
- {
- new SeriesXmlParser().Fetch(item as Series, Path.Combine(args.Path, "series.xml"));
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class SeriesProviderFromXml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class SeriesProviderFromXml : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ return item is Series && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>DateTime.</returns>
+ protected override DateTime CompareDate(BaseItem item)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "series.xml"));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "series.xml"));
+
+ if (metadataFile.HasValue)
+ {
+ var path = metadataFile.Value.Path;
+
+ new SeriesXmlParser().Fetch((Series)item, path, cancellationToken);
+ SetLastRefreshed(item, DateTime.UtcNow);
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
index 36c0a99efd..7516904255 100644
--- a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
+++ b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
@@ -1,69 +1,90 @@
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- public class SeriesXmlParser : BaseItemXmlParser<Series>
- {
- protected override void FetchDataFromXmlNode(XmlReader reader, Series item)
- {
- switch (reader.Name)
- {
- case "id":
- string id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(id))
- {
- item.SetProviderId(MetadataProviders.Tvdb, id);
- }
- break;
-
- case "Airs_DayOfWeek":
- {
- string day = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(day))
- {
- if (day.Equals("Daily", StringComparison.OrdinalIgnoreCase))
- {
- item.AirDays = new DayOfWeek[] {
- DayOfWeek.Sunday,
- DayOfWeek.Monday,
- DayOfWeek.Tuesday,
- DayOfWeek.Wednesday,
- DayOfWeek.Thursday,
- DayOfWeek.Friday,
- DayOfWeek.Saturday
- };
- }
- else
- {
- item.AirDays = new DayOfWeek[] {
- (DayOfWeek)Enum.Parse(typeof(DayOfWeek), day, true)
- };
- }
- }
-
- break;
- }
-
- case "Airs_Time":
- item.AirTime = reader.ReadElementContentAsString();
- break;
-
- case "SeriesName":
- item.Name = reader.ReadElementContentAsString();
- break;
-
- case "Status":
- item.Status = reader.ReadElementContentAsString();
- break;
-
- default:
- base.FetchDataFromXmlNode(reader, item);
- break;
- }
- }
- }
-}
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class SeriesXmlParser
+ /// </summary>
+ public class SeriesXmlParser : BaseItemXmlParser<Series>
+ {
+ /// <summary>
+ /// Fetches the data from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ protected override void FetchDataFromXmlNode(XmlReader reader, Series item)
+ {
+ switch (reader.Name)
+ {
+ case "Series":
+ //MB generated metadata is within a "Series" node
+ using (var subTree = reader.ReadSubtree())
+ {
+ subTree.MoveToContent();
+
+ // Loop through each element
+ while (subTree.Read())
+ {
+ if (subTree.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(subTree, item);
+ }
+ }
+
+ }
+ break;
+
+ case "id":
+ string id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ break;
+
+ case "Airs_DayOfWeek":
+ {
+ item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
+ break;
+ }
+
+ case "Airs_Time":
+ item.AirTime = reader.ReadElementContentAsString();
+ break;
+
+ case "SeriesName":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Status":
+ {
+ var status = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(status))
+ {
+ SeriesStatus seriesStatus;
+ if (Enum.TryParse(status, true, out seriesStatus))
+ {
+ item.Status = seriesStatus;
+ }
+ else
+ {
+ Logger.LogInfo("Unrecognized series status: " + status);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/VideoInfoProvider.cs b/MediaBrowser.Controller/Providers/VideoInfoProvider.cs
deleted file mode 100644
index 264825fe08..0000000000
--- a/MediaBrowser.Controller/Providers/VideoInfoProvider.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.FFMpeg;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-
-namespace MediaBrowser.Controller.Providers
-{
- [Export(typeof(BaseMetadataProvider))]
- public class VideoInfoProvider : BaseMediaInfoProvider<Video>
- {
- public override MetadataProviderPriority Priority
- {
- // Give this second priority
- // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
- get { return MetadataProviderPriority.Second; }
- }
-
- protected override string CacheDirectory
- {
- get { return Kernel.Instance.ApplicationPaths.FFProbeVideoCacheDirectory; }
- }
-
- protected override void Fetch(Video video, FFProbeResult data)
- {
- if (data.format != null)
- {
- if (!string.IsNullOrEmpty(data.format.duration))
- {
- video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration)).Ticks;
- }
-
- if (!string.IsNullOrEmpty(data.format.bit_rate))
- {
- video.BitRate = int.Parse(data.format.bit_rate);
- }
- }
-
- if (data.streams != null)
- {
- // For now, only read info about first video stream
- // Files with multiple video streams are possible, but extremely rare
- bool foundVideo = false;
-
- foreach (MediaStream stream in data.streams)
- {
- if (stream.codec_type.Equals("video", StringComparison.OrdinalIgnoreCase))
- {
- if (!foundVideo)
- {
- FetchFromVideoStream(video, stream);
- }
-
- foundVideo = true;
- }
- else if (stream.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
- {
- FetchFromAudioStream(video, stream);
- }
- }
- }
- }
-
- private void FetchFromVideoStream(Video video, MediaStream stream)
- {
- video.Codec = stream.codec_name;
- video.Width = stream.width;
- video.Height = stream.height;
- video.AspectRatio = stream.display_aspect_ratio;
-
- if (!string.IsNullOrEmpty(stream.avg_frame_rate))
- {
- string[] parts = stream.avg_frame_rate.Split('/');
-
- if (parts.Length == 2)
- {
- video.FrameRate = float.Parse(parts[0]) / float.Parse(parts[1]);
- }
- else
- {
- video.FrameRate = float.Parse(parts[0]);
- }
- }
- }
-
- private void FetchFromAudioStream(Video video, MediaStream stream)
- {
- var audio = new AudioStream{};
-
- audio.Codec = stream.codec_name;
-
- if (!string.IsNullOrEmpty(stream.bit_rate))
- {
- audio.BitRate = int.Parse(stream.bit_rate);
- }
-
- audio.Channels = stream.channels;
-
- if (!string.IsNullOrEmpty(stream.sample_rate))
- {
- audio.SampleRate = int.Parse(stream.sample_rate);
- }
-
- audio.Language = GetDictionaryValue(stream.tags, "language");
-
- List<AudioStream> streams = video.AudioStreams ?? new List<AudioStream>();
- streams.Add(audio);
- video.AudioStreams = streams;
- }
-
- private void FetchFromSubtitleStream(Video video, MediaStream stream)
- {
- var subtitle = new SubtitleStream{};
-
- subtitle.Language = GetDictionaryValue(stream.tags, "language");
-
- List<SubtitleStream> streams = video.Subtitles ?? new List<SubtitleStream>();
- streams.Add(subtitle);
- video.Subtitles = streams;
- }
-
- /// <summary>
- /// Determines if there's already enough info in the Video object to allow us to skip running ffprobe
- /// </summary>
- protected override bool CanSkipFFProbe(Video video)
- {
- if (video.VideoType != VideoType.VideoFile)
- {
- // Not supported yet
- return true;
- }
-
- if (video.AudioStreams == null || !video.AudioStreams.Any())
- {
- return false;
- }
-
- if (string.IsNullOrEmpty(video.AspectRatio))
- {
- return false;
- }
-
- if (string.IsNullOrEmpty(video.Codec))
- {
- return false;
- }
-
- if (string.IsNullOrEmpty(video.ScanType))
- {
- return false;
- }
-
- if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value == 0)
- {
- return false;
- }
-
- if (Convert.ToInt32(video.FrameRate) == 0 || video.Height == 0 || video.Width == 0 || video.BitRate == 0)
- {
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Resolvers/Audio/AudioResolver.cs b/MediaBrowser.Controller/Resolvers/Audio/AudioResolver.cs
new file mode 100644
index 0000000000..f827bf0477
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/Audio/AudioResolver.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers.Audio
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class AudioResolver : BaseItemResolver<Entities.Audio.Audio>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ protected override Entities.Audio.Audio Resolve(ItemResolveArgs args)
+ {
+ // Return audio if the path is a file and has a matching extension
+
+ if (!args.IsDirectory)
+ {
+ if (EntityResolutionHelper.IsAudioFile(args))
+ {
+ return new Entities.Audio.Audio();
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Audio/MusicAlbumResolver.cs b/MediaBrowser.Controller/Resolvers/Audio/MusicAlbumResolver.cs
new file mode 100644
index 0000000000..8b2e49f313
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/Audio/MusicAlbumResolver.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers.Audio
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class MusicAlbumResolver : BaseItemResolver<MusicAlbum>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Third; } // we need to be ahead of the generic folder resolver but behind the movie one
+ }
+
+ protected override MusicAlbum Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory) return null;
+
+ //Avoid mis-identifying top folders
+ if (args.Parent == null) return null;
+ if (args.Parent.IsRoot) return null;
+
+ return EntityResolutionHelper.IsMusicAlbum(args) ? new MusicAlbum() : null;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Audio/MusicArtistResolver.cs b/MediaBrowser.Controller/Resolvers/Audio/MusicArtistResolver.cs
new file mode 100644
index 0000000000..8060e8d334
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/Audio/MusicArtistResolver.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Resolvers.Audio
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class MusicArtistResolver : BaseItemResolver<MusicArtist>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Third; } // we need to be ahead of the generic folder resolver but behind the movie one
+ }
+
+ protected override MusicArtist Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory) return null;
+
+ //Avoid mis-identifying top folders
+ if (args.Parent == null) return null;
+ if (args.Parent.IsRoot) return null;
+
+ // If we contain an album assume we are an artist folder
+ return args.FileSystemChildren.Any(EntityResolutionHelper.IsMusicAlbum) ? new MusicArtist() : null;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/AudioResolver.cs b/MediaBrowser.Controller/Resolvers/AudioResolver.cs
deleted file mode 100644
index 8f10e45e50..0000000000
--- a/MediaBrowser.Controller/Resolvers/AudioResolver.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-
-namespace MediaBrowser.Controller.Resolvers
-{
- [Export(typeof(IBaseItemResolver))]
- public class AudioResolver : BaseItemResolver<Audio>
- {
- public override ResolverPriority Priority
- {
- get { return ResolverPriority.Last; }
- }
-
- protected override Audio Resolve(ItemResolveEventArgs args)
- {
- // Return audio if the path is a file and has a matching extension
-
- if (!args.IsDirectory)
- {
- if (IsAudioFile(args.Path))
- {
- return new Audio();
- }
- }
-
- return null;
- }
-
- private static bool IsAudioFile(string path)
- {
- string extension = Path.GetExtension(path).ToLower();
-
- switch (extension)
- {
- case ".mp3":
- case ".wma":
- case ".aac":
- case ".acc":
- case ".flac":
- case ".m4a":
- case ".m4b":
- case ".wav":
- case ".ape":
- return true;
-
- default:
- return false;
- }
-
- }
- }
-}
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
index 7c9677e4e4..8e43a791fc 100644
--- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
@@ -1,126 +1,165 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Common.Extensions;
-using System;
-using System.IO;
-
-namespace MediaBrowser.Controller.Resolvers
-{
- public abstract class BaseItemResolver<T> : IBaseItemResolver
- where T : BaseItem, new()
- {
- protected virtual T Resolve(ItemResolveEventArgs args)
- {
- return null;
- }
-
- public virtual ResolverPriority Priority
- {
- get
- {
- return ResolverPriority.First;
- }
- }
-
- /// <summary>
- /// Sets initial values on the newly resolved item
- /// </summary>
- protected virtual void SetInitialItemValues(T item, ItemResolveEventArgs args)
- {
- // If the subclass didn't specify this
- if (string.IsNullOrEmpty(item.Path))
- {
- item.Path = args.Path;
- }
-
- // If the subclass didn't specify this
- if (args.Parent != null)
- {
- item.Parent = args.Parent;
- }
-
- item.Id = (item.GetType().FullName + item.Path).GetMD5();
- }
-
- public BaseItem ResolvePath(ItemResolveEventArgs args)
- {
- T item = Resolve(args);
-
- if (item != null)
- {
- // Set initial values on the newly resolved item
- SetInitialItemValues(item, args);
-
- // Make sure the item has a name
- EnsureName(item);
-
- // Make sure DateCreated and DateModified have values
- EnsureDates(item, args);
- }
-
- return item;
- }
-
- private void EnsureName(T item)
- {
- // If the subclass didn't supply a name, add it here
- if (string.IsNullOrEmpty(item.Name))
- {
- item.Name = Path.GetFileNameWithoutExtension(item.Path);
- }
-
- }
-
- /// <summary>
- /// Ensures DateCreated and DateModified have values
- /// </summary>
- private void EnsureDates(T item, ItemResolveEventArgs args)
- {
- if (!Path.IsPathRooted(item.Path))
- {
- return;
- }
-
- // See if a different path came out of the resolver than what went in
- if (!args.Path.Equals(item.Path, StringComparison.OrdinalIgnoreCase))
- {
- WIN32_FIND_DATA? childData = args.GetFileSystemEntry(item.Path);
-
- if (childData != null)
- {
- item.DateCreated = childData.Value.CreationTimeUtc;
- item.DateModified = childData.Value.LastWriteTimeUtc;
- }
- else
- {
- WIN32_FIND_DATA fileData = FileData.GetFileData(item.Path);
- item.DateCreated = fileData.CreationTimeUtc;
- item.DateModified = fileData.LastWriteTimeUtc;
- }
- }
- else
- {
- item.DateCreated = args.FileInfo.CreationTimeUtc;
- item.DateModified = args.FileInfo.LastWriteTimeUtc;
- }
- }
- }
-
- /// <summary>
- /// Weed this to keep a list of resolvers, since Resolvers are built with generics
- /// </summary>
- public interface IBaseItemResolver
- {
- BaseItem ResolvePath(ItemResolveEventArgs args);
- ResolverPriority Priority { get; }
- }
-
- public enum ResolverPriority
- {
- First = 1,
- Second = 2,
- Third = 3,
- Last = 4
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Class BaseItemResolver
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseItemResolver<T> : IBaseItemResolver
+ 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)
+ {
+ // If the subclass didn't specify this
+ if (string.IsNullOrEmpty(item.Path))
+ {
+ item.Path = args.Path;
+ }
+
+ // If the subclass didn't specify this
+ if (args.Parent != null)
+ {
+ item.Parent = args.Parent;
+ }
+
+ item.Id = item.Path.GetMBId(item.GetType());
+ item.DisplayMediaType = item.GetType().Name;
+ }
+
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ public BaseItem ResolvePath(ItemResolveArgs args)
+ {
+ T item = Resolve(args);
+
+ if (item != null)
+ {
+ // Set the args on the item
+ item.ResolveArgs = args;
+
+ // Set initial values on the newly resolved item
+ SetInitialItemValues(item, args);
+
+ // Make sure the item has a name
+ EnsureName(item);
+
+ // Make sure DateCreated and DateModified have values
+ EntityResolutionHelper.EnsureDates(item, args);
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Ensures the name.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void EnsureName(T item)
+ {
+ // If the subclass didn't supply a name, add it here
+ if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
+ {
+ //we use our resolve args name here to get the name of the containg folder, not actual video file
+ item.Name = GetMBName(item.ResolveArgs.FileInfo.cFileName, item.ResolveArgs.FileInfo.IsDirectory);
+ }
+ }
+
+ /// <summary>
+ /// The MB name regex
+ /// </summary>
+ private static readonly Regex MBNameRegex = new Regex("(\\[.*\\])", RegexOptions.Compiled);
+
+ /// <summary>
+ /// Strip out attribute items and return just the name we will use for items
+ /// </summary>
+ /// <param name="path">Assumed to be a file or directory path</param>
+ /// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
+ /// <returns>The cleaned name</returns>
+ private static string GetMBName(string path, bool isDirectory)
+ {
+ //first just get the file or directory name
+ var fn = isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
+
+ //now - strip out anything inside brackets
+ fn = MBNameRegex.Replace(fn, string.Empty);
+
+ return fn;
+ }
+ }
+
+ /// <summary>
+ /// Weed this to keep a list of resolvers, since Resolvers are built with generics
+ /// </summary>
+ public interface IBaseItemResolver
+ {
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem ResolvePath(ItemResolveArgs args);
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ ResolverPriority Priority { get; }
+ }
+
+ /// <summary>
+ /// Enum ResolverPriority
+ /// </summary>
+ public enum ResolverPriority
+ {
+ /// <summary>
+ /// The first
+ /// </summary>
+ First = 1,
+ /// <summary>
+ /// The second
+ /// </summary>
+ Second = 2,
+ /// <summary>
+ /// The third
+ /// </summary>
+ Third = 3,
+ /// <summary>
+ /// The last
+ /// </summary>
+ Last = 4
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/BaseResolutionIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/BaseResolutionIgnoreRule.cs
new file mode 100644
index 0000000000..45effc4da1
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/BaseResolutionIgnoreRule.cs
@@ -0,0 +1,12 @@
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Provides a base "rule" that anyone can use to have paths ignored by the resolver
+ /// </summary>
+ public abstract class BaseResolutionIgnoreRule
+ {
+ public abstract bool ShouldIgnore(ItemResolveArgs args);
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/CoreResolutionIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/CoreResolutionIgnoreRule.cs
new file mode 100644
index 0000000000..2d69f8deff
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/CoreResolutionIgnoreRule.cs
@@ -0,0 +1,52 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Provides the core resolver ignore rules
+ /// </summary>
+ [Export(typeof(BaseResolutionIgnoreRule))]
+ public class CoreResolutionIgnoreRule : BaseResolutionIgnoreRule
+ {
+ /// <summary>
+ /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
+ /// </summary>
+ private static readonly List<string> IgnoreFolders = new List<string>
+ {
+ "trailers",
+ "metadata",
+ "certificate",
+ "backup",
+ "ps3_update",
+ "ps3_vprm",
+ "adv_obj",
+ "extrafanart"
+ };
+
+ public override bool ShouldIgnore(ItemResolveArgs args)
+ {
+ // Ignore hidden files and folders
+ if (args.IsHidden)
+ {
+ return true;
+ }
+
+ if (args.IsDirectory)
+ {
+ var filename = args.FileInfo.cFileName;
+
+ // Ignore any folders in our list
+ if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
index b821f88018..75e1305268 100644
--- a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
+++ b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
@@ -1,70 +1,211 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.IO;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Entities.TV;
-
-namespace MediaBrowser.Controller.Resolvers
-{
- public static class EntityResolutionHelper
- {
- /// <summary>
- /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
- /// </summary>
- public static List<string> IgnoreFolders = new List<string>()
- {
- "trailers",
- "metadata",
- "bdmv",
- "certificate",
- "backup",
- "video_ts",
- "audio_ts",
- "ps3_update",
- "ps3_vprm",
- "adv_obj",
- "hvdvd_ts"
- };
- /// <summary>
- /// Determines whether a path should be resolved or ignored entirely - called before we even look at the contents
- /// </summary>
- /// <param name="path"></param>
- /// <returns>false if the path should be ignored</returns>
- public static bool ShouldResolvePath(WIN32_FIND_DATA path)
- {
- bool resolve = true;
- // Ignore hidden files and folders
- if (path.IsHidden || path.IsSystemFile)
- {
- resolve = false;
- }
-
- // Ignore any folders in our list
- else if (path.IsDirectory && IgnoreFolders.Contains(Path.GetFileName(path.Path), StringComparer.OrdinalIgnoreCase))
- {
- resolve = false;
- }
-
- return resolve;
- }
-
- /// <summary>
- /// Determines whether a path should be ignored based on its contents - called after the contents have been read
- /// </summary>
- public static bool ShouldResolvePathContents(ItemResolveEventArgs args)
- {
- bool resolve = true;
- if (args.ContainsFile(".ignore"))
- {
- // Ignore any folders containing a file called .ignore
- resolve = false;
- }
-
- return resolve;
- }
- }
-}
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Class EntityResolutionHelper
+ /// </summary>
+ public static class EntityResolutionHelper
+ {
+ /// <summary>
+ /// Any extension in this list is considered a metadata file - can be added to at runtime for extensibility
+ /// </summary>
+ public static List<string> MetaExtensions = new List<string>
+ {
+ ".xml",
+ ".jpg",
+ ".png",
+ ".json",
+ ".data"
+ };
+ /// <summary>
+ /// Any extension in this list is considered a video file - can be added to at runtime for extensibility
+ /// </summary>
+ public static List<string> VideoFileExtensions = new List<string>
+ {
+ ".mkv",
+ ".m2t",
+ ".m2ts",
+ ".img",
+ ".iso",
+ ".ts",
+ ".rmvb",
+ ".mov",
+ ".avi",
+ ".mpg",
+ ".mpeg",
+ ".wmv",
+ ".mp4",
+ ".divx",
+ ".dvr-ms",
+ ".wtv",
+ ".ogm",
+ ".ogv",
+ ".asf",
+ ".m4v",
+ ".flv",
+ ".f4v",
+ ".3gp",
+ ".webm"
+ };
+
+ /// <summary>
+ /// Determines whether [is video file] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is video file] [the specified path]; otherwise, <c>false</c>.</returns>
+ public static bool IsVideoFile(string path)
+ {
+ var extension = Path.GetExtension(path) ?? string.Empty;
+ return VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// The audio file extensions
+ /// </summary>
+ public static readonly string[] AudioFileExtensions = new[] {
+ ".mp3",
+ ".flac",
+ ".wma",
+ ".aac",
+ ".acc",
+ ".m4a",
+ ".m4b",
+ ".wav",
+ ".ape",
+ ".ogg",
+ ".oga"
+ };
+
+ /// <summary>
+ /// Determines whether [is audio file] [the specified args].
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if [is audio file] [the specified args]; otherwise, <c>false</c>.</returns>
+ public static bool IsAudioFile(ItemResolveArgs args)
+ {
+ return AudioFileExtensions.Contains(Path.GetExtension(args.Path), StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is audio file] [the specified file].
+ /// </summary>
+ /// <param name="file">The file.</param>
+ /// <returns><c>true</c> if [is audio file] [the specified file]; otherwise, <c>false</c>.</returns>
+ public static bool IsAudioFile(WIN32_FIND_DATA file)
+ {
+ return AudioFileExtensions.Contains(Path.GetExtension(file.Path), StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determine if the supplied file data points to a music album
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <returns><c>true</c> if [is music album] [the specified data]; otherwise, <c>false</c>.</returns>
+ public static bool IsMusicAlbum(WIN32_FIND_DATA data)
+ {
+ return ContainsMusic(FileSystem.GetFiles(data.Path));
+ }
+
+ /// <summary>
+ /// Determine if the supplied reslove 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>
+ public static 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)) return true;
+ }
+
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determine if the supplied list contains what we should consider music
+ /// </summary>
+ /// <param name="list">The list.</param>
+ /// <returns><c>true</c> if the specified list contains music; otherwise, <c>false</c>.</returns>
+ public static bool ContainsMusic(IEnumerable<WIN32_FIND_DATA> list)
+ {
+ // If list contains at least 2 audio files or at least one and no video files consider it to contain music
+ var foundAudio = 0;
+ var foundVideo = 0;
+ foreach (var file in list)
+ {
+ if (IsAudioFile(file)) foundAudio++;
+ if (foundAudio >= 2)
+ {
+ return true;
+ }
+ if (IsVideoFile(file.Path)) foundVideo++;
+ }
+
+ // or a single audio file and no video files
+ if (foundAudio > 0 && foundVideo == 0) return true;
+ return false;
+ }
+
+ /// <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>
+ public static bool ShouldResolvePathContents(ItemResolveArgs args)
+ {
+ // Ignore any folders containing a file called .ignore
+ return !args.ContainsFileSystemEntryByName(".ignore");
+ }
+
+ /// <summary>
+ /// Ensures DateCreated and DateModified have values
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ public static void EnsureDates(BaseItem item, ItemResolveArgs args)
+ {
+ if (!Path.IsPathRooted(item.Path))
+ {
+ return;
+ }
+
+ // See if a different path came out of the resolver than what went in
+ if (!args.Path.Equals(item.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null;
+
+ if (childData.HasValue)
+ {
+ item.DateCreated = childData.Value.CreationTimeUtc;
+ item.DateModified = childData.Value.LastWriteTimeUtc;
+ }
+ else
+ {
+ var fileData = FileSystem.GetFileData(item.Path);
+
+ if (fileData.HasValue)
+ {
+ item.DateCreated = fileData.Value.CreationTimeUtc;
+ item.DateModified = fileData.Value.LastWriteTimeUtc;
+ }
+ }
+ }
+ else
+ {
+ item.DateCreated = args.FileInfo.CreationTimeUtc;
+ item.DateModified = args.FileInfo.LastWriteTimeUtc;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/FolderResolver.cs b/MediaBrowser.Controller/Resolvers/FolderResolver.cs
index 028c85f862..e37c18692d 100644
--- a/MediaBrowser.Controller/Resolvers/FolderResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/FolderResolver.cs
@@ -1,36 +1,71 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-
-namespace MediaBrowser.Controller.Resolvers
-{
- [Export(typeof(IBaseItemResolver))]
- public class FolderResolver : BaseFolderResolver<Folder>
- {
- public override ResolverPriority Priority
- {
- get { return ResolverPriority.Last; }
- }
-
- protected override Folder Resolve(ItemResolveEventArgs args)
- {
- if (args.IsDirectory)
- {
- return new Folder();
- }
-
- return null;
- }
- }
-
- public abstract class BaseFolderResolver<TItemType> : BaseItemResolver<TItemType>
- where TItemType : Folder, new()
- {
- protected override void SetInitialItemValues(TItemType item, ItemResolveEventArgs args)
- {
- base.SetInitialItemValues(item, args);
-
- item.IsRoot = args.Parent == null;
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Class FolderResolver
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class FolderResolver : BaseFolderResolver<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)
+ {
+ if (args.IsPhysicalRoot)
+ {
+ return new AggregateFolder();
+ }
+ if (args.IsRoot)
+ {
+ return new UserRootFolder(); //if we got here and still a root - must be user root
+ }
+ if (args.IsVf)
+ {
+ return new CollectionFolder();
+ }
+
+ return new Folder();
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Class BaseFolderResolver
+ /// </summary>
+ /// <typeparam name="TItemType">The type of the T item type.</typeparam>
+ public abstract class BaseFolderResolver<TItemType> : BaseItemResolver<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;
+ item.IsPhysicalRoot = args.IsPhysicalRoot;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/LocalTrailerResolver.cs b/MediaBrowser.Controller/Resolvers/LocalTrailerResolver.cs
new file mode 100644
index 0000000000..c26b0ce37e
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/LocalTrailerResolver.cs
@@ -0,0 +1,40 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Class LocalTrailerResolver
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class LocalTrailerResolver : BaseVideoResolver<Trailer>
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Trailer.</returns>
+ protected override Trailer Resolve(ItemResolveArgs args)
+ {
+ // Trailers are not Children, therefore this can never happen
+ if (args.Parent != null)
+ {
+ return null;
+ }
+
+ // If the file is within a trailers folder, see if the VideoResolver returns something
+ if (!args.IsDirectory)
+ {
+ if (string.Equals(Path.GetFileName(Path.GetDirectoryName(args.Path)), BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return base.Resolve(args);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs b/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs
index 069068067f..ccca0cfabb 100644
--- a/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs
@@ -1,28 +1,43 @@
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using System;
-using System.ComponentModel.Composition;
-using System.IO;
-
-namespace MediaBrowser.Controller.Resolvers.Movies
-{
- [Export(typeof(IBaseItemResolver))]
- public class BoxSetResolver : BaseFolderResolver<BoxSet>
- {
- protected override BoxSet Resolve(ItemResolveEventArgs args)
- {
- // It's a boxset if all of the following conditions are met:
- // Is a Directory
- // Contains [boxset] in the path
- if (args.IsDirectory)
- {
- if (Path.GetFileName(args.Path).IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return new BoxSet();
- }
- }
-
- return null;
- }
- }
-}
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.Movies
+{
+ /// <summary>
+ /// Class BoxSetResolver
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class BoxSetResolver : BaseFolderResolver<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)
+ {
+ return new BoxSet();
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs
index 825850b20c..14f6357478 100644
--- a/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs
@@ -1,116 +1,210 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System.ComponentModel.Composition;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Resolvers.Movies
-{
- [Export(typeof(IBaseItemResolver))]
- public class MovieResolver : BaseVideoResolver<Movie>
- {
- protected override Movie Resolve(ItemResolveEventArgs args)
- {
- // Must be a directory and under a 'Movies' VF
- if (args.IsDirectory)
- {
- // If the parent is not a boxset, the only other allowed parent type is Folder
- if (!(args.Parent is BoxSet))
- {
- if (args.Parent != null && args.Parent.GetType() != typeof(Folder))
- {
- return null;
- }
- }
-
- // Optimization to avoid running all these tests against VF's
- if (args.Parent != null && args.Parent.IsRoot)
- {
- return null;
- }
-
- // Return a movie if the video resolver finds something in the folder
- return GetMovie(args);
- }
-
- return null;
- }
-
- protected override void SetInitialItemValues(Movie item, ItemResolveEventArgs args)
- {
- base.SetInitialItemValues(item, args);
-
- SetProviderIdFromPath(item);
- }
-
- private void SetProviderIdFromPath(Movie item)
- {
- const string srch = "[tmdbid=";
- int index = item.Path.IndexOf(srch, System.StringComparison.OrdinalIgnoreCase);
-
- if (index != -1)
- {
- string id = item.Path.Substring(index + srch.Length);
-
- id = id.Substring(0, id.IndexOf(']'));
-
- item.SetProviderId(MetadataProviders.Tmdb, id);
- }
- }
-
- private Movie GetMovie(ItemResolveEventArgs args)
- {
- //first see if the discovery process has already determined we are a DVD or BD
- if (args.IsDVDFolder)
- {
- return new Movie()
- {
- Path = args.Path,
- VideoType = VideoType.Dvd
- };
- }
- else if (args.IsBDFolder)
- {
- return new Movie()
- {
- Path = args.Path,
- VideoType = VideoType.BluRay
- };
- }
- else if (args.IsHDDVDFolder)
- {
- return new Movie()
- {
- Path = args.Path,
- VideoType = VideoType.HdDvd
- };
- }
-
- // Loop through each child file/folder and see if we find a video
- foreach (var child in args.FileSystemChildren)
- {
- var childArgs = new ItemResolveEventArgs
- {
- FileInfo = child,
- FileSystemChildren = new WIN32_FIND_DATA[] { },
- Path = child.Path
- };
-
- var item = base.Resolve(childArgs);
-
- if (item != null)
- {
- return new Movie
- {
- Path = item.Path,
- VideoType = item.VideoType
- };
- }
- }
-
- return null;
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers.Movies;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.Movies
+{
+ /// <summary>
+ /// Class MovieResolver
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class MovieResolver : BaseVideoResolver<Movie>
+ {
+ /// <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
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Movie.</returns>
+ protected override Movie Resolve(ItemResolveArgs args)
+ {
+ // Must be a directory and under a 'Movies' VF
+ if (args.IsDirectory)
+ {
+ // Avoid expensive tests against VF's and all their children by not allowing this
+ if (args.Parent == null || args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // If the parent is not a boxset, the only other allowed parent type is Folder
+ if (!(args.Parent is BoxSet))
+ {
+ if (args.Parent.GetType() != typeof(Folder))
+ {
+ return null;
+ }
+ }
+
+ // Optimization to avoid running all these tests against Top folders
+ if (args.Parent != null && args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // The movie must be a video file
+ return FindMovie(args);
+ }
+
+ 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(Movie 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(Movie item)
+ {
+ //we need to only look at the name of this actual item (not parents)
+ var justName = item.Path.Substring(item.Path.LastIndexOf(Path.DirectorySeparatorChar));
+
+ var id = justName.GetAttributeValue("tmdbid");
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, id);
+ }
+ }
+
+ /// <summary>
+ /// Finds a movie based on a child file system entries
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Movie.</returns>
+ private Movie FindMovie(ItemResolveArgs args)
+ {
+ // Since the looping is expensive, this is an optimization to help us avoid it
+ if (args.ContainsMetaFileByName("series.xml") || args.Path.IndexOf("[tvdbid", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return null;
+ }
+
+ // Optimization to avoid having to resolve every file
+ bool? isKnownMovie = null;
+
+ var movies = new List<Movie>();
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in args.FileSystemChildren)
+ {
+ if (child.IsDirectory)
+ {
+ if (IsDvdDirectory(child.cFileName))
+ {
+ return new Movie
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd
+ };
+ }
+ if (IsBluRayDirectory(child.cFileName))
+ {
+ return new Movie
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay
+ };
+ }
+ if (IsHdDvdDirectory(child.cFileName))
+ {
+ return new Movie
+ {
+ Path = args.Path,
+ VideoType = VideoType.HdDvd
+ };
+ }
+
+ continue;
+ }
+
+ var childArgs = new ItemResolveArgs
+ {
+ FileInfo = child,
+ Path = child.Path
+ };
+
+ var item = base.Resolve(childArgs);
+
+ if (item != null)
+ {
+ // If we already know it's a movie, we can stop looping
+ if (!isKnownMovie.HasValue)
+ {
+ isKnownMovie = args.ContainsMetaFileByName("movie.xml") || args.ContainsMetaFileByName(MovieDbProvider.LOCAL_META_FILE_NAME) || args.Path.IndexOf("[tmdbid", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ if (isKnownMovie.Value)
+ {
+ return item;
+ }
+
+ movies.Add(item);
+ }
+ }
+
+ // If there are multiple video files, return null, and let the VideoResolver catch them later as plain videos
+ return movies.Count == 1 ? movies[0] : null;
+ }
+
+ /// <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>
+ private bool IsDvdDirectory(string directoryName)
+ {
+ return directoryName.Equals("video_ts", StringComparison.OrdinalIgnoreCase);
+ }
+ /// <summary>
+ /// Determines whether [is hd DVD directory] [the specified directory name].
+ /// </summary>
+ /// <param name="directoryName">Name of the directory.</param>
+ /// <returns><c>true</c> if [is hd DVD directory] [the specified directory name]; otherwise, <c>false</c>.</returns>
+ private bool IsHdDvdDirectory(string directoryName)
+ {
+ return directoryName.Equals("hvdvd_ts", 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>
+ private bool IsBluRayDirectory(string directoryName)
+ {
+ return directoryName.Equals("bdmv", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs
index 0961edd1ac..f2f3ce122d 100644
--- a/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs
@@ -1,21 +1,65 @@
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-
-namespace MediaBrowser.Controller.Resolvers.TV
-{
- [Export(typeof(IBaseItemResolver))]
- public class EpisodeResolver : BaseVideoResolver<Episode>
- {
- protected override Episode Resolve(ItemResolveEventArgs args)
- {
- // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
- if (args.Parent is Season || args.Parent is Series)
- {
- return base.Resolve(args);
- }
-
- return null;
- }
- }
-}
+using System;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class EpisodeResolver : BaseVideoResolver<Episode>
+ {
+ protected override Episode Resolve(ItemResolveArgs args)
+ {
+ // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ if (args.Parent is Season || args.Parent is Series)
+ {
+ if (args.IsDirectory)
+ {
+ if (args.ContainsFileSystemEntryByName("video_ts"))
+ {
+ return new Episode
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd
+ };
+ }
+ if (args.ContainsFileSystemEntryByName("bdmv"))
+ {
+ return new Episode
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay
+ };
+ }
+ }
+
+ return base.Resolve(args);
+ }
+
+ return null;
+ }
+
+ protected override void SetInitialItemValues(Episode item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ //fill in our season and series ids
+ var season = args.Parent as Season;
+ if (season != null)
+ {
+ item.SeasonItemId = season.Id;
+ var series = season.Parent as Series;
+ if (series != null)
+ {
+ item.SeriesItemId = series.Id;
+ }
+ }
+ else
+ {
+ var series = args.Parent as Series;
+ item.SeriesItemId = series != null ? series.Id : Guid.Empty;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs b/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs
index 0ad0782e07..6569c85bc8 100644
--- a/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs
@@ -1,25 +1,34 @@
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-
-namespace MediaBrowser.Controller.Resolvers.TV
-{
- [Export(typeof(IBaseItemResolver))]
- public class SeasonResolver : BaseFolderResolver<Season>
- {
- protected override Season Resolve(ItemResolveEventArgs args)
- {
- if (args.Parent is Series && args.IsDirectory)
- {
- var season = new Season { };
-
- season.IndexNumber = TVUtils.GetSeasonNumberFromPath(args.Path);
-
- return season;
- }
-
- return null;
- }
- }
-}
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class SeasonResolver : BaseFolderResolver<Season>
+ {
+ protected override Season Resolve(ItemResolveArgs args)
+ {
+ if (args.Parent is Series && args.IsDirectory)
+ {
+ return new Season
+ {
+ IndexNumber = TVUtils.GetSeasonNumberFromPath(args.Path)
+ };
+ }
+
+ return null;
+ }
+
+ protected override void SetInitialItemValues(Season item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ var series = args.Parent as Series;
+ item.SeriesItemId = series != null ? series.Id : Guid.Empty;
+
+ Season.AddMetadataFiles(args);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs
index b8ff2c37be..7c0bc3df14 100644
--- a/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs
@@ -1,64 +1,100 @@
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System;
-using System.ComponentModel.Composition;
-using System.IO;
-
-namespace MediaBrowser.Controller.Resolvers.TV
-{
- [Export(typeof(IBaseItemResolver))]
- public class SeriesResolver : BaseFolderResolver<Series>
- {
- protected override Series Resolve(ItemResolveEventArgs args)
- {
- if (args.IsDirectory)
- {
- // Optimization to avoid running all these tests against VF's
- if (args.Parent != null && args.Parent.IsRoot)
- {
- return null;
- }
-
- // Optimization to avoid running these tests against Seasons
- if (args.Parent is Series)
- {
- return null;
- }
-
- // It's a Series if any of the following conditions are met:
- // series.xml exists
- // [tvdbid= is present in the path
- // TVUtils.IsSeriesFolder returns true
- if (args.ContainsFile("series.xml") || Path.GetFileName(args.Path).IndexOf("[tvdbid=", StringComparison.OrdinalIgnoreCase) != -1 || TVUtils.IsSeriesFolder(args.Path, args.FileSystemChildren))
- {
- return new Series();
- }
- }
-
- return null;
- }
-
- protected override void SetInitialItemValues(Series item, ItemResolveEventArgs args)
- {
- base.SetInitialItemValues(item, args);
-
- SetProviderIdFromPath(item);
- }
-
- private void SetProviderIdFromPath(Series item)
- {
- const string srch = "[tvdbid=";
- int index = item.Path.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
-
- if (index != -1)
- {
- string id = item.Path.Substring(index + srch.Length);
-
- id = id.Substring(0, id.IndexOf(']'));
-
- item.SetProviderId(MetadataProviders.Tvdb, id);
- }
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ /// <summary>
+ /// Class SeriesResolver
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class SeriesResolver : BaseFolderResolver<Series>
+ {
+ /// <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)
+ {
+ // Avoid expensive tests against VF's and all their children by not allowing this
+ if (args.Parent == null || args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // Optimization to avoid running these tests against Seasons
+ if (args.Parent is Series)
+ {
+ return null;
+ }
+
+ // It's a Series if any of the following conditions are met:
+ // series.xml exists
+ // [tvdbid= is present in the path
+ // TVUtils.IsSeriesFolder returns true
+ var filename = Path.GetFileName(args.Path);
+
+ if (string.IsNullOrEmpty(filename))
+ {
+ return null;
+ }
+
+ if (args.ContainsMetaFileByName("series.xml") || filename.IndexOf("[tvdbid=", StringComparison.OrdinalIgnoreCase) != -1 || TVUtils.IsSeriesFolder(args.Path, args.FileSystemChildren))
+ {
+ return new Series();
+ }
+ }
+
+ 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(Series item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ Season.AddMetadataFiles(args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void SetProviderIdFromPath(Series item)
+ {
+ var justName = item.Path.Substring(item.Path.LastIndexOf(Path.DirectorySeparatorChar));
+
+ var id = justName.GetAttributeValue("tvdbid");
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs b/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs
index ec3305e167..3e7421a397 100644
--- a/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs
+++ b/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs
@@ -1,164 +1,251 @@
-using MediaBrowser.Controller.IO;
-using System;
-using System.Text.RegularExpressions;
-
-namespace MediaBrowser.Controller.Resolvers.TV
-{
- public static class TVUtils
- {
- /// <summary>
- /// A season folder must contain one of these somewhere in the name
- /// </summary>
- private static readonly string[] SeasonFolderNames = new string[] {
- "season",
- "sæson",
- "temporada",
- "saison",
- "staffel"
- };
-
- /// <summary>
- /// Used to detect paths that represent episodes, need to make sure they don't also
- /// match movie titles like "2001 A Space..."
- /// Currently we limit the numbers here to 2 digits to try and avoid this
- /// </summary>
- /// <remarks>
- /// The order here is important, if the order is changed some of the later
- /// ones might incorrectly match things that higher ones would have caught.
- /// The most restrictive expressions should appear first
- /// </remarks>
- private static readonly Regex[] episodeExpressions = new Regex[] {
- new Regex(@".*\\[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // 01x02 blah.avi S01x01 balh.avi
- new Regex(@".*\\[s|S](?<seasonnumber>\d{1,2})x?[e|E](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // S01E02 blah.avi, S01xE01 blah.avi
- new Regex(@".*\\(?<seriesname>[^\\]*)[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // 01x02 blah.avi S01x01 balh.avi
- new Regex(@".*\\(?<seriesname>[^\\]*)[s|S](?<seasonnumber>\d{1,2})[x|X|\.]?[e|E](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled) // S01E02 blah.avi, S01xE01 blah.avi
- };
- /// <summary>
- /// To avoid the following matching movies they are only valid when contained in a folder which has been matched as a being season
- /// </summary>
- private static readonly Regex[] episodeExpressionsInASeasonFolder = new Regex[] {
- new Regex(@".*\\(?<epnumber>\d{1,2})\s?-\s?[^\\]*$", RegexOptions.Compiled), // 01 - blah.avi, 01-blah.avi
- new Regex(@".*\\(?<epnumber>\d{1,2})[^\d\\]*[^\\]*$", RegexOptions.Compiled), // 01.avi, 01.blah.avi "01 - 22 blah.avi"
- new Regex(@".*\\(?<seasonnumber>\d)(?<epnumber>\d{1,2})[^\d\\]+[^\\]*$", RegexOptions.Compiled), // 01.avi, 01.blah.avi
- new Regex(@".*\\\D*\d+(?<epnumber>\d{2})", RegexOptions.Compiled) // hell0 - 101 - hello.avi
-
- };
-
- public static int? GetSeasonNumberFromPath(string path)
- {
- // Look for one of the season folder names
- foreach (string name in SeasonFolderNames)
- {
- int index = path.IndexOf(name, StringComparison.OrdinalIgnoreCase);
-
- if (index != -1)
- {
- return GetSeasonNumberFromPathSubstring(path.Substring(index + name.Length));
- }
- }
-
- return null;
- }
-
- /// <summary>
- /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel")
- /// </summary>
- private static int? GetSeasonNumberFromPathSubstring(string path)
- {
- int numericStart = -1;
- int length = 0;
-
- // Find out where the numbers start, and then keep going until they end
- for (int i = 0; i < path.Length; i++)
- {
- if (char.IsNumber(path, i))
- {
- if (numericStart == -1)
- {
- numericStart = i;
- }
- length++;
- }
- else if (numericStart != -1)
- {
- break;
- }
- }
-
- if (numericStart == -1)
- {
- return null;
- }
-
- return int.Parse(path.Substring(numericStart, length));
- }
-
- public static bool IsSeasonFolder(string path)
- {
- return GetSeasonNumberFromPath(path) != null;
- }
-
- public static bool IsSeriesFolder(string path, WIN32_FIND_DATA[] fileSystemChildren)
- {
- // A folder with more than 3 non-season folders in will not becounted as a series
- int nonSeriesFolders = 0;
-
- for (int i = 0; i < fileSystemChildren.Length; i++)
- {
- var child = fileSystemChildren[i];
-
- if (child.IsHidden || child.IsSystemFile)
- {
- continue;
- }
-
- if (child.IsDirectory)
- {
- if (IsSeasonFolder(child.Path))
- {
- return true;
- }
-
- nonSeriesFolders++;
-
- if (nonSeriesFolders >= 3)
- {
- return false;
- }
- }
- else
- {
- if (FileSystemHelper.IsVideoFile(child.Path) && !string.IsNullOrEmpty(EpisodeNumberFromFile(child.Path, false)))
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- public static string EpisodeNumberFromFile(string fullPath, bool isInSeason)
- {
- string fl = fullPath.ToLower();
- foreach (Regex r in episodeExpressions)
- {
- Match m = r.Match(fl);
- if (m.Success)
- return m.Groups["epnumber"].Value;
- }
- if (isInSeason)
- {
- foreach (Regex r in episodeExpressionsInASeasonFolder)
- {
- Match m = r.Match(fl);
- if (m.Success)
- return m.Groups["epnumber"].Value;
- }
-
- }
-
- return null;
- }
- }
-}
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Win32;
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ public static class TVUtils
+ {
+ public static readonly string TVDBApiKey = "B89CE93890E9419B";
+ public static readonly string BannerUrl = "http://www.thetvdb.com/banners/";
+
+ /// <summary>
+ /// A season folder must contain one of these somewhere in the name
+ /// </summary>
+ private static readonly string[] SeasonFolderNames = new[]
+ {
+ "season",
+ "sæson",
+ "temporada",
+ "saison",
+ "staffel"
+ };
+
+ /// <summary>
+ /// Used to detect paths that represent episodes, need to make sure they don't also
+ /// match movie titles like "2001 A Space..."
+ /// Currently we limit the numbers here to 2 digits to try and avoid this
+ /// </summary>
+ /// <remarks>
+ /// The order here is important, if the order is changed some of the later
+ /// ones might incorrectly match things that higher ones would have caught.
+ /// The most restrictive expressions should appear first
+ /// </remarks>
+ private static readonly Regex[] EpisodeExpressions = new[]
+ {
+ new Regex(
+ @".*\\[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$",
+ RegexOptions.Compiled),
+ // 01x02 blah.avi S01x01 balh.avi
+ new Regex(
+ @".*\\[s|S](?<seasonnumber>\d{1,2})x?[e|E](?<epnumber>\d{1,3})[^\\]*$",
+ RegexOptions.Compiled),
+ // S01E02 blah.avi, S01xE01 blah.avi
+ new Regex(
+ @".*\\(?<seriesname>[^\\]*)[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$",
+ RegexOptions.Compiled),
+ // 01x02 blah.avi S01x01 balh.avi
+ new Regex(
+ @".*\\(?<seriesname>[^\\]*)[s|S](?<seasonnumber>\d{1,2})[x|X|\.]?[e|E](?<epnumber>\d{1,3})[^\\]*$",
+ RegexOptions.Compiled)
+ // S01E02 blah.avi, S01xE01 blah.avi
+ };
+
+ /// <summary>
+ /// To avoid the following matching movies they are only valid when contained in a folder which has been matched as a being season
+ /// </summary>
+ private static readonly Regex[] EpisodeExpressionsInASeasonFolder = new[]
+ {
+ new Regex(
+ @".*\\(?<epnumber>\d{1,2})\s?-\s?[^\\]*$",
+ RegexOptions.Compiled),
+ // 01 - blah.avi, 01-blah.avi
+ new Regex(
+ @".*\\(?<epnumber>\d{1,2})[^\d\\]*[^\\]*$",
+ RegexOptions.Compiled),
+ // 01.avi, 01.blah.avi "01 - 22 blah.avi"
+ new Regex(
+ @".*\\(?<seasonnumber>\d)(?<epnumber>\d{1,2})[^\d\\]+[^\\]*$",
+ RegexOptions.Compiled),
+ // 01.avi, 01.blah.avi
+ new Regex(
+ @".*\\\D*\d+(?<epnumber>\d{2})",
+ RegexOptions.Compiled)
+ // hell0 - 101 - hello.avi
+
+ };
+
+ public static int? GetSeasonNumberFromPath(string path)
+ {
+ // Look for one of the season folder names
+ foreach (var name in SeasonFolderNames)
+ {
+ int index = path.IndexOf(name, StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ return GetSeasonNumberFromPathSubstring(path.Substring(index + name.Length));
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel")
+ /// </summary>
+ private static int? GetSeasonNumberFromPathSubstring(string path)
+ {
+ int numericStart = -1;
+ int length = 0;
+
+ // Find out where the numbers start, and then keep going until they end
+ for (int i = 0; i < path.Length; i++)
+ {
+ if (char.IsNumber(path, i))
+ {
+ if (numericStart == -1)
+ {
+ numericStart = i;
+ }
+ length++;
+ }
+ else if (numericStart != -1)
+ {
+ break;
+ }
+ }
+
+ if (numericStart == -1)
+ {
+ return null;
+ }
+
+ return int.Parse(path.Substring(numericStart, length));
+ }
+
+ public static bool IsSeasonFolder(string path)
+ {
+ return GetSeasonNumberFromPath(path) != null;
+ }
+
+ public static bool IsSeriesFolder(string path, IEnumerable<WIN32_FIND_DATA> fileSystemChildren)
+ {
+ // A folder with more than 3 non-season folders in will not becounted as a series
+ var nonSeriesFolders = 0;
+
+ foreach (var child in fileSystemChildren)
+ {
+ if (child.IsHidden || child.IsSystemFile)
+ {
+ continue;
+ }
+
+ if (child.IsDirectory)
+ {
+ if (IsSeasonFolder(child.Path))
+ {
+ return true;
+ }
+
+ nonSeriesFolders++;
+
+ if (nonSeriesFolders >= 3)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (EntityResolutionHelper.IsVideoFile(child.Path) &&
+ !string.IsNullOrEmpty(EpisodeNumberFromFile(child.Path, false)))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static string EpisodeNumberFromFile(string fullPath, bool isInSeason)
+ {
+ string fl = fullPath.ToLower();
+ foreach (var r in EpisodeExpressions)
+ {
+ Match m = r.Match(fl);
+ if (m.Success)
+ return m.Groups["epnumber"].Value;
+ }
+ if (isInSeason)
+ {
+ var match = EpisodeExpressionsInASeasonFolder.Select(r => r.Match(fl))
+ .FirstOrDefault(m => m.Success);
+
+ if (match != null)
+ {
+ return match.Value;
+ }
+ }
+
+ return null;
+ }
+
+ public static string SeasonNumberFromEpisodeFile(string fullPath)
+ {
+ string fl = fullPath.ToLower();
+ foreach (var r in EpisodeExpressions)
+ {
+ Match m = r.Match(fl);
+ if (m.Success)
+ {
+ Group g = m.Groups["seasonnumber"];
+ if (g != null)
+ return g.Value;
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public static List<DayOfWeek> GetAirDays(string day)
+ {
+ if (!string.IsNullOrWhiteSpace(day))
+ {
+ if (day.Equals("Daily", StringComparison.OrdinalIgnoreCase))
+ {
+ return new List<DayOfWeek>
+ {
+ DayOfWeek.Sunday,
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday
+ };
+ }
+
+ DayOfWeek value;
+
+ if (Enum.TryParse(day, true, out value))
+ {
+ return new List<DayOfWeek>
+ {
+ value
+ };
+ }
+
+ Logger.LogWarning("Invalid value passed into GetAirDays: {0}", day);
+
+ return new List<DayOfWeek>
+ {
+ };
+ }
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/VideoResolver.cs b/MediaBrowser.Controller/Resolvers/VideoResolver.cs
index bc3be5e434..bfb364349b 100644
--- a/MediaBrowser.Controller/Resolvers/VideoResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/VideoResolver.cs
@@ -1,100 +1,73 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Controller.IO;
-using System.ComponentModel.Composition;
-using System.IO;
-
-namespace MediaBrowser.Controller.Resolvers
-{
- /// <summary>
- /// Resolves a Path into a Video
- /// </summary>
- [Export(typeof(IBaseItemResolver))]
- public class VideoResolver : BaseVideoResolver<Video>
- {
- public override ResolverPriority Priority
- {
- get { return ResolverPriority.Last; }
- }
- }
-
- /// <summary>
- /// Resolves a Path into a Video or Video subclass
- /// </summary>
- public abstract class BaseVideoResolver<T> : BaseItemResolver<T>
- where T : Video, new()
- {
- protected override T Resolve(ItemResolveEventArgs args)
- {
- // If the path is a file check for a matching extensions
- if (!args.IsDirectory)
- {
- if (FileSystemHelper.IsVideoFile(args.Path))
- {
- VideoType type = Path.GetExtension(args.Path).EndsWith("iso", System.StringComparison.OrdinalIgnoreCase) ? VideoType.Iso : VideoType.VideoFile;
-
- return new T
- {
- VideoType = type,
- Path = args.Path
- };
- }
- }
-
- else
- {
- // If the path is a folder, check if it's bluray or dvd
- T item = ResolveFromFolderName(args.Path);
-
- if (item != null)
- {
- return item;
- }
-
- // Also check the subfolders for bluray or dvd
- for (int i = 0; i < args.FileSystemChildren.Length; i++)
- {
- var folder = args.FileSystemChildren[i];
-
- if (!folder.IsDirectory)
- {
- continue;
- }
-
- item = ResolveFromFolderName(folder.Path);
-
- if (item != null)
- {
- return item;
- }
- }
- }
-
- return null;
- }
-
- private T ResolveFromFolderName(string folder)
- {
- if (folder.IndexOf("video_ts", System.StringComparison.OrdinalIgnoreCase) != -1)
- {
- return new T
- {
- VideoType = VideoType.Dvd,
- Path = Path.GetDirectoryName(folder)
- };
- }
- if (folder.IndexOf("bdmv", System.StringComparison.OrdinalIgnoreCase) != -1)
- {
- return new T
- {
- VideoType = VideoType.BluRay,
- Path = Path.GetDirectoryName(folder)
- };
- }
-
- return null;
- }
-
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class VideoResolver : BaseVideoResolver<Video>
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+ }
+
+ /// <summary>
+ /// Resolves a Path into a Video or Video subclass
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseVideoResolver<T> : BaseItemResolver<T>
+ where T : Video, new()
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>`0.</returns>
+ protected override T Resolve(ItemResolveArgs args)
+ {
+ // If the path is a file check for a matching extensions
+ if (!args.IsDirectory)
+ {
+ if (EntityResolutionHelper.IsVideoFile(args.Path))
+ {
+ var extension = Path.GetExtension(args.Path);
+
+ var type = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
+ VideoType.Iso : VideoType.VideoFile;
+
+ return new T
+ {
+ VideoType = type,
+ Path = 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(T item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.VideoFormat = item.Path.IndexOf("[3d]", StringComparison.OrdinalIgnoreCase) != -1 ? VideoFormat.Digital3D : item.Path.IndexOf("[sbs3d]", StringComparison.OrdinalIgnoreCase) != -1 ? VideoFormat.Sbs3D : VideoFormat.Standard;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs
new file mode 100644
index 0000000000..21f1bce5ac
--- /dev/null
+++ b/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ScheduledTasks
+{
+ [Export(typeof(IScheduledTask))]
+ class ChapterImagesTask : BaseScheduledTask<Kernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ return new BaseTaskTrigger[]
+ {
+ new DailyTrigger { TimeOfDay = TimeSpan.FromHours(4) }
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ var videos = Kernel.RootFolder.RecursiveChildren.OfType<Video>().Where(v => v.Chapters != null).ToList();
+
+ var numComplete = 0;
+
+ var tasks = videos.Select(v => Task.Run(async () =>
+ {
+ try
+ {
+ await Kernel.FFMpegManager.PopulateChapterImages(v, cancellationToken, true, true);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error creating chapter images for {0}", ex, v.Name);
+ }
+ finally
+ {
+ lock (progress)
+ {
+ numComplete++;
+ double percent = numComplete;
+ percent /= videos.Count;
+
+ progress.Report(new TaskProgress { PercentComplete = 100 * percent });
+ }
+ }
+ }));
+
+ return Task.WhenAll(tasks);
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Create video chapter thumbnails"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Creates thumbnails for videos that have chapters."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public override string Category
+ {
+ get
+ {
+ return "Library";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/ScheduledTasks/ImageCleanupTask.cs b/MediaBrowser.Controller/ScheduledTasks/ImageCleanupTask.cs
new file mode 100644
index 0000000000..c16f714dbc
--- /dev/null
+++ b/MediaBrowser.Controller/ScheduledTasks/ImageCleanupTask.cs
@@ -0,0 +1,202 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ScheduledTasks
+{
+ /// <summary>
+ /// Class ImageCleanupTask
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class ImageCleanupTask : BaseScheduledTask<Kernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ return new BaseTaskTrigger[]
+ {
+ new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override async Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ await EnsureChapterImages(cancellationToken).ConfigureAwait(false);
+
+ // First gather all image files
+ var files = GetFiles(Kernel.FFMpegManager.AudioImagesDataPath)
+ .Concat(GetFiles(Kernel.FFMpegManager.VideoImagesDataPath))
+ .Concat(GetFiles(Kernel.ProviderManager.ImagesDataPath))
+ .ToList();
+
+ // Now gather all items
+ var items = Kernel.RootFolder.RecursiveChildren.ToList();
+ items.Add(Kernel.RootFolder);
+
+ // Determine all possible image paths
+ var pathsInUse = items.SelectMany(GetPathsInUse)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(p => p, StringComparer.OrdinalIgnoreCase);
+
+ var numComplete = 0;
+
+ var tasks = files.Select(file => Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!pathsInUse.ContainsKey(file))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ File.Delete(file);
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error deleting {0}", ex, file);
+ }
+ }
+
+ // Update progress
+ lock (progress)
+ {
+ numComplete++;
+ double percent = numComplete;
+ percent /= files.Count;
+
+ progress.Report(new TaskProgress { PercentComplete = 100 * percent });
+ }
+ }));
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Ensures the chapter images.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private Task EnsureChapterImages(CancellationToken cancellationToken)
+ {
+ var videos = Kernel.RootFolder.RecursiveChildren.OfType<Video>().Where(v => v.Chapters != null).ToList();
+
+ var tasks = videos.Select(v => Task.Run(async () =>
+ {
+ await Kernel.FFMpegManager.PopulateChapterImages(v, cancellationToken, false, true);
+ }));
+
+ return Task.WhenAll(tasks);
+ }
+
+ /// <summary>
+ /// Gets the paths in use.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ private IEnumerable<string> GetPathsInUse(BaseItem item)
+ {
+ IEnumerable<string> images = new List<string> { };
+
+ if (item.Images != null)
+ {
+ images = images.Concat(item.Images.Values);
+ }
+
+ if (item.BackdropImagePaths != null)
+ {
+ images = images.Concat(item.BackdropImagePaths);
+ }
+
+ if (item.ScreenshotImagePaths != null)
+ {
+ images = images.Concat(item.ScreenshotImagePaths);
+ }
+
+ var video = item as Video;
+
+ if (video != null && video.Chapters != null)
+ {
+ images = images.Concat(video.Chapters.Where(i => !string.IsNullOrEmpty(i.ImagePath)).Select(i => i.ImagePath));
+ }
+
+ if (item.LocalTrailers != null)
+ {
+ foreach (var subItem in item.LocalTrailers)
+ {
+ images = images.Concat(GetPathsInUse(subItem));
+ }
+ }
+
+ var movie = item as Movie;
+
+ if (movie != null && movie.SpecialFeatures != null)
+ {
+ foreach (var subItem in movie.SpecialFeatures)
+ {
+ images = images.Concat(GetPathsInUse(subItem));
+ }
+ }
+
+ return images;
+ }
+
+ /// <summary>
+ /// Gets the files.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ private IEnumerable<string> GetFiles(string path)
+ {
+ return Directory.EnumerateFiles(path, "*.jpg", SearchOption.AllDirectories).Concat(Directory.EnumerateFiles(path, "*.png", SearchOption.AllDirectories));
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Images cleanup"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Deletes downloaded and extracted images that are no longer being used."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public override string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/ScheduledTasks/PeopleValidationTask.cs b/MediaBrowser.Controller/ScheduledTasks/PeopleValidationTask.cs
new file mode 100644
index 0000000000..a5f87f212b
--- /dev/null
+++ b/MediaBrowser.Controller/ScheduledTasks/PeopleValidationTask.cs
@@ -0,0 +1,72 @@
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ScheduledTasks
+{
+ /// <summary>
+ /// Class PeopleValidationTask
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class PeopleValidationTask : BaseScheduledTask<Kernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ return new BaseTaskTrigger[]
+ {
+ new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) },
+
+ new IntervalTrigger{ Interval = TimeSpan.FromHours(12)}
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ return Kernel.LibraryManager.ValidatePeople(cancellationToken, progress);
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Refresh people"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Updates metadata for actors, artists and directors in your media library."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public override string Category
+ {
+ get
+ {
+ return "Library";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/ScheduledTasks/PluginUpdateTask.cs b/MediaBrowser.Controller/ScheduledTasks/PluginUpdateTask.cs
new file mode 100644
index 0000000000..3a700d94d4
--- /dev/null
+++ b/MediaBrowser.Controller/ScheduledTasks/PluginUpdateTask.cs
@@ -0,0 +1,113 @@
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ScheduledTasks
+{
+ /// <summary>
+ /// Plugin Update Task
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class PluginUpdateTask : BaseScheduledTask<Kernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ return new BaseTaskTrigger[] {
+
+ // 1:30am
+ new DailyTrigger { TimeOfDay = TimeSpan.FromHours(1.5) },
+
+ new IntervalTrigger { Interval = TimeSpan.FromHours(2)}
+ };
+ }
+
+ /// <summary>
+ /// Update installed plugins
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override async Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ progress.Report(new TaskProgress { Description = "Checking for plugin updates", PercentComplete = 0 });
+
+ var packagesToInstall = (await Kernel.InstallationManager.GetAvailablePluginUpdates(true, cancellationToken).ConfigureAwait(false)).ToList();
+
+ progress.Report(new TaskProgress { PercentComplete = 10 });
+
+ var numComplete = 0;
+
+ // Create tasks for each one
+ var tasks = packagesToInstall.Select(i => Task.Run(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ await Kernel.InstallationManager.InstallPackage(i, new Progress<double> { }, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // InstallPackage has it's own inner cancellation token, so only throw this if it's ours
+ if (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ }
+ catch (HttpException ex)
+ {
+ Logger.ErrorException("Error downloading {0}", ex, i.name);
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error updating {0}", ex, i.name);
+ }
+
+ // Update progress
+ lock (progress)
+ {
+ numComplete++;
+ double percent = numComplete;
+ percent /= packagesToInstall.Count;
+
+ progress.Report(new TaskProgress { PercentComplete = (90 * percent) + 10 });
+ }
+ }));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Check for plugin updates"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Downloads and installs updates for plugins that are configured to update automatically."; }
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/ScheduledTasks/RefreshMediaLibraryTask.cs b/MediaBrowser.Controller/ScheduledTasks/RefreshMediaLibraryTask.cs
new file mode 100644
index 0000000000..b01019fce3
--- /dev/null
+++ b/MediaBrowser.Controller/ScheduledTasks/RefreshMediaLibraryTask.cs
@@ -0,0 +1,78 @@
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ScheduledTasks
+{
+ /// <summary>
+ /// Class RefreshMediaLibraryTask
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class RefreshMediaLibraryTask : BaseScheduledTask<Kernel>
+ {
+ /// <summary>
+ /// Gets the default triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ return new BaseTaskTrigger[] {
+
+ new StartupTrigger(Kernel),
+
+ new SystemEventTrigger{ SystemEvent = SystemEvent.WakeFromSleep},
+
+ new IntervalTrigger{ Interval = TimeSpan.FromHours(2)}
+ };
+ }
+
+ /// <summary>
+ /// Executes the internal.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ progress.Report(new TaskProgress { PercentComplete = 0 });
+
+ return Kernel.LibraryManager.ValidateMediaLibrary(progress, cancellationToken);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Scan media library"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Scans your media library and refreshes metatata based on configuration."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public override string Category
+ {
+ get
+ {
+ return "Library";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/ServerApplicationPaths.cs b/MediaBrowser.Controller/ServerApplicationPaths.cs
index f657ee259d..a376afed8a 100644
--- a/MediaBrowser.Controller/ServerApplicationPaths.cs
+++ b/MediaBrowser.Controller/ServerApplicationPaths.cs
@@ -1,278 +1,334 @@
-using System.IO;
-using MediaBrowser.Common.Kernel;
-
-namespace MediaBrowser.Controller
-{
- /// <summary>
- /// Extends BaseApplicationPaths to add paths that are only applicable on the server
- /// </summary>
- public class ServerApplicationPaths : BaseApplicationPaths
- {
- private string _rootFolderPath;
- /// <summary>
- /// Gets the path to the root media directory
- /// </summary>
- public string RootFolderPath
- {
- get
- {
- if (_rootFolderPath == null)
- {
- _rootFolderPath = Path.Combine(ProgramDataPath, "root");
- if (!Directory.Exists(_rootFolderPath))
- {
- Directory.CreateDirectory(_rootFolderPath);
- }
- }
- return _rootFolderPath;
- }
- }
-
- private string _ibnPath;
- /// <summary>
- /// Gets the path to the Images By Name directory
- /// </summary>
- public string ImagesByNamePath
- {
- get
- {
- if (_ibnPath == null)
- {
- _ibnPath = Path.Combine(ProgramDataPath, "ImagesByName");
- if (!Directory.Exists(_ibnPath))
- {
- Directory.CreateDirectory(_ibnPath);
- }
- }
-
- return _ibnPath;
- }
- }
-
- private string _PeoplePath;
- /// <summary>
- /// Gets the path to the People directory
- /// </summary>
- public string PeoplePath
- {
- get
- {
- if (_PeoplePath == null)
- {
- _PeoplePath = Path.Combine(ImagesByNamePath, "People");
- if (!Directory.Exists(_PeoplePath))
- {
- Directory.CreateDirectory(_PeoplePath);
- }
- }
-
- return _PeoplePath;
- }
- }
-
- private string _GenrePath;
- /// <summary>
- /// Gets the path to the Genre directory
- /// </summary>
- public string GenrePath
- {
- get
- {
- if (_GenrePath == null)
- {
- _GenrePath = Path.Combine(ImagesByNamePath, "Genre");
- if (!Directory.Exists(_GenrePath))
- {
- Directory.CreateDirectory(_GenrePath);
- }
- }
-
- return _GenrePath;
- }
- }
-
- private string _StudioPath;
- /// <summary>
- /// Gets the path to the Studio directory
- /// </summary>
- public string StudioPath
- {
- get
- {
- if (_StudioPath == null)
- {
- _StudioPath = Path.Combine(ImagesByNamePath, "Studio");
- if (!Directory.Exists(_StudioPath))
- {
- Directory.CreateDirectory(_StudioPath);
- }
- }
-
- return _StudioPath;
- }
- }
-
- private string _yearPath;
- /// <summary>
- /// Gets the path to the Year directory
- /// </summary>
- public string YearPath
- {
- get
- {
- if (_yearPath == null)
- {
- _yearPath = Path.Combine(ImagesByNamePath, "Year");
- if (!Directory.Exists(_yearPath))
- {
- Directory.CreateDirectory(_yearPath);
- }
- }
-
- return _yearPath;
- }
- }
-
- private string _userConfigurationDirectoryPath;
- /// <summary>
- /// Gets the path to the user configuration directory
- /// </summary>
- public string UserConfigurationDirectoryPath
- {
- get
- {
- if (_userConfigurationDirectoryPath == null)
- {
- _userConfigurationDirectoryPath = Path.Combine(ConfigurationDirectoryPath, "user");
- if (!Directory.Exists(_userConfigurationDirectoryPath))
- {
- Directory.CreateDirectory(_userConfigurationDirectoryPath);
- }
- }
- return _userConfigurationDirectoryPath;
- }
- }
-
- private string _CacheDirectory;
- /// <summary>
- /// Gets the folder path to the cache directory
- /// </summary>
- public string CacheDirectory
- {
- get
- {
- if (_CacheDirectory == null)
- {
- _CacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "cache");
-
- if (!Directory.Exists(_CacheDirectory))
- {
- Directory.CreateDirectory(_CacheDirectory);
- }
- }
-
- return _CacheDirectory;
- }
- }
-
- private string _FFProbeAudioCacheDirectory;
- /// <summary>
- /// Gets the folder path to the ffprobe audio cache directory
- /// </summary>
- public string FFProbeAudioCacheDirectory
- {
- get
- {
- if (_FFProbeAudioCacheDirectory == null)
- {
- _FFProbeAudioCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-audio");
-
- if (!Directory.Exists(_FFProbeAudioCacheDirectory))
- {
- Directory.CreateDirectory(_FFProbeAudioCacheDirectory);
- }
- }
-
- return _FFProbeAudioCacheDirectory;
- }
- }
-
- private string _FFProbeVideoCacheDirectory;
- /// <summary>
- /// Gets the folder path to the ffprobe video cache directory
- /// </summary>
- public string FFProbeVideoCacheDirectory
- {
- get
- {
- if (_FFProbeVideoCacheDirectory == null)
- {
- _FFProbeVideoCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-video");
-
- if (!Directory.Exists(_FFProbeVideoCacheDirectory))
- {
- Directory.CreateDirectory(_FFProbeVideoCacheDirectory);
- }
- }
-
- return _FFProbeVideoCacheDirectory;
- }
- }
-
- private string _FFMpegDirectory;
- /// <summary>
- /// Gets the folder path to ffmpeg
- /// </summary>
- public string FFMpegDirectory
- {
- get
- {
- if (_FFMpegDirectory == null)
- {
- _FFMpegDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "FFMpeg");
-
- if (!Directory.Exists(_FFMpegDirectory))
- {
- Directory.CreateDirectory(_FFMpegDirectory);
- }
- }
-
- return _FFMpegDirectory;
- }
- }
-
- private string _FFMpegPath;
- /// <summary>
- /// Gets the path to ffmpeg.exe
- /// </summary>
- public string FFMpegPath
- {
- get
- {
- if (_FFMpegPath == null)
- {
- _FFMpegPath = Path.Combine(FFMpegDirectory, "ffmpeg.exe");
- }
-
- return _FFMpegPath;
- }
- }
-
- private string _FFProbePath;
- /// <summary>
- /// Gets the path to ffprobe.exe
- /// </summary>
- public string FFProbePath
- {
- get
- {
- if (_FFProbePath == null)
- {
- _FFProbePath = Path.Combine(FFMpegDirectory, "ffprobe.exe");
- }
-
- return _FFProbePath;
- }
- }
- }
-}
+using MediaBrowser.Common.Kernel;
+using System.IO;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Extends BaseApplicationPaths to add paths that are only applicable on the server
+ /// </summary>
+ public class ServerApplicationPaths : BaseApplicationPaths
+ {
+ /// <summary>
+ /// The _root folder path
+ /// </summary>
+ private string _rootFolderPath;
+ /// <summary>
+ /// Gets the path to the base root media directory
+ /// </summary>
+ /// <value>The root folder path.</value>
+ public string RootFolderPath
+ {
+ get
+ {
+ if (_rootFolderPath == null)
+ {
+ _rootFolderPath = Path.Combine(ProgramDataPath, "Root");
+ if (!Directory.Exists(_rootFolderPath))
+ {
+ Directory.CreateDirectory(_rootFolderPath);
+ }
+ }
+ return _rootFolderPath;
+ }
+ }
+
+ /// <summary>
+ /// The _default user views path
+ /// </summary>
+ private string _defaultUserViewsPath;
+ /// <summary>
+ /// Gets the path to the default user view directory. Used if no specific user view is defined.
+ /// </summary>
+ /// <value>The default user views path.</value>
+ public string DefaultUserViewsPath
+ {
+ get
+ {
+ if (_defaultUserViewsPath == null)
+ {
+ _defaultUserViewsPath = Path.Combine(RootFolderPath, "Default");
+ if (!Directory.Exists(_defaultUserViewsPath))
+ {
+ Directory.CreateDirectory(_defaultUserViewsPath);
+ }
+ }
+ return _defaultUserViewsPath;
+ }
+ }
+
+ /// <summary>
+ /// The _localization path
+ /// </summary>
+ private string _localizationPath;
+ /// <summary>
+ /// Gets the path to localization data.
+ /// </summary>
+ /// <value>The localization path.</value>
+ public string LocalizationPath
+ {
+ get
+ {
+ if (_localizationPath == null)
+ {
+ _localizationPath = Path.Combine(ProgramDataPath, "Localization");
+ if (!Directory.Exists(_localizationPath))
+ {
+ Directory.CreateDirectory(_localizationPath);
+ }
+ }
+ return _localizationPath;
+ }
+ }
+
+ /// <summary>
+ /// The _ibn path
+ /// </summary>
+ private string _ibnPath;
+ /// <summary>
+ /// Gets the path to the Images By Name directory
+ /// </summary>
+ /// <value>The images by name path.</value>
+ public string ImagesByNamePath
+ {
+ get
+ {
+ if (_ibnPath == null)
+ {
+ _ibnPath = Path.Combine(ProgramDataPath, "ImagesByName");
+ if (!Directory.Exists(_ibnPath))
+ {
+ Directory.CreateDirectory(_ibnPath);
+ }
+ }
+
+ return _ibnPath;
+ }
+ }
+
+ /// <summary>
+ /// The _people path
+ /// </summary>
+ private string _peoplePath;
+ /// <summary>
+ /// Gets the path to the People directory
+ /// </summary>
+ /// <value>The people path.</value>
+ public string PeoplePath
+ {
+ get
+ {
+ if (_peoplePath == null)
+ {
+ _peoplePath = Path.Combine(ImagesByNamePath, "People");
+ if (!Directory.Exists(_peoplePath))
+ {
+ Directory.CreateDirectory(_peoplePath);
+ }
+ }
+
+ return _peoplePath;
+ }
+ }
+
+ /// <summary>
+ /// The _genre path
+ /// </summary>
+ private string _genrePath;
+ /// <summary>
+ /// Gets the path to the Genre directory
+ /// </summary>
+ /// <value>The genre path.</value>
+ public string GenrePath
+ {
+ get
+ {
+ if (_genrePath == null)
+ {
+ _genrePath = Path.Combine(ImagesByNamePath, "Genre");
+ if (!Directory.Exists(_genrePath))
+ {
+ Directory.CreateDirectory(_genrePath);
+ }
+ }
+
+ return _genrePath;
+ }
+ }
+
+ /// <summary>
+ /// The _studio path
+ /// </summary>
+ private string _studioPath;
+ /// <summary>
+ /// Gets the path to the Studio directory
+ /// </summary>
+ /// <value>The studio path.</value>
+ public string StudioPath
+ {
+ get
+ {
+ if (_studioPath == null)
+ {
+ _studioPath = Path.Combine(ImagesByNamePath, "Studio");
+ if (!Directory.Exists(_studioPath))
+ {
+ Directory.CreateDirectory(_studioPath);
+ }
+ }
+
+ return _studioPath;
+ }
+ }
+
+ /// <summary>
+ /// The _year path
+ /// </summary>
+ private string _yearPath;
+ /// <summary>
+ /// Gets the path to the Year directory
+ /// </summary>
+ /// <value>The year path.</value>
+ public string YearPath
+ {
+ get
+ {
+ if (_yearPath == null)
+ {
+ _yearPath = Path.Combine(ImagesByNamePath, "Year");
+ if (!Directory.Exists(_yearPath))
+ {
+ Directory.CreateDirectory(_yearPath);
+ }
+ }
+
+ return _yearPath;
+ }
+ }
+
+ /// <summary>
+ /// The _general path
+ /// </summary>
+ private string _generalPath;
+ /// <summary>
+ /// Gets the path to the General IBN directory
+ /// </summary>
+ /// <value>The general path.</value>
+ public string GeneralPath
+ {
+ get
+ {
+ if (_generalPath == null)
+ {
+ _generalPath = Path.Combine(ImagesByNamePath, "General");
+ if (!Directory.Exists(_generalPath))
+ {
+ Directory.CreateDirectory(_generalPath);
+ }
+ }
+
+ return _generalPath;
+ }
+ }
+
+ /// <summary>
+ /// The _ratings path
+ /// </summary>
+ private string _ratingsPath;
+ /// <summary>
+ /// Gets the path to the Ratings IBN directory
+ /// </summary>
+ /// <value>The ratings path.</value>
+ public string RatingsPath
+ {
+ get
+ {
+ if (_ratingsPath == null)
+ {
+ _ratingsPath = Path.Combine(ImagesByNamePath, "Ratings");
+ if (!Directory.Exists(_ratingsPath))
+ {
+ Directory.CreateDirectory(_ratingsPath);
+ }
+ }
+
+ return _ratingsPath;
+ }
+ }
+
+ /// <summary>
+ /// The _user configuration directory path
+ /// </summary>
+ private string _userConfigurationDirectoryPath;
+ /// <summary>
+ /// Gets the path to the user configuration directory
+ /// </summary>
+ /// <value>The user configuration directory path.</value>
+ public string UserConfigurationDirectoryPath
+ {
+ get
+ {
+ if (_userConfigurationDirectoryPath == null)
+ {
+ _userConfigurationDirectoryPath = Path.Combine(ConfigurationDirectoryPath, "users");
+ if (!Directory.Exists(_userConfigurationDirectoryPath))
+ {
+ Directory.CreateDirectory(_userConfigurationDirectoryPath);
+ }
+ }
+ return _userConfigurationDirectoryPath;
+ }
+ }
+
+ /// <summary>
+ /// The _f F MPEG stream cache path
+ /// </summary>
+ private string _fFMpegStreamCachePath;
+ /// <summary>
+ /// Gets the FF MPEG stream cache path.
+ /// </summary>
+ /// <value>The FF MPEG stream cache path.</value>
+ public string FFMpegStreamCachePath
+ {
+ get
+ {
+ if (_fFMpegStreamCachePath == null)
+ {
+ _fFMpegStreamCachePath = Path.Combine(CachePath, "ffmpeg-streams");
+
+ if (!Directory.Exists(_fFMpegStreamCachePath))
+ {
+ Directory.CreateDirectory(_fFMpegStreamCachePath);
+ }
+ }
+
+ return _fFMpegStreamCachePath;
+ }
+ }
+
+ /// <summary>
+ /// The _media tools path
+ /// </summary>
+ private string _mediaToolsPath;
+ /// <summary>
+ /// Gets the folder path to tools
+ /// </summary>
+ /// <value>The media tools path.</value>
+ public string MediaToolsPath
+ {
+ get
+ {
+ if (_mediaToolsPath == null)
+ {
+ _mediaToolsPath = Path.Combine(ProgramDataPath, "MediaTools");
+
+ if (!Directory.Exists(_mediaToolsPath))
+ {
+ Directory.CreateDirectory(_mediaToolsPath);
+ }
+ }
+
+ return _mediaToolsPath;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Sorting/BaseItemComparer.cs b/MediaBrowser.Controller/Sorting/BaseItemComparer.cs
new file mode 100644
index 0000000000..6e49f396f7
--- /dev/null
+++ b/MediaBrowser.Controller/Sorting/BaseItemComparer.cs
@@ -0,0 +1,231 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Sorting {
+ /// <summary>
+ /// Class BaseItemComparer
+ /// </summary>
+ public class BaseItemComparer : IComparer<BaseItem> {
+ /// <summary>
+ /// The _order
+ /// </summary>
+ private readonly SortOrder _order;
+ /// <summary>
+ /// The _property name
+ /// </summary>
+ private readonly string _propertyName;
+ /// <summary>
+ /// The _compare culture
+ /// </summary>
+ private readonly StringComparison _compareCulture = StringComparison.CurrentCultureIgnoreCase;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemComparer" /> class.
+ /// </summary>
+ /// <param name="order">The order.</param>
+ public BaseItemComparer(SortOrder order) {
+ _order = order;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemComparer" /> class.
+ /// </summary>
+ /// <param name="order">The order.</param>
+ /// <param name="compare">The compare.</param>
+ public BaseItemComparer(SortOrder order, StringComparison compare) {
+ _order = order;
+ _compareCulture = compare;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemComparer" /> class.
+ /// </summary>
+ /// <param name="property">The property.</param>
+ public BaseItemComparer(string property) {
+ _order = SortOrder.Custom;
+ _propertyName = property;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemComparer" /> class.
+ /// </summary>
+ /// <param name="property">The property.</param>
+ /// <param name="compare">The compare.</param>
+ public BaseItemComparer(string property, StringComparison compare) {
+ _order = SortOrder.Custom;
+ _propertyName = property;
+ _compareCulture = compare;
+ }
+
+ #region IComparer<BaseItem> Members
+
+ /// <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) {
+ int compare = 0;
+
+ switch (_order) {
+
+ case SortOrder.Date:
+ compare = -x.DateCreated.CompareTo(y.DateCreated);
+ break;
+
+ case SortOrder.Year:
+
+ var xProductionYear = x.ProductionYear ?? 0;
+ var yProductionYear = y.ProductionYear ?? 0;
+
+
+ compare = yProductionYear.CompareTo(xProductionYear);
+ break;
+
+ case SortOrder.Rating:
+
+ var xRating = x.CommunityRating ?? 0;
+ var yRating = y.CommunityRating ?? 0;
+
+ compare = yRating.CompareTo(xRating);
+ break;
+
+ case SortOrder.Runtime:
+ var xRuntime = x.RunTimeTicks ?? 0;
+ var yRuntime = y.RunTimeTicks ?? 0;
+
+ compare = xRuntime.CompareTo(yRuntime);
+ break;
+
+ case SortOrder.Custom:
+
+ Logger.LogDebugInfo("Sorting on custom field " + _propertyName);
+ var yProp = y.GetType().GetProperty(_propertyName);
+ var xProp = x.GetType().GetProperty(_propertyName);
+ if (yProp == null || xProp == null) break;
+ var yVal = yProp.GetValue(y, null);
+ var xVal = xProp.GetValue(x,null);
+ if (yVal == null && xVal == null) break;
+ if (yVal == null) return 1;
+ if (xVal == null) return -1;
+ compare = String.Compare(xVal.ToString(), yVal.ToString(),_compareCulture);
+ break;
+
+ default:
+ compare = 0;
+ break;
+ }
+
+ if (compare == 0) {
+
+ var name1 = x.SortName ?? x.Name ?? "";
+ var name2 = y.SortName ?? y.Name ?? "";
+
+ //if (Config.Instance.EnableAlphanumericSorting)
+ compare = AlphaNumericCompare(name1, name2,_compareCulture);
+ //else
+ // compare = String.Compare(name1,name2,_compareCulture);
+ }
+
+ return compare;
+ }
+
+
+ #endregion
+
+ /// <summary>
+ /// Alphas the numeric compare.
+ /// </summary>
+ /// <param name="s1">The s1.</param>
+ /// <param name="s2">The s2.</param>
+ /// <param name="compareCulture">The compare culture.</param>
+ /// <returns>System.Int32.</returns>
+ public static int AlphaNumericCompare(string s1, string s2, StringComparison compareCulture) {
+ // http://dotnetperls.com/Content/Alphanumeric-Sorting.aspx
+
+ int len1 = s1.Length;
+ int len2 = s2.Length;
+ int marker1 = 0;
+ int marker2 = 0;
+
+ // Walk through two the strings with two markers.
+ while (marker1 < len1 && marker2 < len2) {
+ char ch1 = s1[marker1];
+ char ch2 = s2[marker2];
+
+ // Some buffers we can build up characters in for each chunk.
+ var space1 = new char[len1];
+ var loc1 = 0;
+ var space2 = new char[len2];
+ var loc2 = 0;
+
+ // Walk through all following characters that are digits or
+ // characters in BOTH strings starting at the appropriate marker.
+ // Collect char arrays.
+ do {
+ space1[loc1++] = ch1;
+ marker1++;
+
+ if (marker1 < len1) {
+ ch1 = s1[marker1];
+ } else {
+ break;
+ }
+ } while (char.IsDigit(ch1) == char.IsDigit(space1[0]));
+
+ do {
+ space2[loc2++] = ch2;
+ marker2++;
+
+ if (marker2 < len2) {
+ ch2 = s2[marker2];
+ } else {
+ break;
+ }
+ } while (char.IsDigit(ch2) == char.IsDigit(space2[0]));
+
+ // If we have collected numbers, compare them numerically.
+ // Otherwise, if we have strings, compare them alphabetically.
+ var str1 = new string(space1);
+ var str2 = new string(space2);
+
+ var result = 0;
+
+ //biggest int - 2147483647
+ if (char.IsDigit(space1[0]) && char.IsDigit(space2[0]) /*&& str1.Length < 10 && str2.Length < 10*/) //this assumed the entire string was a number...
+ {
+ int thisNumericChunk;
+ var isValid = false;
+
+ if (int.TryParse(str1.Substring(0, str1.Length > 9 ? 10 : str1.Length), out thisNumericChunk))
+ {
+ int thatNumericChunk;
+
+ if (int.TryParse(str2.Substring(0, str2.Length > 9 ? 10 : str2.Length), out thatNumericChunk))
+ {
+ isValid = true;
+ result = thisNumericChunk.CompareTo(thatNumericChunk);
+ }
+ }
+
+ if (!isValid)
+ {
+ Logger.LogError("Error comparing numeric strings: " + str1 + "/" + str2);
+ result = String.Compare(str1, str2, compareCulture);
+ }
+
+ } else {
+ result = String.Compare(str1,str2,compareCulture);
+ }
+
+ if (result != 0) {
+ return result;
+ }
+ }
+ return len1 - len2;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Sorting/SortOrder.cs b/MediaBrowser.Controller/Sorting/SortOrder.cs
new file mode 100644
index 0000000000..3152ac67e4
--- /dev/null
+++ b/MediaBrowser.Controller/Sorting/SortOrder.cs
@@ -0,0 +1,33 @@
+
+namespace MediaBrowser.Controller.Sorting {
+ /// <summary>
+ /// Enum SortOrder
+ /// </summary>
+ public enum SortOrder {
+
+ /// <summary>
+ /// Sort by name
+ /// </summary>
+ Name,
+ /// <summary>
+ /// Sort by date added to the library
+ /// </summary>
+ Date,
+ /// <summary>
+ /// Sort by community rating
+ /// </summary>
+ Rating,
+ /// <summary>
+ /// Sort by runtime
+ /// </summary>
+ Runtime,
+ /// <summary>
+ /// Sort by year
+ /// </summary>
+ Year,
+ /// <summary>
+ /// Custom sort order added by plugins
+ /// </summary>
+ Custom
+ }
+}
diff --git a/MediaBrowser.Controller/Updates/InstallationManager.cs b/MediaBrowser.Controller/Updates/InstallationManager.cs
new file mode 100644
index 0000000000..63afa2ce88
--- /dev/null
+++ b/MediaBrowser.Controller/Updates/InstallationManager.cs
@@ -0,0 +1,486 @@
+using System.Security.Cryptography;
+using Ionic.Zip;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Updates
+{
+ /// <summary>
+ /// Manages all install, uninstall and update operations (both plugins and system)
+ /// </summary>
+ public class InstallationManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// The current installations
+ /// </summary>
+ public readonly List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations =
+ new List<Tuple<InstallationInfo, CancellationTokenSource>>();
+
+ /// <summary>
+ /// The completed installations
+ /// </summary>
+ public readonly ConcurrentBag<InstallationInfo> CompletedInstallations = new ConcurrentBag<InstallationInfo>();
+
+ #region PluginUninstalled Event
+ /// <summary>
+ /// Occurs when [plugin uninstalled].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
+
+ /// <summary>
+ /// Called when [plugin uninstalled].
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ private void OnPluginUninstalled(IPlugin plugin)
+ {
+ EventHelper.QueueEventIfNotNull(PluginUninstalled, this, new GenericEventArgs<IPlugin> { Argument = plugin });
+
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("PluginUninstalled", plugin.GetPluginInfo());
+ }
+ #endregion
+
+ #region PluginUpdated Event
+ /// <summary>
+ /// Occurs when [plugin updated].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated;
+ /// <summary>
+ /// Called when [plugin updated].
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ /// <param name="newVersion">The new version.</param>
+ public void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion)
+ {
+ Logger.Info("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.version, newVersion.classification);
+
+ EventHelper.QueueEventIfNotNull(PluginUpdated, this, new GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) });
+
+ Kernel.NotifyPendingRestart();
+ }
+ #endregion
+
+ #region PluginInstalled Event
+ /// <summary>
+ /// Occurs when [plugin updated].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
+ /// <summary>
+ /// Called when [plugin installed].
+ /// </summary>
+ /// <param name="package">The package.</param>
+ public void OnPluginInstalled(PackageVersionInfo package)
+ {
+ Logger.Info("New plugin installed: {0} {1} {2}", package.name, package.version, package.classification);
+
+ EventHelper.QueueEventIfNotNull(PluginInstalled, this, new GenericEventArgs<PackageVersionInfo> { Argument = package });
+
+ Kernel.NotifyPendingRestart();
+ }
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InstallationManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public InstallationManager(Kernel kernel)
+ : base(kernel)
+ {
+
+ }
+
+ /// <summary>
+ /// Gets all available packages.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="packageType">Type of the package.</param>
+ /// <param name="applicationVersion">The application version.</param>
+ /// <returns>Task{List{PackageInfo}}.</returns>
+ public async Task<IEnumerable<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken,
+ PackageType? packageType = null,
+ Version applicationVersion = null)
+ {
+ var data = new Dictionary<string, string> { { "key", Kernel.PluginSecurityManager.SupporterKey }, { "mac", NetUtils.GetMacAddress() } };
+
+ using (var json = await Kernel.HttpManager.Post(Controller.Kernel.MBAdminUrl + "service/package/retrieveall", data, Kernel.ResourcePools.Mb, cancellationToken).ConfigureAwait(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var packages = JsonSerializer.DeserializeFromStream<List<PackageInfo>>(json).ToList();
+
+ foreach (var package in packages)
+ {
+ package.versions = package.versions.Where(v => !string.IsNullOrWhiteSpace(v.sourceUrl))
+ .OrderByDescending(v => v.version).ToList();
+ }
+
+ if (packageType.HasValue)
+ {
+ packages = packages.Where(p => p.type == packageType.Value).ToList();
+ }
+
+ // If an app version was supplied, filter the versions for each package to only include supported versions
+ if (applicationVersion != null)
+ {
+ foreach (var package in packages)
+ {
+ package.versions = package.versions.Where(v => IsPackageVersionUpToDate(v, applicationVersion)).ToList();
+ }
+ }
+
+ // Remove packages with no versions
+ packages = packages.Where(p => p.versions.Any()).ToList();
+
+ return packages;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether [is package version up to date] [the specified package version info].
+ /// </summary>
+ /// <param name="packageVersionInfo">The package version info.</param>
+ /// <param name="applicationVersion">The application version.</param>
+ /// <returns><c>true</c> if [is package version up to date] [the specified package version info]; otherwise, <c>false</c>.</returns>
+ private bool IsPackageVersionUpToDate(PackageVersionInfo packageVersionInfo, Version applicationVersion)
+ {
+ if (string.IsNullOrEmpty(packageVersionInfo.requiredVersionStr))
+ {
+ return true;
+ }
+
+ Version requiredVersion;
+
+ return Version.TryParse(packageVersionInfo.requiredVersionStr, out requiredVersion) && applicationVersion >= requiredVersion;
+ }
+
+ /// <summary>
+ /// Gets the package.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="classification">The classification.</param>
+ /// <param name="version">The version.</param>
+ /// <returns>Task{PackageVersionInfo}.</returns>
+ public async Task<PackageVersionInfo> GetPackage(string name, PackageVersionClass classification, Version version)
+ {
+ var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false);
+
+ var package = packages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+ if (package == null)
+ {
+ return null;
+ }
+
+ return package.versions.FirstOrDefault(v => v.version.Equals(version) && v.classification == classification);
+ }
+
+ /// <summary>
+ /// Gets the latest compatible version.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="classification">The classification.</param>
+ /// <returns>Task{PackageVersionInfo}.</returns>
+ public async Task<PackageVersionInfo> GetLatestCompatibleVersion(string name, PackageVersionClass classification = PackageVersionClass.Release)
+ {
+ var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false);
+
+ return GetLatestCompatibleVersion(packages, name, classification);
+ }
+
+ /// <summary>
+ /// Gets the latest compatible version.
+ /// </summary>
+ /// <param name="availablePackages">The available packages.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="classification">The classification.</param>
+ /// <returns>PackageVersionInfo.</returns>
+ public PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, PackageVersionClass classification = PackageVersionClass.Release)
+ {
+ var package = availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+ if (package == null)
+ {
+ return null;
+ }
+
+ return package.versions
+ .OrderByDescending(v => v.version)
+ .FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, Kernel.ApplicationVersion));
+ }
+
+ /// <summary>
+ /// Gets the available plugin updates.
+ /// </summary>
+ /// <param name="withAutoUpdateEnabled">if set to <c>true</c> [with auto update enabled].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{PackageVersionInfo}}.</returns>
+ public async Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(bool withAutoUpdateEnabled, CancellationToken cancellationToken)
+ {
+ var catalog = await Kernel.InstallationManager.GetAvailablePackages(cancellationToken).ConfigureAwait(false);
+
+ var plugins = Kernel.Plugins;
+
+ if (withAutoUpdateEnabled)
+ {
+ plugins = plugins.Where(p => p.Configuration.EnableAutoUpdate);
+ }
+
+ // Figure out what needs to be installed
+ return plugins.Select(p =>
+ {
+ var latestPluginInfo = Kernel.InstallationManager.GetLatestCompatibleVersion(catalog, p.Name, p.Configuration.UpdateClass);
+
+ return latestPluginInfo != null && latestPluginInfo.version > p.Version ? latestPluginInfo : null;
+
+ }).Where(p => !CompletedInstallations.Any(i => i.Name.Equals(p.name, StringComparison.OrdinalIgnoreCase)))
+ .Where(p => p != null && !string.IsNullOrWhiteSpace(p.sourceUrl));
+ }
+
+ /// <summary>
+ /// Installs the package.
+ /// </summary>
+ /// <param name="package">The package.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">package</exception>
+ public async Task InstallPackage(PackageVersionInfo package, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException("package");
+ }
+
+ if (progress == null)
+ {
+ throw new ArgumentNullException("progress");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ var installationInfo = new InstallationInfo
+ {
+ Id = Guid.NewGuid(),
+ Name = package.name,
+ UpdateClass = package.classification,
+ Version = package.versionStr
+ };
+
+ var innerCancellationTokenSource = new CancellationTokenSource();
+
+ var tuple = new Tuple<InstallationInfo, CancellationTokenSource>(installationInfo, innerCancellationTokenSource);
+
+ // Add it to the in-progress list
+ lock (CurrentInstallations)
+ {
+ CurrentInstallations.Add(tuple);
+ }
+
+ var innerProgress = new ActionableProgress<double> { };
+
+ // Whenever the progress updates, update the outer progress object and InstallationInfo
+ innerProgress.RegisterAction(percent =>
+ {
+ progress.Report(percent);
+
+ installationInfo.PercentComplete = percent;
+ });
+
+ var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
+
+ Kernel.TcpManager.SendWebSocketMessage("PackageInstalling", installationInfo);
+
+ try
+ {
+ await InstallPackageInternal(package, innerProgress, linkedToken).ConfigureAwait(false);
+
+ lock (CurrentInstallations)
+ {
+ CurrentInstallations.Remove(tuple);
+ }
+
+ CompletedInstallations.Add(installationInfo);
+
+ Kernel.TcpManager.SendWebSocketMessage("PackageInstallationCompleted", installationInfo);
+ }
+ catch (OperationCanceledException)
+ {
+ lock (CurrentInstallations)
+ {
+ CurrentInstallations.Remove(tuple);
+ }
+
+ Logger.Info("Package installation cancelled: {0} {1}", package.name, package.versionStr);
+
+ Kernel.TcpManager.SendWebSocketMessage("PackageInstallationCancelled", installationInfo);
+
+ throw;
+ }
+ catch
+ {
+ lock (CurrentInstallations)
+ {
+ CurrentInstallations.Remove(tuple);
+ }
+
+ Kernel.TcpManager.SendWebSocketMessage("PackageInstallationFailed", installationInfo);
+
+ throw;
+ }
+ finally
+ {
+ // Dispose the progress object and remove the installation from the in-progress list
+
+ innerProgress.Dispose();
+ tuple.Item2.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Installs the package internal.
+ /// </summary>
+ /// <param name="package">The package.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task InstallPackageInternal(PackageVersionInfo package, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ // Target based on if it is an archive or single assembly
+ // zip archives are assumed to contain directory structures relative to our ProgramDataPath
+ var isArchive = string.Equals(Path.GetExtension(package.sourceUrl), ".zip", StringComparison.OrdinalIgnoreCase);
+ var target = isArchive ? Kernel.ApplicationPaths.ProgramDataPath : Path.Combine(Kernel.ApplicationPaths.PluginsPath, package.targetFilename);
+
+ // Download to temporary file so that, if interrupted, it won't destroy the existing installation
+ var tempFile = await Kernel.HttpManager.FetchToTempFile(package.sourceUrl, Kernel.ResourcePools.Mb, cancellationToken, progress).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Validate with a checksum
+ if (package.checksum != Guid.Empty) // support for legacy uploads for now
+ {
+ using (var crypto = new MD5CryptoServiceProvider())
+ using (var stream = new BufferedStream(File.OpenRead(tempFile), 100000))
+ {
+ var check = Guid.Parse(BitConverter.ToString(crypto.ComputeHash(stream)).Replace("-", String.Empty));
+ if (check != package.checksum)
+ {
+ throw new ApplicationException(string.Format("Download validation failed for {0}. Probably corrupted during transfer.", package.name));
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Success - move it to the real target based on type
+ if (isArchive)
+ {
+ try
+ {
+ // Extract to target in full - overwriting
+ using (var zipFile = ZipFile.Read(tempFile))
+ {
+ zipFile.ExtractAll(target, ExtractExistingFileAction.OverwriteSilently);
+ }
+ }
+ catch (IOException e)
+ {
+ Logger.ErrorException("Error attempting to extract archive from {0} to {1}", e, tempFile, target);
+ throw;
+ }
+
+ }
+ else
+ {
+ try
+ {
+ File.Copy(tempFile, target, true);
+ File.Delete(tempFile);
+ }
+ catch (IOException e)
+ {
+ Logger.ErrorException("Error attempting to move file from {0} to {1}", e, tempFile, target);
+ throw;
+ }
+ }
+
+ // Set last update time if we were installed before
+ var plugin = Kernel.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase));
+
+ if (plugin != null)
+ {
+ // Synchronize the UpdateClass value
+ if (plugin.Configuration.UpdateClass != package.classification)
+ {
+ plugin.Configuration.UpdateClass = package.classification;
+ plugin.SaveConfiguration();
+ }
+
+ OnPluginUpdated(plugin, package);
+ }
+ else
+ {
+ OnPluginInstalled(package);
+ }
+ }
+
+ /// <summary>
+ /// Uninstalls a plugin
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ /// <exception cref="System.ArgumentException"></exception>
+ public void UninstallPlugin(IPlugin plugin)
+ {
+ if (plugin.IsCorePlugin)
+ {
+ throw new ArgumentException(string.Format("{0} cannot be uninstalled because it is a core plugin.", plugin.Name));
+ }
+
+ plugin.OnUninstalling();
+
+ // Remove it the quick way for now
+ Kernel.RemovePlugin(plugin);
+
+ File.Delete(plugin.AssemblyFilePath);
+
+ OnPluginUninstalled(plugin);
+
+ Kernel.NotifyPendingRestart();
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ lock (CurrentInstallations)
+ {
+ foreach (var tuple in CurrentInstallations)
+ {
+ tuple.Item2.Dispose();
+ }
+
+ CurrentInstallations.Clear();
+ }
+ }
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs b/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs
index c3d436e667..4ae7a3991c 100644
--- a/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs
+++ b/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs
@@ -1,34 +1,37 @@
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Model.Weather;
-using System;
-using System.Net;
-using System.Net.Cache;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Weather
-{
- public abstract class BaseWeatherProvider : IDisposable
- {
- protected HttpClient HttpClient { get; private set; }
-
- protected BaseWeatherProvider()
- {
- var handler = new WebRequestHandler { };
-
- handler.AutomaticDecompression = DecompressionMethods.Deflate;
- handler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
-
- HttpClient = new HttpClient(handler);
- }
-
- public virtual void Dispose()
- {
- Logger.LogInfo("Disposing " + GetType().Name);
-
- HttpClient.Dispose();
- }
-
- public abstract Task<WeatherInfo> GetWeatherInfoAsync(string zipCode);
- }
-}
+using MediaBrowser.Model.Weather;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Weather
+{
+ /// <summary>
+ /// Class BaseWeatherProvider
+ /// </summary>
+ public abstract class BaseWeatherProvider : IDisposable
+ {
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ }
+
+ /// <summary>
+ /// Gets the weather info async.
+ /// </summary>
+ /// <param name="location">The location.</param>
+ /// <returns>Task{WeatherInfo}.</returns>
+ public abstract Task<WeatherInfo> GetWeatherInfoAsync(string location, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Weather/WeatherProvider.cs b/MediaBrowser.Controller/Weather/WeatherProvider.cs
index 0fc7288790..1560aa92c6 100644
--- a/MediaBrowser.Controller/Weather/WeatherProvider.cs
+++ b/MediaBrowser.Controller/Weather/WeatherProvider.cs
@@ -1,189 +1,311 @@
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Common.Serialization;
-using MediaBrowser.Model.Weather;
-using System;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Weather
-{
- /// <summary>
- /// Based on http://www.worldweatheronline.com/free-weather-feed.aspx
- /// The classes in this file are a reproduction of the json output, which will then be converted to our weather model classes
- /// </summary>
- [Export(typeof(BaseWeatherProvider))]
- public class WeatherProvider : BaseWeatherProvider
- {
- public override async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
- {
- if (string.IsNullOrWhiteSpace(zipCode))
- {
- return null;
- }
-
- const int numDays = 5;
- const string apiKey = "24902f60f1231941120109";
-
- string url = "http://free.worldweatheronline.com/feed/weather.ashx?q=" + zipCode + "&format=json&num_of_days=" + numDays + "&key=" + apiKey;
-
- Logger.LogInfo("Accessing weather from " + url);
-
- using (Stream stream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false))
- {
- WeatherData data = JsonSerializer.DeserializeFromStream<WeatherResult>(stream).data;
-
- return GetWeatherInfo(data);
- }
- }
-
- /// <summary>
- /// Converst the json output to our WeatherInfo model class
- /// </summary>
- private WeatherInfo GetWeatherInfo(WeatherData data)
- {
- var info = new WeatherInfo();
-
- if (data.current_condition != null)
- {
- if (data.current_condition.Any())
- {
- info.CurrentWeather = data.current_condition.First().ToWeatherStatus();
- }
- }
-
- if (data.weather != null)
- {
- info.Forecasts = data.weather.Select(w => w.ToWeatherForecast()).ToArray();
- }
-
- return info;
- }
- }
-
- class WeatherResult
- {
- public WeatherData data { get; set; }
- }
-
- public class WeatherData
- {
- public WeatherCondition[] current_condition { get; set; }
- public DailyWeatherInfo[] weather { get; set; }
- }
-
- public class WeatherCondition
- {
- public string temp_C { get; set; }
- public string temp_F { get; set; }
- public string humidity { get; set; }
- public string weatherCode { get; set; }
-
- public WeatherStatus ToWeatherStatus()
- {
- return new WeatherStatus
- {
- TemperatureCelsius = int.Parse(temp_C),
- TemperatureFahrenheit = int.Parse(temp_F),
- Humidity = int.Parse(humidity),
- Condition = DailyWeatherInfo.GetCondition(weatherCode)
- };
- }
- }
-
- public class DailyWeatherInfo
- {
- public string date { get; set; }
- public string precipMM { get; set; }
- public string tempMaxC { get; set; }
- public string tempMaxF { get; set; }
- public string tempMinC { get; set; }
- public string tempMinF { get; set; }
- public string weatherCode { get; set; }
- public string winddir16Point { get; set; }
- public string winddirDegree { get; set; }
- public string winddirection { get; set; }
- public string windspeedKmph { get; set; }
- public string windspeedMiles { get; set; }
-
- public WeatherForecast ToWeatherForecast()
- {
- return new WeatherForecast
- {
- Date = DateTime.Parse(date),
- HighTemperatureCelsius = int.Parse(tempMaxC),
- HighTemperatureFahrenheit = int.Parse(tempMaxF),
- LowTemperatureCelsius = int.Parse(tempMinC),
- LowTemperatureFahrenheit = int.Parse(tempMinF),
- Condition = GetCondition(weatherCode)
- };
- }
-
- public static WeatherConditions GetCondition(string weatherCode)
- {
- switch (weatherCode)
- {
- case "362":
- case "365":
- case "320":
- case "317":
- case "182":
- return WeatherConditions.Sleet;
- case "338":
- case "335":
- case "332":
- case "329":
- case "326":
- case "323":
- case "377":
- case "374":
- case "371":
- case "368":
- case "395":
- case "392":
- case "350":
- case "227":
- case "179":
- return WeatherConditions.Snow;
- case "314":
- case "311":
- case "308":
- case "305":
- case "302":
- case "299":
- case "296":
- case "293":
- case "284":
- case "281":
- case "266":
- case "263":
- case "359":
- case "356":
- case "353":
- case "185":
- case "176":
- return WeatherConditions.Rain;
- case "260":
- case "248":
- return WeatherConditions.Fog;
- case "389":
- case "386":
- case "200":
- return WeatherConditions.Thunderstorm;
- case "230":
- return WeatherConditions.Blizzard;
- case "143":
- return WeatherConditions.Mist;
- case "122":
- return WeatherConditions.Overcast;
- case "119":
- return WeatherConditions.Cloudy;
- case "115":
- return WeatherConditions.PartlyCloudy;
- default:
- return WeatherConditions.Sunny;
- }
- }
- }
-}
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Weather;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Weather
+{
+ /// <summary>
+ /// Based on http://www.worldweatheronline.com/free-weather-feed.aspx
+ /// The classes in this file are a reproduction of the json output, which will then be converted to our weather model classes
+ /// </summary>
+ [Export(typeof(BaseWeatherProvider))]
+ public class WeatherProvider : BaseWeatherProvider
+ {
+ /// <summary>
+ /// The _weather semaphore
+ /// </summary>
+ private readonly SemaphoreSlim _weatherSemaphore = new SemaphoreSlim(10, 10);
+
+ /// <summary>
+ /// Gets the weather info async.
+ /// </summary>
+ /// <param name="location">The location.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{WeatherInfo}.</returns>
+ /// <exception cref="System.ArgumentNullException">location</exception>
+ public override async Task<WeatherInfo> GetWeatherInfoAsync(string location, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(location))
+ {
+ throw new ArgumentNullException("location");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ const int numDays = 5;
+ const string apiKey = "24902f60f1231941120109";
+
+ var url = "http://free.worldweatheronline.com/feed/weather.ashx?q=" + location + "&format=json&num_of_days=" + numDays + "&key=" + apiKey;
+
+ Logger.LogInfo("Accessing weather from " + url);
+
+ using (var stream = await Kernel.Instance.HttpManager.Get(url, _weatherSemaphore, cancellationToken).ConfigureAwait(false))
+ {
+ var data = JsonSerializer.DeserializeFromStream<WeatherResult>(stream).data;
+
+ return GetWeatherInfo(data);
+ }
+ }
+
+ /// <summary>
+ /// Converst the json output to our WeatherInfo model class
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <returns>WeatherInfo.</returns>
+ private WeatherInfo GetWeatherInfo(WeatherData data)
+ {
+ var info = new WeatherInfo();
+
+ if (data.current_condition != null)
+ {
+ var condition = data.current_condition.FirstOrDefault();
+
+ if (condition != null)
+ {
+ info.CurrentWeather = condition.ToWeatherStatus();
+ }
+ }
+
+ if (data.weather != null)
+ {
+ info.Forecasts = data.weather.Select(w => w.ToWeatherForecast()).ToArray();
+ }
+
+ return info;
+ }
+ }
+
+ /// <summary>
+ /// Class WeatherResult
+ /// </summary>
+ class WeatherResult
+ {
+ /// <summary>
+ /// Gets or sets the data.
+ /// </summary>
+ /// <value>The data.</value>
+ public WeatherData data { get; set; }
+ }
+
+ /// <summary>
+ /// Class WeatherData
+ /// </summary>
+ public class WeatherData
+ {
+ /// <summary>
+ /// Gets or sets the current_condition.
+ /// </summary>
+ /// <value>The current_condition.</value>
+ public WeatherCondition[] current_condition { get; set; }
+ /// <summary>
+ /// Gets or sets the weather.
+ /// </summary>
+ /// <value>The weather.</value>
+ public DailyWeatherInfo[] weather { get; set; }
+ }
+
+ /// <summary>
+ /// Class WeatherCondition
+ /// </summary>
+ public class WeatherCondition
+ {
+ /// <summary>
+ /// Gets or sets the temp_ C.
+ /// </summary>
+ /// <value>The temp_ C.</value>
+ public string temp_C { get; set; }
+ /// <summary>
+ /// Gets or sets the temp_ F.
+ /// </summary>
+ /// <value>The temp_ F.</value>
+ public string temp_F { get; set; }
+ /// <summary>
+ /// Gets or sets the humidity.
+ /// </summary>
+ /// <value>The humidity.</value>
+ public string humidity { get; set; }
+ /// <summary>
+ /// Gets or sets the weather code.
+ /// </summary>
+ /// <value>The weather code.</value>
+ public string weatherCode { get; set; }
+
+ /// <summary>
+ /// To the weather status.
+ /// </summary>
+ /// <returns>WeatherStatus.</returns>
+ public WeatherStatus ToWeatherStatus()
+ {
+ return new WeatherStatus
+ {
+ TemperatureCelsius = int.Parse(temp_C),
+ TemperatureFahrenheit = int.Parse(temp_F),
+ Humidity = int.Parse(humidity),
+ Condition = DailyWeatherInfo.GetCondition(weatherCode)
+ };
+ }
+ }
+
+ /// <summary>
+ /// Class DailyWeatherInfo
+ /// </summary>
+ public class DailyWeatherInfo
+ {
+ /// <summary>
+ /// Gets or sets the date.
+ /// </summary>
+ /// <value>The date.</value>
+ public string date { get; set; }
+ /// <summary>
+ /// Gets or sets the precip MM.
+ /// </summary>
+ /// <value>The precip MM.</value>
+ public string precipMM { get; set; }
+ /// <summary>
+ /// Gets or sets the temp max C.
+ /// </summary>
+ /// <value>The temp max C.</value>
+ public string tempMaxC { get; set; }
+ /// <summary>
+ /// Gets or sets the temp max F.
+ /// </summary>
+ /// <value>The temp max F.</value>
+ public string tempMaxF { get; set; }
+ /// <summary>
+ /// Gets or sets the temp min C.
+ /// </summary>
+ /// <value>The temp min C.</value>
+ public string tempMinC { get; set; }
+ /// <summary>
+ /// Gets or sets the temp min F.
+ /// </summary>
+ /// <value>The temp min F.</value>
+ public string tempMinF { get; set; }
+ /// <summary>
+ /// Gets or sets the weather code.
+ /// </summary>
+ /// <value>The weather code.</value>
+ public string weatherCode { get; set; }
+ /// <summary>
+ /// Gets or sets the winddir16 point.
+ /// </summary>
+ /// <value>The winddir16 point.</value>
+ public string winddir16Point { get; set; }
+ /// <summary>
+ /// Gets or sets the winddir degree.
+ /// </summary>
+ /// <value>The winddir degree.</value>
+ public string winddirDegree { get; set; }
+ /// <summary>
+ /// Gets or sets the winddirection.
+ /// </summary>
+ /// <value>The winddirection.</value>
+ public string winddirection { get; set; }
+ /// <summary>
+ /// Gets or sets the windspeed KMPH.
+ /// </summary>
+ /// <value>The windspeed KMPH.</value>
+ public string windspeedKmph { get; set; }
+ /// <summary>
+ /// Gets or sets the windspeed miles.
+ /// </summary>
+ /// <value>The windspeed miles.</value>
+ public string windspeedMiles { get; set; }
+
+ /// <summary>
+ /// To the weather forecast.
+ /// </summary>
+ /// <returns>WeatherForecast.</returns>
+ public WeatherForecast ToWeatherForecast()
+ {
+ return new WeatherForecast
+ {
+ Date = DateTime.Parse(date),
+ HighTemperatureCelsius = int.Parse(tempMaxC),
+ HighTemperatureFahrenheit = int.Parse(tempMaxF),
+ LowTemperatureCelsius = int.Parse(tempMinC),
+ LowTemperatureFahrenheit = int.Parse(tempMinF),
+ Condition = GetCondition(weatherCode)
+ };
+ }
+
+ /// <summary>
+ /// Gets the condition.
+ /// </summary>
+ /// <param name="weatherCode">The weather code.</param>
+ /// <returns>WeatherConditions.</returns>
+ public static WeatherConditions GetCondition(string weatherCode)
+ {
+ switch (weatherCode)
+ {
+ case "362":
+ case "365":
+ case "320":
+ case "317":
+ case "182":
+ return WeatherConditions.Sleet;
+ case "338":
+ case "335":
+ case "332":
+ case "329":
+ case "326":
+ case "323":
+ case "377":
+ case "374":
+ case "371":
+ case "368":
+ case "395":
+ case "392":
+ case "350":
+ case "227":
+ case "179":
+ return WeatherConditions.Snow;
+ case "314":
+ case "311":
+ case "308":
+ case "305":
+ case "302":
+ case "299":
+ case "296":
+ case "293":
+ case "284":
+ case "281":
+ case "266":
+ case "263":
+ case "359":
+ case "356":
+ case "353":
+ case "185":
+ case "176":
+ return WeatherConditions.Rain;
+ case "260":
+ case "248":
+ return WeatherConditions.Fog;
+ case "389":
+ case "386":
+ case "200":
+ return WeatherConditions.Thunderstorm;
+ case "230":
+ return WeatherConditions.Blizzard;
+ case "143":
+ return WeatherConditions.Mist;
+ case "122":
+ return WeatherConditions.Overcast;
+ case "119":
+ return WeatherConditions.Cloudy;
+ case "115":
+ return WeatherConditions.PartlyCloudy;
+ default:
+ return WeatherConditions.Sunny;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Xml/XmlExtensions.cs b/MediaBrowser.Controller/Xml/XmlExtensions.cs
deleted file mode 100644
index d2e8e19832..0000000000
--- a/MediaBrowser.Controller/Xml/XmlExtensions.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Globalization;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Xml
-{
- public static class XmlExtensions
- {
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- /// <summary>
- /// Reads a float from the current element of an XmlReader
- /// </summary>
- public static float ReadFloatSafe(this XmlReader reader)
- {
- string valueString = reader.ReadElementContentAsString();
-
- float value = 0;
-
- if (!string.IsNullOrWhiteSpace(valueString))
- {
- // float.TryParse is local aware, so it can be probamatic, force us culture
- float.TryParse(valueString, NumberStyles.AllowDecimalPoint, _usCulture, out value);
- }
-
- return value;
- }
-
- /// <summary>
- /// Reads an int from the current element of an XmlReader
- /// </summary>
- public static int ReadIntSafe(this XmlReader reader)
- {
- string valueString = reader.ReadElementContentAsString();
-
- int value = 0;
-
- if (!string.IsNullOrWhiteSpace(valueString))
- {
-
- int.TryParse(valueString, out value);
- }
-
- return value;
- }
- }
-}
diff --git a/MediaBrowser.Controller/packages.config b/MediaBrowser.Controller/packages.config
index 42f16a2676..8a94d35ff5 100644
--- a/MediaBrowser.Controller/packages.config
+++ b/MediaBrowser.Controller/packages.config
@@ -1,6 +1,10 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
- <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
- <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
- <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="DotNetZip" version="1.9.1.8" targetFramework="net45" />
+ <package id="morelinq" version="1.0.15631-beta" targetFramework="net45" />
+ <package id="protobuf-net" version="2.0.0.621" targetFramework="net45" />
+ <package id="Rx-Core" version="2.0.21114" targetFramework="net45" />
+ <package id="Rx-Interfaces" version="2.0.21114" targetFramework="net45" />
+ <package id="Rx-Linq" version="2.0.21114" targetFramework="net45" />
+ <package id="System.Data.SQLite" version="1.0.84.0" targetFramework="net45" />
</packages> \ No newline at end of file