Browse Source

Merge remote-tracking branch 'upstream/master' into support-injecting-iconfiguration

Mark Monteiro 5 years ago
parent
commit
76957213e6
27 changed files with 451 additions and 339 deletions
  1. 2 3
      Emby.Server.Implementations/ApplicationHost.cs
  2. 0 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  3. 1 2
      Emby.Server.Implementations/Library/LibraryManager.cs
  4. 33 30
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  5. 5 5
      Emby.Server.Implementations/Localization/Core/hu.json
  6. 2 1
      Emby.Server.Implementations/Localization/Core/id.json
  7. 25 1
      Emby.Server.Implementations/Localization/Core/mk.json
  8. 2 0
      Jellyfin.Server/Program.cs
  9. 7 1
      Jellyfin.Server/Resources/Configuration/logging.json
  10. 1 1
      MediaBrowser.Api/Library/LibraryService.cs
  11. 1 0
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  12. 2 6
      MediaBrowser.Controller/Entities/BaseItem.cs
  13. 47 6
      MediaBrowser.Controller/Entities/Folder.cs
  14. 15 17
      MediaBrowser.Controller/Sorting/AlphanumComparator.cs
  15. 5 117
      MediaBrowser.Controller/Sorting/SortExtensions.cs
  16. 0 25
      MediaBrowser.Controller/Sorting/SortHelper.cs
  17. 6 1
      MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
  18. 1 1
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  19. 1 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  20. 5 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  21. 0 96
      MediaBrowser.Providers/Music/MusicExternalIds.cs
  22. 22 23
      MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
  23. 18 1
      MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
  24. 44 0
      MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
  25. 69 0
      MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
  26. 98 0
      MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
  27. 39 0
      MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs

+ 2 - 3
Emby.Server.Implementations/ApplicationHost.cs

@@ -666,9 +666,8 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton(JsonSerializer);
 
-            serviceCollection.AddSingleton(LoggerFactory);
-            serviceCollection.AddLogging();
-            serviceCollection.AddSingleton(Logger);
+            // TODO: Support for injecting ILogger should be deprecated in favour of ILogger<T> and this removed
+            serviceCollection.AddSingleton<ILogger>(Logger);
 
             serviceCollection.AddSingleton(FileSystemManager);
             serviceCollection.AddSingleton<TvDbClientManager>();

+ 0 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -8,7 +8,6 @@ namespace Emby.Server.Implementations
         public static Dictionary<string, string> Configuration => new Dictionary<string, string>
         {
             { "HttpListenerHost:DefaultRedirectPath", "web/index.html" },
-            { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" }
         };

+ 1 - 2
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -944,7 +944,6 @@ namespace Emby.Server.Implementations.Library
                     IncludeItemTypes = new[] { typeof(T).Name },
                     Name = name,
                     DtoOptions = options
-
                 }).Cast<MusicArtist>()
                 .OrderBy(i => i.IsAccessedByName ? 1 : 0)
                 .Cast<T>()
@@ -1080,7 +1079,7 @@ namespace Emby.Server.Implementations.Library
 
             var innerProgress = new ActionableProgress<double>();
 
-            innerProgress.RegisterAction(pct => progress.Report(pct * pct * 0.96));
+            innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
 
             // Validate the entire media library
             await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);

+ 33 - 30
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -1,65 +1,68 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1600
-
 using System;
 using System.IO;
 using System.Linq;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.Model.Entities;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
