aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs')
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs794
1 files changed, 598 insertions, 196 deletions
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index f20dbbb6e..9d558b6ce 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -1,20 +1,22 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Xml;
using MediaBrowser.XbmcMetadata.Configuration;
using MediaBrowser.XbmcMetadata.Savers;
using Microsoft.Extensions.Logging;
@@ -24,57 +26,68 @@ namespace MediaBrowser.XbmcMetadata.Parsers
public class BaseNfoParser<T>
where T : BaseItem
{
- /// <summary>
- /// The logger
- /// </summary>
- protected ILogger Logger { get; private set; }
- protected IFileSystem FileSystem { get; private set; }
- protected IProviderManager ProviderManager { get; private set; }
- protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; }
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IConfigurationManager _config;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
private Dictionary<string, string> _validProviderIds;
/// <summary>
/// Initializes a new instance of the <see cref="BaseNfoParser{T}" /> class.
/// </summary>
- public BaseNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory)
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public BaseNfoParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
{
Logger = logger;
_config = config;
ProviderManager = providerManager;
- FileSystem = fileSystem;
- XmlReaderSettingsFactory = xmlReaderSettingsFactory;
+ _validProviderIds = new Dictionary<string, string>();
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <summary>
- /// Fetches metadata for an item from one xml file
+ /// Gets the logger.
+ /// </summary>
+ protected ILogger Logger { get; }
+
+ protected IProviderManager ProviderManager { get; }
+
+ protected virtual bool SupportsUrlAfterClosingXmlTag => false;
+
+ /// <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="ArgumentNullException">
- /// </exception>
+ /// <exception cref="ArgumentNullException"><c>item</c> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException"><c>metadataFile</c> is <c>null</c> or empty.</exception>
public void Fetch(MetadataResult<T> item, string metadataFile, CancellationToken cancellationToken)
{
- if (item == null)
+ if (item.Item == null)
{
- throw new ArgumentNullException(nameof(item));
+ throw new ArgumentException("Item can't be null.", nameof(item));
}
if (string.IsNullOrEmpty(metadataFile))
{
- throw new ArgumentException("The metadata file was empty or null.", nameof(metadataFile));
+ throw new ArgumentException("The metadata filepath was empty.", nameof(metadataFile));
}
- var settings = XmlReaderSettingsFactory.Create(false);
-
- settings.CheckCharacters = false;
- settings.IgnoreProcessingInstructions = true;
- settings.IgnoreComments = true;
-
- _validProviderIds = _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var idInfos = ProviderManager.GetExternalIdInfos(item.Item);
@@ -87,16 +100,14 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- //Additional Mappings
+ // Additional Mappings
_validProviderIds.Add("collectionnumber", "TmdbCollection");
_validProviderIds.Add("tmdbcolid", "TmdbCollection");
_validProviderIds.Add("imdb_id", "Imdb");
- Fetch(item, metadataFile, settings, cancellationToken);
+ Fetch(item, metadataFile, GetXmlReaderSettings(), cancellationToken);
}
- protected virtual bool SupportsUrlAfterClosingXmlTag => false;
-
/// <summary>
/// Fetches the specified item.
/// </summary>
@@ -109,155 +120,127 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!SupportsUrlAfterClosingXmlTag)
{
using (var fileStream = File.OpenRead(metadataFile))
+ using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
+ using (var reader = XmlReader.Create(streamReader, settings))
{
- using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
- {
- // Use XmlReader for best performance
- using (var reader = XmlReader.Create(streamReader, settings))
- {
- item.ResetPeople();
+ item.ResetPeople();
- reader.MoveToContent();
- reader.Read();
+ reader.MoveToContent();
+ reader.Read();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- else
- {
- reader.Read();
- }
- }
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ else
+ {
+ reader.Read();
}
}
}
+
return;
}
- using (var fileStream = File.OpenRead(metadataFile))
- {
- using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
- {
- item.ResetPeople();
+ item.ResetPeople();
- // Need to handle a url after the xml data
- // http://kodi.wiki/view/NFO_files/movies
+ // Need to handle a url after the xml data
+ // http://kodi.wiki/view/NFO_files/movies
- var xml = streamReader.ReadToEnd();
+ var xml = File.ReadAllText(metadataFile);
- // Find last closing Tag
- // Need to do this in two steps to account for random > characters after the closing xml
- var index = xml.LastIndexOf(@"</", StringComparison.Ordinal);
+ // Find last closing Tag
+ // Need to do this in two steps to account for random > characters after the closing xml
+ var index = xml.LastIndexOf(@"</", StringComparison.Ordinal);
- // If closing tag exists, move to end of Tag
- if (index != -1)
- {
- index = xml.IndexOf('>', index);
- }
+ // If closing tag exists, move to end of Tag
+ if (index != -1)
+ {
+ index = xml.IndexOf('>', index);
+ }
- if (index != -1)
- {
- var endingXml = xml.Substring(index);
+ if (index != -1)
+ {
+ var endingXml = xml.AsSpan().Slice(index);
- ParseProviderLinks(item.Item, endingXml);
+ ParseProviderLinks(item.Item, endingXml);
- // If the file is just an imdb url, don't go any further
- if (index == 0)
- {
- return;
- }
+ // If the file is just an imdb url, don't go any further
+ if (index == 0)
+ {
+ return;
+ }
- xml = xml.Substring(0, index + 1);
- }
- else
- {
- // If the file is just an Imdb url, handle that
+ xml = xml.Substring(0, index + 1);
+ }
+ else
+ {
+ // If the file is just provider urls, handle that
+ ParseProviderLinks(item.Item, xml);
- ParseProviderLinks(item.Item, xml);
+ return;
+ }
- return;
- }
+ // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
+ try
+ {
+ using (var stringReader = new StringReader(xml))
+ using (var reader = XmlReader.Create(stringReader, settings))
+ {
+ reader.MoveToContent();
+ reader.Read();
- // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
- try
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- using (var stringReader = new StringReader(xml))
- {
- // Use XmlReader for best performance
- using (var reader = XmlReader.Create(stringReader, settings))
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ cancellationToken.ThrowIfCancellationRequested();
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- else
- {
- reader.Read();
- }
- }
- }
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ else
+ {
+ reader.Read();
}
- }
- catch (XmlException)
- {
-
}
}
}
+ catch (XmlException)
+ {
+ }
}
- protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/";
-
- protected void ParseProviderLinks(T item, string xml)
+ protected void ParseProviderLinks(T item, ReadOnlySpan<char> xml)
{
- //Look for a match for the Regex pattern "tt" followed by 7 digits
- var m = Regex.Match(xml, @"tt([0-9]{7})", RegexOptions.IgnoreCase);
- if (m.Success)
+ if (ProviderIdParsers.TryFindImdbId(xml, out var imdbId))
{
- item.SetProviderId(MetadataProviders.Imdb, m.Value);
+ item.SetProviderId(MetadataProvider.Imdb, imdbId.ToString());
}
- // Support Tmdb
- // https://www.themoviedb.org/movie/30287-fallo
- var srch = MovieDbParserSearchString;
- var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
-
- if (index != -1)
+ if (item is Movie)
{
- var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0];
- if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ if (ProviderIdParsers.TryFindTmdbMovieId(xml, out var tmdbId))
{
- item.SetProviderId(MetadataProviders.Tmdb, value.ToString(_usCulture));
+ item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString());
}
}
if (item is Series)
{
- srch = "thetvdb.com/?tab=series&id=";
-
- index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ if (ProviderIdParsers.TryFindTmdbSeriesId(xml, out var tmdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString());
+ }
- if (index != -1)
+ if (ProviderIdParsers.TryFindTvdbId(xml, out var tvdbId))
{
- var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
- if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- item.SetProviderId(MetadataProviders.Tvdb, value.ToString(_usCulture));
- }
+ item.SetProviderId(MetadataProvider.Tvdb, tvdbId.ToString());
}
}
}
@@ -266,6 +249,14 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var item = itemResult.Item;
+ var nfoConfiguration = _config.GetNfoConfiguration();
+ UserItemData? userData = null;
+ if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
+ {
+ var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
+ userData = _userDataManager.GetUserData(user, item);
+ }
+
switch (reader.Name)
{
// DateCreated
@@ -275,19 +266,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, BaseNfoSaver.DateAddedFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- item.DateCreated = added.ToUniversalTime();
- }
- else if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out added))
- {
- item.DateCreated = added.ToUniversalTime();
+ item.DateCreated = added;
}
else
{
- Logger.LogWarning("Invalid Added value found: " + val);
+ Logger.LogWarning("Invalid Added value found: {Value}", val);
}
}
+
break;
}
@@ -299,21 +287,27 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.OriginalTitle = val;
}
+
break;
}
+ case "name":
case "title":
case "localtitle":
item.Name = reader.ReadElementContentAsString();
break;
+ case "sortname":
+ item.SortName = reader.ReadElementContentAsString();
+ break;
+
case "criticrating":
{
var text = reader.ReadElementContentAsString();
if (!string.IsNullOrEmpty(text))
{
- if (float.TryParse(text, NumberStyles.Any, _usCulture, out var value))
+ if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
item.CriticRating = value;
}
@@ -330,6 +324,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.ForcedSortName = val;
}
+
break;
}
@@ -356,6 +351,50 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "watched":
+ {
+ var val = reader.ReadElementContentAsBoolean();
+
+ if (userData != null)
+ {
+ userData.Played = val;
+ }
+
+ break;
+ }
+
+ case "playcount":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val) && userData != null)
+ {
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
+ {
+ userData.PlayCount = count;
+ }
+ }
+
+ break;
+ }
+
+ case "lastplayed":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val) && userData != null)
+ {
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
+ {
+ userData.LastPlayedDate = added;
+ }
+ else
+ {
+ Logger.LogWarning("Invalid lastplayed value found: {Value}", val);
+ }
+ }
+
+ break;
+ }
+
case "countrycode":
{
var val = reader.ReadElementContentAsString();
@@ -373,29 +412,21 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.LockedFields = val.Split('|').Select(i =>
{
- if (Enum.TryParse(i, true, out MetadataFields field))
+ if (Enum.TryParse(i, true, out MetadataField field))
{
- return (MetadataFields?)field;
+ return (MetadataField?)field;
}
return null;
-
- }).Where(i => i.HasValue).Select(i => i.Value).ToArray();
+ }).OfType<MetadataField>().ToArray();
}
break;
}
case "tagline":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.Tagline = val;
- }
- break;
- }
+ item.Tagline = reader.ReadElementContentAsString();
+ break;
case "country":
{
@@ -408,6 +439,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
.Where(i => !string.IsNullOrWhiteSpace(i))
.ToArray();
}
+
break;
}
@@ -419,6 +451,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.OfficialRating = rating;
}
+
break;
}
@@ -430,6 +463,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.CustomRating = val;
}
+
break;
}
@@ -439,11 +473,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(text))
{
- if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out var runtime))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
}
+
break;
}
@@ -451,11 +486,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasAspectRatio = item as IHasAspectRatio;
- if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
+ if (!string.IsNullOrWhiteSpace(val)
+ && item is IHasAspectRatio hasAspectRatio)
{
hasAspectRatio.AspectRatio = val;
}
+
break;
}
@@ -467,6 +503,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
}
+
break;
}
@@ -476,16 +513,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- //var parts = val.Split('/')
- // .Select(i => i.Trim())
- // .Where(i => !string.IsNullOrWhiteSpace(i));
-
- //foreach (var p in parts)
- //{
- // item.AddStudio(p);
- //}
item.AddStudio(val);
}
+
break;
}
@@ -498,10 +528,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
continue;
}
+
itemResult.AddPerson(p);
}
+
break;
}
+
case "credits":
{
var val = reader.ReadElementContentAsString();
@@ -517,9 +550,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
continue;
}
+
itemResult.AddPerson(p);
}
}
+
break;
}
@@ -532,8 +567,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
continue;
}
+
itemResult.AddPerson(p);
}
+
break;
}
@@ -555,6 +592,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
reader.Read();
}
+
break;
}
@@ -568,6 +606,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
item.AddTrailerUrl(val);
}
+
break;
}
@@ -583,6 +622,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
hasDisplayOrder.DisplayOrder = val;
}
}
+
break;
}
@@ -603,7 +643,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "rating":
{
-
var rating = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(rating))
@@ -614,6 +653,22 @@ namespace MediaBrowser.XbmcMetadata.Parsers
item.CommunityRating = val;
}
}
+
+ break;
+ }
+
+ case "ratings":
+ {
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchFromRatingsNode(subtree, item);
+ }
+ else
+ {
+ reader.Read();
+ }
+
break;
}
@@ -622,15 +677,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "premiered":
case "releasedate":
{
- var formatString = _config.GetNfoConfiguration().ReleaseDateFormat;
+ var formatString = nfoConfiguration.ReleaseDateFormat;
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.PremiereDate = date.ToUniversalTime();
+ item.PremiereDate = date;
item.ProductionYear = date.Year;
}
}
@@ -640,15 +695,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "enddate":
{
- var formatString = _config.GetNfoConfiguration().ReleaseDateFormat;
+ var formatString = nfoConfiguration.ReleaseDateFormat;
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.EndDate = date.ToUniversalTime();
+ item.EndDate = date;
}
}
@@ -670,6 +725,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
item.AddGenre(p);
}
}
+
break;
}
@@ -681,6 +737,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.AddTag(val);
}
+
break;
}
@@ -697,15 +754,48 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
reader.Read();
}
+
+ break;
+ }
+
+ case "uniqueid":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
+ var provider = reader.GetAttribute("type");
+ var id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(provider, id);
+ }
+
+ break;
+ }
+
+ case "thumb":
+ {
+ FetchThumbNode(reader, itemResult);
+ break;
+ }
+
+ case "fanart":
+ {
+ var subtree = reader.ReadSubtree();
+ subtree.ReadToDescendant("thumb");
+ FetchThumbNode(subtree, itemResult);
break;
}
default:
string readerName = reader.Name;
- if (_validProviderIds.TryGetValue(readerName, out string providerIdValue))
+ if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
{
var id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(id))
+ if (!string.IsNullOrWhiteSpace(providerIdValue) && !string.IsNullOrWhiteSpace(id))
{
item.SetProviderId(providerIdValue, id);
}
@@ -714,10 +804,73 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
reader.Skip();
}
+
break;
}
}
+ private void FetchThumbNode(XmlReader reader, MetadataResult<T> itemResult)
+ {
+ var artType = reader.GetAttribute("aspect");
+ var val = reader.ReadElementContentAsString();
+
+ // artType is null if the thumb node is a child of the fanart tag
+ // -> set image type to fanart
+ if (string.IsNullOrWhiteSpace(artType))
+ {
+ artType = "fanart";
+ }
+
+ // skip:
+ // - empty uri
+ // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies
+ if (string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ ImageType imageType = GetImageType(artType);
+
+ if (!Uri.TryCreate(val, UriKind.Absolute, out var uri))
+ {
+ Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, itemResult.Item.Name);
+ return;
+ }
+
+ if (uri.IsFile)
+ {
+ // only allow one item of each type
+ if (itemResult.Images.Any(x => x.Type == imageType))
+ {
+ return;
+ }
+
+ var fileSystemMetadata = _directoryService.GetFile(val);
+ // non existing file returns null
+ if (fileSystemMetadata == null || !fileSystemMetadata.Exists)
+ {
+ Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, itemResult.Item.Name);
+ return;
+ }
+
+ itemResult.Images.Add(new LocalImageInfo()
+ {
+ FileInfo = fileSystemMetadata,
+ Type = imageType
+ });
+ }
+ else
+ {
+ // only allow one item of each type
+ if (itemResult.RemoteImages.Any(x => x.type == imageType))
+ {
+ return;
+ }
+
+ itemResult.RemoteImages.Add((uri.ToString(), imageType));
+ }
+ }
+
private void FetchFromFileInfoNode(XmlReader reader, T item)
{
reader.MoveToContent();
@@ -737,10 +890,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
reader.Read();
continue;
}
+
using (var subtree = reader.ReadSubtree())
{
FetchFromStreamDetailsNode(subtree, item);
}
+
break;
}
@@ -775,10 +930,28 @@ namespace MediaBrowser.XbmcMetadata.Parsers
reader.Read();
continue;
}
+
using (var subtree = reader.ReadSubtree())
{
FetchFromVideoNode(subtree, item);
}
+
+ break;
+ }
+
+ case "subtitle":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ using (var subtree = reader.ReadSubtree())
+ {
+ FetchFromSubtitleNode(subtree, item);
+ }
+
break;
}
@@ -835,6 +1008,91 @@ namespace MediaBrowser.XbmcMetadata.Parsers
video.Video3DFormat = Video3DFormat.MVC;
}
}
+
+ break;
+ }
+
+ case "aspect":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (item is Video video)
+ {
+ video.AspectRatio = val;
+ }
+
+ break;
+ }
+
+ case "width":
+ {
+ var val = reader.ReadElementContentAsInt();
+
+ if (item is Video video)
+ {
+ video.Width = val;
+ }
+
+ break;
+ }
+
+ case "height":
+ {
+ var val = reader.ReadElementContentAsInt();
+
+ if (item is Video video)
+ {
+ video.Height = val;
+ }
+
+ break;
+ }
+
+ case "durationinseconds":
+ {
+ var val = reader.ReadElementContentAsInt();
+
+ if (item is Video video)
+ {
+ video.RunTimeTicks = new TimeSpan(0, 0, val).Ticks;
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
+ private void FetchFromSubtitleNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "language":
+ {
+ _ = reader.ReadElementContentAsString();
+
+ if (item is Video video)
+ {
+ video.HasSubtitles = true;
+ }
+
break;
}
@@ -850,6 +1108,92 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
+ private void FetchFromRatingsNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "rating":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ var ratingName = reader.GetAttribute("name");
+
+ using var subtree = reader.ReadSubtree();
+ FetchFromRatingNode(subtree, item, ratingName);
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
+ private void FetchFromRatingNode(XmlReader reader, T item, string? ratingName)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "value":
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
+ {
+ // if ratingName contains tomato --> assume critic rating
+ if (ratingName != null &&
+ ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) &&
+ !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
+ {
+ item.CriticRating = ratingValue;
+ }
+ else
+ {
+ item.CommunityRating = ratingValue;
+ }
+ }
+ }
+
+ break;
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
/// <summary>
/// Gets the persons from XML node.
/// </summary>
@@ -861,6 +1205,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var type = PersonType.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
+ string? imageUrl = null;
reader.MoveToContent();
reader.Read();
@@ -873,7 +1218,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name)
{
case "name":
- name = reader.ReadElementContentAsString() ?? string.Empty;
+ name = reader.ReadElementContentAsString();
break;
case "role":
@@ -884,19 +1229,58 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
role = val;
}
+
break;
}
+
+ case "type":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ type = val switch
+ {
+ PersonType.Composer => PersonType.Composer,
+ PersonType.Conductor => PersonType.Conductor,
+ PersonType.Director => PersonType.Director,
+ PersonType.Lyricist => PersonType.Lyricist,
+ PersonType.Producer => PersonType.Producer,
+ PersonType.Writer => PersonType.Writer,
+ PersonType.GuestStar => PersonType.GuestStar,
+ // unknown type --> actor
+ _ => PersonType.Actor
+ };
+ }
+
+ break;
+ }
+
+ case "order":
case "sortorder":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
- if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var intVal))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
}
+
+ break;
+ }
+
+ case "thumb":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ imageUrl = val;
+ }
+
break;
}
@@ -916,38 +1300,56 @@ namespace MediaBrowser.XbmcMetadata.Parsers
Name = name.Trim(),
Role = role,
Type = type,
- SortOrder = sortOrder
+ SortOrder = sortOrder,
+ ImageUrl = imageUrl
};
}
+ internal XmlReaderSettings GetXmlReaderSettings()
+ => new XmlReaderSettings()
+ {
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
+
/// <summary>
- /// Used to split names of comma or pipe delimeted genres and people
+ /// 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.IndexOf(';') == -1 ? new[] { ',' } : new[] { '|', ';' };
+ var separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal)
+ ? new[] { ',' }
+ : new[] { '|', ';' };
value = value.Trim().Trim(separator);
- return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
+ return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
- /// Provides an additional overload for string.split
+ /// Parses the ImageType from the nfo aspect property.
/// </summary>
- /// <param name="val">The val.</param>
- /// <param name="separators">The separators.</param>
- /// <param name="options">The options.</param>
- /// <returns>System.String[][].</returns>
- private string[] Split(string val, char[] separators, StringSplitOptions options)
+ /// <param name="aspect">The nfo aspect property.</param>
+ /// <returns>The image type.</returns>
+ private static ImageType GetImageType(string aspect)
{
- return val.Split(separators, options);
+ return aspect switch
+ {
+ "banner" => ImageType.Banner,
+ "clearlogo" => ImageType.Logo,
+ "discart" => ImageType.Disc,
+ "landscape" => ImageType.Thumb,
+ "clearart" => ImageType.Art,
+ "fanart" => ImageType.Backdrop,
+ // unknown type (including "poster") --> primary
+ _ => ImageType.Primary,
+ };
}
}
}