From 3d47b495a96fce84c03d9f3177dc6dbc8a4afa3c Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 29 Jun 2014 23:04:50 -0400 Subject: fixes #795 - Support reading Xbmc nfo's --- MediaBrowser.LocalMetadata/BaseXmlProvider.cs | 89 +++ .../Images/CollectionFolderImageProvider.cs | 35 + .../Images/EpisodeLocalImageProvider.cs | 81 +++ .../Images/ImagesByNameImageProvider.cs | 56 ++ .../Images/InternalMetadataFolderImageProvider.cs | 68 ++ .../Images/LocalImageProvider.cs | 327 ++++++++++ .../MediaBrowser.LocalMetadata.csproj | 110 ++++ .../Parsers/BoxSetXmlParser.cs | 129 ++++ .../Parsers/EpisodeXmlParser.cs | 271 ++++++++ .../Parsers/GameSystemXmlParser.cs | 64 ++ .../Parsers/GameXmlParser.cs | 105 +++ .../Parsers/MovieXmlParser.cs | 67 ++ .../Parsers/MusicVideoXmlParser.cs | 42 ++ .../Parsers/SeasonXmlParser.cs | 46 ++ .../Parsers/SeriesXmlParser.cs | 118 ++++ .../Properties/AssemblyInfo.cs | 36 ++ .../Providers/AdultVideoXmlProvider.cs | 37 ++ .../Providers/AlbumXmlProvider.cs | 30 + .../Providers/ArtistXmlProvider.cs | 30 + .../Providers/BoxSetXmlProvider.cs | 34 + .../Providers/ChannelXmlProvider.cs | 30 + .../Providers/EpisodeXmlProvider.cs | 53 ++ .../Providers/FolderXmlProvider.cs | 33 + .../Providers/GameSystemXmlProvider.cs | 31 + .../Providers/GameXmlProvider.cs | 46 ++ .../Providers/MovieXmlProvider.cs | 66 ++ .../Providers/MusicVideoXmlProvider.cs | 31 + .../Providers/PersonXmlProvider.cs | 30 + .../Providers/SeasonXmlProvider.cs | 44 ++ .../Providers/SeriesXmlProvider.cs | 43 ++ .../Providers/TrailerXmlProvider.cs | 37 ++ .../Providers/VideoXmlProvider.cs | 37 ++ MediaBrowser.LocalMetadata/Savers/AlbumXmlSaver.cs | 68 ++ .../Savers/ArtistXmlSaver.cs | 68 ++ .../Savers/BoxSetXmlSaver.cs | 68 ++ .../Savers/ChannelXmlSaver.cs | 73 +++ .../Savers/EpisodeXmlSaver.cs | 151 +++++ .../Savers/FolderXmlSaver.cs | 80 +++ .../Savers/GameSystemXmlSaver.cs | 75 +++ MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs | 112 ++++ MediaBrowser.LocalMetadata/Savers/MovieXmlSaver.cs | 133 ++++ .../Savers/PersonXmlSaver.cs | 81 +++ .../Savers/SeasonXmlSaver.cs | 87 +++ .../Savers/SeriesXmlSaver.cs | 135 ++++ .../Savers/XmlSaverHelpers.cs | 718 +++++++++++++++++++++ 45 files changed, 4105 insertions(+) create mode 100644 MediaBrowser.LocalMetadata/BaseXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Images/ImagesByNameImageProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs create mode 100644 MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj create mode 100644 MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/EpisodeXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/GameSystemXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/GameXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/MovieXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/MusicVideoXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/SeasonXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Parsers/SeriesXmlParser.cs create mode 100644 MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/AdultVideoXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/AlbumXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/ArtistXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/ChannelXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/EpisodeXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/FolderXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/GameSystemXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/GameXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/MovieXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/MusicVideoXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/PersonXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/SeasonXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/SeriesXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/TrailerXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Providers/VideoXmlProvider.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/AlbumXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/ArtistXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/ChannelXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/EpisodeXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/GameSystemXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/MovieXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/PersonXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/SeasonXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/SeriesXmlSaver.cs create mode 100644 MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs (limited to 'MediaBrowser.LocalMetadata') diff --git a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs new file mode 100644 index 000000000..cc9bc7bed --- /dev/null +++ b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.LocalMetadata +{ + public abstract class BaseXmlProvider : ILocalMetadataProvider, IHasChangeMonitor + where T : IHasMetadata, new() + { + protected IFileSystem FileSystem; + + public async Task> GetMetadata(ItemInfo info, CancellationToken cancellationToken) + { + var result = new LocalMetadataResult(); + + var file = GetXmlFile(info, new DirectoryService(new NullLogger())); + + if (file == null) + { + return result; + } + + var path = file.FullName; + + await XmlProviderUtils.XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + result.Item = new T(); + + Fetch(result, path, cancellationToken); + result.HasMetadata = true; + } + catch (FileNotFoundException) + { + result.HasMetadata = false; + } + catch (DirectoryNotFoundException) + { + result.HasMetadata = false; + } + finally + { + XmlProviderUtils.XmlParsingResourcePool.Release(); + } + + return result; + } + + protected abstract void Fetch(LocalMetadataResult result, string path, CancellationToken cancellationToken); + + protected BaseXmlProvider(IFileSystem fileSystem) + { + FileSystem = fileSystem; + } + + protected abstract FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService); + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + var file = GetXmlFile(new ItemInfo { IsInMixedFolder = item.IsInMixedFolder, Path = item.Path }, directoryService); + + if (file == null) + { + return false; + } + + return file.Exists && FileSystem.GetLastWriteTimeUtc(file) > date; + } + + public string Name + { + get + { + return "Media Browser Xml"; + } + } + } + + static class XmlProviderUtils + { + internal static readonly SemaphoreSlim XmlParsingResourcePool = new SemaphoreSlim(4, 4); + } +} diff --git a/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs new file mode 100644 index 000000000..29fd76aa5 --- /dev/null +++ b/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; + +namespace MediaBrowser.LocalMetadata.Images +{ + public class CollectionFolderLocalImageProvider : ILocalImageFileProvider, IHasOrder + { + public string Name + { + get { return "Collection Folder Images"; } + } + + public bool Supports(IHasImages item) + { + return item is CollectionFolder && item.SupportsLocalMetadata; + } + + public int Order + { + get + { + // Run after LocalImageProvider + return 1; + } + } + + public List GetImages(IHasImages item, IDirectoryService directoryService) + { + var collectionFolder = (CollectionFolder)item; + + return new LocalImageProvider().GetImages(item, collectionFolder.PhysicalLocations, directoryService); + } + } +} diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs new file mode 100644 index 000000000..f1e7426aa --- /dev/null +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.LocalMetadata.Images +{ + public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider + { + public string Name + { + get { return "Local Images"; } + } + + public bool Supports(IHasImages item) + { + return item is Episode && item.SupportsLocalMetadata; + } + + public List GetImages(IHasImages item, IDirectoryService directoryService) + { + var parentPath = Path.GetDirectoryName(item.Path); + + var parentPathFiles = directoryService.GetFileSystemEntries(parentPath); + + var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path); + + var files = GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles); + + if (files.Count > 0) + { + return files; + } + + var metadataPath = Path.Combine(parentPath, "metadata"); + + if (parentPathFiles.Any(i => string.Equals(i.FullName, metadataPath, StringComparison.OrdinalIgnoreCase))) + { + return GetFilesFromParentFolder(nameWithoutExtension, directoryService.GetFiles(metadataPath)); + } + + return new List(); + } + + private List GetFilesFromParentFolder(string filenameWithoutExtension, IEnumerable parentPathFiles) + { + var thumbName = filenameWithoutExtension + "-thumb"; + + return parentPathFiles + .Where(i => + { + if (BaseItem.SupportedImageExtensions.Contains(i.Extension)) + { + var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.Name); + + if (string.Equals(filenameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + }) + .Select(i => new LocalImageInfo + { + FileInfo = (FileInfo)i, + Type = ImageType.Primary + }) + .ToList(); + } + } +} diff --git a/MediaBrowser.LocalMetadata/Images/ImagesByNameImageProvider.cs b/MediaBrowser.LocalMetadata/Images/ImagesByNameImageProvider.cs new file mode 100644 index 000000000..3f84df462 --- /dev/null +++ b/MediaBrowser.LocalMetadata/Images/ImagesByNameImageProvider.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.IO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; + +namespace MediaBrowser.LocalMetadata.Images +{ + public class ImagesByNameImageProvider : ILocalImageFileProvider, IHasOrder + { + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + + public ImagesByNameImageProvider(IFileSystem fileSystem, IServerConfigurationManager config) + { + _fileSystem = fileSystem; + _config = config; + } + + public string Name + { + get { return "Images By Name"; } + } + + public bool Supports(IHasImages item) + { + return item is CollectionFolder; + } + + public int Order + { + get + { + // Run after LocalImageProvider, and after CollectionFolderImageProvider + return 2; + } + } + + public List GetImages(IHasImages item, IDirectoryService directoryService) + { + var name = _fileSystem.GetValidFilename(item.Name); + + var path = Path.Combine(_config.ApplicationPaths.GeneralPath, name); + + try + { + return new LocalImageProvider().GetImages(item, path, directoryService); + } + catch (DirectoryNotFoundException) + { + return new List(); + } + } + } +} diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs new file mode 100644 index 000000000..8c4f6247c --- /dev/null +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; + +namespace MediaBrowser.LocalMetadata.Images +{ + public class InternalMetadataFolderImageProvider : ILocalImageFileProvider, IHasOrder + { + private readonly IServerConfigurationManager _config; + + public InternalMetadataFolderImageProvider(IServerConfigurationManager config) + { + _config = config; + } + + public string Name + { + get { return "Internal Images"; } + } + + public bool Supports(IHasImages item) + { + if (!item.IsSaveLocalMetadataEnabled()) + { + return true; + } + + // Extracted images will be saved in here + if (item is Audio) + { + return true; + } + + if (item.SupportsLocalMetadata) + { + return false; + } + + return true; + } + + public int Order + { + get + { + // Make sure this is last so that all other locations are scanned first + return 1000; + } + } + + public List GetImages(IHasImages item, IDirectoryService directoryService) + { + var path = _config.ApplicationPaths.GetInternalMetadataPath(item.Id); + + try + { + return new LocalImageProvider().GetImages(item, path, directoryService); + } + catch (DirectoryNotFoundException) + { + return new List(); + } + } + } +} diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs new file mode 100644 index 000000000..a5ef7977b --- /dev/null +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.LocalMetadata.Images +{ + public class LocalImageProvider : ILocalImageFileProvider + { + public string Name + { + get { return "Local Images"; } + } + + public int Order + { + get { return 0; } + } + + public bool Supports(IHasImages item) + { + if (item.SupportsLocalMetadata) + { + // Episode has it's own provider + if (item.IsOwnedItem || item is Episode || item is Audio) + { + return false; + } + + return true; + } + + if (item.LocationType == LocationType.Virtual) + { + var season = item as Season; + + if (season != null) + { + var series = season.Series; + + if (series != null && series.LocationType == LocationType.FileSystem) + { + return true; + } + } + } + + return false; + } + + private IEnumerable GetFiles(IHasImages item, bool includeDirectories, IDirectoryService directoryService) + { + if (item.LocationType != LocationType.FileSystem) + { + return new List(); + } + + var path = item.ContainingFolderPath; + + if (includeDirectories) + { + return directoryService.GetFileSystemEntries(path) + .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) || + (i.Attributes & FileAttributes.Directory) == FileAttributes.Directory); + } + + return directoryService.GetFiles(path) + .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)); + } + + public List GetImages(IHasImages item, IDirectoryService directoryService) + { + var files = GetFiles(item, true, directoryService).ToList(); + + var list = new List(); + + PopulateImages(item, list, files, true, directoryService); + + return list; + } + + public List GetImages(IHasImages item, string path, IDirectoryService directoryService) + { + return GetImages(item, new[] { path }, directoryService); + } + + public List GetImages(IHasImages item, IEnumerable paths, IDirectoryService directoryService) + { + var files = paths.SelectMany(directoryService.GetFiles) + .Where(i => + { + var ext = i.Extension; + + return !string.IsNullOrEmpty(ext) && + BaseItem.SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase); + }) + .ToList(); + + var list = new List(); + + PopulateImages(item, list, files, false, directoryService); + + return list; + } + + private void PopulateImages(IHasImages item, List images, List files, bool supportParentSeriesFiles, IDirectoryService directoryService) + { + var imagePrefix = string.Empty; + + var baseItem = item as BaseItem; + if (baseItem != null && baseItem.IsInMixedFolder) + { + imagePrefix = Path.GetFileNameWithoutExtension(item.Path) + "-"; + } + + PopulatePrimaryImages(item, images, files, imagePrefix); + PopulateBackdrops(item, images, files, imagePrefix, directoryService); + PopulateScreenshots(images, files, imagePrefix); + + AddImage(files, images, imagePrefix + "logo", ImageType.Logo); + AddImage(files, images, imagePrefix + "clearart", ImageType.Art); + AddImage(files, images, imagePrefix + "disc", ImageType.Disc); + AddImage(files, images, imagePrefix + "cdart", ImageType.Disc); + AddImage(files, images, imagePrefix + "box", ImageType.Box); + AddImage(files, images, imagePrefix + "back", ImageType.BoxRear); + AddImage(files, images, imagePrefix + "boxrear", ImageType.BoxRear); + AddImage(files, images, imagePrefix + "menu", ImageType.Menu); + + // Banner + AddImage(files, images, imagePrefix + "banner", ImageType.Banner); + + // Thumb + AddImage(files, images, imagePrefix + "thumb", ImageType.Thumb); + AddImage(files, images, imagePrefix + "landscape", ImageType.Thumb); + + if (supportParentSeriesFiles) + { + var season = item as Season; + + if (season != null) + { + PopulateSeasonImagesFromSeriesFolder(season, images, directoryService); + } + } + } + + private void PopulatePrimaryImages(IHasImages item, List images, List files, string imagePrefix) + { + AddImage(files, images, imagePrefix + "folder", ImageType.Primary); + AddImage(files, images, imagePrefix + "cover", ImageType.Primary); + AddImage(files, images, imagePrefix + "poster", ImageType.Primary); + AddImage(files, images, imagePrefix + "default", ImageType.Primary); + + // Support plex/xbmc convention + if (item is Series) + { + AddImage(files, images, imagePrefix + "show", ImageType.Primary); + } + + // Support plex/xbmc convention + if (item is Movie || item is MusicVideo || item is AdultVideo) + { + AddImage(files, images, imagePrefix + "movie", ImageType.Primary); + } + + if (!string.IsNullOrEmpty(item.Path)) + { + var name = Path.GetFileNameWithoutExtension(item.Path); + + if (!string.IsNullOrEmpty(name)) + { + AddImage(files, images, name, ImageType.Primary); + AddImage(files, images, name + "-poster", ImageType.Primary); + } + } + } + + private void PopulateBackdrops(IHasImages item, List images, List files, string imagePrefix, IDirectoryService directoryService) + { + PopulateBackdrops(images, files, imagePrefix, "backdrop", "backdrop", ImageType.Backdrop); + + if (!string.IsNullOrEmpty(item.Path)) + { + var name = Path.GetFileNameWithoutExtension(item.Path); + + if (!string.IsNullOrEmpty(name)) + { + AddImage(files, images, imagePrefix + name + "-fanart", ImageType.Backdrop); + } + } + + PopulateBackdrops(images, files, imagePrefix, "fanart", "fanart-", ImageType.Backdrop); + PopulateBackdrops(images, files, imagePrefix, "background", "background-", ImageType.Backdrop); + PopulateBackdrops(images, files, imagePrefix, "art", "art-", ImageType.Backdrop); + + var extraFanartFolder = files + .FirstOrDefault(i => string.Equals(i.Name, "extrafanart", StringComparison.OrdinalIgnoreCase)); + + if (extraFanartFolder != null) + { + PopulateBackdropsFromExtraFanart(extraFanartFolder.FullName, images, directoryService); + } + } + + private void PopulateBackdropsFromExtraFanart(string path, List images, IDirectoryService directoryService) + { + var imageFiles = directoryService.GetFiles(path) + .Where(i => + { + var extension = i.Extension; + + if (string.IsNullOrEmpty(extension)) + { + return false; + } + + return BaseItem.SupportedImageExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + }); + + images.AddRange(imageFiles.Select(i => new LocalImageInfo + { + FileInfo = i, + Type = ImageType.Backdrop + })); + } + + private void PopulateScreenshots(List images, List files, string imagePrefix) + { + PopulateBackdrops(images, files, imagePrefix, "screenshot", "screenshot", ImageType.Screenshot); + } + + private void PopulateBackdrops(List images, List files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, ImageType type) + { + AddImage(files, images, imagePrefix + firstFileName, type); + + var unfound = 0; + for (var i = 1; i <= 20; i++) + { + // Screenshot Image + var found = AddImage(files, images, imagePrefix + subsequentFileNamePrefix + i, type); + + if (!found) + { + unfound++; + + if (unfound >= 3) + { + break; + } + } + } + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private void PopulateSeasonImagesFromSeriesFolder(Season season, List images, IDirectoryService directoryService) + { + var seasonNumber = season.IndexNumber; + + var series = season.Series; + if (!seasonNumber.HasValue || series.LocationType != LocationType.FileSystem) + { + return; + } + + var seriesFiles = GetFiles(series, false, directoryService).ToList(); + + // Try using the season name + var prefix = season.Name.ToLower().Replace(" ", string.Empty); + + var filenamePrefixes = new List { prefix }; + + var seasonMarker = seasonNumber.Value == 0 + ? "-specials" + : seasonNumber.Value.ToString("00", _usCulture); + + // Get this one directly from the file system since we have to go up a level + if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase)) + { + filenamePrefixes.Add("season" + seasonMarker); + } + + foreach (var filename in filenamePrefixes) + { + AddImage(seriesFiles, images, filename + "-poster", ImageType.Primary); + AddImage(seriesFiles, images, filename + "-fanart", ImageType.Backdrop); + AddImage(seriesFiles, images, filename + "-banner", ImageType.Banner); + AddImage(seriesFiles, images, filename + "-landscape", ImageType.Thumb); + } + } + + private bool AddImage(IEnumerable files, List images, string name, ImageType type) + { + var image = GetImage(files, name) as FileInfo; + + if (image != null) + { + images.Add(new LocalImageInfo + { + FileInfo = image, + Type = type + }); + + return true; + } + + return false; + } + + private FileSystemInfo GetImage(IEnumerable files, string name) + { + var candidates = files + .Where(i => string.Equals(name, Path.GetFileNameWithoutExtension(i.Name), StringComparison.OrdinalIgnoreCase)) + .ToList(); + + return BaseItem.SupportedImageExtensions + .Select(i => candidates.FirstOrDefault(c => string.Equals(c.Extension, i, StringComparison.OrdinalIgnoreCase))) + .FirstOrDefault(i => i != null); + } + } +} diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj new file mode 100644 index 000000000..0d2c0b97f --- /dev/null +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -0,0 +1,110 @@ + + + + + Debug + AnyCPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392} + Library + Properties + MediaBrowser.LocalMetadata + MediaBrowser.LocalMetadata + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + + \ No newline at end of file diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs new file mode 100644 index 000000000..51a4684d7 --- /dev/null +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.LocalMetadata.Parsers +{ + public class BoxSetXmlParser : BaseItemXmlParser + { + private readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + public BoxSetXmlParser(ILogger logger) + : base(logger) + { + } + + protected override void FetchDataFromXmlNode(XmlReader reader, BoxSet item) + { + switch (reader.Name) + { + case "CollectionItems": + + using (var subReader = reader.ReadSubtree()) + { + FetchFromCollectionItemsNode(subReader, item); + } + break; + + default: + base.FetchDataFromXmlNode(reader, item); + break; + } + } + + private void FetchFromCollectionItemsNode(XmlReader reader, BoxSet item) + { + reader.MoveToContent(); + + var list = new List(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "CollectionItem": + { + using (var subReader = reader.ReadSubtree()) + { + var child = GetLinkedChild(subReader); + + if (child != null) + { + list.Add(child); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + + item.LinkedChildren = list; + } + + private LinkedChild GetLinkedChild(XmlReader reader) + { + reader.MoveToContent(); + + var linkedItem = new LinkedChild + { + Type = LinkedChildType.Manual + }; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Name": + { + linkedItem.ItemName = reader.ReadElementContentAsString(); + break; + } + + case "Type": + { + linkedItem.ItemType = reader.ReadElementContentAsString(); + break; + } + + case "Year": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + linkedItem.ItemYear = rval; + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + + return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem; + } + } +} diff --git a/MediaBrowser.LocalMetadata/Parsers/EpisodeXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/EpisodeXmlParser.cs new file mode 100644 index 000000000..8430f3b3c --- /dev/null +++ b/MediaBrowser.LocalMetadata/Parsers/EpisodeXmlParser.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Xml; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.LocalMetadata.Parsers +{ + /// + /// Class EpisodeXmlParser + /// + public class EpisodeXmlParser : BaseItemXmlParser + { + private List _imagesFound; + private List _chaptersFound; + + public EpisodeXmlParser(ILogger logger) + : base(logger) + { + } + + private string _xmlPath; + + public void Fetch(Episode item, + List images, + List chapters, + string metadataFile, + CancellationToken cancellationToken) + { + _imagesFound = images; + _chaptersFound = chapters; + _xmlPath = metadataFile; + + Fetch(item, metadataFile, cancellationToken); + } + + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Fetches the data from XML node. + /// + /// The reader. + /// The item. + protected override void FetchDataFromXmlNode(XmlReader reader, Episode item) + { + switch (reader.Name) + { + case "Chapters": + + _chaptersFound.AddRange(FetchChaptersFromXmlNode(item, reader.ReadSubtree())); + break; + + 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": + { + var 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); + + var parentFolder = Path.GetDirectoryName(_xmlPath); + filename = Path.Combine(parentFolder, filename); + var file = new FileInfo(filename); + + if (file.Exists) + { + _imagesFound.Add(new LocalImageInfo + { + Type = ImageType.Primary, + FileInfo = file + }); + } + } + 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 "EpisodeNumberEnd": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + int num; + + if (int.TryParse(number, out num)) + { + item.IndexNumberEnd = num; + } + } + break; + } + + case "absolute_number": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AbsoluteEpisodeNumber = rval; + } + } + + break; + } + case "DVD_episodenumber": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + float num; + + if (float.TryParse(number, NumberStyles.Any, UsCulture, out num)) + { + item.DvdEpisodeNumber = num; + } + } + break; + } + + case "DVD_season": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + float num; + + if (float.TryParse(number, NumberStyles.Any, UsCulture, out num)) + { + item.DvdSeasonNumber = Convert.ToInt32(num); + } + } + break; + } + + case "airsbefore_episode": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AirsBeforeEpisodeNumber = rval; + } + } + + break; + } + + case "airsafter_season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AirsAfterSeasonNumber = rval; + } + } + + break; + } + + case "airsbefore_season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AirsBeforeSeasonNumber = rval; + } + } + + 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.LocalMetadata/Parsers/GameSystemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/GameSystemXmlParser.cs new file mode 100644 index 000000000..d449108c4 --- /dev/null +++ b/MediaBrowser.LocalMetadata/Parsers/GameSystemXmlParser.cs @@ -0,0 +1,64 @@ +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.LocalMetadata.Parsers +{ + public class GameSystemXmlParser : BaseItemXmlParser + { + public GameSystemXmlParser(ILogger logger) + : base(logger) + { + } + + private readonly Task _cachedTask = Task.FromResult(true); + public Task FetchAsync(GameSystem item, string metadataFile, CancellationToken cancellationToken) + { + Fetch(item, metadataFile, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + return _cachedTask; + } + + /// + /// Fetches the data from XML node. + /// + /// The reader. + /// The item. + protected override void FetchDataFromXmlNode(XmlReader reader, GameSystem item) + { + switch (reader.Name) + { + case "GameSystem": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.GameSystemName = val; + } + break; + } + + case "GamesDbId": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Gamesdb, val); + } + break; + } + + + default: + base.FetchDataFromXmlNode(reader, item); + break; + } + } + } +} diff --git a/MediaBrowser.LocalMetadata/Parsers/GameXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/GameXmlParser.cs new file mode 100644 index 000000000..2caced8a9 --- /dev/null +++ b/MediaBrowser.LocalMetadata/Parsers/GameXmlParser.cs @@ -0,0 +1,105 @@ +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.LocalMetadata.Parsers +{ + /// + /// Class EpisodeXmlParser + /// + public class GameXmlParser : BaseItemXmlParser + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public GameXmlParser(ILogger logger) + : base(logger) + { + } + + private readonly Task _cachedTask = Task.FromResult(true); + public Task FetchAsync(Game item, string metadataFile, CancellationToken cancellationToken) + { + Fetch(item, metadataFile, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + return _cachedTask; + } + + /// + /// Fetches the data from XML node. + /// + /// The reader. + /// The item. + protected override void FetchDataFromXmlNode(XmlReader reader, Game item) + { + switch (reader.Name) + { + case "GameSystem": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.GameSystem = val; + } + break; + } + + case "GamesDbId": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Gamesdb, val); + } + break; + } + + case "NesBox": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.NesBox, val); + } + break; + } + + case "NesBoxRom": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.NesBoxRom, val); + } + break; + } + + case "Players": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + int num; + + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num)) + { + item.PlayersSupported = num; + } + } + break; + } + + + default: + base.FetchDataFromXmlNode(reader, item); + break; + } + } + } +} diff --git a/MediaBrowser.LocalMetadata/Parsers/MovieXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/MovieXmlParser.cs new file mode 100644 index 000000000..388a0d20d --- /dev/null +++ b/MediaBrowser.LocalMetadata/Parsers/MovieXmlParser.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Threading; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.LocalMetadata.Parsers +{ + /// + /// Class EpisodeXmlParser + /// + public class MovieXmlParser : BaseItemXmlParser