+    /// <summary>
+    /// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
+    /// </summary>
     public class PlaylistResolver : FolderResolver<Playlist>
     {
-        private string[] SupportedCollectionTypes = new string[] {
-
+        private string[] _musicPlaylistCollectionTypes = new string[] {
             string.Empty,
             CollectionType.Music
         };
 
-        /// <summary>
-        /// Resolves the specified args.
-        /// </summary>
-        /// <param name="args">The args.</param>
-        /// <returns>BoxSet.</returns>
+        /// <inheritdoc/>
         protected override Playlist Resolve(ItemResolveArgs args)
         {
-            // It's a boxset if all of the following conditions are met:
-            // Is a Directory
-            // Contains [playlist] in the path
             if (args.IsDirectory)
             {
-                var filename = Path.GetFileName(args.Path);
-
-                if (string.IsNullOrEmpty(filename))
+                // It's a boxset if the path is a directory with [playlist] in it's the name
+                // TODO: Should this use Path.GetDirectoryName() instead?
+                bool isBoxSet = Path.GetFileName(args.Path)
+                    ?.Contains("[playlist]", StringComparison.OrdinalIgnoreCase)
+                    ?? false;
+                if (isBoxSet)
                 {
-                    return null;
+                    return new Playlist
+                    {
+                        Path = args.Path,
+                        Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                    };
                 }
 
-                if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1)
+                // It's a directory-based playlist if the directory contains a playlist file
+                var filePaths = Directory.EnumerateFiles(args.Path);
+                if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
                 {
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                        Name = Path.GetFileName(args.Path)
                     };
                 }
             }
-            else
+
+            // Check if this is a music playlist file
+            // It should have the correct collection type and a supported file extension
+            else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
             {
-                if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                var extension = Path.GetExtension(args.Path);
+                if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
                 {
-                    var extension = Path.GetExtension(args.Path);
-                    if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                    return new Playlist
                     {
-                        return new Playlist
-                        {
-                            Path = args.Path,
-                            Name = Path.GetFileNameWithoutExtension(args.Path),
-                            IsInMixedFolder = true
-                        };
-                    }
+                        Path = args.Path,
+                        Name = Path.GetFileNameWithoutExtension(args.Path),
+                        IsInMixedFolder = true
+                    };
                 }
             }
 

+ 5 - 5
Emby.Server.Implementations/Localization/Core/hu.json

@@ -11,7 +11,7 @@
     "Collections": "Gyűjtemények",
     "DeviceOfflineWithName": "{0} kijelentkezett",
     "DeviceOnlineWithName": "{0} belépett",
-    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet {0}",
+    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}",
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
@@ -27,7 +27,7 @@
     "HeaderNextUp": "Következik",
     "HeaderRecordingGroups": "Felvételi csoportok",
     "HomeVideos": "Házi videók",
-    "Inherit": "Öröklés",
+    "Inherit": "Örökölt",
     "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
     "ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
     "LabelIpAddressValue": "IP cím: {0}",
@@ -73,7 +73,7 @@
     "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
     "Shows": "Műsorok",
     "Songs": "Dalok",
-    "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek próbáld újra később.",
+    "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen:  {0} ehhez: {1}",
     "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz: {0}",
@@ -86,11 +86,11 @@
     "UserDownloadingItemWithValues": "{0} letölti {1}",
     "UserLockedOutWithName": "{0}  felhasználó zárolva van",
     "UserOfflineFromDevice": "{0} kijelentkezett innen:  {1}",
-    "UserOnlineFromDevice": "{0} online itt:  {1}",
+    "UserOnlineFromDevice": "{0} online innen: {1}",
     "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
     "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
     "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt:  {2}",
-    "UserStoppedPlayingItemWithValues": "{0} befejezte a következőt: {1} itt:  {2}",
+    "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
     "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Verzió: {0}"

+ 2 - 1
Emby.Server.Implementations/Localization/Core/id.json

@@ -91,5 +91,6 @@
     "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
     "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
     "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
-    "MixedContent": "Konten campur"
+    "MixedContent": "Konten campur",
+    "PluginUninstalledWithName": "{0} telah dihapus"
 }

+ 25 - 1
Emby.Server.Implementations/Localization/Core/mk.json

@@ -68,5 +68,29 @@
     "Artists": "Изведувач",
     "Application": "Апликација",
     "AppDeviceValues": "Аплиакција: {0}, Уред: {1}",
-    "Albums": "Албуми"
+    "Albums": "Албуми",
+    "VersionNumber": "Верзија {0}",
+    "ValueSpecialEpisodeName": "Специјално - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} е додадено во твојата библиотека",
+    "UserStoppedPlayingItemWithValues": "{0} заврши со репродукција {1} во {2}",
+    "UserStartedPlayingItemWithValues": "{0} пушти {1} на {2}",
+    "UserPolicyUpdatedWithName": "Полисата на користење беше надоградена за {0}",
+    "UserPasswordChangedWithName": "Лозинката е сменета за корисникот {0}",
+    "UserOnlineFromDevice": "{0} е приклучен од {1}",
+    "UserOfflineFromDevice": "{0} е дисконектиран од {1}",
+    "UserLockedOutWithName": "Корисникот {0} е заклучен",
+    "UserDownloadingItemWithValues": "{0} се спушта {1}",
+    "UserDeletedWithName": "Корисникот {0} е избришан",
+    "UserCreatedWithName": "Корисникот {0} е креиран",
+    "User": "Корисник",
+    "TvShows": "ТВ Серии",
+    "System": "Систем",
+    "Sync": "Синхронизација",
+    "SubtitlesDownloadedForItem": "Спуштање превод за {0}",
+    "SubtitleDownloadFailureFromForItem": "Преводот неуспешно се спушти од {0} за {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin Server се пушта. Ве молиме причекајте.",
+    "Songs": "Песни",
+    "Shows": "Серии",
+    "ServerNameNeedsToBeRestarted": "{0} треба да се рестартира",
+    "ScheduledTaskStartedWithName": "{0} започна"
 }

