diff options
38 files changed, 1138 insertions, 3897 deletions
diff --git a/.gitmodules b/.gitmodules index c10f5905c..2b97b1331 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "MediaBrowser.WebDashboard/jellyfin-web"] path = MediaBrowser.WebDashboard/jellyfin-web url = https://github.com/jellyfin/jellyfin-web.git + branch = . diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 39149910c..758202af6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -19,6 +19,7 @@ - [LogicalPhallacy](https://github.com/LogicalPhallacy/) - [RazeLighter777](https://github.com/RazeLighter777) - [WillWill56](https://github.com/WillWill56) + - [Liggy](https://github.com/Liggy) - [fruhnow](https://github.com/fruhnow) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index 6c0d2515f..978b0d540 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN dotnet publish \ --output /jellyfin \ Jellyfin.Server -FROM jrottenberg/ffmpeg:4.0-vaapi as ffmpeg +FROM jellyfin/ffmpeg as ffmpeg FROM microsoft/dotnet:${DOTNET_VERSION}-runtime # libfontconfig1 is required for Skia RUN apt-get update \ diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index ef97b8739..afedc30ef 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -175,25 +175,52 @@ namespace Emby.Naming.Video return videos; } + var list = new List<VideoInfo>(); + var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path)); if (!string.IsNullOrEmpty(folderName) && folderName.Length > 1) { - var ordered = videos.OrderBy(i => i.Name); - - return ordered.GroupBy(v => new {v.Name, v.Year}).Select(group => new VideoInfo + if (videos.All(i => i.Files.Count == 1 && IsEligibleForMultiVersion(folderName, i.Files[0].Path))) { - Name = folderName, - Year = group.First().Year, - Files = group.First().Files, - AlternateVersions = group.Skip(1).Select(i => i.Files[0]).ToList(), - Extras = group.First().Extras.Concat(group.Skip(1).SelectMany(i => i.Extras)).ToList() - }); + if (HaveSameYear(videos)) + { + var ordered = videos.OrderBy(i => i.Name).ToList(); + + list.Add(ordered[0]); + + list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList(); + list[0].Name = folderName; + list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); + + return list; + } + } } return videos; } + private bool HaveSameYear(List<VideoInfo> videos) + { + return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; + } + + private bool IsEligibleForMultiVersion(string folderName, string testFilename) + { + testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty; + + if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) + { + testFilename = testFilename.Substring(folderName.Length).Trim(); + return string.IsNullOrEmpty(testFilename) || + testFilename.StartsWith("-") || + string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)) ; + } + + return false; + } + private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames) { foreach (var name in baseNames.ToList()) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8daba0585..45a819f26 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -102,6 +102,7 @@ using MediaBrowser.Model.Xml; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Subtitles; +using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.Extensions.Configuration; @@ -549,16 +550,18 @@ namespace Emby.Server.Implementations var entryPoints = GetExports<IServerEntryPoint>(); - var now = DateTime.UtcNow; + var stopWatch = new Stopwatch(); + stopWatch.Start(); await Task.WhenAll(StartEntryPoints(entryPoints, true)); - Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:fff} ms", DateTime.Now - now); + Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); HttpServer.GlobalResponse = null; - now = DateTime.UtcNow; + stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)); - Logger.LogInformation("Executed all post-startup entry points in {Elapsed:fff} ms", DateTime.Now - now); + Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); + stopWatch.Stop(); } private IEnumerable<Task> StartEntryPoints(IEnumerable<IServerEntryPoint> entryPoints, bool isBeforeStartup) @@ -623,12 +626,13 @@ namespace Emby.Server.Implementations /// </summary> protected async Task RegisterResources(IServiceCollection serviceCollection) { + serviceCollection.AddMemoryCache(); + serviceCollection.AddSingleton(ConfigurationManager); serviceCollection.AddSingleton<IApplicationHost>(this); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton(JsonSerializer); serviceCollection.AddSingleton(LoggerFactory); @@ -638,6 +642,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(EnvironmentInfo); serviceCollection.AddSingleton(FileSystemManager); + serviceCollection.AddSingleton<TvDbClientManager>(); HttpClient = CreateHttpClient(); serviceCollection.AddSingleton(HttpClient); diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index ad6c537ef..3c7cbb115 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels public static string GetUserDistinctValue(User user) { var channels = user.Policy.EnabledChannels - .OrderBy(i => i) - .ToList(); + .OrderBy(i => i); - return string.Join("|", channels.ToArray()); + return string.Join("|", channels); } private void CleanDatabase(CancellationToken cancellationToken) { var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); - var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery + var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Channel).Name } + IncludeItemTypes = new[] { typeof(Channel).Name }, + ExcludeItemIds = installedChannelIds.ToArray() }); - var invalidIds = databaseIds - .Except(installedChannelIds) - .ToList(); - - foreach (var id in invalidIds) + foreach (var channel in uninstalledChannels) { cancellationToken.ThrowIfCancellationRequested(); - CleanChannel(id, cancellationToken); + CleanChannel((Channel)channel, cancellationToken); } } - private void CleanChannel(Guid id, CancellationToken cancellationToken) + private void CleanChannel(Channel channel, CancellationToken cancellationToken) { - _logger.LogInformation("Cleaning channel {0} from database", id); + _logger.LogInformation("Cleaning channel {0} from database", channel.Id); // Delete all channel items - var allIds = _libraryManager.GetItemIds(new InternalItemsQuery + var items = _libraryManager.GetItemList(new InternalItemsQuery { - ChannelIds = new[] { id } + ChannelIds = new[] { channel.Id } }); - foreach (var deleteId in allIds) + foreach (var item in items) { cancellationToken.ThrowIfCancellationRequested(); - DeleteItem(deleteId); - } - - // Finally, delete the channel itself - DeleteItem(id); - } + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false - private void DeleteItem(Guid id) - { - var item = _libraryManager.GetItemById(id); - - if (item == null) - { - return; + }, false); } - _libraryManager.DeleteItem(item, new DeleteOptions + // Finally, delete the channel itself + _libraryManager.DeleteItem(channel, new DeleteOptions { DeleteFileLocation = false diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 6502e4aed..70e5fa640 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2747,7 +2747,7 @@ namespace Emby.Server.Implementations.Data if (elapsed >= slowThreshold) { - Logger.LogWarning("{0} query time (slow): {1}ms. Query: {2}", + Logger.LogWarning("{0} query time (slow): {1:g}. Query: {2}", methodName, elapsed, commandText); diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 2232b3eeb..2e0728136 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -539,21 +539,10 @@ namespace Emby.Server.Implementations.HttpClientManager var contentLength = GetContentLength(httpResponse); - if (contentLength.HasValue) - { - using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await httpResponse.GetResponseStream().CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); - } - } - else + using (var stream = httpResponse.GetResponseStream()) + using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { - // We're not able to track progress - using (var stream = httpResponse.GetResponseStream()) - using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); - } + await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); } options.Progress.Report(100); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index d78891ac7..ee746c669 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -67,7 +68,7 @@ namespace Emby.Server.Implementations.HttpServer _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - + _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); Instance = this; @@ -286,31 +287,6 @@ namespace Emby.Server.Implementations.HttpServer } } - private static readonly string[] _skipLogExtensions = - { - ".js", - ".css", - ".woff", - ".woff2", - ".ttf", - ".html" - }; - - private bool EnableLogging(string url, string localPath) - { - var extension = GetExtension(url); - - return ((string.IsNullOrEmpty(extension) || !_skipLogExtensions.Contains(extension)) - && (string.IsNullOrEmpty(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)); - } - - private static string GetExtension(string url) - { - var parts = url.Split(new[] { '?' }, 2); - - return Path.GetExtension(parts[0]); - } - public static string RemoveQueryStringByKey(string url, string key) { var uri = new Uri(url); @@ -448,10 +424,9 @@ namespace Emby.Server.Implementations.HttpServer /// </summary> protected async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { - var date = DateTime.Now; + var stopWatch = new Stopwatch(); + stopWatch.Start(); var httpRes = httpReq.Response; - bool enableLog = false; - bool logHeaders = false; string urlToLog = null; string remoteIp = httpReq.RemoteIp; @@ -498,18 +473,8 @@ namespace Emby.Server.Implementations.HttpServer return; } - var operationName = httpReq.OperationName; - - enableLog = EnableLogging(urlString, localPath); - urlToLog = urlString; - logHeaders = enableLog && urlToLog.IndexOf("/videos/", StringComparison.OrdinalIgnoreCase) != -1; - - if (enableLog) - { - urlToLog = GetUrlToLog(urlString); - - LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent, logHeaders ? httpReq.Headers : null); - } + urlToLog = GetUrlToLog(urlString); + Logger.LogDebug("HTTP {HttpMethod} {Url} UserAgent: {UserAgent} \nHeaders: {@Headers}", urlToLog, httpReq.UserAgent ?? string.Empty, httpReq.HttpMethod, httpReq.Headers); if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) @@ -517,6 +482,7 @@ namespace Emby.Server.Implementations.HttpServer RedirectToUrl(httpRes, DefaultRedirectPath); return; } + if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) { @@ -562,16 +528,19 @@ namespace Emby.Server.Implementations.HttpServer RedirectToUrl(httpRes, DefaultRedirectPath); return; } + if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) { RedirectToUrl(httpRes, "../" + DefaultRedirectPath); return; } + if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { RedirectToUrl(httpRes, DefaultRedirectPath); return; } + if (string.IsNullOrEmpty(localPath)) { RedirectToUrl(httpRes, "/" + DefaultRedirectPath); @@ -607,33 +576,21 @@ namespace Emby.Server.Implementations.HttpServer if (handler != null) { - await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, operationName, cancellationToken).ConfigureAwait(false); + await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false); } else { await ErrorHandler(new FileNotFoundException(), httpReq, false, false).ConfigureAwait(false); } } - catch (OperationCanceledException ex) - { - await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); - } - - catch (IOException ex) - { - await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); - } - - catch (SocketException ex) + catch (Exception ex) when (ex is SocketException || ex is IOException || ex is OperationCanceledException) { await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); } - catch (SecurityException ex) { await ErrorHandler(ex, httpReq, false, true).ConfigureAwait(false); } - catch (Exception ex) { var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase); @@ -644,13 +601,15 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.Close(); - if (enableLog) + stopWatch.Stop(); + var elapsed = stopWatch.Elapsed; + if (elapsed.TotalMilliseconds > 500) { - var statusCode = httpRes.StatusCode; - - var duration = DateTime.Now - date; - - LoggerUtils.LogResponse(_logger, statusCode, urlToLog, remoteIp, duration, logHeaders ? httpRes.Headers : null); + _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + } + else + { + _logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } } } @@ -663,12 +622,11 @@ namespace Emby.Server.Implementations.HttpServer var pathParts = pathInfo.TrimStart('/').Split('/'); if (pathParts.Length == 0) { - _logger.LogError("Path parts empty for PathInfo: {pathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); + _logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); return null; } var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType); - if (restPath != null) { return new ServiceHandler diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs deleted file mode 100644 index d22d9db26..000000000 --- a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Globalization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.HttpServer -{ - public static class LoggerUtils - { - public static void LogRequest(ILogger logger, string url, string method, string userAgent, QueryParamCollection headers) - { - if (headers == null) - { - logger.LogInformation("{0} {1}. UserAgent: {2}", "HTTP " + method, url, userAgent ?? string.Empty); - } - else - { - var headerText = string.Empty; - var index = 0; - - foreach (var i in headers) - { - if (index > 0) - { - headerText += ", "; - } - - headerText += i.Name + "=" + i.Value; - - index++; - } - - logger.LogInformation("HTTP {0} {1}. {2}", method, url, headerText); - } - } - - /// <summary> - /// Logs the response. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="statusCode">The status code.</param> - /// <param name="url">The URL.</param> - /// <param name="endPoint">The end point.</param> - /// <param name="duration">The duration.</param> - public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration, QueryParamCollection headers) - { - var durationMs = duration.TotalMilliseconds; - var logSuffix = durationMs >= 1000 && durationMs < 60000 ? "ms (slow)" : "ms"; - - //var headerText = headers == null ? string.Empty : "Headers: " + string.Join(", ", headers.Where(i => i.Name.IndexOf("Access-", StringComparison.OrdinalIgnoreCase) == -1).Select(i => i.Name + "=" + i.Value).ToArray()); - var headerText = string.Empty; - logger.LogInformation("HTTP Response {0} to {1}. Time: {2}{3}. {4} {5}", statusCode, endPoint, Convert.ToInt32(durationMs).ToString(CultureInfo.InvariantCulture), logSuffix, url, headerText); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index b825ea3b0..a2ac60b31 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -43,12 +43,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var jsonFile = path + ".json"; - try + if (!File.Exists(jsonFile)) { - return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile) ?? new List<T>(); + return new List<T>(); } - catch (FileNotFoundException) + + try { + return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile) ?? new List<T>(); } catch (IOException) { @@ -57,6 +59,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { Logger.LogError(ex, "Error deserializing {jsonFile}", jsonFile); } + return new List<T>(); } diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv index 4441c5650..d546bff53 100644 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ b/Emby.Server.Implementations/Localization/Ratings/kz.csv @@ -1,6 +1,7 @@ -KZ-К,1 -KZ-БА,6 -KZ-Б14,7 -KZ-Е16,8 -KZ-Е18,10 -KZ-НА,15 +KZ-6-,0 +KZ-6+,6 +KZ-12+,12 +KZ-14+,14 +KZ-16+,16 +KZ-18+,18 +KZ-21+,21 diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index 98685cebe..ec9466c4a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var dueTime = triggerDate - now; - logger.LogInformation("Daily trigger for {0} set to fire at {1}, which is {2} minutes from now.", taskName, triggerDate.ToString(), dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:g}, which is {DueTime:g} from now.", taskName, triggerDate, dueTime); Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); } diff --git a/Jellyfin.Server/SocketSharp/RequestMono.cs b/Jellyfin.Server/SocketSharp/RequestMono.cs index f2a08c9ae..8396ad600 100644 --- a/Jellyfin.Server/SocketSharp/RequestMono.cs +++ b/Jellyfin.Server/SocketSharp/RequestMono.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Server.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { - internal static string GetParameter(string header, string attr) + internal static string GetParameter(ReadOnlySpan<char> header, string attr) { int ap = header.IndexOf(attr, StringComparison.Ordinal); if (ap == -1) @@ -31,13 +31,14 @@ namespace Jellyfin.Server.SocketSharp ending = ' '; } - int end = header.IndexOf(ending, ap + 1); + var slice = header.Slice(ap + 1); + int end = slice.IndexOf(ending); if (end == -1) { - return ending == '"' ? null : header.Substring(ap); + return ending == '"' ? null : header.Slice(ap).ToString(); } - return header.Substring(ap + 1, end - ap - 1); + return slice.Slice(0, end - ap - 1).ToString(); } private async Task LoadMultiPart(WebROCollection form) @@ -394,7 +395,7 @@ namespace Jellyfin.Server.SocketSharp } var elem = new Element(); - string header; + ReadOnlySpan<char> header; while ((header = ReadHeaders()) != null) { if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase)) @@ -404,7 +405,7 @@ namespace Jellyfin.Server.SocketSharp } else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase)) { - elem.ContentType = header.Substring("Content-Type:".Length).Trim(); + elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString(); elem.Encoding = GetEncoding(elem.ContentType); } } @@ -452,7 +453,7 @@ namespace Jellyfin.Server.SocketSharp return sb.ToString(); } - private static string GetContentDispositionAttribute(string l, string name) + private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name) { int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); if (idx < 0) @@ -461,7 +462,7 @@ namespace Jellyfin.Server.SocketSharp } int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); + int end = l.Slice(begin).IndexOf('"'); if (end < 0) { return null; @@ -472,10 +473,10 @@ namespace Jellyfin.Server.SocketSharp return string.Empty; } - return l.Substring(begin, end - begin); + return l.Slice(begin, end - begin).ToString(); } - private string GetContentDispositionAttributeWithEncoding(string l, string name) + private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name) { int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); if (idx < 0) @@ -484,7 +485,7 @@ namespace Jellyfin.Server.SocketSharp } int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); + int end = l.Slice(begin).IndexOf('"'); if (end < 0) { return null; @@ -495,7 +496,7 @@ namespace Jellyfin.Server.SocketSharp return string.Empty; } - string temp = l.Substring(begin, end - begin); + ReadOnlySpan<char> temp = l.Slice(begin, end - begin); byte[] source = new byte[temp.Length]; for (int i = temp.Length - 1; i >= 0; i--) { diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 736f9feef..693c2328c 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -164,33 +164,19 @@ namespace Jellyfin.Server.SocketSharp Endpoint = endpoint }); - await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); + await socket.StartReceive().ConfigureAwait(false); } } else { _logger.LogWarning("Web socket connection not allowed"); - ctx.Response.StatusCode = 401; - ctx.Response.Close(); + TryClose(ctx, 401); } } catch (Exception ex) { _logger.LogError(ex, "AcceptWebSocketAsync error"); - ctx.Response.StatusCode = 500; - ctx.Response.Close(); - } - } - - private async Task ReceiveWebSocketAsync(HttpListenerContext ctx, SharpWebSocket socket) - { - try - { - await socket.StartReceive().ConfigureAwait(false); - } - finally - { - TryClose(ctx, 200); + TryClose(ctx, 500); } } @@ -201,10 +187,6 @@ namespace Jellyfin.Server.SocketSharp ctx.Response.StatusCode = statusCode; ctx.Response.Close(); } - catch (ObjectDisposedException) - { - // TODO: Investigate and properly fix. - } catch (Exception ex) { _logger.LogError(ex, "Error closing web socket response"); diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs index 6458707d9..069f47f9a 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs @@ -57,18 +57,37 @@ namespace Jellyfin.Server.SocketSharp public string XRealIp => string.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"]; private string remoteIp; - public string RemoteIp => - remoteIp ?? - (remoteIp = CheckBadChars(XForwardedFor) ?? - NormalizeIp(CheckBadChars(XRealIp) ?? - (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null))); + public string RemoteIp + { + get + { + if (remoteIp != null) + { + return remoteIp; + } + + var temp = CheckBadChars(XForwardedFor); + if (temp.Length != 0) + { + return remoteIp = temp.ToString(); + } + + temp = CheckBadChars(XRealIp); + if (temp.Length != 0) + { + return remoteIp = NormalizeIp(temp).ToString(); + } + + return remoteIp = NormalizeIp(request.RemoteEndPoint?.Address.ToString()).ToString(); + } + } private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; // CheckBadChars - throws on invalid chars to be not found in header name/value - internal static string CheckBadChars(string name) + internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name) { - if (name == null || name.Length == 0) + if (name.Length == 0) { return name; } @@ -99,7 +118,7 @@ namespace Jellyfin.Server.SocketSharp } else if (c == 127 || (c < ' ' && c != '\t')) { - throw new ArgumentException("net_WebHeaderInvalidControlChars"); + throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name)); } break; @@ -113,7 +132,7 @@ namespace Jellyfin.Server.SocketSharp break; } - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); } case 2: @@ -124,14 +143,14 @@ namespace Jellyfin.Server.SocketSharp break; } - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); } } } if (crlf != 0) { - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); } return name; @@ -150,16 +169,16 @@ namespace Jellyfin.Server.SocketSharp return false; } - private string NormalizeIp(string ip) + private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip) { - if (!string.IsNullOrWhiteSpace(ip)) + if (ip.Length != 0 && !ip.IsWhiteSpace()) { // Handle ipv4 mapped to ipv6 const string srch = "::ffff:"; var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); if (index == 0) { - ip = ip.Substring(srch.Length); + ip = ip.Slice(srch.Length); } } @@ -302,17 +321,6 @@ namespace Jellyfin.Server.SocketSharp return null; } - public static string LeftPart(string strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle, StringComparison.Ordinal); - return pos == -1 ? strVal : strVal.Substring(0, pos); - } - public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle) { if (strVal == null) @@ -350,7 +358,7 @@ namespace Jellyfin.Server.SocketSharp } this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); - this.pathInfo = NormalizePathInfo(pathInfo, mode); + this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString(); } return this.pathInfo; @@ -517,14 +525,14 @@ namespace Jellyfin.Server.SocketSharp } } - public static string NormalizePathInfo(string pathInfo, string handlerPath) + public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath) { if (handlerPath != null) { - var trimmed = pathInfo.TrimStart('/'); + var trimmed = pathInfo.AsSpan().TrimStart('/'); if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase)) { - return trimmed.Substring(handlerPath.Length); + return trimmed.Slice(handlerPath.Length).ToString(); } } diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 8dbc26356..ceff6b02e 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -170,7 +170,7 @@ namespace MediaBrowser.Api /// </summary> private void DeleteEncodedMediaCache() { - var path = _config.ApplicationPaths.TranscodingTempPath; + var path = _config.ApplicationPaths.GetTranscodingTempPath(); foreach (var file in _fileSystem.GetFilePaths(path, true)) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 72c4e3573..43fee79a1 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1283,6 +1283,35 @@ namespace MediaBrowser.Controller.Entities }).OrderBy(i => i.Path).ToArray(); } + protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + { + var files = fileSystemChildren.Where(i => i.IsDirectory) + .SelectMany(i => FileSystem.GetFiles(i.FullName)); + + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + .OfType<Video>() + .Select(item => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(item.Id) as Video; + + if (dbItem != null) + { + item = dbItem; + } + else + { + // item is new + item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip; + } + + return item; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToArray(); + } + + public Task RefreshMetadata(CancellationToken cancellationToken) { return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); @@ -1371,6 +1400,8 @@ namespace MediaBrowser.Controller.Entities var themeVideosChanged = false; + var extrasChanged = false; + var localTrailersChanged = false; if (IsFileProtocol && SupportsOwnedItems) @@ -1382,6 +1413,8 @@ namespace MediaBrowser.Controller.Entities themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + extrasChanged = await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); } } @@ -1392,7 +1425,7 @@ namespace MediaBrowser.Controller.Entities } } - return themeSongsChanged || themeVideosChanged || localTrailersChanged; + return themeSongsChanged || themeVideosChanged || extrasChanged || localTrailersChanged; } protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) @@ -1435,6 +1468,31 @@ namespace MediaBrowser.Controller.Entities return itemsChanged; } + private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService).Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)).Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService)); + + var newExtraIds = newExtras.Select(i => i.Id).ToArray(); + + var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds); + + if (extrasChanged) + { + var ownerId = item.Id; + + var tasks = newExtras.Select(i => + { + return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.ExtraIds = newExtraIds; + } + + return extrasChanged; + } + private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); @@ -2775,17 +2833,17 @@ namespace MediaBrowser.Controller.Entities public IEnumerable<BaseItem> GetExtras() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); } - public IEnumerable<BaseItem> GetExtras(ExtraType[] unused) + public IEnumerable<BaseItem> GetExtras(ExtraType[] extraTypes) { - return GetExtras(); + return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)).OrderBy(i => i.SortName); } public IEnumerable<BaseItem> GetDisplayExtras() { - return GetExtras(); + return GetExtras(DisplayExtraTypes); } public virtual bool IsHD => Height >= 720; @@ -2798,8 +2856,10 @@ namespace MediaBrowser.Controller.Entities { return RunTimeTicks ?? 0; } - // what does this do? - public static ExtraType[] DisplayExtraTypes = new[] { Model.Entities.ExtraType.ThemeSong, Model.Entities.ExtraType.ThemeVideo }; + + // Possible types of extra videos + public static ExtraType[] DisplayExtraTypes = new[] { Model.Entities.ExtraType.BehindTheScenes, Model.Entities.ExtraType.Clip, Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, Model.Entities.ExtraType.Scene }; + public virtual bool SupportsExternalTransfer => false; } } diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index 5b66e7497..fd5fb6748 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -8,16 +8,6 @@ namespace MediaBrowser.Controller.Library public static class TVUtils { /// <summary> - /// The TVDB API key - /// </summary> - public static readonly string TvdbApiKey = "72930AE1CB7E2DB3"; - public static readonly string TvdbBaseUrl = "https://www.thetvdb.com/"; - /// <summary> - /// The banner URL - /// </summary> - public static readonly string BannerUrl = TvdbBaseUrl + "banners/"; - - /// <summary> /// Gets the air days. /// </summary> /// <param name="day">The day.</param> @@ -28,24 +18,24 @@ namespace MediaBrowser.Controller.Library { if (string.Equals(day, "Daily", StringComparison.OrdinalIgnoreCase)) { - return new DayOfWeek[] - { - DayOfWeek.Sunday, - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday, - DayOfWeek.Saturday - }; + return new[] + { + DayOfWeek.Sunday, + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday + }; } if (Enum.TryParse(day, true, out DayOfWeek value)) { - return new DayOfWeek[] - { - value - }; + return new[] + { + value + }; } return new DayOfWeek[] { }; diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index b812a8ddc..46593fb2f 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -32,16 +32,17 @@ namespace MediaBrowser.Controller.MediaEncoding var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + // If ffmpeg process is closed, the state is disposed, so don't write to target in that case + if (!target.CanWrite) + { + break; + } + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); await target.FlushAsync().ConfigureAwait(false); } } } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux - } catch (Exception ex) { _logger.LogError(ex, "Error reading ffmpeg log"); diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 7b8d629ee..52a52efdc 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -11,8 +11,10 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.0.2" /> + <PackageReference Include="TvDbSharper" Version="2.0.0" /> </ItemGroup> <PropertyGroup> diff --git a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs index 181e88820..8c8b99e89 100644 --- a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs @@ -1,42 +1,35 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Xml; -using MediaBrowser.Providers.TV; using MediaBrowser.Providers.TV.TheTVDB; +using Microsoft.Extensions.Logging; +using TvDbSharper; namespace MediaBrowser.Providers.People { public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IServerConfigurationManager _config; - private readonly ILibraryManager _libraryManager; private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IXmlReaderSettingsFactory _xmlSettings; + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly TvDbClientManager _tvDbClientManager; - public TvdbPersonImageProvider(IServerConfigurationManager config, ILibraryManager libraryManager, IHttpClient httpClient, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlSettings) + public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvDbClientManager tvDbClientManager) { - _config = config; _libraryManager = libraryManager; _httpClient = httpClient; - _fileSystem = fileSystem; - _xmlSettings = xmlSettings; + _logger = logger; + _tvDbClientManager = tvDbClientManager; } public string Name => ProviderName; @@ -56,7 +49,7 @@ namespace MediaBrowser.Providers.People }; } - public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery { @@ -71,152 +64,44 @@ namespace MediaBrowser.Providers.People .Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds)) .ToList(); - var infos = seriesWithPerson.Select(i => GetImageFromSeriesData(i, item.Name, cancellationToken)) + var infos = (await Task.WhenAll(seriesWithPerson.Select(async i => + await GetImageFromSeriesData(i, item.Name, cancellationToken).ConfigureAwait(false))) + .ConfigureAwait(false)) .Where(i => i != null) .Take(1); - return Task.FromResult(infos); + return infos; } - private RemoteImageInfo GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken) + private async Task<RemoteImageInfo> GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken) { - var tvdbPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds); - - var actorXmlPath = Path.Combine(tvdbPath, "actors.xml"); + var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProviders.Tvdb)); try { - return GetImageInfo(actorXmlPath, personName, cancellationToken); - } - catch (FileNotFoundException) - { - return null; - } - catch (IOException) - { - return null; - } - } - - private RemoteImageInfo GetImageInfo(string xmlFile, string personName, CancellationToken cancellationToken) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var fileStream = _fileSystem.GetFileStream(xmlFile, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) - { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Actor": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - var info = FetchImageInfoFromActorNode(personName, subtree); - - if (info != null) - { - return info; - } - } - break; - } - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - } - - return null; - } - - /// <summary> - /// Fetches the data from actor node. - /// </summary> - /// <param name="personName">Name of the person.</param> - /// <param name="reader">The reader.</param> - /// <returns>System.String.</returns> - private RemoteImageInfo FetchImageInfoFromActorNode(string personName, XmlReader reader) - { - string name = null; - string image = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Name": - { - name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Image": - { - image = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - default: - reader.Skip(); - break; - } - } - else + var actorsResult = await _tvDbClientManager + .GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken) + .ConfigureAwait(false); + var actor = actorsResult.Data.FirstOrDefault(a => + string.Equals(a.Name, personName, StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(a.Image)); + if (actor == null) { - reader.Read(); + return null; } - } - if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(image) && - string.Equals(name, personName, StringComparison.OrdinalIgnoreCase)) - { return new RemoteImageInfo { - Url = TVUtils.BannerUrl + image, + Url = TvdbUtils.BannerUrl + actor.Image, Type = ImageType.Primary, ProviderName = Name - }; } - - return null; + catch (TvDbServerException e) + { + _logger.LogError(e, "Failed to retrieve actor {ActorName} from series {SeriesTvdbId}", personName, tvdbId); + return null; + } } public int Order => 1; diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index 25ad36620..0a2975e0f 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.Providers.TV.TheTVDB; using Microsoft.Extensions.Logging; @@ -28,77 +27,58 @@ namespace MediaBrowser.Providers.TV private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IFileSystem _fileSystem; + private readonly TvDbClientManager _tvDbClientManager; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IXmlReaderSettingsFactory _xmlSettings; + private const double UnairedEpisodeThresholdDays = 2; - public MissingEpisodeProvider(ILogger logger, IServerConfigurationManager config, ILibraryManager libraryManager, ILocalizationManager localization, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlSettings) + public MissingEpisodeProvider( + ILogger logger, + IServerConfigurationManager config, + ILibraryManager libraryManager, + ILocalizationManager localization, + IFileSystem fileSystem, + TvDbClientManager tvDbClientManager) { _logger = logger; _config = config; _libraryManager = libraryManager; _localization = localization; _fileSystem = fileSystem; - _xmlSettings = xmlSettings; + _tvDbClientManager = tvDbClientManager; } public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken) { var tvdbId = series.GetProviderId(MetadataProviders.Tvdb); - - // Todo: Support series by imdb id - var seriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - seriesProviderIds[MetadataProviders.Tvdb.ToString()] = tvdbId; - - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - // Doesn't have required provider id's - if (string.IsNullOrWhiteSpace(seriesDataPath)) - { - return false; - } - - // Check this in order to avoid logging an exception due to directory not existing - if (!Directory.Exists(seriesDataPath)) + if (string.IsNullOrEmpty(tvdbId)) { return false; } - var episodeFiles = _fileSystem.GetFilePaths(seriesDataPath) - .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) - .Select(Path.GetFileNameWithoutExtension) - .Where(i => i.StartsWith("episode-", StringComparison.OrdinalIgnoreCase)) - .ToList(); + var episodes = await _tvDbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken); - var episodeLookup = episodeFiles + var episodeLookup = episodes .Select(i => { - var parts = i.Split('-'); - - if (parts.Length == 3) - { - if (int.TryParse(parts[1], NumberStyles.Integer, _usCulture, out var seasonNumber)) - { - if (int.TryParse(parts[2], NumberStyles.Integer, _usCulture, out var episodeNumber)) - { - return new ValueTuple<int, int>(seasonNumber, episodeNumber); - } - } - } - - return new ValueTuple<int, int>(-1, -1); + DateTime.TryParse(i.FirstAired, out var firstAired); + var seasonNumber = i.AiredSeason.GetValueOrDefault(-1); + var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1); + return (seasonNumber: seasonNumber, episodeNumber: episodeNumber, firstAired: firstAired); }) - .Where(i => i.Item1 != -1 && i.Item2 != -1) + .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1) + .OrderBy(i => i.seasonNumber) + .ThenBy(i => i.episodeNumber) .ToList(); var allRecursiveChildren = series.GetRecursiveChildren(); - var hasBadData = HasInvalidContent(series, allRecursiveChildren); + var hasBadData = HasInvalidContent(allRecursiveChildren); // Be conservative here to avoid creating missing episodes for ones they already have var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes; - var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(series, allRecursiveChildren, episodeLookup); + var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup); if (anySeasonsRemoved) { @@ -106,7 +86,7 @@ namespace MediaBrowser.Providers.TV allRecursiveChildren = series.GetRecursiveChildren(); } - var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(series, allRecursiveChildren, episodeLookup, addMissingEpisodes); + var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes); if (anyEpisodesRemoved) { @@ -118,7 +98,7 @@ namespace MediaBrowser.Providers.TV if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name)) { - hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, seriesDataPath, episodeLookup, cancellationToken) + hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken) .ConfigureAwait(false); } @@ -134,7 +114,7 @@ namespace MediaBrowser.Providers.TV /// Returns true if a series has any seasons or episodes without season or episode numbers /// If this data is missing no virtual items will be added in order to prevent possible duplicates /// </summary> - private bool HasInvalidContent(Series series, IList<BaseItem> allItems) + private bool HasInvalidContent(IList<BaseItem> allItems) { return allItems.OfType<Season>().Any(i => !i.IndexNumber.HasValue) || allItems.OfType<Episode>().Any(i => @@ -149,43 +129,24 @@ namespace MediaBrowser.Providers.TV }); } - private const double UnairedEpisodeThresholdDays = 2; - - /// <summary> - /// Adds the missing episodes. - /// </summary> - /// <param name="series">The series.</param> - /// <returns>Task.</returns> - private async Task<bool> AddMissingEpisodes(Series series, - IList<BaseItem> allItems, + private async Task<bool> AddMissingEpisodes( + Series series, + IEnumerable<BaseItem> allItems, bool addMissingEpisodes, - string seriesDataPath, - IEnumerable<ValueTuple<int, int>> episodeLookup, + IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup, CancellationToken cancellationToken) { - var existingEpisodes = allItems.OfType<Episode>() - .ToList(); + var existingEpisodes = allItems.OfType<Episode>().ToList(); - var lookup = episodeLookup as IList<ValueTuple<int, int>> ?? episodeLookup.ToList(); - - var seasonCounts = (from e in lookup - group e by e.Item1 into g - select g) - .ToDictionary(g => g.Key, g => g.Count()); + var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count()); var hasChanges = false; - foreach (var tuple in lookup) + foreach (var tuple in episodeLookup) { - if (tuple.Item1 <= 0) - { - // Ignore season zeros - continue; - } - - if (tuple.Item2 <= 0) + if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0) { - // Ignore episode zeros + // Ignore episode/season zeros continue; } @@ -196,33 +157,15 @@ namespace MediaBrowser.Providers.TV continue; } - var airDate = GetAirDate(seriesDataPath, tuple.Item1, tuple.Item2); - - if (!airDate.HasValue) - { - continue; - } - - var now = DateTime.UtcNow; + var airDate = tuple.firstAired; - now = now.AddDays(0 - UnairedEpisodeThresholdDays); - - if (airDate.Value < now) - { - if (addMissingEpisodes) - { - // tvdb has a lot of nearly blank episodes - _logger.LogInformation("Creating virtual missing episode {0} {1}x{2}", series.Name, tuple.Item1, tuple.Item2); - await AddEpisode(series, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false); + var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays); - hasChanges = true; - } - } - else if (airDate.Value > now) + if (airDate < now && addMissingEpisodes || airDate > now) { // tvdb has a lot of nearly blank episodes - _logger.LogInformation("Creating virtual unaired episode {0} {1}x{2}", series.Name, tuple.Item1, tuple.Item2); - await AddEpisode(series, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber); + await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false); hasChanges = true; } @@ -234,59 +177,58 @@ namespace MediaBrowser.Providers.TV /// <summary> /// Removes the virtual entry after a corresponding physical version has been added /// </summary> - private bool RemoveObsoleteOrMissingEpisodes(Series series, - IList<BaseItem> allRecursiveChildren, - IEnumerable<ValueTuple<int, int>> episodeLookup, + private bool RemoveObsoleteOrMissingEpisodes( + IEnumerable<BaseItem> allRecursiveChildren, + IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup, bool allowMissingEpisodes) { - var existingEpisodes = allRecursiveChildren.OfType<Episode>() - .ToList(); - - var physicalEpisodes = existingEpisodes - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); + var existingEpisodes = allRecursiveChildren.OfType<Episode>(); - var virtualEpisodes = existingEpisodes - .Where(i => i.LocationType == LocationType.Virtual) - .ToList(); + var physicalEpisodes = new List<Episode>(); + var virtualEpisodes = new List<Episode>(); + foreach (var episode in existingEpisodes) + { + if (episode.LocationType == LocationType.Virtual) + { + virtualEpisodes.Add(episode); + } + else + { + physicalEpisodes.Add(episode); + } + } var episodesToRemove = virtualEpisodes .Where(i => { - if (i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue) + if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue) { - var seasonNumber = i.ParentIndexNumber.Value; - var episodeNumber = i.IndexNumber.Value; - - // If there's a physical episode with the same season and episode number, delete it - if (physicalEpisodes.Any(p => - p.ParentIndexNumber.HasValue && (p.ParentIndexNumber.Value) == seasonNumber && - p.ContainsEpisodeNumber(episodeNumber))) - { - return true; - } + return true; + } - // If the episode no longer exists in the remote lookup, delete it - if (!episodeLookup.Any(e => e.Item1 == seasonNumber && e.Item2 == episodeNumber)) - { - return true; - } + var seasonNumber = i.ParentIndexNumber.Value; + var episodeNumber = i.IndexNumber.Value; - if (!allowMissingEpisodes && i.IsMissingEpisode) - { - // If it's missing, but not unaired, remove it - if (!i.PremiereDate.HasValue || i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) < DateTime.Now.Date) - { - return true; - } - } + // If there's a physical episode with the same season and episode number, delete it + if (physicalEpisodes.Any(p => + p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber && + p.ContainsEpisodeNumber(episodeNumber))) + { + return true; + } - return false; + // If the episode no longer exists in the remote lookup, delete it + if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber)) + { + return true; } - return true; - }) - .ToList(); + // If it's missing, but not unaired, remove it + return !allowMissingEpisodes && i.IsMissingEpisode && + (!i.PremiereDate.HasValue || + i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) < + DateTime.Now.Date); + }); var hasChanges = false; @@ -295,7 +237,6 @@ namespace MediaBrowser.Providers.TV _libraryManager.DeleteItem(episodeToRemove, new DeleteOptions { DeleteFileLocation = true - }, false); hasChanges = true; @@ -307,22 +248,27 @@ namespace MediaBrowser.Providers.TV /// <summary> /// Removes the obsolete or missing seasons. /// </summary> - /// <param name="series">The series.</param> + /// <param name="allRecursiveChildren"></param> /// <param name="episodeLookup">The episode lookup.</param> /// <returns>Task{System.Boolean}.</returns> - private bool RemoveObsoleteOrMissingSeasons(Series series, - IList<BaseItem> allRecursiveChildren, - IEnumerable<ValueTuple<int, int>> episodeLookup) + private bool RemoveObsoleteOrMissingSeasons(IList<BaseItem> allRecursiveChildren, + IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup) { var existingSeasons = allRecursiveChildren.OfType<Season>().ToList(); - var physicalSeasons = existingSeasons - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); - - var virtualSeasons = existingSeasons - .Where(i => i.LocationType == LocationType.Virtual) - .ToList(); + var physicalSeasons = new List<Season>(); + var virtualSeasons = new List<Season>(); + foreach (var season in existingSeasons) + { + if (season.LocationType == LocationType.Virtual) + { + virtualSeasons.Add(season); + } + else + { + physicalSeasons.Add(season); + } + } var allEpisodes = allRecursiveChildren.OfType<Episode>().ToList(); @@ -334,28 +280,19 @@ namespace MediaBrowser.Providers.TV var seasonNumber = i.IndexNumber.Value; // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && (p.IndexNumber.Value) == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal))) + if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal))) { return true; } // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it - if (episodeLookup.All(e => e.Item1 != seasonNumber)) - { - if (allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder)) - { - return true; - } - } - - return false; + return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder); } // Season does not have a number // Remove if there are no episodes directly in series without a season number return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder); - }) - .ToList(); + }); var hasChanges = false; @@ -392,21 +329,19 @@ namespace MediaBrowser.Providers.TV season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false); } - var name = string.Format("Episode {0}", episodeNumber.ToString(_usCulture)); + var name = $"Episode {episodeNumber.ToString(_usCulture)}"; var episode = new Episode { Name = name, IndexNumber = episodeNumber, ParentIndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId((series.Id + seasonNumber.ToString(_usCulture) + name), typeof(Episode)), + Id = _libraryManager.GetNewItemId(series.Id + seasonNumber.ToString(_usCulture) + name, typeof(Episode)), IsVirtualItem = true, - SeasonId = season == null ? Guid.Empty : season.Id, + SeasonId = season?.Id ?? Guid.Empty, SeriesId = series.Id }; - episode.SetParent(season); - season.AddChild(episode, cancellationToken); await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); @@ -417,25 +352,31 @@ namespace MediaBrowser.Providers.TV /// </summary> /// <param name="existingEpisodes">The existing episodes.</param> /// <param name="seasonCounts"></param> - /// <param name="tuple">The tuple.</param> + /// <param name="episodeTuple"></param> /// <returns>Episode.</returns> - private Episode GetExistingEpisode(IList<Episode> existingEpisodes, Dictionary<int, int> seasonCounts, ValueTuple<int, int> tuple) + private Episode GetExistingEpisode(IList<Episode> existingEpisodes, IReadOnlyDictionary<int, int> seasonCounts, (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple) { - var s = tuple.Item1; - var e = tuple.Item2; + var seasonNumber = episodeTuple.seasonNumber; + var episodeNumber = episodeTuple.episodeNumber; while (true) { - var episode = GetExistingEpisode(existingEpisodes, s, e); + var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber); if (episode != null) + { return episode; + } - s--; + seasonNumber--; - if (seasonCounts.ContainsKey(s)) - e += seasonCounts[s]; + if (seasonCounts.ContainsKey(seasonNumber)) + { + episodeNumber += seasonCounts[seasonNumber]; + } else + { break; + } } return null; @@ -446,88 +387,5 @@ namespace MediaBrowser.Providers.TV return existingEpisodes .FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode)); } - - /// <summary> - /// Gets the air date. - /// </summary> - /// <param name="seriesDataPath">The series data path.</param> - /// <param name="seasonNumber">The season number.</param> - /// <param name="episodeNumber">The episode number.</param> - /// <returns>System.Nullable{DateTime}.</returns> - private DateTime? GetAirDate(string seriesDataPath, int seasonNumber, int episodeNumber) - { - // First open up the tvdb xml file and make sure it has valid data - var filename = string.Format("episode-{0}-{1}.xml", seasonNumber.ToString(_usCulture), episodeNumber.ToString(_usCulture)); - - var xmlPath = Path.Combine(seriesDataPath, filename); - - DateTime? airDate = null; - - using (var fileStream = _fileSystem.GetFileStream(xmlPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) - { - // It appears the best way to filter out invalid entries is to only include those with valid air dates - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "EpisodeName": - { - var val = reader.ReadElementContentAsString(); - if (string.IsNullOrWhiteSpace(val)) - { - // Not valid, ignore these - return null; - } - break; - } - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (DateTime.TryParse(val, out var date)) - { - airDate = date.ToUniversalTime(); - } - } - - break; - } - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - } - } - } - - return airDate; - } } } diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 5f4f39d45..afbd838e4 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Xml; using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.TV.TheTVDB; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV @@ -18,11 +19,24 @@ namespace MediaBrowser.Providers.TV { private readonly ILocalizationManager _localization; private readonly IXmlReaderSettingsFactory _xmlSettings; + private readonly TvDbClientManager _tvDbClientManager; - public SeriesMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager, ILocalizationManager localization, IXmlReaderSettingsFactory xmlSettings) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + public SeriesMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + IUserDataManager userDataManager, + ILibraryManager libraryManager, + ILocalizationManager localization, + IXmlReaderSettingsFactory xmlSettings, + TvDbClientManager tvDbClientManager + ) + : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) { _localization = localization; _xmlSettings = xmlSettings; + _tvDbClientManager = tvDbClientManager; } protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) @@ -32,12 +46,13 @@ namespace MediaBrowser.Providers.TV var seasonProvider = new DummySeasonProvider(ServerConfigurationManager, Logger, _localization, LibraryManager, FileSystem); await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false); + // TODO why does it not register this itself omg var provider = new MissingEpisodeProvider(Logger, ServerConfigurationManager, LibraryManager, _localization, FileSystem, - _xmlSettings); + _tvDbClientManager); try { diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs b/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs new file mode 100644 index 000000000..efb8a0fe8 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Caching.Memory; +using TvDbSharper; +using TvDbSharper.Dto; + +namespace MediaBrowser.Providers.TV.TheTVDB +{ + public class TvDbClientManager + { + private readonly SemaphoreSlim _cacheWriteLock = new SemaphoreSlim(1, 1); + private readonly IMemoryCache _cache; + private readonly TvDbClient _tvDbClient; + private DateTime _tokenCreatedAt; + private const string DefaultLanguage = "en"; + + public TvDbClientManager(IMemoryCache memoryCache) + { + _cache = memoryCache; + _tvDbClient = new TvDbClient(); + _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey); + _tokenCreatedAt = DateTime.Now; + } + + public TvDbClient TvDbClient + { + get + { + // Refresh if necessary + if (_tokenCreatedAt > DateTime.Now.Subtract(TimeSpan.FromHours(20))) + { + try + { + _tvDbClient.Authentication.RefreshTokenAsync(); + } + catch + { + _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey); + } + + _tokenCreatedAt = DateTime.Now; + } + + return _tvDbClient; + } + } + + public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByNameAsync(string name, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("series", name, language); + return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken)); + } + + public Task<TvDbResponse<Series>> GetSeriesByIdAsync(int tvdbId, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("series", tvdbId, language); + return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetAsync(tvdbId, cancellationToken)); + } + + public Task<TvDbResponse<EpisodeRecord>> GetEpisodesAsync(int episodeTvdbId, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("episode", episodeTvdbId, language); + return TryGetValue(cacheKey, language,() => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); + } + + public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language, + CancellationToken cancellationToken) + { + // Traverse all episode pages and join them together + var episodes = new List<EpisodeRecord>(); + var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken) + .ConfigureAwait(false); + episodes.AddRange(episodePage.Data); + if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue) + { + return episodes; + } + + int next = episodePage.Links.Next.Value; + int last = episodePage.Links.Last.Value; + + for (var page = next; page <= last; ++page) + { + episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken) + .ConfigureAwait(false); + episodes.AddRange(episodePage.Data); + } + + return episodes; + } + + public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(string imdbId, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("series", imdbId, language); + return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken)); + } + + public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync(string zap2ItId, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("series", zap2ItId, language); + return TryGetValue( cacheKey, language,() => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken)); + } + public Task<TvDbResponse<Actor[]>> GetActorsAsync(int tvdbId, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("actors", tvdbId, language); + return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken)); + } + + public Task<TvDbResponse<Image[]>> GetImagesAsync(int tvdbId, ImagesQuery imageQuery, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("images", tvdbId, language, imageQuery); + return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken)); + } + + public Task<TvDbResponse<Language[]>> GetLanguagesAsync(CancellationToken cancellationToken) + { + return TryGetValue("languages", null,() => TvDbClient.Languages.GetAllAsync(cancellationToken)); + } + + public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync(int tvdbId, string language, + CancellationToken cancellationToken) + { + var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language); + return TryGetValue(cacheKey, language, + () => TvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken)); + } + + public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(int tvdbId, int page, EpisodeQuery episodeQuery, + string language, CancellationToken cancellationToken) + { + var cacheKey = GenerateKey(language, tvdbId, episodeQuery); + + return TryGetValue(cacheKey, language, + () => TvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken)); + } + + public Task<string> GetEpisodeTvdbId(EpisodeInfo searchInfo, string language, + CancellationToken cancellationToken) + { + searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), + out var seriesTvdbId); + + var episodeQuery = new EpisodeQuery(); + + // Prefer SxE over premiere date as it is more robust + if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue) + { + episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value; + episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value; + } + else if (searchInfo.PremiereDate.HasValue) + { + // tvdb expects yyyy-mm-dd format + episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd"); + } + + return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken); + } + + public async Task<string> GetEpisodeTvdbId(int seriesTvdbId, EpisodeQuery episodeQuery, + string language, + CancellationToken cancellationToken) + { + var episodePage = + await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken) + .ConfigureAwait(false); + return episodePage.Data.FirstOrDefault()?.Id.ToString(); + } + + public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(int tvdbId, EpisodeQuery episodeQuery, + string language, CancellationToken cancellationToken) + { + return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); + } + + private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory) + { + if (_cache.TryGetValue(key, out T cachedValue)) + { + return cachedValue; + } + + await _cacheWriteLock.WaitAsync().ConfigureAwait(false); + try + { + if (_cache.TryGetValue(key, out cachedValue)) + { + return cachedValue; + } + + _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; + var result = await resultFactory.Invoke().ConfigureAwait(false); + _cache.Set(key, result, TimeSpan.FromHours(1)); + return result; + } + finally + { + _cacheWriteLock.Release(); + } + } + + private static string GenerateKey(params object[] objects) + { + var key = string.Empty; + + foreach (var obj in objects) + { + var objType = obj.GetType(); + if (objType.IsPrimitive || objType == typeof(string)) + { + key += obj + ";"; + } + else + { + foreach (PropertyInfo propertyInfo in objType.GetProperties()) + { + var currentValue = propertyInfo.GetValue(obj, null); + if (currentValue == null) + { + continue; + } + + key += propertyInfo.Name + "=" + currentValue + ";"; + } + } + } + + return key; + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs index 102a3d4ec..c04e98e64 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs @@ -1,33 +1,30 @@ +using System; using System.Collections.Generic; -using System.Globalization; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Xml; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; +using Microsoft.Extensions.Logging; +using TvDbSharper; +using TvDbSharper.Dto; namespace MediaBrowser.Providers.TV.TheTVDB { public class TvdbEpisodeImageProvider : IRemoteImageProvider { - private readonly IServerConfigurationManager _config; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly TvDbClientManager _tvDbClientManager; - public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvDbClientManager tvDbClientManager) { - _config = config; _httpClient = httpClient; - _fileSystem = fileSystem; + _logger = logger; + _tvDbClientManager = tvDbClientManager; } public string Name => "TheTVDB"; @@ -45,113 +42,70 @@ namespace MediaBrowser.Providers.TV.TheTVDB }; } - public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var episode = (Episode)item; var series = episode.Series; - + var imageResult = new List<RemoteImageInfo>(); + var language = item.GetPreferredMetadataLanguage(); if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) { - // Process images - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds); - - var nodes = TvdbEpisodeProvider.Current.GetEpisodeXmlNodes(seriesDataPath, episode.GetLookupInfo()); - - var result = nodes.Select(i => GetImageInfo(i, cancellationToken)) - .Where(i => i != null) - .ToList(); + var episodeTvdbId = episode.GetProviderId(MetadataProviders.Tvdb); - return Task.FromResult<IEnumerable<RemoteImageInfo>>(result); - } - - return Task.FromResult<IEnumerable<RemoteImageInfo>>(new RemoteImageInfo[] { }); - } - - private RemoteImageInfo GetImageInfo(XmlReader reader, CancellationToken cancellationToken) - { - var height = 225; - var width = 400; - var url = string.Empty; - - // Use XmlReader for best performance - using (reader) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Process images + try { - if (reader.NodeType == XmlNodeType.Element) + if (string.IsNullOrEmpty(episodeTvdbId)) { - cancellationToken.ThrowIfCancellationRequested(); - - switch (reader.Name) + var episodeInfo = new EpisodeInfo { - case "thumb_width": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - width = rval; - } - } - break; - } - - case "thumb_height": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - height = rval; - } - } - break; - } - - case "filename": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - url = TVUtils.BannerUrl + val; - } - break; - } - default: - { - reader.Skip(); - break; - } + IndexNumber = episode.IndexNumber.Value, + ParentIndexNumber = episode.ParentIndexNumber.Value, + SeriesProviderIds = series.ProviderIds + }; + episodeTvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(episodeTvdbId)) + { + _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + episodeInfo.ParentIndexNumber, episodeInfo.IndexNumber, series.GetProviderId(MetadataProviders.Tvdb)); + return imageResult; } } - else + + var episodeResult = + await _tvDbClientManager + .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken) + .ConfigureAwait(false); + + var image = GetImageInfo(episodeResult.Data); + if (image != null) { - reader.Read(); + imageResult.Add(image); } } + catch (TvDbServerException e) + { + _logger.LogError(e, "Failed to retrieve episode images for {TvDbId}", episodeTvdbId); + } } - if (string.IsNullOrEmpty(url)) + return imageResult; + } + + private RemoteImageInfo GetImageInfo(EpisodeRecord episode) + { + if (string.IsNullOrEmpty(episode.Filename)) { return null; } return new RemoteImageInfo { - Width = width, - Height = height, + Width = Convert.ToInt32(episode.ThumbWidth), + Height = Convert.ToInt32(episode.ThumbHeight), ProviderName = Name, - Url = url, + Url = TvdbUtils.BannerUrl + episode.Filename, Type = ImageType.Primary }; } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs index be137e879..b256f2667 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -1,22 +1,16 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; +using TvDbSharper; +using TvDbSharper.Dto; namespace MediaBrowser.Providers.TV.TheTVDB { @@ -24,44 +18,52 @@ namespace MediaBrowser.Providers.TV.TheTVDB /// <summary> /// Class RemoteEpisodeProvider /// </summary> - class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo> + class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { - private static readonly string FullIdKey = MetadataProviders.Tvdb + "-Full"; - - internal static TvdbEpisodeProvider Current; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly IXmlReaderSettingsFactory _xmlSettings; + private readonly TvDbClientManager _tvDbClientManager; - public TvdbEpisodeProvider(IFileSystem fileSystem, IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IXmlReaderSettingsFactory xmlSettings) + public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvDbClientManager tvDbClientManager) { - _fileSystem = fileSystem; - _config = config; _httpClient = httpClient; _logger = logger; - _xmlSettings = xmlSettings; - Current = this; + _tvDbClientManager = tvDbClientManager; } - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) { var list = new List<RemoteSearchResult>(); // The search query must either provide an episode number or date - if (!searchInfo.IndexNumber.HasValue && !searchInfo.PremiereDate.HasValue) + if (!searchInfo.IndexNumber.HasValue || !searchInfo.PremiereDate.HasValue) { - return Task.FromResult((IEnumerable<RemoteSearchResult>)list); + return list; } if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) { - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, searchInfo.SeriesProviderIds); - try { - var metadataResult = FetchEpisodeData(searchInfo, seriesDataPath, cancellationToken); + var episodeTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); + if (string.IsNullOrEmpty(episodeTvdbId)) + { + searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), + out var seriesTvdbId); + episodeTvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + if (string.IsNullOrEmpty(episodeTvdbId)) + { + _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); + return list; + } + } + + var episodeResult = await _tvDbClientManager.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), + searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + var metadataResult = MapEpisodeToResult(searchInfo, episodeResult.Data); if (metadataResult.HasMetadata) { @@ -80,689 +82,117 @@ namespace MediaBrowser.Providers.TV.TheTVDB }); } } - catch (FileNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - catch (IOException) + catch (TvDbServerException e) { - // Don't fail the provider because this will just keep on going and going. + _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", searchInfo.IndexNumber); } } - return Task.FromResult((IEnumerable<RemoteSearchResult>)list); + return list; } public string Name => "TheTVDB"; public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken) { - var result = new MetadataResult<Episode>(); - result.QueriedById = true; + var result = new MetadataResult<Episode> + { + QueriedById = true + }; if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) { - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, null, null, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(seriesDataPath)) - { - return result; - } - + var tvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); try { - result = FetchEpisodeData(searchInfo, seriesDataPath, cancellationToken); - } - catch (FileNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. + if (string.IsNullOrEmpty(tvdbId)) + { + tvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + if (string.IsNullOrEmpty(tvdbId)) + { + _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + searchInfo.ParentIndexNumber, searchInfo.IndexNumber, tvdbId); + return result; + } + } + + var episodeResult = await _tvDbClientManager.GetEpisodesAsync( + Convert.ToInt32(tvdbId), searchInfo.MetadataLanguage, + cancellationToken).ConfigureAwait(false); + + result = MapEpisodeToResult(searchInfo, episodeResult.Data); } - catch (IOException) + catch (TvDbServerException e) { - // Don't fail the provider because this will just keep on going and going. + _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", tvdbId); } } else { - _logger.LogDebug("No series identity found for {0}", searchInfo.Name); + _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name); } return result; } - /// <summary> - /// Gets the episode XML files. - /// </summary> - /// <param name="seriesDataPath">The series data path.</param> - /// <param name="searchInfo">The search information.</param> - /// <returns>List{FileInfo}.</returns> - internal List<XmlReader> GetEpisodeXmlNodes(string seriesDataPath, EpisodeInfo searchInfo) - { - var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage); - - try - { - return GetXmlNodes(seriesXmlPath, searchInfo); - } - catch (FileNotFoundException) - { - return new List<XmlReader>(); - } - catch (IOException) - { - return new List<XmlReader>(); - } - } - - /// <summary> - /// Fetches the episode data. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="seriesDataPath">The series data path.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - private MetadataResult<Episode> FetchEpisodeData(EpisodeInfo id, string seriesDataPath, CancellationToken cancellationToken) + private static MetadataResult<Episode> MapEpisodeToResult(EpisodeInfo id, EpisodeRecord episode) { - var result = new MetadataResult<Episode>() + var result = new MetadataResult<Episode> { + HasMetadata = true, Item = new Episode { IndexNumber = id.IndexNumber, ParentIndexNumber = id.ParentIndexNumber, - IndexNumberEnd = id.IndexNumberEnd - } - }; - - var xmlNodes = GetEpisodeXmlNodes(seriesDataPath, id); - - if (xmlNodes.Count > 0) - { - FetchMainEpisodeInfo(result, xmlNodes[0], id.SeriesDisplayOrder, cancellationToken); - - result.HasMetadata = true; - } - - foreach (var node in xmlNodes.Skip(1)) - { - FetchAdditionalPartInfo(result, node, cancellationToken); - } - - return result; - } - - private List<XmlReader> GetXmlNodes(string xmlFile, EpisodeInfo searchInfo) - { - var list = new List<XmlReader>(); - - if (searchInfo.IndexNumber.HasValue) - { - var files = GetEpisodeXmlFiles(searchInfo.SeriesDisplayOrder, searchInfo.ParentIndexNumber, searchInfo.IndexNumber, searchInfo.IndexNumberEnd, Path.GetDirectoryName(xmlFile)); - - list = files.Select(GetXmlReader).ToList(); - } - - if (list.Count == 0 && searchInfo.PremiereDate.HasValue) - { - list = GetXmlNodesByPremiereDate(xmlFile, searchInfo.PremiereDate.Value); - } - - return list; - } - - private string GetEpisodeFileName(string seriesDisplayOrder, int? seasonNumber, int? episodeNumber) - { - if (string.Equals(seriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase)) - { - return string.Format("episode-abs-{0}.xml", episodeNumber); - } - else if (string.Equals(seriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase)) - { - return string.Format("episode-dvd-{0}-{1}.xml", seasonNumber.Value, episodeNumber); - } - else - { - return string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber); - } - } - - private FileSystemMetadata GetEpisodeFileInfoWithFallback(string seriesDataPath, string seriesDisplayOrder, int? seasonNumber, int? episodeNumber) - { - var file = Path.Combine(seriesDataPath, GetEpisodeFileName(seriesDisplayOrder, seasonNumber, episodeNumber)); - var fileInfo = _fileSystem.GetFileInfo(file); - - if (fileInfo.Exists) - { - return fileInfo; - } - - if (!seasonNumber.HasValue) - { - return fileInfo; - } - - // revert to aired order - if (string.Equals(seriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase) || string.Equals(seriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase)) - { - file = Path.Combine(seriesDataPath, GetEpisodeFileName(null, seasonNumber, episodeNumber)); - return _fileSystem.GetFileInfo(file); - } - - return fileInfo; - } - - private List<FileSystemMetadata> GetEpisodeXmlFiles(string seriesDisplayOrder, int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber, string seriesDataPath) - { - var files = new List<FileSystemMetadata>(); - - if (episodeNumber == null) - { - return files; - } - - if (!seasonNumber.HasValue) - { - seriesDisplayOrder = "absolute"; - } - - var fileInfo = GetEpisodeFileInfoWithFallback(seriesDataPath, seriesDisplayOrder, seasonNumber, episodeNumber); - - if (fileInfo.Exists) - { - files.Add(fileInfo); - } - - var end = endingEpisodeNumber ?? episodeNumber; - episodeNumber++; - - while (episodeNumber <= end) - { - fileInfo = GetEpisodeFileInfoWithFallback(seriesDataPath, seriesDisplayOrder, seasonNumber, episodeNumber); - - if (fileInfo.Exists) - { - files.Add(fileInfo); - } - else - { - break; - } - - episodeNumber++; - } - - return files; - } - - private XmlReader GetXmlReader(FileSystemMetadata xmlFile) - { - return GetXmlReader(File.ReadAllText(xmlFile.FullName, Encoding.UTF8)); - } - - private XmlReader GetXmlReader(string xml) - { - var streamReader = new StringReader(xml); - - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - return XmlReader.Create(streamReader, settings); - } - - private List<XmlReader> GetXmlNodesByPremiereDate(string xmlFile, DateTime premiereDate) - { - var list = new List<XmlReader>(); - - using (var fileStream = _fileSystem.GetFileStream(xmlFile, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) - { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - // Use XmlReader for best performance - - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); + IndexNumberEnd = id.IndexNumberEnd, + AirsBeforeEpisodeNumber = episode.AirsBeforeEpisode, + AirsAfterSeasonNumber = episode.AirsAfterSeason, + AirsBeforeSeasonNumber = episode.AirsBeforeSeason, + Name = episode.EpisodeName, + Overview = episode.Overview, + CommunityRating = (float?)episode.SiteRating, - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Episode": - { - var outerXml = reader.ReadOuterXml(); - - var airDate = GetEpisodeAirDate(outerXml); - - if (airDate.HasValue && premiereDate.Date == airDate.Value.Date) - { - list.Add(GetXmlReader(outerXml)); - return list; - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } } - } - - return list; - } - - private DateTime? GetEpisodeAirDate(string xml) - { - using (var streamReader = new StringReader(xml)) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (DateTime.TryParse(val, out var date)) - { - date = date.ToUniversalTime(); - - return date; - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - return null; - } - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + }; + result.ResetPeople(); - private void FetchMainEpisodeInfo(MetadataResult<Episode> result, XmlReader reader, string seriesOrder, CancellationToken cancellationToken) - { var item = result.Item; + item.SetProviderId(MetadataProviders.Tvdb, episode.Id.ToString()); + item.SetProviderId(MetadataProviders.Imdb, episode.ImdbId); - int? episodeNumber = null; - int? seasonNumber = null; - int? combinedEpisodeNumber = null; - int? combinedSeasonNumber = null; - - // Use XmlReader for best performance - using (reader) + if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase)) { - result.ResetPeople(); - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "id": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Tvdb, val); - } - break; - } - - case "IMDB_ID": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Imdb, val); - } - break; - } - - case "EpisodeNumber": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - episodeNumber = rval; - } - } - - break; - } - - case "SeasonNumber": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - seasonNumber = rval; - } - } - - break; - } - - case "Combined_episodenumber": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (float.TryParse(val, NumberStyles.Any, _usCulture, out var num)) - { - combinedEpisodeNumber = Convert.ToInt32(num); - } - } - - break; - } - - case "Combined_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (float.TryParse(val, NumberStyles.Any, _usCulture, out var num)) - { - combinedSeasonNumber = Convert.ToInt32(num); - } - } - - break; - } - - case "airsbefore_episode": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - item.AirsBeforeEpisodeNumber = rval; - } - } - - break; - } - - case "airsafter_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - item.AirsAfterSeasonNumber = rval; - } - } - - break; - } - - case "airsbefore_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - item.AirsBeforeSeasonNumber = rval; - } - } - - break; - } - - case "EpisodeName": - { - var val = reader.ReadElementContentAsString(); - if (!item.LockedFields.Contains(MetadataFields.Name)) - { - if (!string.IsNullOrWhiteSpace(val)) - { - item.Name = val; - } - } - break; - } - - case "Overview": - { - var val = reader.ReadElementContentAsString(); - if (!item.LockedFields.Contains(MetadataFields.Overview)) - { - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview = val; - } - } - break; - } - case "Rating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // float.TryParse is local aware, so it can be probamatic, force us culture - if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out var rval)) - { - item.CommunityRating = rval; - } - } - break; - } - case "RatingCount": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - //item.VoteCount = rval; - } - } - - break; - } - - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (DateTime.TryParse(val, out var date)) - { - date = date.ToUniversalTime(); - - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - } - - break; - } - - case "Director": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(result, val, PersonType.Director); - } - } - - break; - } - case "GuestStars": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddGuestStars(result, val); - } - } - - break; - } - case "Writer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - //AddPeople(result, val, PersonType.Writer); - } - } - - break; - } - case "Language": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - result.ResultLanguage = val; - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } + item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber); + item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason; } - - if (string.Equals(seriesOrder, "dvd", StringComparison.OrdinalIgnoreCase)) + else if (episode.AiredEpisodeNumber.HasValue) { - episodeNumber = combinedEpisodeNumber ?? episodeNumber; - seasonNumber = combinedSeasonNumber ?? seasonNumber; + item.IndexNumber = episode.AiredEpisodeNumber; } - - if (episodeNumber.HasValue) + else if (episode.AiredSeason.HasValue) { - item.IndexNumber = episodeNumber; + item.ParentIndexNumber = episode.AiredSeason; } - if (seasonNumber.HasValue) + if (DateTime.TryParse(episode.FirstAired, out var date)) { - item.ParentIndexNumber = seasonNumber; + // dates from tvdb are UTC but without offset or Z + item.PremiereDate = date; + item.ProductionYear = date.Year; } - } - private void AddPeople<T>(MetadataResult<T> result, string val, string personType) - { - // Sometimes tvdb actors have leading spaces - foreach (var person in val.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(str => new PersonInfo { Type = personType, Name = str.Trim() })) + foreach (var director in episode.Directors) { - result.AddPerson(person); + result.AddPerson(new PersonInfo + { + Name = director, + Type = PersonType.Director + }); } - } - - private void AddGuestStars<T>(MetadataResult<T> result, string val) - where T : BaseItem - { - // example: - // <GuestStars>|Mark C. Thomas| Dennis Kiefer| David Nelson (David)| Angela Nicholas| Tzi Ma| Kevin P. Kearns (Pasco)|</GuestStars> - var persons = val.Split('|') - .Select(i => i.Trim()) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .ToList(); - - foreach (var person in persons) + foreach (var person in episode.GuestStars) { var index = person.IndexOf('('); string role = null; @@ -782,106 +212,17 @@ namespace MediaBrowser.Providers.TV.TheTVDB Role = role }); } - } - - private void FetchAdditionalPartInfo(MetadataResult<Episode> result, XmlReader reader, CancellationToken cancellationToken) - { - var item = result.Item; - - // Use XmlReader for best performance - using (reader) + foreach (var writer in episode.Writers) { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + result.AddPerson(new PersonInfo { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "EpisodeName": - { - var val = reader.ReadElementContentAsString(); - if (!item.LockedFields.Contains(MetadataFields.Name)) - { - if (!string.IsNullOrWhiteSpace(val)) - { - item.Name += ", " + val; - } - } - break; - } - - case "Overview": - { - var val = reader.ReadElementContentAsString(); - if (!item.LockedFields.Contains(MetadataFields.Overview)) - { - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview += Environment.NewLine + Environment.NewLine + val; - } - } - break; - } - case "Director": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(result, val, PersonType.Director); - } - } - - break; - } - case "GuestStars": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddGuestStars(result, val); - } - } - - break; - } - case "Writer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - //AddPeople(result, val, PersonType.Writer); - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } + Name = writer, + Type = PersonType.Writer + }); } + + result.ResultLanguage = episode.Language.EpisodeName; + return result; } public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs deleted file mode 100644 index d45696057..000000000 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs +++ /dev/null @@ -1,398 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Xml; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV.TheTVDB -{ - /// <summary> - /// Class TvdbPrescanTask - /// </summary> - public class TvdbPrescanTask : ILibraryPostScanTask - { - public const string TvdbBaseUrl = "https://thetvdb.com/"; - - /// <summary> - /// The server time URL - /// </summary> - private const string ServerTimeUrl = TvdbBaseUrl + "api/Updates.php?type=none"; - - /// <summary> - /// The updates URL - /// </summary> - private const string UpdatesUrl = TvdbBaseUrl + "api/Updates.php?type=all&time={0}"; - - /// <summary> - /// The _HTTP client - /// </summary> - private readonly IHttpClient _httpClient; - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - /// <summary> - /// The _config - /// </summary> - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - private readonly IXmlReaderSettingsFactory _xmlSettings; - - /// <summary> - /// Initializes a new instance of the <see cref="TvdbPrescanTask"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="httpClient">The HTTP client.</param> - /// <param name="config">The config.</param> - public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem, ILibraryManager libraryManager, IXmlReaderSettingsFactory xmlSettings) - { - _logger = logger; - _httpClient = httpClient; - _config = config; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _xmlSettings = xmlSettings; - } - - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var path = TvdbSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); - - Directory.CreateDirectory(path); - - var timestampFile = Path.Combine(path, "time.txt"); - - var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile); - - // Don't check for tvdb updates anymore frequently than 24 hours - if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 1) - { - return; - } - - // Find out the last time we queried tvdb for updates - var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; - - string newUpdateTime; - - var existingDirectories = _fileSystem.GetDirectoryPaths(path) - .Select(Path.GetFileName) - .ToList(); - - var seriesList = _libraryManager.GetItemList(new InternalItemsQuery() - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Recursive = true, - GroupByPresentationUniqueKey = false, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - - }).Cast<Series>() - .ToList(); - - var seriesIdsInLibrary = seriesList - .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) - .Select(i => i.GetProviderId(MetadataProviders.Tvdb)) - .ToList(); - - var missingSeries = seriesIdsInLibrary.Except(existingDirectories, StringComparer.OrdinalIgnoreCase) - .ToList(); - - var enableInternetProviders = seriesList.Count == 0 ? false : seriesList[0].IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(seriesList[0]), TvdbSeriesProvider.Current.Name); - if (!enableInternetProviders) - { - progress.Report(100); - return; - } - - // If this is our first time, update all series - if (string.IsNullOrEmpty(lastUpdateTime)) - { - // First get tvdb server time - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = ServerTimeUrl, - CancellationToken = cancellationToken, - EnableHttpCompression = true, - BufferContent = false - - }, "GET").ConfigureAwait(false)) - { - // First get tvdb server time - using (var stream = response.Content) - { - newUpdateTime = GetUpdateTime(stream); - } - } - - existingDirectories.AddRange(missingSeries); - - await UpdateSeries(existingDirectories, path, null, progress, cancellationToken).ConfigureAwait(false); - } - else - { - var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false); - - newUpdateTime = seriesToUpdate.Item2; - - long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out var lastUpdateValue); - - var nullableUpdateValue = lastUpdateValue == 0 ? (long?)null : lastUpdateValue; - - var listToUpdate = seriesToUpdate.Item1.ToList(); - listToUpdate.AddRange(missingSeries); - - await UpdateSeries(listToUpdate, path, nullableUpdateValue, progress, cancellationToken).ConfigureAwait(false); - } - - File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); - progress.Report(100); - } - - /// <summary> - /// Gets the update time. - /// </summary> - /// <param name="response">The response.</param> - /// <returns>System.String.</returns> - private string GetUpdateTime(Stream response) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var streamReader = new StreamReader(response, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Time": - { - return (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - } - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - - return null; - } - - /// <summary> - /// Gets the series ids to update. - /// </summary> - /// <param name="existingSeriesIds">The existing series ids.</param> - /// <param name="lastUpdateTime">The last update time.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{System.String}}.</returns> - private async Task<Tuple<IEnumerable<string>, string>> GetSeriesIdsToUpdate(IEnumerable<string> existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken) - { - // First get last time - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = string.Format(UpdatesUrl, lastUpdateTime), - CancellationToken = cancellationToken, - EnableHttpCompression = true, - BufferContent = false - - }, "GET").ConfigureAwait(false)) - { - using (var stream = response.Content) - { - var data = GetUpdatedSeriesIdList(stream); - - var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - var seriesList = data.Item1 - .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); - - return new Tuple<IEnumerable<string>, string>(seriesList, data.Item2); - } - } - } - - private Tuple<List<string>, string> GetUpdatedSeriesIdList(Stream stream) - { - string updateTime = null; - var idList = new List<string>(); - - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var streamReader = new StreamReader(stream, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Time": - { - updateTime = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - case "Series": - { - var id = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - idList.Add(id); - break; - } - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - - return new Tuple<List<string>, string>(idList, updateTime); - } - - /// <summary> - /// Updates the series. - /// </summary> - /// <param name="seriesIds">The series ids.</param> - /// <param name="seriesDataPath">The series data path.</param> - /// <param name="lastTvDbUpdateTime">The last tv db update time.</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task UpdateSeries(List<string> seriesIds, string seriesDataPath, long? lastTvDbUpdateTime, IProgress<double> progress, CancellationToken cancellationToken) - { - var numComplete = 0; - - var seriesList = _libraryManager.GetItemList(new InternalItemsQuery() - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Recursive = true, - GroupByPresentationUniqueKey = false, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - - }).Cast<Series>(); - - // Gather all series into a lookup by tvdb id - var allSeries = seriesList - .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) - .ToLookup(i => i.GetProviderId(MetadataProviders.Tvdb)); - - foreach (var seriesId in seriesIds) - { - // Find the preferred language(s) for the movie in the library - var languages = allSeries[seriesId] - .Select(i => i.GetPreferredMetadataLanguage()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - foreach (var language in languages) - { - try - { - await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, language, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - _logger.LogError(ex, "Error updating tvdb series id {ID}, language {Language}", seriesId, language); - - // Already logged at lower levels, but don't fail the whole operation, unless timed out - // We have to fail this to make it run again otherwise new episode data could potentially be missing - if (ex.IsTimedOut) - { - throw; - } - } - } - - numComplete++; - double percent = numComplete; - percent /= seriesIds.Count; - percent *= 100; - - progress.Report(percent); - } - } - - /// <summary> - /// Updates the series. - /// </summary> - /// <param name="id">The id.</param> - /// <param name="seriesDataPath">The series data path.</param> - /// <param name="lastTvDbUpdateTime">The last tv db update time.</param> - /// <param name="preferredMetadataLanguage">The preferred metadata language.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - _logger.LogInformation("Updating series from tvdb " + id + ", language " + preferredMetadataLanguage); - - seriesDataPath = Path.Combine(seriesDataPath, id); - - Directory.CreateDirectory(seriesDataPath); - - return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), null, null, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs index 01ede44bb..94ca603f2 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs @@ -1,41 +1,32 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Xml; +using Microsoft.Extensions.Logging; +using TvDbSharper; +using TvDbSharper.Dto; +using RatingType = MediaBrowser.Model.Dto.RatingType; namespace MediaBrowser.Providers.TV.TheTVDB { public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IXmlReaderSettingsFactory _xmlSettings; + private readonly ILogger _logger; + private readonly TvDbClientManager _tvDbClientManager; - public TvdbSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlSettings) + public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvDbClientManager tvDbClientManager) { - _config = config; _httpClient = httpClient; - _fileSystem = fileSystem; - _xmlSettings = xmlSettings; + _logger = logger; + _tvDbClientManager = tvDbClientManager; } public string Name => ProviderName; @@ -62,91 +53,66 @@ namespace MediaBrowser.Providers.TV.TheTVDB var season = (Season)item; var series = season.Series; - if (series != null && season.IndexNumber.HasValue && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) + if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) { - var seriesProviderIds = series.ProviderIds; - var seasonNumber = season.IndexNumber.Value; + return new RemoteImageInfo[] { }; + } - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(seriesProviderIds, series.Name, series.ProductionYear, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); + var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProviders.Tvdb)); + var seasonNumber = season.IndexNumber.Value; + var language = item.GetPreferredMetadataLanguage(); + var remoteImages = new List<RemoteImageInfo>(); - if (!string.IsNullOrEmpty(seriesDataPath)) + var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart }; + foreach (var keyType in keyTypes) + { + var imageQuery = new ImagesQuery { - var path = Path.Combine(seriesDataPath, "banners.xml"); - - try - { - return GetImages(path, item.GetPreferredMetadataLanguage(), seasonNumber, _xmlSettings, _fileSystem, cancellationToken); - } - catch (FileNotFoundException) - { - // No tvdb data yet. Don't blow up - } - catch (IOException) - { - // No tvdb data yet. Don't blow up - } + KeyType = keyType, + SubKey = seasonNumber.ToString() + }; + try + { + var imageResults = await _tvDbClientManager + .GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false); + remoteImages.AddRange(GetImages(imageResults.Data, language)); + } + catch (TvDbServerException) + { + _logger.LogDebug("No images of type {KeyType} found for series {TvdbId}", keyType, tvdbId); } } - return new RemoteImageInfo[] { }; + return remoteImages; } - internal static IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, int seasonNumber, IXmlReaderSettingsFactory xmlReaderSettingsFactory, IFileSystem fileSystem, CancellationToken cancellationToken) + private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { - var settings = xmlReaderSettingsFactory.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - var list = new List<RemoteImageInfo>(); - - using (var fileStream = fileSystem.GetFileStream(xmlPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + var languages = _tvDbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; + foreach (Image image in images) { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) + var imageInfo = new RemoteImageInfo { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); + RatingType = RatingType.Score, + CommunityRating = (double?)image.RatingsInfo.Average, + VoteCount = image.RatingsInfo.Count, + Url = TvdbUtils.BannerUrl + image.FileName, + ProviderName = ProviderName, + Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation, + ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail + }; - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Banner": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - AddImage(subtree, list, seasonNumber); - } - break; - } - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } + var resolution = image.Resolution.Split('x'); + if (resolution.Length == 2) + { + imageInfo.Width = Convert.ToInt32(resolution[0]); + imageInfo.Height = Convert.ToInt32(resolution[1]); } - } + imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); + list.Add(imageInfo); + } var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); return list.OrderByDescending(i => @@ -155,6 +121,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB { return 3; } + if (!isLanguageEn) { if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) @@ -162,177 +129,18 @@ namespace MediaBrowser.Providers.TV.TheTVDB return 2; } } + if (string.IsNullOrEmpty(i.Language)) { return isLanguageEn ? 3 : 2; } + return 0; }) .ThenByDescending(i => i.CommunityRating ?? 0) .ThenByDescending(i => i.VoteCount ?? 0); } - private static void AddImage(XmlReader reader, List<RemoteImageInfo> images, int seasonNumber) - { - reader.MoveToContent(); - - string bannerType = null; - string bannerType2 = null; - string url = null; - int? bannerSeason = null; - int? width = null; - int? height = null; - string language = null; - double? rating = null; - int? voteCount = null; - string thumbnailUrl = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Rating": - { - var val = reader.ReadElementContentAsString() ?? string.Empty; - - if (double.TryParse(val, NumberStyles.Any, UsCulture, out var rval)) - { - rating = rval; - } - - break; - } - - case "RatingCount": - { - var val = reader.ReadElementContentAsString() ?? string.Empty; - - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval)) - { - voteCount = rval; - } - - break; - } - - case "Language": - { - language = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "ThumbnailPath": - { - thumbnailUrl = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType": - { - bannerType = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType2": - { - bannerType2 = reader.ReadElementContentAsString() ?? string.Empty; - - // Sometimes the resolution is stuffed in here - var resolutionParts = bannerType2.Split('x'); - - if (resolutionParts.Length == 2) - { - if (int.TryParse(resolutionParts[0], NumberStyles.Integer, UsCulture, out var rval)) - { - width = rval; - } - - if (int.TryParse(resolutionParts[1], NumberStyles.Integer, UsCulture, out rval)) - { - height = rval; - } - - } - - break; - } - - case "BannerPath": - { - url = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "Season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - bannerSeason = int.Parse(val); - } - break; - } - - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - if (!string.IsNullOrEmpty(url) && bannerSeason.HasValue && bannerSeason.Value == seasonNumber) - { - var imageInfo = new RemoteImageInfo - { - RatingType = RatingType.Score, - CommunityRating = rating, - VoteCount = voteCount, - Url = TVUtils.BannerUrl + url, - ProviderName = ProviderName, - Language = language, - Width = width, - Height = height - }; - - if (!string.IsNullOrEmpty(thumbnailUrl)) - { - imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl; - } - - if (string.Equals(bannerType, "season", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(bannerType2, "season", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Primary; - images.Add(imageInfo); - } - else if (string.Equals(bannerType2, "seasonwide", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Banner; - images.Add(imageInfo); - } - } - else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Backdrop; - images.Add(imageInfo); - } - } - - } - public int Order => 0; public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs index 2b4337ed1..365f49fb7 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs @@ -1,40 +1,32 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Xml; +using Microsoft.Extensions.Logging; +using TvDbSharper; +using TvDbSharper.Dto; +using RatingType = MediaBrowser.Model.Dto.RatingType; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Providers.TV.TheTVDB { public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IFileSystem _fileSystem; - private readonly IXmlReaderSettingsFactory _xmlReaderSettingsFactory; + private readonly ILogger _logger; + private readonly TvDbClientManager _tvDbClientManager; - public TvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvDbClientManager tvDbClientManager) { - _config = config; _httpClient = httpClient; - _fileSystem = fileSystem; - _xmlReaderSettingsFactory = xmlReaderSettingsFactory; + _logger = logger; + _tvDbClientManager = tvDbClientManager; } public string Name => ProviderName; @@ -58,273 +50,92 @@ namespace MediaBrowser.Providers.TV.TheTVDB public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - if (TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) + if (!TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) { - var language = item.GetPreferredMetadataLanguage(); - - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(item.ProviderIds, item.Name, item.ProductionYear, language, cancellationToken).ConfigureAwait(false); + return Array.Empty<RemoteImageInfo>(); + } - if (string.IsNullOrEmpty(seriesDataPath)) + var language = item.GetPreferredMetadataLanguage(); + var remoteImages = new List<RemoteImageInfo>(); + var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart }; + var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProviders.Tvdb)); + foreach (KeyType keyType in keyTypes) + { + var imageQuery = new ImagesQuery { - return new RemoteImageInfo[] { }; - } - - var path = Path.Combine(seriesDataPath, "banners.xml"); - + KeyType = keyType + }; try { - return GetImages(path, language, cancellationToken); - } - catch (FileNotFoundException) - { - // No tvdb data yet. Don't blow up + var imageResults = + await _tvDbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) + .ConfigureAwait(false); + + remoteImages.AddRange(GetImages(imageResults.Data, language)); } - catch (IOException) + catch (TvDbServerException) { - // No tvdb data yet. Don't blow up + _logger.LogDebug("No images of type {KeyType} exist for series {TvDbId}", keyType, + tvdbId); } } - - return new RemoteImageInfo[] { }; + return remoteImages; } - private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken) + private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { - var settings = _xmlReaderSettingsFactory.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - var list = new List<RemoteImageInfo>(); + var languages = _tvDbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; - using (var fileStream = _fileSystem.GetFileStream(xmlPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + foreach (Image image in images) { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) + var imageInfo = new RemoteImageInfo { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); + RatingType = RatingType.Score, + CommunityRating = (double?)image.RatingsInfo.Average, + VoteCount = image.RatingsInfo.Count, + Url = TvdbUtils.BannerUrl + image.FileName, + ProviderName = Name, + Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation, + ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail + }; - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Banner": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - AddImage(subtree, list); - } - break; - } - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } + var resolution = image.Resolution.Split('x'); + if (resolution.Length == 2) + { + imageInfo.Width = Convert.ToInt32(resolution[0]); + imageInfo.Height = Convert.ToInt32(resolution[1]); } - } + imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); + list.Add(imageInfo); + } var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); return list.OrderByDescending(i => - { - if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) { - return 2; + return 3; } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private void AddImage(XmlReader reader, List<RemoteImageInfo> images) - { - reader.MoveToContent(); - string bannerType = null; - string url = null; - int? bannerSeason = null; - int? width = null; - int? height = null; - string language = null; - double? rating = null; - int? voteCount = null; - string thumbnailUrl = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) + if (!isLanguageEn) { - case "Rating": - { - var val = reader.ReadElementContentAsString() ?? string.Empty; - - if (double.TryParse(val, NumberStyles.Any, _usCulture, out var rval)) - { - rating = rval; - } - - break; - } - - case "RatingCount": - { - var val = reader.ReadElementContentAsString() ?? string.Empty; - - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - voteCount = rval; - } - - break; - } - - case "Language": - { - language = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "ThumbnailPath": - { - thumbnailUrl = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType": - { - bannerType = reader.ReadElementContentAsString() ?? string.Empty; - - break; - } - - case "BannerPath": - { - url = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType2": - { - var bannerType2 = reader.ReadElementContentAsString() ?? string.Empty; - - // Sometimes the resolution is stuffed in here - var resolutionParts = bannerType2.Split('x'); - - if (resolutionParts.Length == 2) - { - if (int.TryParse(resolutionParts[0], NumberStyles.Integer, _usCulture, out var rval)) - { - width = rval; - } - - if (int.TryParse(resolutionParts[1], NumberStyles.Integer, _usCulture, out rval)) - { - height = rval; - } - - } - - break; - } - - case "Season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - bannerSeason = int.Parse(val); - } - break; - } - - - default: - reader.Skip(); - break; + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } } - } - else - { - reader.Read(); - } - } - if (!string.IsNullOrEmpty(url) && !bannerSeason.HasValue) - { - var imageInfo = new RemoteImageInfo - { - RatingType = RatingType.Score, - CommunityRating = rating, - VoteCount = voteCount, - Url = TVUtils.BannerUrl + url, - ProviderName = Name, - Language = language, - Width = width, - Height = height - }; - - if (!string.IsNullOrEmpty(thumbnailUrl)) - { - imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl; - } - - if (string.Equals(bannerType, "poster", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Primary; - images.Add(imageInfo); - } - else if (string.Equals(bannerType, "series", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Banner; - images.Add(imageInfo); - } - else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Backdrop; - images.Add(imageInfo); - } - } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0); } public int Order => 0; diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs index 52e60a8ed..9c24e4c98 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -1,72 +1,42 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; +using TvDbSharper; +using TvDbSharper.Dto; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Providers.TV.TheTVDB { public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { internal static TvdbSeriesProvider Current { get; private set; } - private readonly IZipClient _zipClient; private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IXmlReaderSettingsFactory _xmlSettings; - private readonly IServerConfigurationManager _config; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; + private readonly TvDbClientManager _tvDbClientManager; - public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager, IXmlReaderSettingsFactory xmlSettings, ILocalizationManager localizationManager) + public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvDbClientManager tvDbClientManager) { - _zipClient = zipClient; _httpClient = httpClient; - _fileSystem = fileSystem; - _config = config; _logger = logger; _libraryManager = libraryManager; - _xmlSettings = xmlSettings; _localizationManager = localizationManager; Current = this; - } - - public const string TvdbBaseUrl = "https://www.thetvdb.com/"; - - private const string SeriesSearchUrl = TvdbBaseUrl + "api/GetSeries.php?seriesname={0}&language={1}"; - private const string SeriesGetZip = TvdbBaseUrl + "api/{0}/series/{1}/all/{2}.zip"; - private const string GetSeriesByImdbId = TvdbBaseUrl + "api/GetSeriesByRemoteID.php?imdbid={0}&language={1}"; - private const string GetSeriesByZap2ItId = TvdbBaseUrl + "api/GetSeriesByRemoteID.php?zap2it={0}&language={1}"; - - private string NormalizeLanguage(string language) - { - if (string.IsNullOrWhiteSpace(language)) - { - return language; - } - - // pt-br is just pt to tvdb - return language.Split('-')[0].ToLowerInvariant(); + _tvDbClientManager = tvDbClientManager; } public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) @@ -96,8 +66,10 @@ namespace MediaBrowser.Providers.TV.TheTVDB public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken) { - var result = new MetadataResult<Series>(); - result.QueriedById = true; + var result = new MetadataResult<Series> + { + QueriedById = true + }; if (!IsValidSeries(itemId.ProviderIds)) { @@ -109,428 +81,99 @@ namespace MediaBrowser.Providers.TV.TheTVDB if (IsValidSeries(itemId.ProviderIds)) { - var seriesDataPath = await EnsureSeriesInfo(itemId.ProviderIds, itemId.Name, itemId.Year, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(seriesDataPath)) - { - return result; - } - result.Item = new Series(); result.HasMetadata = true; - FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken); + await FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken) + .ConfigureAwait(false); } return result; } - /// <summary> - /// Fetches the series data. - /// </summary> - /// <param name="result">The result.</param> - /// <param name="metadataLanguage">The metadata language.</param> - /// <param name="seriesProviderIds">The series provider ids.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - private void FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken) + private async Task FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken) { var series = result.Item; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out string id) && !string.IsNullOrEmpty(id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId)) { - series.SetProviderId(MetadataProviders.Tvdb, id); + series.SetProviderId(MetadataProviders.Tvdb, tvdbId); } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId)) { - series.SetProviderId(MetadataProviders.Imdb, id); + series.SetProviderId(MetadataProviders.Imdb, imdbId); + tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProviders.Imdb.ToString(), metadataLanguage, + cancellationToken).ConfigureAwait(false); } - if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id) && !string.IsNullOrEmpty(id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It)) { - series.SetProviderId(MetadataProviders.Zap2It, id); + series.SetProviderId(MetadataProviders.Zap2It, zap2It); + tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProviders.Zap2It.ToString(), metadataLanguage, + cancellationToken).ConfigureAwait(false); } - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - var seriesXmlPath = GetSeriesXmlPath(seriesProviderIds, metadataLanguage); - var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); - - FetchSeriesInfo(result, seriesXmlPath, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - result.ResetPeople(); - - FetchActors(result, actorsXmlPath); - } - - /// <summary> - /// Downloads the series zip. - /// </summary> - internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesName, int? seriesYear, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) - { try { - await DownloadSeriesZip(seriesId, idType, seriesName, seriesYear, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase)) - { - await DownloadSeriesZip(seriesId, idType, seriesName, seriesYear, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - } - - private async Task DownloadSeriesZip(string seriesId, string idType, string seriesName, int? seriesYear, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(seriesId)) - { - throw new ArgumentNullException(nameof(seriesId)); - } - - if (!string.Equals(idType, "tvdb", StringComparison.OrdinalIgnoreCase)) - { - seriesId = await GetSeriesByRemoteId(seriesId, idType, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + var seriesResult = + await _tvDbClientManager + .GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken) + .ConfigureAwait(false); + MapSeriesToResult(result, seriesResult.Data, metadataLanguage); } - - // If searching by remote id came up empty, then do a regular search - if (string.IsNullOrWhiteSpace(seriesId) && !string.IsNullOrWhiteSpace(seriesName)) + catch (TvDbServerException e) { - var searchInfo = new SeriesInfo - { - Name = seriesName, - Year = seriesYear, - MetadataLanguage = preferredMetadataLanguage - }; - var results = await GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - var result = results.FirstOrDefault(); - if (result != null) - { - seriesId = result.GetProviderId(MetadataProviders.Tvdb); - } - } - - if (string.IsNullOrWhiteSpace(seriesId)) - { - throw new ArgumentNullException(nameof(seriesId)); + _logger.LogError(e, "Failed to retrieve series with id {TvdbId}", tvdbId); + return; } - var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, NormalizeLanguage(preferredMetadataLanguage)); - - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - - }, "GET").ConfigureAwait(false)) - { - using (var zipStream = response.Content) - { - // Delete existing files - DeleteXmlFiles(seriesDataPath); - - // Copy to memory stream because we need a seekable stream - using (var ms = new MemoryStream()) - { - await zipStream.CopyToAsync(ms).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); - ms.Position = 0; - _zipClient.ExtractAllFromZip(ms, seriesDataPath, true); - } - } - } + result.ResetPeople(); - // Sanitize all files, except for extracted episode files - foreach (var file in _fileSystem.GetFilePaths(seriesDataPath, true).ToList() - .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) - .Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase))) + try { - await SanitizeXmlFile(file).ConfigureAwait(false); + var actorsResult = await _tvDbClientManager + .GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false); + MapActorsToResult(result, actorsResult.Data); } - - var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, NormalizeLanguage(preferredMetadataLanguage) + ".xml"); - var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml"); - - if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase)) + catch (TvDbServerException e) { - File.Copy(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true); + _logger.LogError(e, "Failed to retrieve actors for series {TvdbId}", tvdbId); } - - await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false); } private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken) { - string url; - if (string.Equals(idType, MetadataProviders.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) - { - url = string.Format(GetSeriesByZap2ItId, id, NormalizeLanguage(language)); - } - else - { - url = string.Format(GetSeriesByImdbId, id, NormalizeLanguage(language)); - } - - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - - }, "GET").ConfigureAwait(false)) - { - using (var result = response.Content) - { - return FindSeriesId(result); - } - } - } - - private string FindSeriesId(Stream stream) - { - using (var streamReader = new StreamReader(stream, Encoding.UTF8)) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Series": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - return FindSeriesId(subtree); - } - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - - return null; - } - - private string FindSeriesId(XmlReader reader) - { - reader.MoveToContent(); - reader.Read(); + TvDbResponse<SeriesSearchResult[]> result = null; - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + try { - if (reader.NodeType == XmlNodeType.Element) + if (string.Equals(idType, MetadataProviders.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) { - switch (reader.Name) - { - case "seriesid": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - return val; - } - - return null; - } - - default: - reader.Skip(); - break; - } + result = await _tvDbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) + .ConfigureAwait(false); } else { - reader.Read(); + result = await _tvDbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) + .ConfigureAwait(false); } } - - return null; - } - - internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) - { - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out string id)) + catch (TvDbServerException e) { - // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. - if (!string.IsNullOrWhiteSpace(id)) - { - return true; - } + _logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id); } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id)) - { - // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. - if (!string.IsNullOrWhiteSpace(id)) - { - return true; - } - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id)) - { - // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. - if (!string.IsNullOrWhiteSpace(id)) - { - return true; - } - } - return false; - } - - private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); - internal async Task<string> EnsureSeriesInfo(Dictionary<string, string> seriesProviderIds, string seriesName, int? seriesYear, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out string seriesId) && !string.IsNullOrWhiteSpace(seriesId)) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - // Only download if not already there - // The post-scan task will take care of updates so we don't need to re-download here - if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) - { - await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesName, seriesYear, seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - - return seriesDataPath; - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrWhiteSpace(seriesId)) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - // Only download if not already there - // The post-scan task will take care of updates so we don't need to re-download here - if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) - { - try - { - await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesName, seriesYear, seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - catch (ArgumentNullException) - { - // Unable to determine tvdb id based on imdb id - return null; - } - } - - return seriesDataPath; - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out seriesId) && !string.IsNullOrWhiteSpace(seriesId)) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - // Only download if not already there - // The post-scan task will take care of updates so we don't need to re-download here - if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) - { - try - { - await DownloadSeriesZip(seriesId, MetadataProviders.Zap2It.ToString(), seriesName, seriesYear, seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - catch (ArgumentNullException) - { - // Unable to determine tvdb id based on Zap2It id - return null; - } - } - - return seriesDataPath; - } - - return null; - } - finally - { - _ensureSemaphore.Release(); - } + return result?.Data.First().Id.ToString(); } - private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage) + internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) { - try - { - var files = _fileSystem.GetFiles(seriesDataPath, new[] { ".xml" }, true, false) - .ToList(); - - var seriesXmlFilename = preferredMetadataLanguage + ".xml"; - - const int cacheHours = 12; - - var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase)); - // No need to check age if automatic updates are enabled - if (seriesFile == null || !seriesFile.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalHours > cacheHours) - { - return false; - } - - var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase)); - // No need to check age if automatic updates are enabled - if (actorsXml == null || !actorsXml.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalHours > cacheHours) - { - return false; - } - - var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase)); - // No need to check age if automatic updates are enabled - if (bannersXml == null || !bannersXml.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalHours > cacheHours) - { - return false; - } - return true; - } - catch (FileNotFoundException) - { - return false; - } - catch (IOException) - { - return false; - } + return seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out _) || + seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out _) || + seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out _); } /// <summary> @@ -543,7 +186,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB /// <returns>Task{System.String}.</returns> private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken) { - var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)); + var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false); if (results.Count == 0) { @@ -552,7 +195,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase)) { - results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)); + results = await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false); } } @@ -570,194 +213,59 @@ namespace MediaBrowser.Providers.TV.TheTVDB private async Task<List<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken) { - var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name), NormalizeLanguage(language)); - var comparableName = GetComparableName(name); - var list = new List<Tuple<List<string>, RemoteSearchResult>>(); - - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - - }, "GET").ConfigureAwait(false)) + TvDbResponse<SeriesSearchResult[]> result; + try { - using (var stream = response.Content) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var streamReader = new StreamReader(stream, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Series": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - var searchResultInfo = GetSeriesSearchResultFromSubTree(subtree); - if (searchResultInfo != null) - { - searchResultInfo.Item2.SearchProviderName = Name; - list.Add(searchResultInfo); - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - } + result = await _tvDbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) + .ConfigureAwait(false); } - - return list - .OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1) - .ThenBy(i => list.IndexOf(i)) - .Select(i => i.Item2) - .ToList(); - } - - private Tuple<List<string>, RemoteSearchResult> GetSeriesSearchResultFromSubTree(XmlReader reader) - { - var searchResult = new RemoteSearchResult + catch (TvDbServerException e) { - SearchProviderName = Name - }; - - var tvdbTitles = new List<string>(); - string seriesId = null; - - reader.MoveToContent(); - reader.Read(); + _logger.LogError(e, "No series results found for {Name}", comparableName); + return new List<RemoteSearchResult>(); + } - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + foreach (var seriesSearchResult in result.Data) { - if (reader.NodeType == XmlNodeType.Element) + var tvdbTitles = new List<string> { - switch (reader.Name) - { - case "SeriesName": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - tvdbTitles.Add(GetComparableName(val)); - } - break; - } - - case "AliasNames": - { - var val = reader.ReadElementContentAsString(); - - var alias = (val ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(GetComparableName); - tvdbTitles.AddRange(alias); - break; - } - - case "IMDB_ID": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - searchResult.SetProviderId(MetadataProviders.Imdb, val); - } - break; - } - - case "banner": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - searchResult.ImageUrl = TVUtils.BannerUrl + val; - } - break; - } - - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (DateTime.TryParse(val, out var date)) - { - searchResult.ProductionYear = date.Year; - } - } - break; - } - - case "id": - case "seriesid": - { - var val = reader.ReadElementContentAsString(); + GetComparableName(seriesSearchResult.SeriesName) + }; + tvdbTitles.AddRange(seriesSearchResult.Aliases.Select(GetComparableName)); - if (!string.IsNullOrWhiteSpace(val)) - { - seriesId = val; - } - break; - } + DateTime.TryParse(seriesSearchResult.FirstAired, out var firstAired); + var remoteSearchResult = new RemoteSearchResult + { + Name = tvdbTitles.FirstOrDefault(), + ProductionYear = firstAired.Year, + SearchProviderName = Name, + ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner - default: - reader.Skip(); - break; - } + }; + try + { + var seriesSesult = + await _tvDbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) + .ConfigureAwait(false); + remoteSearchResult.SetProviderId(MetadataProviders.Imdb, seriesSesult.Data.ImdbId); + remoteSearchResult.SetProviderId(MetadataProviders.Zap2It, seriesSesult.Data.Zap2itId); } - else + catch (TvDbServerException e) { - reader.Read(); + _logger.LogError(e, "Unable to retrieve series with id {TvdbId}", seriesSearchResult.Id); } - } - if (tvdbTitles.Count == 0) - { - return null; + remoteSearchResult.SetProviderId(MetadataProviders.Tvdb, seriesSearchResult.Id.ToString()); + list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult)); } - searchResult.Name = tvdbTitles.FirstOrDefault(); - searchResult.SetProviderId(MetadataProviders.Tvdb, seriesId); - - return new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, searchResult); + return list + .OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(i => list.IndexOf(i)) + .Select(i => i.Item2) + .ToList(); } /// <summary> @@ -767,7 +275,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB /// <summary> /// The spacers /// </summary> - const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes) + const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are two types of dashes, short and long) /// <summary> /// Gets the name of the comparable. @@ -781,7 +289,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB var sb = new StringBuilder(); foreach (var c in name) { - if ((int)c >= 0x2B0 && (int)c <= 0x0333) + if (c >= 0x2B0 && c <= 0x0333) { // skip char modifier and diacritics } @@ -817,895 +325,83 @@ namespace MediaBrowser.Providers.TV.TheTVDB return name.Trim(); } - private void FetchSeriesInfo(MetadataResult<Series> result, string seriesXmlPath, CancellationToken cancellationToken) + private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - var episiodeAirDates = new List<DateTime>(); + Series series = result.Item; + series.SetProviderId(MetadataProviders.Tvdb, tvdbSeries.Id.ToString()); + series.Name = tvdbSeries.SeriesName; + series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim(); + result.ResultLanguage = metadataLanguage; + series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek); + series.AirTime = tvdbSeries.AirsTime; - using (var fileStream = _fileSystem.GetFileStream(seriesXmlPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + series.CommunityRating = (float?)tvdbSeries.SiteRating; + series.SetProviderId(MetadataProviders.Imdb, tvdbSeries.ImdbId); + series.SetProviderId(MetadataProviders.Zap2It, tvdbSeries.Zap2itId); + if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus)) { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Series": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - FetchDataFromSeriesNode(result, subtree, cancellationToken); - } - break; - } - - case "Episode": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken); - - if (date.HasValue) - { - episiodeAirDates.Add(date.Value); - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - } - - if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0) - { - result.Item.EndDate = episiodeAirDates.Max(); - } - } - - private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken) - { - DateTime? airDate = null; - int? seasonNumber = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (DateTime.TryParse(val, out var date)) - { - airDate = date.ToUniversalTime(); - } - } - - break; - } - - case "SeasonNumber": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - seasonNumber = rval; - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - if (seasonNumber.HasValue && seasonNumber.Value != 0) - { - return airDate; - } - - return null; - } - - /// <summary> - /// Fetches the actors. - /// </summary> - /// <param name="result">The result.</param> - /// <param name="actorsXmlPath">The actors XML path.</param> - private void FetchActors(MetadataResult<Series> result, string actorsXmlPath) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var fileStream = _fileSystem.GetFileStream(actorsXmlPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) - { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Actor": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subtree = reader.ReadSubtree()) - { - FetchDataFromActorNode(result, subtree); - } - break; - } - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - } - } - - /// <summary> - /// Fetches the data from actor node. - /// </summary> - /// <param name="result">The result.</param> - /// <param name="reader">The reader.</param> - private void FetchDataFromActorNode(MetadataResult<Series> result, XmlReader reader) - { - reader.MoveToContent(); - - var personInfo = new PersonInfo(); - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Name": - { - personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Role": - { - personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "id": - { - reader.Skip(); - break; - } - - case "Image": - { - var url = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - if (!string.IsNullOrWhiteSpace(url)) - { - personInfo.ImageUrl = TVUtils.BannerUrl + url; - } - break; - } - - case "SortOrder": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - personInfo.SortOrder = rval; - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - personInfo.Type = PersonType.Actor; - - if (!string.IsNullOrWhiteSpace(personInfo.Name)) - { - result.AddPerson(personInfo); - } - } - - private void FetchDataFromSeriesNode(MetadataResult<Series> result, XmlReader reader, CancellationToken cancellationToken) - { - Series item = result.Item; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "id": - { - item.SetProviderId(MetadataProviders.Tvdb.ToString(), (reader.ReadElementContentAsString() ?? string.Empty).Trim()); - break; - } - - case "SeriesName": - { - item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Overview": - { - item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Language": - { - result.ResultLanguage = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Airs_DayOfWeek": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AirDays = TVUtils.GetAirDays(val); - } - break; - } - - case "Airs_Time": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AirTime = val; - } - break; - } - - case "ContentRating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.OfficialRating = val; - } - break; - } - - case "Rating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // float.TryParse is local aware, so it can be probamatic, force us culture - if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out var rval)) - { - item.CommunityRating = rval; - } - } - break; - } - case "RatingCount": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - //item.VoteCount = rval; - } - } - - break; - } - - case "IMDB_ID": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Imdb, val); - } - - break; - } - - case "zap2it_id": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Zap2It, val); - } - - break; - } - - case "Status": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (Enum.TryParse(val, true, out SeriesStatus seriesStatus)) - item.Status = seriesStatus; - } - - break; - } - - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (DateTime.TryParse(val, out var date)) - { - date = date.ToUniversalTime(); - - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - } - - break; - } - - case "Runtime": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var rval)) - { - item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks; - } - } - - break; - } - - case "Genre": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - var vals = val - .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) - .Select(i => i.Trim()) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .ToList(); - - if (vals.Count > 0) - { - item.Genres = Array.Empty<string>(); - - foreach (var genre in vals) - { - item.AddGenre(genre); - } - } - } - - break; - } - - case "Network": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - var vals = val - .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) - .Select(i => i.Trim()) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .ToList(); - - if (vals.Count > 0) - { - item.SetStudios(vals); - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - - /// <summary> - /// Extracts info for each episode into invididual xml files so that they can be easily accessed without having to step through the entire series xml - /// </summary> - /// <param name="seriesDataPath">The series data path.</param> - /// <param name="xmlFile">The XML file.</param> - /// <param name="lastTvDbUpdateTime">The last tv db update time.</param> - /// <returns>Task.</returns> - private async Task ExtractEpisodes(string seriesDataPath, string xmlFile, long? lastTvDbUpdateTime) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var fileStream = _fileSystem.GetFileStream(xmlFile, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) - { - using (var streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Episode": - { - var outerXml = reader.ReadOuterXml(); - - await SaveEpsiodeXml(seriesDataPath, outerXml, lastTvDbUpdateTime).ConfigureAwait(false); - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - } - } - } - - private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime) - { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - var seasonNumber = -1; - var episodeNumber = -1; - var absoluteNumber = -1; - var lastUpdateString = string.Empty; - - var dvdSeasonNumber = -1; - var dvdEpisodeNumber = -1.0; - - using (var streamReader = new StringReader(xml)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "lastupdated": - { - lastUpdateString = reader.ReadElementContentAsString(); - break; - } - - case "EpisodeNumber": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var num)) - { - episodeNumber = num; - } - } - break; - } - - case "Combined_episodenumber": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (float.TryParse(val, NumberStyles.Any, _usCulture, out var num)) - { - dvdEpisodeNumber = num; - } - } - - break; - } - - case "Combined_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (float.TryParse(val, NumberStyles.Any, _usCulture, out var num)) - { - dvdSeasonNumber = Convert.ToInt32(num); - } - } - - break; - } - - case "absolute_number": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var num)) - { - absoluteNumber = num; - } - } - break; - } - - case "SeasonNumber": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var num)) - { - seasonNumber = num; - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } + series.Status = seriesStatus; } - var hasEpisodeChanged = true; - if (!string.IsNullOrWhiteSpace(lastUpdateString) && lastTvDbUpdateTime.HasValue) + if (DateTime.TryParse(tvdbSeries.FirstAired, out var date)) { - if (long.TryParse(lastUpdateString, NumberStyles.Any, _usCulture, out var num)) - { - hasEpisodeChanged = num >= lastTvDbUpdateTime.Value; - } + // dates from tvdb are UTC but without offset or Z + series.PremiereDate = date; + series.ProductionYear = date.Year; } - var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber)); - - // Only save the file if not already there, or if the episode has changed - if (hasEpisodeChanged || !File.Exists(file)) + series.RunTimeTicks = TimeSpan.FromMinutes(Convert.ToDouble(tvdbSeries.Runtime)).Ticks; + foreach (var genre in tvdbSeries.Genre) { - using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true)) - { - using (var writer = XmlWriter.Create(fileStream, new XmlWriterSettings - { - Encoding = Encoding.UTF8, - Async = true - })) - { - await writer.WriteRawAsync(xml).ConfigureAwait(false); - } - } + series.AddGenre(genre); } - if (absoluteNumber != -1) - { - file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", absoluteNumber)); + series.AddStudio(tvdbSeries.Network); - // Only save the file if not already there, or if the episode has changed - if (hasEpisodeChanged || !File.Exists(file)) - { - using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true)) - { - using (var writer = XmlWriter.Create(fileStream, new XmlWriterSettings - { - Encoding = Encoding.UTF8, - Async = true - })) - { - await writer.WriteRawAsync(xml).ConfigureAwait(false); - } - } - } - } - - if (dvdSeasonNumber != -1 && dvdEpisodeNumber != -1 && (dvdSeasonNumber != seasonNumber || dvdEpisodeNumber != episodeNumber)) + if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended) { - file = Path.Combine(seriesDataPath, string.Format("episode-dvd-{0}-{1}.xml", dvdSeasonNumber, dvdEpisodeNumber)); - - // Only save the file if not already there, or if the episode has changed - if (hasEpisodeChanged || !File.Exists(file)) + try { - using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true)) + var episodeSummary = _tvDbClientManager + .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data; + var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); + var episodeQuery = new EpisodeQuery { - using (var writer = XmlWriter.Create(fileStream, new XmlWriterSettings - { - Encoding = Encoding.UTF8, - Async = true - })) + AiredSeason = maxSeasonNumber + }; + var episodesPage = + _tvDbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; + result.Item.EndDate = episodesPage.Select(e => { - await writer.WriteRawAsync(xml).ConfigureAwait(false); - } - } + DateTime.TryParse(e.FirstAired, out var firstAired); + return firstAired; + }).Max(); } - } - } - - /// <summary> - /// Gets the series data path. - /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <param name="seriesProviderIds">The series provider ids.</param> - /// <returns>System.String.</returns> - internal static string GetSeriesDataPath(IApplicationPaths appPaths, Dictionary<string, string> seriesProviderIds) - { - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out string seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - return null; - } - - public string GetSeriesXmlPath(Dictionary<string, string> seriesProviderIds, string language) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - var seriesXmlFilename = language.ToLowerInvariant() + ".xml"; - - return Path.Combine(seriesDataPath, seriesXmlFilename); - } - - /// <summary> - /// Gets the series data path. - /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <returns>System.String.</returns> - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tvdb"); - - return dataPath; - } - - private void DeleteXmlFiles(string path) - { - try - { - foreach (var file in _fileSystem.GetFilePaths(path, true) - .ToList()) + catch (TvDbServerException e) { - _fileSystem.DeleteFile(file); + _logger.LogError(e, "Failed to find series end date for series {TvdbId}", tvdbSeries.Id); } } - catch (IOException) - { - // No biggie - } } - /// <summary> - /// Sanitizes the XML file. - /// </summary> - /// <param name="file">The file.</param> - /// <returns>Task.</returns> - private async Task SanitizeXmlFile(string file) + private static void MapActorsToResult(MetadataResult<Series> result, IEnumerable<Actor> actors) { - string validXml; - - using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) - { - using (var reader = new StreamReader(fileStream)) - { - var xml = await reader.ReadToEndAsync().ConfigureAwait(false); - - validXml = StripInvalidXmlCharacters(xml); - } - } - - using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + foreach (Actor actor in actors) { - using (var writer = new StreamWriter(fileStream)) + var personInfo = new PersonInfo { - await writer.WriteAsync(validXml).ConfigureAwait(false); - } - } - } - - /// <summary> - /// Strips the invalid XML characters. - /// </summary> - /// <param name="inString">The in string.</param> - /// <returns>System.String.</returns> - public static string StripInvalidXmlCharacters(string inString) - { - if (inString == null) return null; - - var sbOutput = new StringBuilder(); - char ch; + Type = PersonType.Actor, + Name = (actor.Name ?? string.Empty).Trim(), + Role = actor.Role, + ImageUrl = TvdbUtils.BannerUrl + actor.Image, + SortOrder = actor.SortOrder + }; - for (int i = 0; i < inString.Length; i++) - { - ch = inString[i]; - if ((ch >= 0x0020 && ch <= 0xD7FF) || - (ch >= 0xE000 && ch <= 0xFFFD) || - ch == 0x0009 || - ch == 0x000A || - ch == 0x000D) + if (!string.IsNullOrWhiteSpace(personInfo.Name)) { - sbOutput.Append(ch); + result.AddPerson(personInfo); } } - return sbOutput.ToString(); } public string Name => "TheTVDB"; @@ -1717,7 +413,8 @@ namespace MediaBrowser.Providers.TV.TheTVDB return; } - var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false); + var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None) + .ConfigureAwait(false); var entry = srch.FirstOrDefault(); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs new file mode 100644 index 000000000..112cbf800 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using MediaBrowser.Model.Entities; +namespace MediaBrowser.Providers.TV.TheTVDB +{ + public static class TvdbUtils + { + public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K"; + public const string TvdbBaseUrl = "https://www.thetvdb.com/"; + public const string BannerUrl = TvdbBaseUrl + "banners/"; + + public static ImageType GetImageTypeFromKeyType(string keyType) + { + switch (keyType.ToLowerInvariant()) + { + case "poster": + case "season": return ImageType.Primary; + case "series": + case "seasonwide": return ImageType.Banner; + case "fanart": return ImageType.Backdrop; + default: throw new ArgumentException($"Invalid or unknown keytype: {keyType}", nameof(keyType)); + } + } + + public static string NormalizeLanguage(string language) + { + if (string.IsNullOrWhiteSpace(language)) + { + return null; + } + + // pt-br is just pt to tvdb + return language.Split('-')[0].ToLowerInvariant(); + } + } +} diff --git a/MediaBrowser.Providers/TV/TvExternalIds.cs b/MediaBrowser.Providers/TV/TvExternalIds.cs index 5c246e300..3f889fbbe 100644 --- a/MediaBrowser.Providers/TV/TvExternalIds.cs +++ b/MediaBrowser.Providers/TV/TvExternalIds.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.TV public string Key => MetadataProviders.Tvdb.ToString(); - public string UrlFormatString => TvdbPrescanTask.TvdbBaseUrl + "?tab=series&id={0}"; + public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}"; public bool Supports(IHasProviderIds item) { @@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.TV public string Key => MetadataProviders.Tvdb.ToString(); - public string UrlFormatString => TvdbPrescanTask.TvdbBaseUrl + "?tab=episode&id={0}"; + public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}"; public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web -Subproject b4842e325e9d7d708193b4a27060cfe4c978df5 +Subproject ec5a3b6e5efb6041153b92818aee562f20ee994 diff --git a/SharedVersion.cs b/SharedVersion.cs index 294748b77..41eda393a 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.2.0")] -[assembly: AssemblyFileVersion("10.2.0")] +[assembly: AssemblyVersion("10.2.1")] +[assembly: AssemblyFileVersion("10.2.1")] diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 869dc4a5e..7b7efff27 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,19 @@ +jellyfin (10.2.1-1) unstable; urgency=medium + + * jellyfin: + * PR920 Fix cachedir missing from Docker container + * PR924 Use the movie name instead of folder name + * PR933 Semi-revert to prefer old movie grouping behaviour + * PR948 Revert movie matching (supercedes PR933, PR924, PR739) + * PR960 Use jellyfin/ffmpeg image + * jellyfin-web: + * PR136 Re-add OpenSubtitles configuration page + * PR137 Replace HeaderEmbyServer with HeaderJellyfinServer on plugincatalog + * PR138 Remove left-over JS for Customize Home Screen + * PR141 Exit fullscreen automatically after video playback ends + + -- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 20 Feb 2019 11:36:16 -0500 + jellyfin (10.2.0-2) unstable; urgency=medium * jellyfin: diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 75821cb17..146486428 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -7,8 +7,8 @@ %endif Name: jellyfin -Version: 10.2.0 -Release: 2%{?dist} +Version: 10.2.1 +Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 URL: https://jellyfin.media @@ -140,6 +140,18 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org> +- jellyfin: +- PR920 Fix cachedir missing from Docker container +- PR924 Use the movie name instead of folder name +- PR933 Semi-revert to prefer old movie grouping behaviour +- PR948 Revert movie matching (supercedes PR933, PR924, PR739) +- PR960 Use jellyfin/ffmpeg image +- jellyfin-web: +- PR136 Re-add OpenSubtitles configuration page +- PR137 Replace HeaderEmbyServer with HeaderJellyfinServer on plugincatalog +- PR138 Remove left-over JS for Customize Home Screen +- PR141 Exit fullscreen automatically after video playback ends * Fri Feb 15 2019 Jellyfin Packaging Team <packaging@jellyfin.org> - jellyfin: - PR452 Use EF Core for Activity database diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index 1121c3398..2c83f264c 100644 --- a/deployment/windows/build-jellyfin.ps1 +++ b/deployment/windows/build-jellyfin.ps1 @@ -102,8 +102,8 @@ if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){ Write-Verbose "Starting NSSM Install" Install-NSSM $InstallLocation $Architecture } -Copy-Item .\deployment\win-generic\install-jellyfin.ps1 $InstallLocation\install-jellyfin.ps1 -Copy-Item .\deployment\win-generic\install.bat $InstallLocation\install.bat +Copy-Item .\deployment\windows\install-jellyfin.ps1 $InstallLocation\install-jellyfin.ps1 +Copy-Item .\deployment\windows\install.bat $InstallLocation\install.bat if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){ Compress-Archive -Path $InstallLocation -DestinationPath "$InstallLocation/jellyfin.zip" -Force } |
