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

This commit is contained in:
Mark Monteiro 2020-03-03 22:27:49 +01:00
commit 76957213e6
27 changed files with 455 additions and 343 deletions

View File

@ -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>();

View File

@ -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" }
};

View File

@ -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);

View File

@ -1,43 +1,35 @@
#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))
{
return null;
}
if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1)
// 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 new Playlist
{
@ -45,21 +37,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
};
}
}
else
{
if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
// 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)))
{
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.GetFileName(args.Path)
};
}
}
// 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))
{
var extension = Path.GetExtension(args.Path);
if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
return new Playlist
{
Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true
};
}
}

View File

@ -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}"

View File

@ -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"
}

View File

@ -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} започна"
}

View File

@ -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 =>
{

View File

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

View File

@ -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);

View File

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

View File

@ -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));
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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 />

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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"
};
}
}
}