+ 2 - 0
Jellyfin.Server/Program.cs

@@ -26,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Serilog;
+using Serilog.Events;
 using Serilog.Extensions.Logging;
 using SQLitePCL;
 using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -262,6 +263,7 @@ namespace Jellyfin.Server
                     }
                 })
                 .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths))
+                .UseSerilog()
                 .UseContentRoot(appHost.ContentRoot)
                 .ConfigureServices(services =>
                 {

+ 7 - 1
Jellyfin.Server/Resources/Configuration/logging.json

@@ -1,6 +1,12 @@
 {
     "Serilog": {
-        "MinimumLevel": "Information",
+        "MinimumLevel": {
+            "Default": "Information",
+            "Override": {
+                "Microsoft": "Warning",
+                "System": "Warning"
+            }
+        },
         "WriteTo": [
             {
                 "Name": "Console",

+ 1 - 1
MediaBrowser.Api/Library/LibraryService.cs

@@ -815,7 +815,7 @@ namespace MediaBrowser.Api.Library
             if (!string.IsNullOrWhiteSpace(filename))
             {
                 // Kestrel doesn't support non-ASCII characters in headers
-                if (Regex.IsMatch(filename, "[^[:ascii:]]"))
+                if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
                 {
                     // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
                     headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);

+ 1 - 0
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -198,6 +198,7 @@ namespace MediaBrowser.Controller.Entities.Audio
                     return true;
                 }
             }
+
             return base.RequiresRefresh();
         }
 

+ 2 - 6
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -387,15 +387,12 @@ namespace MediaBrowser.Controller.Entities
 
             while (thisMarker < s1.Length)
             {
-                if (thisMarker >= s1.Length)
-                {
-                    break;
-                }
                 char thisCh = s1[thisMarker];
 
                 var thisChunk = new StringBuilder();
+                bool isNumeric = char.IsDigit(thisCh);
 
-                while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0])))
+                while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric)
                 {
                     thisChunk.Append(thisCh);
                     thisMarker++;
@@ -406,7 +403,6 @@ namespace MediaBrowser.Controller.Entities
                     }
                 }
 
-                var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]);
                 list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
             }
 

+ 47 - 6
MediaBrowser.Controller/Entities/Folder.cs

@@ -322,10 +322,10 @@ namespace MediaBrowser.Controller.Entities
                     ProviderManager.OnRefreshProgress(this, 5);
                 }
 
-                //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
                 var currentChildren = GetActualChildrenDictionary();
 
-                //create a list for our validated children
+                // Create a list for our validated children
                 var newItems = new List<BaseItem>();
 
                 cancellationToken.ThrowIfCancellationRequested();
@@ -391,7 +391,7 @@ namespace MediaBrowser.Controller.Entities
                 var folder = this;
                 innerProgress.RegisterAction(p =>
                 {
-                    double newPct = .80 * p + 10;
+                    double newPct = 0.80 * p + 10;
                     progress.Report(newPct);
                     ProviderManager.OnRefreshProgress(folder, newPct);
                 });
