using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; namespace MediaBrowser.Model.Entities; /// /// Class ProviderIdsExtensions. /// public static partial class ProviderIdsExtensions { /// /// Case-insensitive dictionary of string representation. /// private static readonly Dictionary _metadataProviderEnumDictionary = Enum.GetValues() .ToDictionary( enumValue => enumValue.ToString(), enumValue => enumValue.ToString(), StringComparer.OrdinalIgnoreCase); /// /// The known id formats, keyed by provider name. /// private static readonly Dictionary> _providerIdValidators = new(StringComparer.OrdinalIgnoreCase) { [MetadataProvider.Imdb.ToString()] = value => ImdbIdRegex().IsMatch(value), [MetadataProvider.Tmdb.ToString()] = IsPositiveNumber, [MetadataProvider.TmdbCollection.ToString()] = IsPositiveNumber, [MetadataProvider.AudioDbArtist.ToString()] = IsPositiveNumber, [MetadataProvider.AudioDbAlbum.ToString()] = IsPositiveNumber, // Every MusicBrainz id is an MBID. [MetadataProvider.MusicBrainzAlbum.ToString()] = IsGuid, [MetadataProvider.MusicBrainzAlbumArtist.ToString()] = IsGuid, [MetadataProvider.MusicBrainzArtist.ToString()] = IsGuid, [MetadataProvider.MusicBrainzReleaseGroup.ToString()] = IsGuid, [MetadataProvider.MusicBrainzRecording.ToString()] = IsGuid, [MetadataProvider.MusicBrainzTrack.ToString()] = IsGuid }; /// /// Checks if this instance has an id for the given provider. /// /// The instance. /// The of the provider name. /// true if a provider id with the given name was found; otherwise false. public static bool HasProviderId(this IHasProviderIds instance, string name) => instance.TryGetProviderId(name, out _); /// /// Checks if this instance has an id for the given provider. /// /// The instance. /// The provider. /// true if a provider id with the given name was found; otherwise false. public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider) => instance.HasProviderId(provider.ToString()); /// /// Gets a provider id. /// /// The instance. /// The name. /// The provider id. /// true if a provider id with the given name was found; otherwise false. public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id) { ArgumentNullException.ThrowIfNull(instance); if (instance.ProviderIds is null) { id = null; return false; } var foundProviderId = instance.ProviderIds.TryGetValue(name, out id); // This occurs when searching with Identify (and possibly in other places) if (string.IsNullOrEmpty(id)) { id = null; foundProviderId = false; } return foundProviderId; } /// /// Gets a provider id. /// /// The instance. /// The provider. /// The provider id. /// true if a provider id with the given name was found; otherwise false. public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id) { return instance.TryGetProviderId(provider.ToString(), out id); } /// /// Gets a provider id. /// /// The instance. /// The name. /// System.String. public static string? GetProviderId(this IHasProviderIds instance, string name) { instance.TryGetProviderId(name, out string? id); return id; } /// /// Gets a provider id. /// /// The instance. /// The provider. /// System.String. public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider) { return instance.GetProviderId(provider.ToString()); } /// /// Checks whether a value can be an id of the given provider. /// /// The provider name. /// The provider id. /// true if the value has a plausible format for the provider; otherwise, false. /// /// Providers regularly hand out an id belonging to a different service, e.g. an IMDb person id in the /// TMDb field. Such an id is not just useless, it also makes the owning provider fail for the item. /// public static bool IsValidProviderId(string? name, string? value) { if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value)) { return false; } return !_providerIdValidators.TryGetValue(name, out var isValid) || isValid(value); } /// /// Sets a provider id. /// /// The instance. /// The name, this should not contain a '=' character. /// The value. /// Due to how deserialization from the database works the name cannot contain '='. /// true if the provider id got set successfully; otherwise, false. public static bool TrySetProviderId(this IHasProviderIds instance, string? name, string? value) { ArgumentNullException.ThrowIfNull(instance); // When name contains a '=' it can't be deserialized from the database if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value) || name.Contains('=', StringComparison.Ordinal)) { return false; } name = name.Trim(); value = value.Trim(); if (!IsValidProviderId(name, value)) { return false; } // Ensure it exists instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase); // Match on internal MetadataProvider enum string values before adding arbitrary providers if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue)) { instance.ProviderIds[enumValue] = value; } else { instance.ProviderIds[name] = value; } return true; } /// /// Sets a provider id. /// /// The instance. /// The provider. /// The value. /// true if the provider id got set successfully; otherwise, false. public static bool TrySetProviderId(this IHasProviderIds instance, MetadataProvider provider, string? value) => instance.TrySetProviderId(provider.ToString(), value); /// /// Sets a provider id. /// /// The instance. /// The name, this should not contain a '=' character. /// The value. public static void SetProviderId(this IHasProviderIds instance, string name, string value) { ArgumentNullException.ThrowIfNull(instance); ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(value); // When name contains a '=' it can't be deserialized from the database if (name.Contains('=', StringComparison.Ordinal)) { throw new ArgumentException("Provider id name cannot contain '='", nameof(name)); } instance.TrySetProviderId(name, value); } /// /// Replaces all provider ids, dropping the ones that cannot belong to the provider they are filed under. /// /// The instance. /// The provider ids to set. public static void SetProviderIds(this IHasProviderIds instance, IReadOnlyDictionary? providerIds) { ArgumentNullException.ThrowIfNull(instance); instance.ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); if (providerIds is null) { return; } foreach (var (name, value) in providerIds) { instance.TrySetProviderId(name, value); } } /// /// Sets a provider id. /// /// The instance. /// The provider. /// The value. public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value) => instance.SetProviderId(provider.ToString(), value); /// /// Removes a provider id. /// /// The instance. /// The name. public static void RemoveProviderId(this IHasProviderIds instance, string name) { ArgumentNullException.ThrowIfNull(instance); ArgumentException.ThrowIfNullOrEmpty(name); instance.ProviderIds?.Remove(name); } /// /// Removes a provider id. /// /// The instance. /// The provider. public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider) { ArgumentNullException.ThrowIfNull(instance); instance.ProviderIds?.Remove(provider.ToString()); } private static bool IsPositiveNumber(string value) => int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var id) && id > 0; private static bool IsGuid(string value) => Guid.TryParse(value, CultureInfo.InvariantCulture, out _); // An IMDb id is a type prefix (tt for titles, nm for people, co for companies, ...) followed by // digits. The prefix is optional because a bare number has always been accepted for a title. [GeneratedRegex(@"^(tt|nm|co|ev|ch|ni)?[0-9]+$", RegexOptions.IgnoreCase)] private static partial Regex ImdbIdRegex(); }