@@ -421,7 +421,7 @@ namespace MediaBrowser.Controller.Entities
                 var folder = this;
                 innerProgress.RegisterAction(p =>
                 {
-                    double newPct = .10 * p + 90;
+                    double newPct = 0.10 * p + 90;
                     progress.Report(newPct);
                     if (recursive)
                     {
@@ -807,11 +807,45 @@ namespace MediaBrowser.Controller.Entities
             return false;
         }
 
+        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
+        {
+            var ids = query.ItemIds;
+            int size = items.Count;
+
+            // ids can potentially contain non-unique guids, but query result cannot,
+            // so we include only first occurrence of each guid
+            var positions = new Dictionary<Guid, int>(size);
+            int index = 0;
+            for (int i = 0; i < ids.Length; i++)
+            {
+                if (positions.TryAdd(ids[i], index))
+                {
+                    index++;
+                }
+            }
+
+            var newItems = new BaseItem[size];
+            for (int i = 0; i < size; i++)
+            {
+                var item = items[i];
+                newItems[positions[item.Id]] = item;
+            }
+
+            return newItems;
+        }
+
         public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
         {
             if (query.ItemIds.Length > 0)
             {
-                return LibraryManager.GetItemsResult(query);
+                var result = LibraryManager.GetItemsResult(query);
+
+                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
+                {
+                    result.Items = SortItemsByRequest(query, result.Items);
+                }
+
+                return result;
             }
 
             return GetItemsInternal(query);
@@ -823,7 +857,14 @@ namespace MediaBrowser.Controller.Entities
 
             if (query.ItemIds.Length > 0)
             {
-                return LibraryManager.GetItemList(query);
+                var result = LibraryManager.GetItemList(query);
+
+                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
+                {
+                    return SortItemsByRequest(query, result);
+                }
+
+                return result.ToArray();
             }
 
             return GetItemsInternal(query).Items;

+ 15 - 17
Emby.Server.Implementations/Sorting/AlphanumComparator.cs → MediaBrowser.Controller/Sorting/AlphanumComparator.cs

@@ -2,7 +2,7 @@ using System.Collections.Generic;
 using System.Text;
 using MediaBrowser.Controller.Sorting;
 
-namespace Emby.Server.Implementations.Sorting
+namespace MediaBrowser.Controller.Sorting
 {
     public class AlphanumComparator : IComparer<string>
     {
@@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Sorting
 
                 var thisChunk = new StringBuilder();
                 var thatChunk = new StringBuilder();
+                bool thisNumeric = char.IsDigit(thisCh), thatNumeric = char.IsDigit(thatCh);
 
-                while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0])))
+                while (thisMarker < s1.Length && char.IsDigit(thisCh) == thisNumeric)
                 {
                     thisChunk.Append(thisCh);
                     thisMarker++;
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Sorting
                     }
                 }
 
-                while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || SortHelper.InChunk(thatCh, thatChunk[0])))
+                while (thatMarker < s2.Length && char.IsDigit(thatCh) == thatNumeric)
                 {
                     thatChunk.Append(thatCh);
                     thatMarker++;
@@ -54,38 +55,35 @@ namespace Emby.Server.Implementations.Sorting
                     }
                 }
 
-                int result = 0;
+
                 // If both chunks contain numeric characters, sort them numerically
-                if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
+                if (thisNumeric && thatNumeric)
                 {
-                    if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk))
-                    {
-                        return 0;
-                    }
-                    if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk))
+                    if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)
+                        || !int.TryParse(thatChunk.ToString(), out thatNumericChunk))
                     {
                         return 0;
                     }
 
                     if (thisNumericChunk < thatNumericChunk)
                     {
-                        result = -1;
+                        return -1;
                     }
 
                     if (thisNumericChunk > thatNumericChunk)
                     {
-                        result = 1;
+                        return 1;
                     }
                 }
                 else
                 {
-                    result = thisChunk.ToString().CompareTo(thatChunk.ToString());
+                    int result = thisChunk.ToString().CompareTo(thatChunk.ToString());
+                    if (result != 0)
+                    {
+                        return result;
+                    }
                 }
 
-                if (result != 0)
-                {
-                    return result;
-                }
             }
 
             return 0;

+ 5 - 117
MediaBrowser.Controller/Sorting/SortExtensions.cs

@@ -7,137 +7,25 @@ namespace MediaBrowser.Controller.Sorting
 {
     public static class SortExtensions
     {
+        private static readonly AlphanumComparator _comparer = new AlphanumComparator();
         public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName)
         {
-            return list.OrderBy(getName, new AlphanumComparator());
+            return list.OrderBy(getName, _comparer);
         }
 
         public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName)
         {
-            return list.OrderByDescending(getName, new AlphanumComparator());
+            return list.OrderByDescending(getName, _comparer);
         }
 
         public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName)
         {
-            return list.ThenBy(getName, new AlphanumComparator());
+            return list.ThenBy(getName, _comparer);
         }
 
         public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName)
         {
-            return list.ThenByDescending(getName, new AlphanumComparator());
-        }
-
-        private class AlphanumComparator : IComparer<string>
-        {
-            private enum ChunkType { Alphanumeric, Numeric };
-
-            private static bool InChunk(char ch, char otherCh)
-            {
-                var type = ChunkType.Alphanumeric;
-
-                if (char.IsDigit(otherCh))
-                {
-                    type = ChunkType.Numeric;
-                }
-
-                if ((type == ChunkType.Alphanumeric && char.IsDigit(ch))
-                    || (type == ChunkType.Numeric && !char.IsDigit(ch)))
-                {
-                    return false;
-                }
-
-                return true;
-            }
-
-            public static int CompareValues(string s1, string s2)
-            {
-                if (s1 == null || s2 == null)
-                {
-                    return 0;
-                }
-
-                int thisMarker = 0, thisNumericChunk = 0;
-                int thatMarker = 0, thatNumericChunk = 0;
-
-                while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
-                {
-                    if (thisMarker >= s1.Length)
-                    {
-                        return -1;
-                    }
-                    else if (thatMarker >= s2.Length)
-                    {
-                        return 1;
-                    }
-                    char thisCh = s1[thisMarker];
-                    char thatCh = s2[thatMarker];
-
-                    var thisChunk = new StringBuilder();
-                    var thatChunk = new StringBuilder();
-
-                    while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0])))
-                    {
-                        thisChunk.Append(thisCh);
-                        thisMarker++;
-
-                        if (thisMarker < s1.Length)
-                        {
-                            thisCh = s1[thisMarker];
-                        }
-                    }
-
-                    while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0])))
-                    {
-                        thatChunk.Append(thatCh);
-                        thatMarker++;
-
-                        if (thatMarker < s2.Length)
-                        {
-                            thatCh = s2[thatMarker];
-                        }
-                    }
-
-                    int result = 0;
-                    // If both chunks contain numeric characters, sort them numerically
-                    if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
-                    {
-                        if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk))
-                        {
-                            return 0;
-                        }
-                        if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk))
-                        {
-                            return 0;
-                        }
-
-                        if (thisNumericChunk < thatNumericChunk)
-                        {
-                            result = -1;
-                        }
-
-                        if (thisNumericChunk > thatNumericChunk)
-                        {
-                            result = 1;
-                        }
-                    }
-                    else
-                    {
-                        result = thisChunk.ToString().CompareTo(thatChunk.ToString());
-                    }
-
-                    if (result != 0)
-                    {
-                        return result;
-                    }
-                }
-
-                return 0;
-            }
-
-            public int Compare(string x, string y)
-            {
-                return CompareValues(x, y);
-            }
+            return list.ThenByDescending(getName, _comparer);
         }
     }
 }

+ 0 - 25
MediaBrowser.Controller/Sorting/SortHelper.cs

@@ -1,25 +0,0 @@
-namespace MediaBrowser.Controller.Sorting
-{
-    public static class SortHelper
-    {
-        private enum ChunkType { Alphanumeric, Numeric };
-
-        public static bool InChunk(char ch, char otherCh)
-        {
-            var type = ChunkType.Alphanumeric;
-
-            if (char.IsDigit(otherCh))
-            {
-                type = ChunkType.Numeric;
-            }
-
-            if ((type == ChunkType.Alphanumeric && char.IsDigit(ch))
-                || (type == ChunkType.Numeric && !char.IsDigit(ch)))
-            {
-                return false;
-            }
-
-            return true;
-        }
-    }
-}

+ 6 - 1
MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs

@@ -11,6 +11,11 @@ namespace MediaBrowser.LocalMetadata.Savers
 {
     public class PlaylistXmlSaver : BaseXmlSaver
     {
+        /// <summary>
+        /// The default file name to use when creating a new playlist.
+        /// </summary>
+        public const string DefaultPlaylistFilename = "playlist.xml";
+
         public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
         {
             if (!item.SupportsLocalMetadata)
@@ -45,7 +50,7 @@ namespace MediaBrowser.LocalMetadata.Savers
                 return Path.ChangeExtension(itemPath, ".xml");
             }
 
-            return Path.Combine(path, "playlist.xml");
+            return Path.Combine(path, DefaultPlaylistFilename);
         }
 
         public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)

+ 1 - 1
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -34,7 +34,7 @@ namespace MediaBrowser.Model.Configuration
         public EncodingOptions()
         {
             DownMixAudioBoost = 2;
-            EnableThrottling = true;
+            EnableThrottling = false;
             ThrottleDelaySeconds = 180;
             EncodingThreadCount = -1;
             // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything

+ 1 - 1
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -238,7 +238,7 @@ namespace MediaBrowser.Model.Configuration
             CodecsUsed = Array.Empty<string>();
             PathSubstitutions = Array.Empty<PathSubstitution>();
             IgnoreVirtualInterfaces = false;
-            EnableSimpleArtistDetection = true;
+            EnableSimpleArtistDetection = false;
 
             DisplaySpecialsWithinSeasons = true;
             EnableExternalContentInSuggestions = true;

+ 5 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -24,4 +24,9 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
 
+  <ItemGroup>
+    <None Remove="Plugins\MusicBrainz\Configuration\config.html" />
+    <EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" />
+  </ItemGroup>
+
 </Project>

+ 0 - 96
MediaBrowser.Providers/Music/MusicExternalIds.cs

@@ -1,105 +1,9 @@
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Providers.Music
 {
-    public class MusicBrainzReleaseGroupExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string Name => "MusicBrainz Release Group";
-
-        /// <inheritdoc />
-        public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString();
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://musicbrainz.org/release-group/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item)
-            => item is Audio || item is MusicAlbum;
-    }
-
-    public class MusicBrainzAlbumArtistExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string Name => "MusicBrainz Album Artist";
-
-        /// <inheritdoc />
-        public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString();
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://musicbrainz.org/artist/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item)
-            => item is Audio;
-    }
-
-    public class MusicBrainzAlbumExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string Name => "MusicBrainz Album";
-
-        /// <inheritdoc />
-        public string Key => MetadataProviders.MusicBrainzAlbum.ToString();
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://musicbrainz.org/release/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item)
-            => item is Audio || item is MusicAlbum;
-    }
-
-    public class MusicBrainzArtistExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string Name => "MusicBrainz";
-
-        /// <inheritdoc />
-        public string Key => MetadataProviders.MusicBrainzArtist.ToString();
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://musicbrainz.org/artist/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is MusicArtist;
-    }
-
-    public class MusicBrainzOtherArtistExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string Name => "MusicBrainz Artist";
-
-        /// <inheritdoc />
-
-        public string Key => MetadataProviders.MusicBrainzArtist.ToString();
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://musicbrainz.org/artist/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item)
-            => item is Audio || item is MusicAlbum;
-    }
-
-    public class MusicBrainzTrackId : IExternalId
-    {
-        /// <inheritdoc />
-        public string Name => "MusicBrainz Track";
-
-        /// <inheritdoc />
-        public string Key => MetadataProviders.MusicBrainzTrack.ToString();
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://musicbrainz.org/track/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Audio;
-    }
-
     public class ImvdbId : IExternalId
     {
         /// <inheritdoc />

+ 22 - 23
MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs → MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs

@@ -15,7 +15,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Configuration;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Music
@@ -28,7 +28,7 @@ namespace MediaBrowser.Providers.Music
         /// Be prudent, use a value slightly above the minimun required.
         /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
         /// </summary>
-        private const long MusicBrainzQueryIntervalMs = 1050u;
+        private readonly long _musicBrainzQueryIntervalMs;
 
         /// <summary>
         /// For each single MB lookup/search, this is the maximum number of
@@ -50,14 +50,14 @@ namespace MediaBrowser.Providers.Music
         public MusicBrainzAlbumProvider(
             IHttpClient httpClient,
             IApplicationHost appHost,
-            ILogger logger,
-            IConfiguration configuration)
+            ILogger logger)
         {
             _httpClient = httpClient;
             _appHost = appHost;
             _logger = logger;
 
-            _musicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"];
+            _musicBrainzBaseUrl = Plugin.Instance.Configuration.Server;
+            _musicBrainzQueryIntervalMs = Plugin.Instance.Configuration.RateLimit;
 
             // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit
             _stopWatchMusicBrainz.Start();
@@ -74,6 +74,12 @@ namespace MediaBrowser.Providers.Music
         /// <inheritdoc />
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
         {
+            // TODO maybe remove when artist metadata can be disabled
+            if (!Plugin.Instance.Configuration.Enable)
+            {
+                return Enumerable.Empty<RemoteSearchResult>();
+            }
+
             var releaseId = searchInfo.GetReleaseId();
             var releaseGroupId = searchInfo.GetReleaseGroupId();
 
@@ -107,8 +113,8 @@ namespace MediaBrowser.Providers.Music
                     url = string.Format(
                         CultureInfo.InvariantCulture,
                         "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"",
-                       WebUtility.UrlEncode(queryName),
-                       WebUtility.UrlEncode(searchInfo.GetAlbumArtist()));
+                        WebUtility.UrlEncode(queryName),
+                        WebUtility.UrlEncode(searchInfo.GetAlbumArtist()));
                 }
             }
 
@@ -170,7 +176,6 @@ namespace MediaBrowser.Providers.Music
                         }
 
                         return result;
-
                     });
                 }
             }
@@ -187,6 +192,12 @@ namespace MediaBrowser.Providers.Music
                 Item = new MusicAlbum()
             };
 
+            // TODO maybe remove when artist metadata can be disabled
+            if (!Plugin.Instance.Configuration.Enable)
+            {
+                return result;
+            }
+
             // If we have a release group Id but not a release Id...
             if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId))
             {
@@ -456,18 +467,6 @@ namespace MediaBrowser.Providers.Music
                                 }
                             case "artist-credit":
                                 {
-                                    // TODO
-
-                                    /*
-                                     * <artist-credit>
-<name-credit>
-<artist id="e225cda5-882d-4b80-b8a3-b36d7175b1ea">
-<name>SARCASTIC+ZOOKEEPER</name>
-<sort-name>SARCASTIC+ZOOKEEPER</sort-name>
-</artist>
-</name-credit>
-</artist-credit>
-                                     */
                                     using (var subReader = reader.ReadSubtree())
                                     {
                                         var artist = ParseArtistCredit(subReader);
@@ -764,10 +763,10 @@ namespace MediaBrowser.Providers.Music
             {
                 attempts++;
 
-                if (_stopWatchMusicBrainz.ElapsedMilliseconds < MusicBrainzQueryIntervalMs)
+                if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
                 {
                     // MusicBrainz is extremely adamant about limiting to one request per second
-                    var delayMs = MusicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
+                    var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
                     await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
                 }
 
@@ -778,7 +777,7 @@ namespace MediaBrowser.Providers.Music
 
                 response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false);
 
-                // We retry a finite number of times, and only whilst MB is indcating 503 (throttling)
+                // We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
             }
             while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
 

+ 18 - 1
MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs → MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs

@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
 
 namespace MediaBrowser.Providers.Music
 {
@@ -22,6 +23,12 @@ namespace MediaBrowser.Providers.Music
         /// <inheritdoc />
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
         {
+            // TODO maybe remove when artist metadata can be disabled
+            if (!Plugin.Instance.Configuration.Enable)
+            {
+                return Enumerable.Empty<RemoteSearchResult>();
+            }
+
             var musicBrainzId = searchInfo.GetMusicBrainzArtistId();
 
             if (!string.IsNullOrWhiteSpace(musicBrainzId))
@@ -226,6 +233,12 @@ namespace MediaBrowser.Providers.Music
                 Item = new MusicArtist()
             };
 
+            // TODO maybe remove when artist metadata can be disabled
+            if (!Plugin.Instance.Configuration.Enable)
+            {
+                return result;
+            }
+
             var musicBrainzId = id.GetMusicBrainzArtistId();
 
             if (string.IsNullOrWhiteSpace(musicBrainzId))
@@ -237,8 +250,12 @@ namespace MediaBrowser.Providers.Music
                 if (singleResult != null)
                 {
                     musicBrainzId = singleResult.GetProviderId(MetadataProviders.MusicBrainzArtist);
-                    //result.Item.Name = singleResult.Name;
                     result.Item.Overview = singleResult.Overview;
+
+                    if (Plugin.Instance.Configuration.ReplaceArtistName)
+                    {
+                        result.Item.Name = singleResult.Name;
+                    }
                 }
             }
 

+ 44 - 0
MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs

@@ -0,0 +1,44 @@
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz
+{
+    public class PluginConfiguration : BasePluginConfiguration
+    {
+        private string _server = Plugin.DefaultServer;
+
+        private long _rateLimit = Plugin.DefaultRateLimit;
+
+        public string Server
+        {
+            get
+            {
+                return _server;
+            }
+
+            set
+            {
+                _server = value.TrimEnd('/');
+            }
+        }
+
+        public long RateLimit
+        {
+            get
+            {
+                return _rateLimit;
+            }
+
+            set
+            {
+                if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer)
+                {
+                    RateLimit = Plugin.DefaultRateLimit;
+                }
+            }
+        }
+
+        public bool Enable { get; set; }
+
+        public bool ReplaceArtistName { get; set; }
+    }
+}

+ 69 - 0
MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html

@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>MusicBrainz</title>
+</head>
+<body>
+    <div data-role="page" class="page type-interior pluginConfigurationPage musicBrainzConfigPage" data-require="emby-input,emby-button,emby-checkbox">
+        <div data-role="content">
+            <div class="content-primary">
+                <form class="musicBrainzConfigForm">
+                    <div class="inputContainer">
+                        <input is="emby-input" type="text" id="server" required label="Server" />
+                        <div class="fieldDescription">This can be a mirror of the official server or even a custom server.</div>
+                    </div>
+                    <div class="inputContainer">
+                        <input is="emby-input" type="number" id="rateLimit" pattern="[0-9]*" required min="0" max="10000" label="Rate Limit" />
+                        <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div>
+                    </div>
+                    <label class="checkboxContainer">
+                        <input is="emby-checkbox" type="checkbox" id="enable" />
+                        <span>Enable this provider for metadata searches on artists and albums.</span>
+                    </label>
+                    <label class="checkboxContainer">
+                        <input is="emby-checkbox" type="checkbox" id="replaceArtistName" />
+                        <span>When an artist is found during a metadata search, replace the artist name with the value on the server.</span>
+                    </label>
+                    <br />
+                    <div>
+                        <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+                    </div>
+                </form>
+            </div>
+        </div>
+        <script type="text/javascript">
+            var MusicBrainzPluginConfig = {
+                uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
+            };
+
+            $('.musicBrainzConfigPage').on('pageshow', function () {
+                Dashboard.showLoadingMsg();
+                ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+                    $('#server').val(config.Server).change();
+                    $('#rateLimit').val(config.RateLimit).change();
+                    $('#enable').checked(config.Enable);
+                    $('#replaceArtistName').checked(config.ReplaceArtistName);
+
+                    Dashboard.hideLoadingMsg();
+                });
+            });
+
+            $('.musicBrainzConfigForm').on('submit', function (e) {
+                Dashboard.showLoadingMsg();
+
+                var form = this;
+                ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+                    config.Server = $('#server', form).val();
+                    config.RateLimit = $('#rateLimit', form).val();
+                    config.Enable = $('#enable', form).checked();
+                    config.ReplaceArtistName = $('#replaceArtistName', form).checked();
+
+                    ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+                });
+
+                return false;
+            });
+        </script>
+    </div>
+</body>
+</html>

+ 98 - 0
MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs

@@ -0,0 +1,98 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+    public class MusicBrainzReleaseGroupExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string Name => "MusicBrainz Release Group";
+
+        /// <inheritdoc />
+        public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString();
+
+        /// <inheritdoc />
+        public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+    }
+
+    public class MusicBrainzAlbumArtistExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string Name => "MusicBrainz Album Artist";
+
+        /// <inheritdoc />
+        public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString();
+
+        /// <inheritdoc />
+        public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio;
+    }
+
+    public class MusicBrainzAlbumExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string Name => "MusicBrainz Album";
+
+        /// <inheritdoc />
+        public string Key => MetadataProviders.MusicBrainzAlbum.ToString();
+
+        /// <inheritdoc />
+        public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+    }
+
+    public class MusicBrainzArtistExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string Name => "MusicBrainz";
+
+        /// <inheritdoc />
+        public string Key => MetadataProviders.MusicBrainzArtist.ToString();
+
+        /// <inheritdoc />
+        public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is MusicArtist;
+    }
+
+    public class MusicBrainzOtherArtistExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string Name => "MusicBrainz Artist";
+
+        /// <inheritdoc />
+
+        public string Key => MetadataProviders.MusicBrainzArtist.ToString();
+
+        /// <inheritdoc />
+        public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+    }
+
+    public class MusicBrainzTrackId : IExternalId
+    {
+        /// <inheritdoc />
+        public string Name => "MusicBrainz Track";
+
+        /// <inheritdoc />
+        public string Key => MetadataProviders.MusicBrainzTrack.ToString();
+
+        /// <inheritdoc />
+        public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio;
+    }
+}

+ 39 - 0
MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz
+{
+    public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+    {
+        public static Plugin Instance { get; private set; }
+
+        public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
+
+        public override string Name => "MusicBrainz";
+
+        public override string Description => "Get artist and album metadata from any MusicBrainz server.";
+
+        public const string DefaultServer = "https://musicbrainz.org";
+
+        public const long DefaultRateLimit = 2000u;
+
+        public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+            : base(applicationPaths, xmlSerializer)
+        {
+            Instance = this;
+        }
+
+        public IEnumerable<PluginPageInfo> GetPages()
+        {
+            yield return new PluginPageInfo
+            {
+                Name = Name,
+                EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+            };
+        }
+    }
+}