diff options
127 files changed, 2423 insertions, 5571 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 28690f36f..4b397b328 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -19,6 +19,10 @@ - [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) + - [Lynxy](https://github.com/Lynxy) + - [fasheng](https://github.com/fasheng) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index 6c0d2515f..91a4f5a2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,10 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -RUN dotnet publish \ - --configuration release \ - --output /jellyfin \ - Jellyfin.Server +RUN bash -c "source deployment/common.build.sh && \ + build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin" -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 \ @@ -22,6 +20,16 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin + +ARG JELLYFIN_WEB_VERSION=10.2.2 +RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && rm -rf /jellyfin/jellyfin-web \ + && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web + EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache +ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ + --datadir /config \ + --cachedir /cache \ + --ffmpeg /usr/local/bin/ffmpeg \ + --ffprobe /usr/local/bin/ffprobe diff --git a/Dockerfile.arm b/Dockerfile.arm index 9d1c30619..42f0354a3 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -17,11 +17,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish \ - -r linux-arm \ - --configuration release \ - --output /jellyfin \ - Jellyfin.Server +RUN bash -c "source deployment/common.build.sh && \ + build_jellyfin Jellyfin.Server Release linux-arm /jellyfin" FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7 @@ -31,6 +28,16 @@ RUN apt-get update \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin + +ARG JELLYFIN_WEB_VERSION=10.2.2 +RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && rm -rf /jellyfin/jellyfin-web \ + && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web + EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache +ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ + --datadir /config \ + --cachedir /cache \ + --ffmpeg /usr/bin/ffmpeg \ + --ffprobe /usr/bin/ffprobe diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index e61aaa167..d3103d389 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -18,11 +18,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish \ - -r linux-arm64 \ - --configuration release \ - --output /jellyfin \ - Jellyfin.Server +RUN bash -c "source deployment/common.build.sh && \ + build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin" FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8 @@ -32,6 +29,16 @@ RUN apt-get update \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin + +ARG JELLYFIN_WEB_VERSION=10.2.2 +RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && rm -rf /jellyfin/jellyfin-web \ + && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web + EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache +ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ + --datadir /config \ + --cachedir /cache \ + --ffmpeg /usr/bin/ffmpeg \ + --ffprobe /usr/bin/ffprobe diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs index f784be83e..90125fa3e 100644 --- a/DvdLib/Ifo/Dvd.cs +++ b/DvdLib/Ifo/Dvd.cs @@ -26,17 +26,17 @@ namespace DvdLib.Ifo if (vmgPath == null) { - var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)); - - foreach (var ifo in allIfos) + foreach (var ifo in allFiles) { - var num = ifo.Name.Split('_').ElementAtOrDefault(1); - var numbersRead = new List<ushort>(); + if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out var ifoNumber) && !numbersRead.Contains(ifoNumber)) + var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries); + if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) { ReadVTS(ifoNumber, ifo.FullName); - numbersRead.Add(ifoNumber); } } } @@ -76,7 +76,7 @@ namespace DvdLib.Ifo } } - private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles) + private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles) { var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum); diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index 0ebb490a1..c7cb364a8 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -7,6 +7,7 @@ namespace Emby.Dlna.Configuration public bool EnableServer { get; set; } public bool EnableDebugLog { get; set; } public bool BlastAliveMessages { get; set; } + public bool SendOnlyMatchedHost { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; } public int BlastAliveMessageIntervalSeconds { get; set; } public string DefaultUserId { get; set; } @@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration EnablePlayTo = true; EnableServer = true; BlastAliveMessages = true; + SendOnlyMatchedHost = true; ClientDiscoveryIntervalSeconds = 60; BlastAliveMessageIntervalSeconds = 1800; } diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 1150afdba..84f38ff76 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) { - var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount)); + var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); } @@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory } else { - var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount)); + var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); totalCount = childrenResult.TotalRecordCount; provided = childrenResult.Items.Length; diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 605f4f37b..1268f3d5c 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl { AddCommonFields(item, itemStubType, context, writer, filter); - var hasArtists = item as IHasArtist; var hasAlbumArtists = item as IHasAlbumArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { foreach (var artist in hasArtists.Artists) { diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index f53d27451..d6ee5d13a 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -29,7 +29,7 @@ namespace Emby.Dlna private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; - private readonly IAssemblyInfo _assemblyInfo; + private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); @@ -39,8 +39,7 @@ namespace Emby.Dlna IApplicationPaths appPaths, ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, - IServerApplicationHost appHost, - IAssemblyInfo assemblyInfo) + IServerApplicationHost appHost) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; @@ -48,7 +47,6 @@ namespace Emby.Dlna _logger = loggerFactory.CreateLogger("Dlna"); _jsonSerializer = jsonSerializer; _appHost = appHost; - _assemblyInfo = assemblyInfo; } public async Task InitProfilesAsync() @@ -368,15 +366,18 @@ namespace Emby.Dlna var systemProfilesPath = SystemProfilesPath; - foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType()) - .Where(i => i.StartsWith(namespaceName)) - .ToList()) + foreach (var name in _assembly.GetManifestResourceNames()) { + if (!name.StartsWith(namespaceName)) + { + continue; + } + var filename = Path.GetFileName(name).Substring(namespaceName.Length); var path = Path.Combine(systemProfilesPath, filename); - using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name)) + using (var stream = _assembly.GetManifestResourceStream(name)) { var fileInfo = _fileSystem.GetFileInfo(path); @@ -514,7 +515,7 @@ namespace Emby.Dlna return new ImageStream { Format = format, - Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource) + Stream = _assembly.GetManifestResourceStream(resource) }; } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a20006578..57ed0097a 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -169,9 +169,10 @@ namespace Emby.Dlna.Main { if (_communicationsServer == null) { - var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; + var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows || + _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux; - _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) + _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) { IsShared = true }; @@ -229,7 +230,7 @@ namespace Emby.Dlna.Main try { - _Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion); + _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _Publisher.LogFunction = LogMessage; _Publisher.SupportPnpRootDevice = false; @@ -245,17 +246,17 @@ namespace Emby.Dlna.Main private async Task RegisterServerEndpoints() { - var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList(); + var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); var udn = CreateUuid(_appHost.SystemId); foreach (var address in addresses) { - // TODO: Remove this condition on platforms that support it - //if (address.AddressFamily == IpAddressFamily.InterNetworkV6) - //{ - // continue; - //} + if (address.AddressFamily == IpAddressFamily.InterNetworkV6) + { + // Not support IPv6 right now + continue; + } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; @@ -268,6 +269,8 @@ namespace Emby.Dlna.Main { CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info. Location = uri, // Must point to the URL that serves your devices UPnP description document. + Address = address, + SubnetMask = _networkManager.GetLocalIpSubnetMask(address), FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index b96fa43e5..4f9e398e9 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { if (arg.Direction == "out") + { continue; + } if (arg.Name == "InstanceID") + { stateString += BuildArgumentXml(arg, "0"); + } else + { stateString += BuildArgumentXml(arg, null); + } } return string.Format(CommandBase, action.Name, xmlNamespace, stateString); @@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { if (arg.Direction == "out") + { continue; + } + if (arg.Name == "InstanceID") + { stateString += BuildArgumentXml(arg, "0"); + } else + { stateString += BuildArgumentXml(arg, value.ToString(), commandParameter); + } } return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); @@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { if (arg.Name == "InstanceID") + { stateString += BuildArgumentXml(arg, "0"); + } else if (dictionary.ContainsKey(arg.Name)) + { stateString += BuildArgumentXml(arg, dictionary[arg.Name]); + } else + { stateString += BuildArgumentXml(arg, value.ToString()); + } } return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index 9485d697b..a8f81a3b8 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using Emby.Naming.Common; namespace Emby.Naming.TV @@ -22,7 +21,9 @@ namespace Emby.Naming.TV // There were no failed tests without this block, but to be safe, we can keep it until // the regex which require file extensions are modified so that they don't need them. if (IsDirectory) + { path += ".mp4"; + } EpisodePathParserResult result = null; @@ -35,6 +36,7 @@ namespace Emby.Naming.TV continue; } } + if (isNamed.HasValue) { if (expression.IsNamed != isNamed.Value) @@ -42,6 +44,7 @@ namespace Emby.Naming.TV continue; } } + if (isOptimistic.HasValue) { if (expression.IsOptimistic != isOptimistic.Value) @@ -191,13 +194,20 @@ namespace Emby.Naming.TV private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions) { - var results = expressions - .Where(i => i.IsNamed) - .Select(i => Parse(path, i)) - .Where(i => i.Success); - - foreach (var result in results) + foreach (var i in expressions) { + if (!i.IsNamed) + { + continue; + } + + var result = Parse(path, i); + + if (!result.Success) + { + continue; + } + if (string.IsNullOrEmpty(info.SeriesName)) { info.SeriesName = result.SeriesName; @@ -208,12 +218,10 @@ namespace Emby.Naming.TV info.EndingEpsiodeNumber = result.EndingEpsiodeNumber; } - if (!string.IsNullOrEmpty(info.SeriesName)) + if (!string.IsNullOrEmpty(info.SeriesName) + && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)) { - if (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue) - { - break; - } + break; } } } 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/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs index 6febcc2f7..0c513ea12 100644 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ b/Emby.Server.Implementations/Activity/ActivityManager.cs @@ -39,8 +39,13 @@ namespace Emby.Server.Implementations.Activity { var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty))) + foreach (var item in result.Items) { + if (item.UserId == Guid.Empty) + { + continue; + } + var user = _userManager.GetUserById(item.UserId); if (user != null) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dcf2098d4..b3bb4f740 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); @@ -762,7 +767,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(SessionManager); serviceCollection.AddSingleton<IDlnaManager>( - new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo)); + new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this)); CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager); serviceCollection.AddSingleton(CollectionManager); @@ -1572,7 +1577,7 @@ namespace Emby.Server.Implementations if (addresses.Count == 0) { - addresses.AddRange(NetworkManager.GetLocalIpAddresses()); + addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); } var resultList = new List<IpAddressInfo>(); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 949b89226..7e50650d7 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels { foreach (var item in returnItems) { - var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None); - Task.WaitAll(task); + RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); } } @@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels } numComplete++; - double percent = numComplete; - percent /= allChannelsList.Count; - + double percent = (double)numComplete / allChannelsList.Count; progress.Report(100 * percent); } @@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels foreach (var item in result.Items) { - var folder = item as Folder; - - if (folder != null) + if (item is Folder folder) { await GetChannelItemsInternal(new InternalItemsQuery { 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..06f6563a3 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2279,11 +2279,10 @@ namespace Emby.Server.Implementations.Data private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { - "Audio", - "MusicAlbum", - "MusicVideo", + "Book", "AudioBook", - "AudioPodcast" + "Episode", + "Season" }; private bool HasSeriesFields(InternalItemsQuery query) @@ -2747,7 +2746,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/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 7a9b72244..4109b7ad1 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data { list.Add(row[0].ReadGuidFromBlob()); } - catch + catch (Exception ex) { - + Logger.LogError(ex, "Error while getting user"); } } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2233d3d40..7b28a22a8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -5,8 +5,6 @@ using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; @@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto return GetBaseItemDto(item, options, user, owner); } - public BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) - { - return GetBaseItemDtos(items, items.Count, options, user, owner); - } - - public BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null) - { - return GetBaseItemDtos(items, items.Length, options, user, owner); - } + public BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) + => GetBaseItemDtos(items, items.Count, options, user, owner); public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null) { 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/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 60cc9b88e..ace93ebde 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -79,13 +79,13 @@ namespace Emby.Server.Implementations.Networking private IpAddressInfo[] _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); - public IpAddressInfo[] GetLocalIpAddresses() + public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true) { lock (_localIpAddressSyncLock) { if (_localIpAddresses == null) { - var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray(); + var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray(); _localIpAddresses = addresses; @@ -95,9 +95,9 @@ namespace Emby.Server.Implementations.Networking } } - private async Task<List<IPAddress>> GetLocalIpAddressesInternal() + private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool ignoreVirtualInterface) { - var list = GetIPsDefault() + var list = GetIPsDefault(ignoreVirtualInterface) .ToList(); if (list.Count == 0) @@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking return Dns.GetHostAddressesAsync(hostName); } - private List<IPAddress> GetIPsDefault() + private List<IPAddress> GetIPsDefault(bool ignoreVirtualInterface) { NetworkInterface[] interfaces; @@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking // Try to exclude virtual adapters // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms var addr = ipProperties.GatewayAddresses.FirstOrDefault(); - if (addr == null || string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase)) + if (addr == null || ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase)) { return new List<IPAddress>(); } @@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking return false; } + public bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask) + { + IPAddress network1 = GetNetworkAddress(ToIPAddress(address1), ToIPAddress(subnetMask)); + IPAddress network2 = GetNetworkAddress(ToIPAddress(address2), ToIPAddress(subnetMask)); + return network1.Equals(network2); + } + + private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) + { + byte[] ipAdressBytes = address.GetAddressBytes(); + byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + + if (ipAdressBytes.Length != subnetMaskBytes.Length) + { + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + } + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) + { + broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i])); + } + return new IPAddress(broadcastAddress); + } + + public IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address) + { + NetworkInterface[] interfaces; + IPAddress ipaddress = ToIPAddress(address); + + try + { + var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown }; + + interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => validStatuses.Contains(i.OperationalStatus)) + .ToArray(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in GetAllNetworkInterfaces"); + return null; + } + + foreach (NetworkInterface ni in interfaces) + { + if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null) + { + foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) + { + if (ip.Address.Equals(ipaddress) && ip.IPv4Mask != null) + { + return ToIpAddressInfo(ip.IPv4Mask); + } + } + } + } + return null; + } + public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint) { if (endpoint == null) 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/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index f575baca3..ccb28e8df 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Services private const char ComponentSeperator = '.'; private const string VariablePrefix = "{"; - readonly bool[] componentsWithSeparators; + private readonly bool[] componentsWithSeparators; private readonly string restPath; public bool IsWildCardPath { get; private set; } @@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services public string Description { get; private set; } public bool IsHidden { get; private set; } - public int Priority { get; set; } //passed back to RouteAttribute - - public IEnumerable<string> PathVariables => this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e)); - public static string[] GetPathPartsForMatching(string pathInfo) { return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); @@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services { list.Add(hashPrefix + part); - var subParts = part.Split(ComponentSeperator); - if (subParts.Length == 1) continue; + if (part.IndexOf(ComponentSeperator) == -1) + { + continue; + } + var subParts = part.Split(ComponentSeperator); foreach (var subPart in subParts) { list.Add(hashPrefix + subPart); @@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services { if (string.IsNullOrEmpty(component)) continue; - if (StringContains(component, VariablePrefix) + if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1 && component.IndexOf(ComponentSeperator) != -1) { hasSeparators.Add(true); @@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services for (var i = 0; i < components.Length - 1; i++) { - if (!this.isWildcard[i]) continue; + if (!this.isWildcard[i]) + { + continue; + } + if (this.literalsToMatch[i + 1] == null) { throw new ArgumentException( @@ -173,7 +176,7 @@ namespace Emby.Server.Implementations.Services } } - this.wildcardCount = this.isWildcard.Count(x => x); + this.wildcardCount = this.isWildcard.Length; this.IsWildCardPath = this.wildcardCount > 0; this.FirstMatchHashKey = !this.IsWildCardPath @@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services : WildCardChar + PathSeperator + firstLiteralMatch; this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType); - RegisterCaseInsenstivePropertyNameMappings(); - } - private void RegisterCaseInsenstivePropertyNameMappings() - { - foreach (var propertyInfo in GetSerializableProperties(RequestType)) - { - var propertyName = propertyInfo.Name; - propertyNamesMap.Add(propertyName.ToLowerInvariant(), propertyName); - } + _propertyNamesMap = new HashSet<string>( + GetSerializableProperties(RequestType).Select(x => x.Name), + StringComparer.OrdinalIgnoreCase); } - internal static string[] IgnoreAttributesNamed = new[] { + internal static string[] IgnoreAttributesNamed = new[] + { "IgnoreDataMemberAttribute", "JsonIgnoreAttribute" }; @@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services private static Type excludeType = typeof(Stream); - internal static List<PropertyInfo> GetSerializableProperties(Type type) + internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type) { - var list = new List<PropertyInfo>(); - var props = GetPublicProperties(type); - - foreach (var prop in props) + foreach (var prop in GetPublicProperties(type)) { - if (prop.GetMethod == null) - { - continue; - } - - if (excludeType == prop.PropertyType) + if (prop.GetMethod == null + || excludeType == prop.PropertyType) { continue; } @@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services if (!ignored) { - list.Add(prop); + yield return prop; } } - - // else return those properties that are not decorated with IgnoreDataMember - return list; } - private static List<PropertyInfo> GetPublicProperties(Type type) + private static IEnumerable<PropertyInfo> GetPublicProperties(Type type) { - if (type.GetTypeInfo().IsInterface) + if (type.IsInterface) { var propertyInfos = new List<PropertyInfo>(); - - var considered = new List<Type>(); + var considered = new List<Type>() + { + type + }; var queue = new Queue<Type>(); - considered.Add(type); queue.Enqueue(type); while (queue.Count > 0) @@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services var subType = queue.Dequeue(); foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces) { - if (considered.Contains(subInterface)) continue; + if (considered.Contains(subInterface)) + { + continue; + } considered.Add(subInterface); queue.Enqueue(subInterface); } - var typeProperties = GetTypesPublicProperties(subType); - - var newPropertyInfos = typeProperties + var newPropertyInfos = GetTypesPublicProperties(subType) .Where(x => !propertyInfos.Contains(x)); propertyInfos.InsertRange(0, newPropertyInfos); @@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services return propertyInfos; } - var list = new List<PropertyInfo>(); - - foreach (var t in GetTypesPublicProperties(type)) - { - if (t.GetIndexParameters().Length == 0) - { - list.Add(t); - } - } - return list; + return GetTypesPublicProperties(type) + .Where(x => x.GetIndexParameters().Length == 0); } - private static PropertyInfo[] GetTypesPublicProperties(Type subType) + private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType) { - var pis = new List<PropertyInfo>(); foreach (var pi in subType.GetRuntimeProperties()) { var mi = pi.GetMethod ?? pi.SetMethod; - if (mi != null && mi.IsStatic) continue; - pis.Add(pi); + if (mi != null && mi.IsStatic) + { + continue; + } + + yield return pi; } - return pis.ToArray(); } /// <summary> @@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services private readonly StringMapTypeDeserializer typeDeserializer; - private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>(); + private readonly HashSet<string> _propertyNamesMap; public int MatchScore(string httpMethod, string[] withPathInfoParts) { @@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services return -1; } - var score = 0; - //Routes with least wildcard matches get the highest score - score += Math.Max((100 - wildcardMatchCount), 1) * 1000; - - //Routes with less variable (and more literal) matches - score += Math.Max((10 - VariableArgsCount), 1) * 100; + var score = Math.Max((100 - wildcardMatchCount), 1) * 1000 + //Routes with less variable (and more literal) matches + + Math.Max((10 - VariableArgsCount), 1) * 100; //Exact verb match is better than ANY if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) @@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services return score; } - private bool StringContains(string str1, string str2) - { - return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1; - } - /// <summary> /// For performance withPathInfoParts should already be a lower case string /// to minimize redundant matching operations. @@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services if (i < this.TotalComponentsCount - 1) { // Continue to consume up until a match with the next literal - while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1])) + while (pathIx < withPathInfoParts.Length + && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase)) { pathIx++; wildcardMatchCount++; @@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services continue; } - if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch)) + if (withPathInfoParts.Length <= pathIx + || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase)) { return false; } + pathIx++; } } @@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services return pathIx == withPathInfoParts.Length; } - private static bool LiteralsEqual(string str1, string str2) - { - // Most cases - if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Handle turkish i - str1 = str1.ToUpperInvariant(); - str2 = str2.ToUpperInvariant(); - - // Invariant IgnoreCase would probably be better but it's not available in PCL - return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase); - } - private bool ExplodeComponents(ref string[] withPathInfoParts) { var totalComponents = new List<string>(); for (var i = 0; i < withPathInfoParts.Length; i++) { var component = withPathInfoParts[i]; - if (string.IsNullOrEmpty(component)) continue; + if (string.IsNullOrEmpty(component)) + { + continue; + } if (this.PathComponentsCount != this.TotalComponentsCount && this.componentsWithSeparators[i]) { var subComponents = component.Split(ComponentSeperator); - if (subComponents.Length < 2) return false; + if (subComponents.Length < 2) + { + return false; + } + totalComponents.AddRange(subComponents); } else @@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services continue; } - if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest)) + if (!this._propertyNamesMap.Contains(variableName)) { if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) { @@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services { sb.Append(PathSeperatorChar + requestComponents[j]); } + value = sb.ToString(); } else @@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) { - var sb = new StringBuilder(); - sb.Append(value); + var sb = new StringBuilder(value); pathIx++; while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) { sb.Append(PathSeperatorChar + requestComponents[pathIx++]); } + value = sb.ToString(); } else @@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services pathIx++; } - requestKeyValuesMap[propertyNameOnRequest] = value; + requestKeyValuesMap[variableName] = value; } if (queryStringAndFormData != null) diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index d13935fba..f835aa1b5 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services { internal class PropertySerializerEntry { - public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn) + public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType) { PropertySetFn = propertySetFn; PropertyParseStringFn = propertyParseStringFn; + PropertyType = PropertyType; } - public Action<object, object> PropertySetFn; - public Func<string, object> PropertyParseStringFn; - public Type PropertyType; + public Action<object, object> PropertySetFn { get; private set; } + public Func<string, object> PropertyParseStringFn { get; private set; } + public Type PropertyType { get; private set; } } private readonly Type type; @@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services public Func<string, object> GetParseFn(Type propertyType) { if (propertyType == typeof(string)) + { return s => s; + } return _GetParseFn(propertyType); } @@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); var propertyType = propertyInfo.PropertyType; var propertyParseStringFn = GetParseFn(propertyType); - var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType }; + var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); propertySetterMap[propertyInfo.Name] = propertySerializer; } @@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs) { - string propertyName = null; - string propertyTextValue = null; PropertySerializerEntry propertySerializerEntry = null; if (instance == null) + { instance = _CreateInstanceFn(type); + } foreach (var pair in keyValuePairs) { - propertyName = pair.Key; - propertyTextValue = pair.Value; - - if (string.IsNullOrEmpty(propertyTextValue)) - { - continue; - } + string propertyName = pair.Key; + string propertyTextValue = pair.Value; - if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)) - { - if (propertyName == "v") - { - continue; - } - - continue; - } - - if (propertySerializerEntry.PropertySetFn == null) + if (string.IsNullOrEmpty(propertyTextValue) + || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) + || propertySerializerEntry.PropertySetFn == null) { continue; } @@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services { continue; } + propertySerializerEntry.PropertySetFn(instance, value); } @@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services public static string LeftPart(string strVal, char needle) { - if (strVal == null) return null; + if (strVal == null) + { + return null; + } + var pos = strVal.IndexOf(needle); return pos == -1 ? strVal @@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services { public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) { - if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) return null; + if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) + { + return null; + } var setMethodInfo = propertyInfo.SetMethod; return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index fa0ab62d3..03e7b2654 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1090,7 +1090,7 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } - private IList<BaseItem> TranslateItemForPlayback(Guid id, User user) + private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); diff --git a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs index 52ec7a135..46bf6cc21 100644 --- a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs +++ b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs @@ -495,9 +495,7 @@ namespace Emby.XmlTv.Classes ParseMovieDbSystem(reader, result); break; case "SxxExx": - // TODO - // <episode-num system="SxxExx">S03E12</episode-num> - reader.Skip(); + ParseSxxExxSystem(reader, result); break; default: // Handles empty string and nulls reader.Skip(); @@ -505,6 +503,29 @@ namespace Emby.XmlTv.Classes } } + public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result) + { + // <episode-num system="SxxExx">S012E32</episode-num> + + var value = reader.ReadElementContentAsString(); + var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase); + + if (res.Success) + { + int parsedInt; + + if (int.TryParse(res.Groups[1].Value, out parsedInt)) + { + result.Episode.Series = parsedInt; + } + + if (int.TryParse(res.Groups[2].Value, out parsedInt)) + { + result.Episode.Episode = parsedInt; + } + } + } + public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result) { // <episode-num system="thetvdb.com">series/248841</episode-num> diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs index 0d5a1d3c0..c72f295fd 100644 --- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs +++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia foregroundWidth *= percent; foregroundWidth /= 100; - paint.Color = SKColor.Parse("#FF52B54B"); + paint.Color = SKColor.Parse("#FF00A4DC"); canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint); } } diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 62497da27..7f3c18bb2 100644 --- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia using (var paint = new SKPaint()) { - paint.Color = SKColor.Parse("#CC52B54B"); + paint.Color = SKColor.Parse("#CC00A4DC"); paint.Style = SKPaintStyle.Fill; canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); } diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index ba712bff7..dbf935f4e 100644 --- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia using (var paint = new SKPaint()) { - paint.Color = SKColor.Parse("#CC52B54B"); + paint.Color = SKColor.Parse("#CC00A4DC"); paint.Style = SKPaintStyle.Fill; canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 2a2f1dde6..41ee73a56 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -187,26 +187,13 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(dataDir)) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - dataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - } - else - { - // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. - dataDir = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); - - // If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. - if (string.IsNullOrEmpty(dataDir)) - { - dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share"); - } - } - - dataDir = Path.Combine(dataDir, "jellyfin"); + // LocalApplicationData follows the XDG spec on unix machines + dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "jellyfin"); } } + Directory.CreateDirectory(dataDir); + // configDir // IF --configdir // ELSE IF $JELLYFIN_CONFIG_DIR @@ -216,7 +203,6 @@ namespace Jellyfin.Server // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin // ELSE $HOME/.config/jellyfin var configDir = options.ConfigDir; - if (string.IsNullOrEmpty(configDir)) { configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); @@ -300,7 +286,6 @@ namespace Jellyfin.Server // Ensure the main folders exist before we continue try { - Directory.CreateDirectory(dataDir); Directory.CreateDirectory(logDir); Directory.CreateDirectory(configDir); Directory.CreateDirectory(cacheDir); diff --git a/Jellyfin.Server/SocketSharp/HttpFile.cs b/Jellyfin.Server/SocketSharp/HttpFile.cs index 89c75e536..448b666b6 100644 --- a/Jellyfin.Server/SocketSharp/HttpFile.cs +++ b/Jellyfin.Server/SocketSharp/HttpFile.cs @@ -6,9 +6,13 @@ namespace Jellyfin.Server.SocketSharp public class HttpFile : IHttpFile { public string Name { get; set; } + public string FileName { get; set; } + public long ContentLength { get; set; } + public string ContentType { get; set; } + public Stream InputStream { get; set; } } } diff --git a/Jellyfin.Server/SocketSharp/HttpPostedFile.cs b/Jellyfin.Server/SocketSharp/HttpPostedFile.cs new file mode 100644 index 000000000..f38ed848e --- /dev/null +++ b/Jellyfin.Server/SocketSharp/HttpPostedFile.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +public sealed class HttpPostedFile : IDisposable +{ + private string _name; + private string _contentType; + private Stream _stream; + private bool _disposed = false; + + internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) + { + _name = name; + _contentType = content_type; + _stream = new ReadSubStream(base_stream, offset, length); + } + + public string ContentType => _contentType; + + public int ContentLength => (int)_stream.Length; + + public string FileName => _name; + + public Stream InputStream => _stream; + + /// <summary> + /// Releases the unmanaged resources and disposes of the managed resources used. + /// </summary> + public void Dispose() + { + if (_disposed) + { + return; + } + + _stream.Dispose(); + _stream = null; + + _name = null; + _contentType = null; + + _disposed = true; + } + + private class ReadSubStream : Stream + { + private Stream _stream; + private long _offset; + private long _end; + private long _position; + + public ReadSubStream(Stream s, long offset, long length) + { + _stream = s; + _offset = offset; + _end = offset + length; + _position = offset; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int dest_offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (dest_offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "< 0"); + } + + int len = buffer.Length; + if (dest_offset > len) + { + throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset)); + } + + // reordered to avoid possible integer overflow + if (dest_offset > len - count) + { + throw new ArgumentException("Reading would overrun buffer", nameof(count)); + } + + if (count > _end - _position) + { + count = (int)(_end - _position); + } + + if (count <= 0) + { + return 0; + } + + _stream.Position = _position; + int result = _stream.Read(buffer, dest_offset, count); + if (result > 0) + { + _position += result; + } + else + { + _position = _end; + } + + return result; + } + + public override int ReadByte() + { + if (_position >= _end) + { + return -1; + } + + _stream.Position = _position; + int result = _stream.ReadByte(); + if (result < 0) + { + _position = _end; + } + else + { + _position++; + } + + return result; + } + + public override long Seek(long d, SeekOrigin origin) + { + long real; + switch (origin) + { + case SeekOrigin.Begin: + real = _offset + d; + break; + case SeekOrigin.End: + real = _end + d; + break; + case SeekOrigin.Current: + real = _position + d; + break; + default: + throw new ArgumentException("Unknown SeekOrigin value", nameof(origin)); + } + + long virt = real - _offset; + if (virt < 0 || virt > Length) + { + throw new ArgumentException("Invalid position", nameof(d)); + } + + _position = _stream.Seek(real, SeekOrigin.Begin); + return _position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _end - _offset; + + public override long Position + { + get => _position - _offset; + set + { + if (value > Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _position = Seek(value, SeekOrigin.Begin); + } + } + } +} diff --git a/Jellyfin.Server/SocketSharp/RequestMono.cs b/Jellyfin.Server/SocketSharp/RequestMono.cs index 24cf994ea..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) @@ -225,7 +226,7 @@ namespace Jellyfin.Server.SocketSharp if (starts_with) { - return StrUtils.StartsWith(ContentType, ct, true); + return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); } return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); @@ -324,215 +325,6 @@ namespace Jellyfin.Server.SocketSharp return result.ToString(); } } - - public sealed class HttpPostedFile - { - private string name; - private string content_type; - private Stream stream; - - private class ReadSubStream : Stream - { - private Stream s; - private long offset; - private long end; - private long position; - - public ReadSubStream(Stream s, long offset, long length) - { - this.s = s; - this.offset = offset; - this.end = offset + length; - position = offset; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int dest_offset, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (dest_offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0"); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "< 0"); - } - - int len = buffer.Length; - if (dest_offset > len) - { - throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset)); - } - - // reordered to avoid possible integer overflow - if (dest_offset > len - count) - { - throw new ArgumentException("Reading would overrun buffer", nameof(count)); - } - - if (count > end - position) - { - count = (int)(end - position); - } - - if (count <= 0) - { - return 0; - } - - s.Position = position; - int result = s.Read(buffer, dest_offset, count); - if (result > 0) - { - position += result; - } - else - { - position = end; - } - - return result; - } - - public override int ReadByte() - { - if (position >= end) - { - return -1; - } - - s.Position = position; - int result = s.ReadByte(); - if (result < 0) - { - position = end; - } - else - { - position++; - } - - return result; - } - - public override long Seek(long d, SeekOrigin origin) - { - long real; - switch (origin) - { - case SeekOrigin.Begin: - real = offset + d; - break; - case SeekOrigin.End: - real = end + d; - break; - case SeekOrigin.Current: - real = position + d; - break; - default: - throw new ArgumentException("Unknown SeekOrigin value", nameof(origin)); - } - - long virt = real - offset; - if (virt < 0 || virt > Length) - { - throw new ArgumentException("Invalid position", nameof(d)); - } - - position = s.Seek(real, SeekOrigin.Begin); - return position; - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => end - offset; - - public override long Position - { - get => position - offset; - set - { - if (value > Length) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - position = Seek(value, SeekOrigin.Begin); - } - } - } - - internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) - { - this.name = name; - this.content_type = content_type; - this.stream = new ReadSubStream(base_stream, offset, length); - } - - public string ContentType => content_type; - - public int ContentLength => (int)stream.Length; - - public string FileName => name; - - public Stream InputStream => stream; - } - - internal static class StrUtils - { - public static bool StartsWith(string str1, string str2, bool ignore_case) - { - if (string.IsNullOrEmpty(str1)) - { - return false; - } - - var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return str1.IndexOf(str2, comparison) == 0; - } - - public static bool EndsWith(string str1, string str2, bool ignore_case) - { - int l2 = str2.Length; - if (l2 == 0) - { - return true; - } - - int l1 = str1.Length; - if (l2 > l1) - { - return false; - } - - var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return str1.IndexOf(str2, comparison) == str1.Length - str2.Length - 1; - } - } - private class HttpMultipart { @@ -603,17 +395,17 @@ namespace Jellyfin.Server.SocketSharp } var elem = new Element(); - string header; + ReadOnlySpan<char> header; while ((header = ReadHeaders()) != null) { - if (StrUtils.StartsWith(header, "Content-Disposition:", true)) + if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase)) { elem.Name = GetContentDispositionAttribute(header, "name"); elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); } - else if (StrUtils.StartsWith(header, "Content-Type:", true)) + 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); } } @@ -661,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) @@ -670,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; @@ -681,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) @@ -693,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; @@ -704,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--) { @@ -730,13 +522,14 @@ namespace Jellyfin.Server.SocketSharp return false; } - if (!StrUtils.EndsWith(line, boundary, false)) + if (!line.EndsWith(boundary, StringComparison.Ordinal)) { return true; } } catch { + } return false; diff --git a/Jellyfin.Server/SocketSharp/SharpWebSocket.cs b/Jellyfin.Server/SocketSharp/SharpWebSocket.cs index 6eee4cd12..9b0951857 100644 --- a/Jellyfin.Server/SocketSharp/SharpWebSocket.cs +++ b/Jellyfin.Server/SocketSharp/SharpWebSocket.cs @@ -44,10 +44,11 @@ namespace Jellyfin.Server.SocketSharp socket.OnMessage += OnSocketMessage; socket.OnClose += OnSocketClose; socket.OnError += OnSocketError; - - WebSocket.ConnectAsServer(); } + public Task ConnectAsServerAsync() + => WebSocket.ConnectAsServer(); + public Task StartReceive() { return _taskCompletionSource.Task; @@ -133,7 +134,7 @@ namespace Jellyfin.Server.SocketSharp _cancellationTokenSource.Cancel(); - WebSocket.Close(); + WebSocket.CloseAsync().GetAwaiter().GetResult(); } _disposed = true; diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 58c4d38a2..693c2328c 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Server.SocketSharp { if (_listener == null) { - _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _networkManager, _streamHelper, _fileSystem, _environment); + _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); } _listener.EnableDualMode = _enableDualMode; @@ -79,22 +79,14 @@ namespace Jellyfin.Server.SocketSharp _listener.LoadCert(_certificate); } - foreach (var prefix in urlPrefixes) - { - _logger.LogInformation("Adding HttpListener prefix " + prefix); - _listener.Prefixes.Add(prefix); - } + _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); + _listener.Prefixes.AddRange(urlPrefixes); - _listener.OnContext = ProcessContext; + _listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); _listener.Start(); } - private void ProcessContext(HttpListenerContext context) - { - _ = Task.Run(async () => await InitTask(context, _disposeCancellationToken).ConfigureAwait(false)); - } - private static void LogRequest(ILogger logger, HttpListenerRequest request) { var url = request.Url.ToString(); @@ -151,10 +143,7 @@ namespace Jellyfin.Server.SocketSharp Endpoint = endpoint }; - if (WebSocketConnecting != null) - { - WebSocketConnecting(connectingArgs); - } + WebSocketConnecting?.Invoke(connectingArgs); if (connectingArgs.AllowConnection) { @@ -165,6 +154,7 @@ namespace Jellyfin.Server.SocketSharp if (WebSocketConnected != null) { var socket = new SharpWebSocket(webSocketContext.WebSocket, _logger); + await socket.ConnectAsServerAsync().ConfigureAwait(false); WebSocketConnected(new WebSocketConnectEventArgs { @@ -174,33 +164,19 @@ namespace Jellyfin.Server.SocketSharp Endpoint = endpoint }); - await ReceiveWebSocket(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 ReceiveWebSocket(HttpListenerContext ctx, SharpWebSocket socket) - { - try - { - await socket.StartReceive().ConfigureAwait(false); - } - finally - { - TryClose(ctx, 200); + TryClose(ctx, 500); } } @@ -211,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/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs index 56e5c73d6..cf5aee5d4 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs @@ -55,6 +55,41 @@ namespace Jellyfin.Server.SocketSharp public QueryParamCollection Headers => _response.Headers; + private static string AsHeaderValue(Cookie cookie) + { + DateTime defaultExpires = DateTime.MinValue; + + var path = cookie.Expires == defaultExpires + ? "/" + : cookie.Path ?? "/"; + + var sb = new StringBuilder(); + + sb.Append($"{cookie.Name}={cookie.Value};path={path}"); + + if (cookie.Expires != defaultExpires) + { + sb.Append($";expires={cookie.Expires:R}"); + } + + if (!string.IsNullOrEmpty(cookie.Domain)) + { + sb.Append($";domain={cookie.Domain}"); + } + + if (cookie.Secure) + { + sb.Append(";Secure"); + } + + if (cookie.HttpOnly) + { + sb.Append(";HttpOnly"); + } + + return sb.ToString(); + } + public void AddHeader(string name, string value) { if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) @@ -126,41 +161,6 @@ namespace Jellyfin.Server.SocketSharp _response.Headers.Add("Set-Cookie", cookieStr); } - public static string AsHeaderValue(Cookie cookie) - { - var defaultExpires = DateTime.MinValue; - - var path = cookie.Expires == defaultExpires - ? "/" - : cookie.Path ?? "/"; - - var sb = new StringBuilder(); - - sb.Append($"{cookie.Name}={cookie.Value};path={path}"); - - if (cookie.Expires != defaultExpires) - { - sb.Append($";expires={cookie.Expires:R}"); - } - - if (!string.IsNullOrEmpty(cookie.Domain)) - { - sb.Append($";domain={cookie.Domain}"); - } - - if (cookie.Secure) - { - sb.Append(";Secure"); - } - - if (cookie.HttpOnly) - { - sb.Append(";HttpOnly"); - } - - return sb.ToString(); - } - public bool SendChunked { get => _response.SendChunked; 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.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index a037357ed..69673a49c 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -172,16 +172,9 @@ namespace MediaBrowser.Api if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes)) { - if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes)) - { - options.ImageTypes = Array.Empty<ImageType>(); - } - else - { - options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)) - .ToArray(); - } + options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)) + .ToArray(); } } diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 9caf07cea..201efe737 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -180,7 +181,7 @@ namespace MediaBrowser.Api return ToOptimizedResult(filters); } - private QueryFiltersLegacy GetFilters(BaseItem[] items) + private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items) { var result = new QueryFiltersLegacy(); diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 7aeb0e9e8..bf15cc756 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive [Route("/Videos/{Id}/stream.mov", "GET")] [Route("/Videos/{Id}/stream.iso", "GET")] [Route("/Videos/{Id}/stream.flv", "GET")] + [Route("/Videos/{Id}/stream.rm", "GET")] [Route("/Videos/{Id}/stream", "GET")] [Route("/Videos/{Id}/stream.ts", "HEAD")] [Route("/Videos/{Id}/stream.webm", "HEAD")] diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 96b0aa003..3c7ad1d0a 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using MediaBrowser.Controller.Dto; @@ -12,6 +13,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { @@ -196,48 +198,48 @@ namespace MediaBrowser.Api.UserLibrary request.ParentId = null; } - var item = string.IsNullOrEmpty(request.ParentId) ? - null : - _libraryManager.GetItemById(request.ParentId); + BaseItem item = null; - if (item == null) + if (!string.IsNullOrEmpty(request.ParentId)) { - item = string.IsNullOrEmpty(request.ParentId) ? - user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() : - _libraryManager.GetItemById(request.ParentId); + item = _libraryManager.GetItemById(request.ParentId); } - // Default list type = children + if (item == null) + { + item = _libraryManager.GetUserRootFolder(); + } - var folder = item as Folder; + Folder folder = item as Folder; if (folder == null) { - folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); + folder = _libraryManager.GetUserRootFolder(); } var hasCollectionType = folder as IHasCollectionType; - var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)); - - if (isPlaylistQuery) + if (hasCollectionType != null + && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { request.Recursive = true; request.IncludeItemTypes = "Playlist"; } - if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null) + if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id)) { - return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); + Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name); + return new QueryResult<BaseItem> + { + Items = Array.Empty<BaseItem>(), + TotalRecordCount = 0 + }; } - var userRoot = item as UserRootFolder; - - if (userRoot == null) + if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder)) { return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); } var itemsArray = folder.GetChildren(user, true).ToArray(); - return new QueryResult<BaseItem> { Items = itemsArray, diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 72fb6e2b8..34c6f5866 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Net /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns> bool IsInLocalNetwork(string endpoint); - IpAddressInfo[] GetLocalIpAddresses(); + IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface); IpAddressInfo ParseIpAddress(string ipAddress); @@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net Task<IpAddressInfo[]> GetHostAddressesAsync(string host); bool IsAddressInSubnets(string addressString, string[] subnets); + + bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask); + IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address); } } diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index aa99f6b58..cdaf95f5c 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -36,9 +36,7 @@ namespace MediaBrowser.Controller.Dto .ToArray(); public bool ContainsField(ItemFields field) - { - return AllItemFields.Contains(field); - } + => Fields.Contains(field); public DtoOptions(bool allFields) { @@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto EnableUserData = true; AddCurrentProgram = true; - if (allFields) - { - Fields = AllItemFields; - } - else - { - Fields = new ItemFields[] { }; - } - + Fields = allFields ? AllItemFields : Array.Empty<ItemFields>(); ImageTypes = AllImageTypes; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index df5ec5dd0..4b6fd58fe 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto /// <param name="options">The options.</param> /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> - BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null); - - BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); + BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); /// <summary> /// Gets the item by name dto. 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/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8bfadbee6..e49ff20ba 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities { if (query.ItemIds.Length > 0) { - var result = LibraryManager.GetItemsResult(query); - - if (query.OrderBy.Length == 0) - { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); - } - return result; + return LibraryManager.GetItemsResult(query); } return GetItemsInternal(query); } - public BaseItem[] GetItemList(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query) { query.EnableTotalRecordCount = false; if (query.ItemIds.Length > 0) { - var result = LibraryManager.GetItemList(query); - - if (query.OrderBy.Length == 0) - { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); - } - return result.ToArray(); + return LibraryManager.GetItemList(query); } return GetItemsInternal(query).Items; 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/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f5f147db1..e378c2b89 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding { flags.Add("+ignidx"); } - if (state.GenPtsInput) + if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { flags.Add("+genpts"); } 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.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 1a7654bfd..7c330ad86 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images var path = item.ContainingFolderPath; + // Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs... + if (!Directory.Exists(path)) + { + return Array.Empty<FileSystemMetadata>(); + } + if (includeDirectories) { return directoryService.GetFileSystemEntries(path) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 262772959..f725d2c01 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.Collections.ObjectModel; using System.Linq; using System.Text.RegularExpressions; using MediaBrowser.Model.Diagnostics; @@ -58,18 +58,107 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } - output = " " + output + " "; + // The min and max FFmpeg versions required to run jellyfin successfully + var minRequired = new Version(4, 0); + var maxRequired = new Version(4, 0); - for (var i = 2013; i <= 2015; i++) + // Work out what the version under test is + var underTest = GetFFmpegVersion(output); + + if (logOutput) { - var yearString = i.ToString(CultureInfo.InvariantCulture); - if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1) + _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown"); + + if (underTest == null) // Version is unknown + { + if (minRequired.Equals(maxRequired)) + { + _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString()); + } + else + { + _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString()); + } + } + else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend { - return false; + _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString()); } + else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend + { + _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString()); + } + else // Version is ok + { + _logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version"); + } + } + + // underTest shall be null if versions is unknown + return (underTest == null) ? false : (underTest.CompareTo(minRequired) >= 0 && underTest.CompareTo(maxRequired) <= 0); + } + + /// <summary> + /// Using the output from "ffmpeg -version" work out the FFmpeg version. + /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy + /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions. + /// If that fails then we use one of the main libraries to determine if it's new/older than the latest + /// we have stored. + /// </summary> + /// <param name="output"></param> + /// <returns></returns> + static private Version GetFFmpegVersion(string output) + { + // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output + var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)"); + + if (match.Success) + { + return new Version(match.Groups[1].Value); + } + else + { + // Try and use the individual library versions to determine a FFmpeg version + // This lookup table is to be maintained with the following command line: + // $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' + var lut = new ReadOnlyDictionary<Version, string> + (new Dictionary<Version, string> + { + { new Version("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," }, + { new Version("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," }, + { new Version("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," }, + { new Version("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," }, + { new Version("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," }, + { new Version("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," } + }); + + // Create a reduced version string and lookup key from dictionary + var reducedVersion = GetVersionString(output); + + // Try to lookup the string and return Key, otherwise if not found returns null + return lut.FirstOrDefault(x => x.Value == reducedVersion).Key; + } + } + + /// <summary> + /// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output + /// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc." + /// </summary> + /// <param name="output"></param> + /// <returns></returns> + static private string GetVersionString(string output) + { + string pattern = @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))"; + RegexOptions options = RegexOptions.Multiline; + + string rc = null; + + foreach (Match m in Regex.Matches(output, pattern, options)) + { + rc += string.Concat(m.Groups["name"], '=', m.Groups["major"], '.', m.Groups["minor"], ','); } - return true; + return rc; } private static readonly string[] requiredDecoders = new[] diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index d922f1068..7f29c06b4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -694,7 +694,8 @@ namespace MediaBrowser.MediaEncoding.Encoder FileName = FFMpegPath, Arguments = args, IsHidden = true, - ErrorDialog = false + ErrorDialog = false, + EnableRaisingEvents = true }); _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -816,7 +817,8 @@ namespace MediaBrowser.MediaEncoding.Encoder FileName = FFMpegPath, Arguments = args, IsHidden = true, - ErrorDialog = false + ErrorDialog = false, + EnableRaisingEvents = true }); _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index ed5800329..0ba36b4b9 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration public string[] LocalNetworkSubnets { get; set; } public string[] LocalNetworkAddresses { get; set; } public string[] CodecsUsed { get; set; } + public bool IgnoreVirtualInterfaces { get; set; } public bool EnableExternalContentInSuggestions { get; set; } public bool RequireHttps { get; set; } public bool IsBehindProxy { get; set; } @@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration CodecsUsed = Array.Empty<string>(); ImageExtractionTimeoutMs = 0; PathSubstitutions = Array.Empty<PathSubstitution>(); + IgnoreVirtualInterfaces = false; EnableSimpleArtistDetection = true; DisplaySpecialsWithinSeasons = true; diff --git a/MediaBrowser.Model/Net/IpAddressInfo.cs b/MediaBrowser.Model/Net/IpAddressInfo.cs index 7a278d4d4..87fa55bca 100644 --- a/MediaBrowser.Model/Net/IpAddressInfo.cs +++ b/MediaBrowser.Model/Net/IpAddressInfo.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Net public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6); public string Address { get; set; } + public IpAddressInfo SubnetMask { get; set; } public IpAddressFamily AddressFamily { get; set; } public IpAddressInfo(string address, IpAddressFamily addressFamily) diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 23805b79f..27ce23778 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users public UserPolicy() { - EnableContentDeletion = true; + EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty<string>(); EnableSyncTranscoding = true; diff --git a/MediaBrowser.Providers/Manager/GenericPriorityQueue.cs b/MediaBrowser.Providers/Manager/GenericPriorityQueue.cs deleted file mode 100644 index 10ff2515c..000000000 --- a/MediaBrowser.Providers/Manager/GenericPriorityQueue.cs +++ /dev/null @@ -1,402 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -//TODO Fix namespace or replace -namespace Priority_Queue -{ - /// <summary> - /// Credit: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp - /// A copy of StablePriorityQueue which also has generic priority-type - /// </summary> - /// <typeparam name="TItem">The values in the queue. Must extend the GenericPriorityQueue class</typeparam> - /// <typeparam name="TPriority">The priority-type. Must extend IComparable<TPriority></typeparam> - public sealed class GenericPriorityQueue<TItem, TPriority> : IFixedSizePriorityQueue<TItem, TPriority> - where TItem : GenericPriorityQueueNode<TPriority> - where TPriority : IComparable<TPriority> - { - private int _numNodes; - private TItem[] _nodes; - private long _numNodesEverEnqueued; - - /// <summary> - /// Instantiate a new Priority Queue - /// </summary> - /// <param name="maxNodes">The max nodes ever allowed to be enqueued (going over this will cause undefined behavior)</param> - public GenericPriorityQueue(int maxNodes) - { -#if DEBUG - if (maxNodes <= 0) - { - throw new InvalidOperationException("New queue size cannot be smaller than 1"); - } -#endif - - _numNodes = 0; - _nodes = new TItem[maxNodes + 1]; - _numNodesEverEnqueued = 0; - } - - /// <summary> - /// Returns the number of nodes in the queue. - /// O(1) - /// </summary> - public int Count => _numNodes; - - /// <summary> - /// Returns the maximum number of items that can be enqueued at once in this queue. Once you hit this number (ie. once Count == MaxSize), - /// attempting to enqueue another item will cause undefined behavior. O(1) - /// </summary> - public int MaxSize => _nodes.Length - 1; - - /// <summary> - /// Removes every node from the queue. - /// O(n) (So, don't do this often!) - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Clear() - { - Array.Clear(_nodes, 1, _numNodes); - _numNodes = 0; - } - - /// <summary> - /// Returns (in O(1)!) whether the given node is in the queue. O(1) - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Contains(TItem node) - { -#if DEBUG - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - if (node.QueueIndex < 0 || node.QueueIndex >= _nodes.Length) - { - throw new InvalidOperationException("node.QueueIndex has been corrupted. Did you change it manually? Or add this node to another queue?"); - } -#endif - - return (_nodes[node.QueueIndex] == node); - } - - /// <summary> - /// Enqueue a node to the priority queue. Lower values are placed in front. Ties are broken by first-in-first-out. - /// If the queue is full, the result is undefined. - /// If the node is already enqueued, the result is undefined. - /// O(log n) - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Enqueue(TItem node, TPriority priority) - { -#if DEBUG - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - if (_numNodes >= _nodes.Length - 1) - { - throw new InvalidOperationException("Queue is full - node cannot be added: " + node); - } - if (Contains(node)) - { - throw new InvalidOperationException("Node is already enqueued: " + node); - } -#endif - - node.Priority = priority; - _numNodes++; - _nodes[_numNodes] = node; - node.QueueIndex = _numNodes; - node.InsertionIndex = _numNodesEverEnqueued++; - CascadeUp(_nodes[_numNodes]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Swap(TItem node1, TItem node2) - { - //Swap the nodes - _nodes[node1.QueueIndex] = node2; - _nodes[node2.QueueIndex] = node1; - - //Swap their indicies - int temp = node1.QueueIndex; - node1.QueueIndex = node2.QueueIndex; - node2.QueueIndex = temp; - } - - //Performance appears to be slightly better when this is NOT inlined o_O - private void CascadeUp(TItem node) - { - //aka Heapify-up - int parent = node.QueueIndex / 2; - while (parent >= 1) - { - var parentNode = _nodes[parent]; - if (HasHigherPriority(parentNode, node)) - break; - - //Node has lower priority value, so move it up the heap - Swap(node, parentNode); //For some reason, this is faster with Swap() rather than (less..?) individual operations, like in CascadeDown() - - parent = node.QueueIndex / 2; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CascadeDown(TItem node) - { - //aka Heapify-down - TItem newParent; - int finalQueueIndex = node.QueueIndex; - while (true) - { - newParent = node; - int childLeftIndex = 2 * finalQueueIndex; - - //Check if the left-child is higher-priority than the current node - if (childLeftIndex > _numNodes) - { - //This could be placed outside the loop, but then we'd have to check newParent != node twice - node.QueueIndex = finalQueueIndex; - _nodes[finalQueueIndex] = node; - break; - } - - var childLeft = _nodes[childLeftIndex]; - if (HasHigherPriority(childLeft, newParent)) - { - newParent = childLeft; - } - - //Check if the right-child is higher-priority than either the current node or the left child - int childRightIndex = childLeftIndex + 1; - if (childRightIndex <= _numNodes) - { - var childRight = _nodes[childRightIndex]; - if (HasHigherPriority(childRight, newParent)) - { - newParent = childRight; - } - } - - //If either of the children has higher (smaller) priority, swap and continue cascading - if (newParent != node) - { - //Move new parent to its new index. node will be moved once, at the end - //Doing it this way is one less assignment operation than calling Swap() - _nodes[finalQueueIndex] = newParent; - - int temp = newParent.QueueIndex; - newParent.QueueIndex = finalQueueIndex; - finalQueueIndex = temp; - } - else - { - //See note above - node.QueueIndex = finalQueueIndex; - _nodes[finalQueueIndex] = node; - break; - } - } - } - - /// <summary> - /// Returns true if 'higher' has higher priority than 'lower', false otherwise. - /// Note that calling HasHigherPriority(node, node) (ie. both arguments the same node) will return false - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool HasHigherPriority(TItem higher, TItem lower) - { - var cmp = higher.Priority.CompareTo(lower.Priority); - return (cmp < 0 || (cmp == 0 && higher.InsertionIndex < lower.InsertionIndex)); - } - - /// <summary> - /// Removes the head of the queue (node with minimum priority; ties are broken by order of insertion), and returns it. - /// If queue is empty, result is undefined - /// O(log n) - /// </summary> - public bool TryDequeue(out TItem item) - { - if (_numNodes <= 0) - { - item = default(TItem); - return false; - } - -#if DEBUG - - if (!IsValidQueue()) - { - throw new InvalidOperationException("Queue has been corrupted (Did you update a node priority manually instead of calling UpdatePriority()?" + - "Or add the same node to two different queues?)"); - } -#endif - - var returnMe = _nodes[1]; - Remove(returnMe); - item = returnMe; - return true; - } - - /// <summary> - /// Resize the queue so it can accept more nodes. All currently enqueued nodes are remain. - /// Attempting to decrease the queue size to a size too small to hold the existing nodes results in undefined behavior - /// O(n) - /// </summary> - public void Resize(int maxNodes) - { -#if DEBUG - if (maxNodes <= 0) - { - throw new InvalidOperationException("Queue size cannot be smaller than 1"); - } - - if (maxNodes < _numNodes) - { - throw new InvalidOperationException("Called Resize(" + maxNodes + "), but current queue contains " + _numNodes + " nodes"); - } -#endif - - TItem[] newArray = new TItem[maxNodes + 1]; - int highestIndexToCopy = Math.Min(maxNodes, _numNodes); - for (int i = 1; i <= highestIndexToCopy; i++) - { - newArray[i] = _nodes[i]; - } - _nodes = newArray; - } - - /// <summary> - /// Returns the head of the queue, without removing it (use Dequeue() for that). - /// If the queue is empty, behavior is undefined. - /// O(1) - /// </summary> - public TItem First - { - get - { -#if DEBUG - if (_numNodes <= 0) - { - throw new InvalidOperationException("Cannot call .First on an empty queue"); - } -#endif - - return _nodes[1]; - } - } - - /// <summary> - /// This method must be called on a node every time its priority changes while it is in the queue. - /// <b>Forgetting to call this method will result in a corrupted queue!</b> - /// Calling this method on a node not in the queue results in undefined behavior - /// O(log n) - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UpdatePriority(TItem node, TPriority priority) - { -#if DEBUG - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - if (!Contains(node)) - { - throw new InvalidOperationException("Cannot call UpdatePriority() on a node which is not enqueued: " + node); - } -#endif - - node.Priority = priority; - OnNodeUpdated(node); - } - - private void OnNodeUpdated(TItem node) - { - //Bubble the updated node up or down as appropriate - int parentIndex = node.QueueIndex / 2; - var parentNode = _nodes[parentIndex]; - - if (parentIndex > 0 && HasHigherPriority(node, parentNode)) - { - CascadeUp(node); - } - else - { - //Note that CascadeDown will be called if parentNode == node (that is, node is the root) - CascadeDown(node); - } - } - - /// <summary> - /// Removes a node from the queue. The node does not need to be the head of the queue. - /// If the node is not in the queue, the result is undefined. If unsure, check Contains() first - /// O(log n) - /// </summary> - public void Remove(TItem node) - { -#if DEBUG - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - if (!Contains(node)) - { - throw new InvalidOperationException("Cannot call Remove() on a node which is not enqueued: " + node); - } -#endif - - //If the node is already the last node, we can remove it immediately - if (node.QueueIndex == _numNodes) - { - _nodes[_numNodes] = null; - _numNodes--; - return; - } - - //Swap the node with the last node - var formerLastNode = _nodes[_numNodes]; - Swap(node, formerLastNode); - _nodes[_numNodes] = null; - _numNodes--; - - //Now bubble formerLastNode (which is no longer the last node) up or down as appropriate - OnNodeUpdated(formerLastNode); - } - - public IEnumerator<TItem> GetEnumerator() - { - for (int i = 1; i <= _numNodes; i++) - yield return _nodes[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// <summary> - /// <b>Should not be called in production code.</b> - /// Checks to make sure the queue is still in a valid state. Used for testing/debugging the queue. - /// </summary> - public bool IsValidQueue() - { - for (int i = 1; i < _nodes.Length; i++) - { - if (_nodes[i] != null) - { - int childLeftIndex = 2 * i; - if (childLeftIndex < _nodes.Length && _nodes[childLeftIndex] != null && HasHigherPriority(_nodes[childLeftIndex], _nodes[i])) - return false; - - int childRightIndex = childLeftIndex + 1; - if (childRightIndex < _nodes.Length && _nodes[childRightIndex] != null && HasHigherPriority(_nodes[childRightIndex], _nodes[i])) - return false; - } - } - return true; - } - } -} diff --git a/MediaBrowser.Providers/Manager/GenericPriorityQueueNode.cs b/MediaBrowser.Providers/Manager/GenericPriorityQueueNode.cs deleted file mode 100644 index b45ae0fd8..000000000 --- a/MediaBrowser.Providers/Manager/GenericPriorityQueueNode.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Priority_Queue -{ - /// Credit: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp - public class GenericPriorityQueueNode<TPriority> - { - /// <summary> - /// The Priority to insert this node at. Must be set BEFORE adding a node to the queue (ideally just once, in the node's constructor). - /// Should not be manually edited once the node has been enqueued - use queue.UpdatePriority() instead - /// </summary> - public TPriority Priority { get; protected internal set; } - - /// <summary> - /// Represents the current position in the queue - /// </summary> - public int QueueIndex { get; internal set; } - - /// <summary> - /// Represents the order the node was inserted in - /// </summary> - public long InsertionIndex { get; internal set; } - } -} diff --git a/MediaBrowser.Providers/Manager/IFixedSizePriorityQueue.cs b/MediaBrowser.Providers/Manager/IFixedSizePriorityQueue.cs deleted file mode 100644 index 509d98e42..000000000 --- a/MediaBrowser.Providers/Manager/IFixedSizePriorityQueue.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Priority_Queue -{ - /// <summary> - /// Credit: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp - /// A helper-interface only needed to make writing unit tests a bit easier (hence the 'internal' access modifier) - /// </summary> - internal interface IFixedSizePriorityQueue<TItem, in TPriority> : IPriorityQueue<TItem, TPriority> - where TPriority : IComparable<TPriority> - { - /// <summary> - /// Resize the queue so it can accept more nodes. All currently enqueued nodes are remain. - /// Attempting to decrease the queue size to a size too small to hold the existing nodes results in undefined behavior - /// </summary> - void Resize(int maxNodes); - - /// <summary> - /// Returns the maximum number of items that can be enqueued at once in this queue. Once you hit this number (ie. once Count == MaxSize), - /// attempting to enqueue another item will cause undefined behavior. - /// </summary> - int MaxSize { get; } - } -} diff --git a/MediaBrowser.Providers/Manager/IPriorityQueue.cs b/MediaBrowser.Providers/Manager/IPriorityQueue.cs deleted file mode 100644 index dc319a7f8..000000000 --- a/MediaBrowser.Providers/Manager/IPriorityQueue.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Priority_Queue -{ - /// <summary> - /// Credit: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp - /// The IPriorityQueue interface. This is mainly here for purists, and in case I decide to add more implementations later. - /// For speed purposes, it is actually recommended that you *don't* access the priority queue through this interface, since the JIT can - /// (theoretically?) optimize method calls from concrete-types slightly better. - /// </summary> - public interface IPriorityQueue<TItem, in TPriority> : IEnumerable<TItem> - where TPriority : IComparable<TPriority> - { - /// <summary> - /// Enqueue a node to the priority queue. Lower values are placed in front. Ties are broken by first-in-first-out. - /// See implementation for how duplicates are handled. - /// </summary> - void Enqueue(TItem node, TPriority priority); - - /// <summary> - /// Removes the head of the queue (node with minimum priority; ties are broken by order of insertion), and returns it. - /// </summary> - bool TryDequeue(out TItem item); - - /// <summary> - /// Removes every node from the queue. - /// </summary> - void Clear(); - - /// <summary> - /// Returns whether the given node is in the queue. - /// </summary> - bool Contains(TItem node); - - /// <summary> - /// Removes a node from the queue. The node does not need to be the head of the queue. - /// </summary> - void Remove(TItem node); - - /// <summary> - /// Call this method to change the priority of a node. - /// </summary> - void UpdatePriority(TItem node, TPriority priority); - - /// <summary> - /// Returns the head of the queue, without removing it (use Dequeue() for that). - /// </summary> - TItem First { get; } - - /// <summary> - /// Returns the number of nodes in the queue. - /// </summary> - int Count { get; } - } -} diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 77028e526..f0716f201 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { localImagesFailed = true; - if (!(item is IItemByName)) - { - Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name"); - } + Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name"); } var metadataResult = new MetadataResult<TItemType> diff --git a/MediaBrowser.Providers/Manager/SimplePriorityQueue.cs b/MediaBrowser.Providers/Manager/SimplePriorityQueue.cs deleted file mode 100644 index d064312cf..000000000 --- a/MediaBrowser.Providers/Manager/SimplePriorityQueue.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Priority_Queue -{ - /// <summary> - /// Credit: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp - /// A simplified priority queue implementation. Is stable, auto-resizes, and thread-safe, at the cost of being slightly slower than - /// FastPriorityQueue - /// </summary> - /// <typeparam name="TItem">The type to enqueue</typeparam> - /// <typeparam name="TPriority">The priority-type to use for nodes. Must extend IComparable<TPriority></typeparam> - public class SimplePriorityQueue<TItem, TPriority> : IPriorityQueue<TItem, TPriority> - where TPriority : IComparable<TPriority> - { - private class SimpleNode : GenericPriorityQueueNode<TPriority> - { - public TItem Data { get; private set; } - - public SimpleNode(TItem data) - { - Data = data; - } - } - - private const int INITIAL_QUEUE_SIZE = 10; - private readonly GenericPriorityQueue<SimpleNode, TPriority> _queue; - - public SimplePriorityQueue() - { - _queue = new GenericPriorityQueue<SimpleNode, TPriority>(INITIAL_QUEUE_SIZE); - } - - /// <summary> - /// Given an item of type T, returns the exist SimpleNode in the queue - /// </summary> - private SimpleNode GetExistingNode(TItem item) - { - var comparer = EqualityComparer<TItem>.Default; - foreach (var node in _queue) - { - if (comparer.Equals(node.Data, item)) - { - return node; - } - } - throw new InvalidOperationException("Item cannot be found in queue: " + item); - } - - /// <summary> - /// Returns the number of nodes in the queue. - /// O(1) - /// </summary> - public int Count - { - get - { - lock (_queue) - { - return _queue.Count; - } - } - } - - - /// <summary> - /// Returns the head of the queue, without removing it (use Dequeue() for that). - /// Throws an exception when the queue is empty. - /// O(1) - /// </summary> - public TItem First - { - get - { - lock (_queue) - { - if (_queue.Count <= 0) - { - throw new InvalidOperationException("Cannot call .First on an empty queue"); - } - - SimpleNode first = _queue.First; - return (first != null ? first.Data : default(TItem)); - } - } - } - - /// <summary> - /// Removes every node from the queue. - /// O(n) - /// </summary> - public void Clear() - { - lock (_queue) - { - _queue.Clear(); - } - } - - /// <summary> - /// Returns whether the given item is in the queue. - /// O(n) - /// </summary> - public bool Contains(TItem item) - { - lock (_queue) - { - var comparer = EqualityComparer<TItem>.Default; - foreach (var node in _queue) - { - if (comparer.Equals(node.Data, item)) - { - return true; - } - } - return false; - } - } - - /// <summary> - /// Removes the head of the queue (node with minimum priority; ties are broken by order of insertion), and returns it. - /// If queue is empty, throws an exception - /// O(log n) - /// </summary> - public bool TryDequeue(out TItem item) - { - lock (_queue) - { - if (_queue.Count <= 0) - { - item = default(TItem); - return false; - } - - if (_queue.TryDequeue(out SimpleNode node)) - { - item = node.Data; - return true; - } - - item = default(TItem); - return false; - } - } - - /// <summary> - /// Enqueue a node to the priority queue. Lower values are placed in front. Ties are broken by first-in-first-out. - /// This queue automatically resizes itself, so there's no concern of the queue becoming 'full'. - /// Duplicates are allowed. - /// O(log n) - /// </summary> - public void Enqueue(TItem item, TPriority priority) - { - lock (_queue) - { - var node = new SimpleNode(item); - if (_queue.Count == _queue.MaxSize) - { - _queue.Resize(_queue.MaxSize * 2 + 1); - } - _queue.Enqueue(node, priority); - } - } - - /// <summary> - /// Removes an item from the queue. The item does not need to be the head of the queue. - /// If the item is not in the queue, an exception is thrown. If unsure, check Contains() first. - /// If multiple copies of the item are enqueued, only the first one is removed. - /// O(n) - /// </summary> - public void Remove(TItem item) - { - lock (_queue) - { - try - { - _queue.Remove(GetExistingNode(item)); - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Cannot call Remove() on a node which is not enqueued: " + item, ex); - } - } - } - - /// <summary> - /// Call this method to change the priority of an item. - /// Calling this method on a item not in the queue will throw an exception. - /// If the item is enqueued multiple times, only the first one will be updated. - /// (If your requirements are complex enough that you need to enqueue the same item multiple times <i>and</i> be able - /// to update all of them, please wrap your items in a wrapper class so they can be distinguished). - /// O(n) - /// </summary> - public void UpdatePriority(TItem item, TPriority priority) - { - lock (_queue) - { - try - { - SimpleNode updateMe = GetExistingNode(item); - _queue.UpdatePriority(updateMe, priority); - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Cannot call UpdatePriority() on a node which is not enqueued: " + item, ex); - } - } - } - - public IEnumerator<TItem> GetEnumerator() - { - var queueData = new List<TItem>(); - lock (_queue) - { - //Copy to a separate list because we don't want to 'yield return' inside a lock - foreach (var node in _queue) - { - queueData.Add(node.Data); - } - } - - return queueData.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public bool IsValidQueue() - { - lock (_queue) - { - return _queue.IsValidQueue(); - } - } - } - - /// <summary> - /// A simplified priority queue implementation. Is stable, auto-resizes, and thread-safe, at the cost of being slightly slower than - /// FastPriorityQueue - /// This class is kept here for backwards compatibility. It's recommended you use Simple - /// </summary> - /// <typeparam name="TItem">The type to enqueue</typeparam> - public class SimplePriorityQueue<TItem> : SimplePriorityQueue<TItem, float> { } -} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index e6ef889c3..52a52efdc 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> @@ -11,7 +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/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs index ef75f997f..c99d684a1 100644 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ b/RSSDP/ISsdpCommunicationsServer.cs @@ -45,8 +45,8 @@ namespace Rssdp.Infrastructure /// <summary> /// Sends a message to the SSDP multicast address and port. /// </summary> - Task SendMulticastMessage(string message, CancellationToken cancellationToken); - Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken); #endregion @@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure #endregion } -}
\ No newline at end of file +} diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index f06d4687b..456a93aa8 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -3,6 +3,7 @@ <ItemGroup> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> </ItemGroup> <PropertyGroup> diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 04e76ef59..d9a4b6ac0 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Configuration; namespace Rssdp.Infrastructure { @@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure private readonly ILogger _logger; private ISocketFactory _SocketFactory; private readonly INetworkManager _networkManager; + private readonly IServerConfigurationManager _config; private int _LocalPort; private int _MulticastTtl; @@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// </summary> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> - public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) + public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory, + INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) { + _config = config; } /// <summary> @@ -236,15 +240,15 @@ namespace Rssdp.Infrastructure } } - public Task SendMulticastMessage(string message, CancellationToken cancellationToken) + public Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken) { - return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken); + return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken); } /// <summary> /// Sends a message to the SSDP multicast address and port. /// </summary> - public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken) + public async Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken) { if (message == null) throw new ArgumentNullException(nameof(message)); @@ -264,7 +268,7 @@ namespace Rssdp.Infrastructure IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork), Port = SsdpConstants.MulticastPort - }, cancellationToken).ConfigureAwait(false); + }, fromLocalIpAddress, cancellationToken).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false); } @@ -332,14 +336,15 @@ namespace Rssdp.Infrastructure #region Private Methods - private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken) + private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken) { var sockets = _sendSockets; if (sockets != null) { sockets = sockets.ToList(); - var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); + var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress))) + .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -363,11 +368,11 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces)) { if (address.AddressFamily == IpAddressFamily.InterNetworkV6) { - // Not supported ? + // Not support IPv6 right now continue; } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 128bdfcbb..e17e14c1a 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure var message = BuildMessage(header, values); - return _CommunicationsServer.SendMulticastMessage(message, cancellationToken); + return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken); } private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index ce64ba117..921f33c21 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; +using MediaBrowser.Common.Net; using Rssdp; namespace Rssdp.Infrastructure @@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure /// </summary> public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher { + private readonly INetworkManager _networkManager; private ISsdpCommunicationsServer _CommsServer; private string _OSName; private string _OSVersion; + private bool _sendOnlyMatchedHost; private bool _SupportPnpRootDevice; @@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure /// <summary> /// Default constructor. /// </summary> - public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion) + public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager, + string osName, string osVersion, bool sendOnlyMatchedHost) { if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer)); + if (networkManager == null) throw new ArgumentNullException(nameof(networkManager)); if (osName == null) throw new ArgumentNullException(nameof(osName)); if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName)); if (osVersion == null) throw new ArgumentNullException(nameof(osVersion)); @@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase); _Random = new Random(); + _networkManager = networkManager; _CommsServer = communicationsServer; _CommsServer.RequestReceived += CommsServer_RequestReceived; _OSName = osName; _OSVersion = osVersion; + _sendOnlyMatchedHost = sendOnlyMatchedHost; _CommsServer.BeginListeningForBroadcasts(); } @@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); + if (!_sendOnlyMatchedHost || + _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.IpAddress, device.ToRootDevice().SubnetMask)) + { + SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); + } } } else @@ -427,7 +438,7 @@ namespace Rssdp.Infrastructure var message = BuildMessage(header, values); - _CommsServer.SendMulticastMessage(message, cancellationToken); + _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken); //WriteTrace(String.Format("Sent alive notification"), device); } @@ -472,7 +483,7 @@ namespace Rssdp.Infrastructure var sendCount = IsDisposed ? 1 : 3; WriteTrace(String.Format("Sent byebye notification"), device); - return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken); + return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); } private void DisposeRebroadcastTimer() diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs index a2b0f60f5..d918b9040 100644 --- a/RSSDP/SsdpRootDevice.cs +++ b/RSSDP/SsdpRootDevice.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using System.Xml; using Rssdp.Infrastructure; +using MediaBrowser.Model.Net; namespace Rssdp { @@ -52,6 +53,15 @@ namespace Rssdp /// </summary> public Uri Location { get; set; } + /// <summary> + /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required. + /// </summary> + public IpAddressInfo Address { get; set; } + + /// <summary> + /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required. + /// </summary> + public IpAddressInfo SubnetMask { get; set; } /// <summary> /// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional. diff --git a/SharedVersion.cs b/SharedVersion.cs index 294748b77..785ba9301 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.2")] +[assembly: AssemblyFileVersion("10.2.2")] diff --git a/SocketHttpListener/Ext.cs b/SocketHttpListener/Ext.cs index a02b48061..2b3c67071 100644 --- a/SocketHttpListener/Ext.cs +++ b/SocketHttpListener/Ext.cs @@ -74,18 +74,20 @@ namespace SocketHttpListener } } - private static byte[] readBytes(this Stream stream, byte[] buffer, int offset, int length) + private static async Task<byte[]> ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length) { - var len = stream.Read(buffer, offset, length); + var len = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false); if (len < 1) return buffer.SubArray(0, offset); var tmp = 0; while (len < length) { - tmp = stream.Read(buffer, offset + len, length - len); + tmp = await stream.ReadAsync(buffer, offset + len, length - len).ConfigureAwait(false); if (tmp < 1) + { break; + } len += tmp; } @@ -95,10 +97,9 @@ namespace SocketHttpListener : buffer; } - private static bool readBytes( - this Stream stream, byte[] buffer, int offset, int length, Stream dest) + private static async Task<bool> ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length, Stream dest) { - var bytes = stream.readBytes(buffer, offset, length); + var bytes = await stream.ReadBytesAsync(buffer, offset, length).ConfigureAwait(false); var len = bytes.Length; dest.Write(bytes, 0, len); @@ -109,16 +110,16 @@ namespace SocketHttpListener #region Internal Methods - internal static byte[] Append(this ushort code, string reason) + internal static async Task<byte[]> AppendAsync(this ushort code, string reason) { using (var buffer = new MemoryStream()) { var tmp = code.ToByteArrayInternally(ByteOrder.Big); - buffer.Write(tmp, 0, 2); + await buffer.WriteAsync(tmp, 0, 2).ConfigureAwait(false); if (reason != null && reason.Length > 0) { tmp = Encoding.UTF8.GetBytes(reason); - buffer.Write(tmp, 0, tmp.Length); + await buffer.WriteAsync(tmp, 0, tmp.Length).ConfigureAwait(false); } return buffer.ToArray(); @@ -331,12 +332,10 @@ namespace SocketHttpListener : string.Format("\"{0}\"", value.Replace("\"", "\\\"")); } - internal static byte[] ReadBytes(this Stream stream, int length) - { - return stream.readBytes(new byte[length], 0, length); - } + internal static Task<byte[]> ReadBytesAsync(this Stream stream, int length) + => stream.ReadBytesAsync(new byte[length], 0, length); - internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength) + internal static async Task<byte[]> ReadBytesAsync(this Stream stream, long length, int bufferLength) { using (var result = new MemoryStream()) { @@ -347,7 +346,7 @@ namespace SocketHttpListener var end = false; for (long i = 0; i < count; i++) { - if (!stream.readBytes(buffer, 0, bufferLength, result)) + if (!await stream.ReadBytesAsync(buffer, 0, bufferLength, result).ConfigureAwait(false)) { end = true; break; @@ -355,26 +354,14 @@ namespace SocketHttpListener } if (!end && rem > 0) - stream.readBytes(new byte[rem], 0, rem, result); + { + await stream.ReadBytesAsync(new byte[rem], 0, rem, result).ConfigureAwait(false); + } return result.ToArray(); } } - internal static async Task<byte[]> ReadBytesAsync(this Stream stream, int length) - { - var buffer = new byte[length]; - - var len = await stream.ReadAsync(buffer, 0, length).ConfigureAwait(false); - var bytes = len < 1 - ? new byte[0] - : len < length - ? stream.readBytes(buffer, len, length - len) - : buffer; - - return bytes; - } - internal static string RemovePrefix(this string value, params string[] prefixes) { var i = 0; @@ -493,19 +480,16 @@ namespace SocketHttpListener return string.Format("{0}; {1}", m, parameters.ToString("; ")); } - internal static List<TSource> ToList<TSource>(this IEnumerable<TSource> source) - { - return new List<TSource>(source); - } - internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) { - return BitConverter.ToUInt16(src.ToHostOrder(srcOrder), 0); + src.ToHostOrder(srcOrder); + return BitConverter.ToUInt16(src, 0); } internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) { - return BitConverter.ToUInt64(src.ToHostOrder(srcOrder), 0); + src.ToHostOrder(srcOrder); + return BitConverter.ToUInt64(src, 0); } internal static string TrimEndSlash(this string value) @@ -852,14 +836,17 @@ namespace SocketHttpListener /// <exception cref="ArgumentNullException"> /// <paramref name="src"/> is <see langword="null"/>. /// </exception> - public static byte[] ToHostOrder(this byte[] src, ByteOrder srcOrder) + public static void ToHostOrder(this byte[] src, ByteOrder srcOrder) { if (src == null) + { throw new ArgumentNullException(nameof(src)); + } - return src.Length > 1 && !srcOrder.IsHostOrder() - ? src.Reverse() - : src; + if (src.Length > 1 && !srcOrder.IsHostOrder()) + { + Array.Reverse(src); + } } /// <summary> diff --git a/SocketHttpListener/Net/HttpListener.cs b/SocketHttpListener/Net/HttpListener.cs index b80180679..f17036a21 100644 --- a/SocketHttpListener/Net/HttpListener.cs +++ b/SocketHttpListener/Net/HttpListener.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Net; using System.Security.Cryptography.X509Certificates; -using MediaBrowser.Common.Net; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -18,47 +17,55 @@ namespace SocketHttpListener.Net internal ISocketFactory SocketFactory { get; private set; } internal IFileSystem FileSystem { get; private set; } internal IStreamHelper StreamHelper { get; private set; } - internal INetworkManager NetworkManager { get; private set; } internal IEnvironmentInfo EnvironmentInfo { get; private set; } public bool EnableDualMode { get; set; } - AuthenticationSchemes auth_schemes; - HttpListenerPrefixCollection prefixes; - AuthenticationSchemeSelector auth_selector; - string realm; - bool unsafe_ntlm_auth; - bool listening; - bool disposed; + private AuthenticationSchemes auth_schemes; + private HttpListenerPrefixCollection prefixes; + private AuthenticationSchemeSelector auth_selector; + private string realm; + private bool unsafe_ntlm_auth; + private bool listening; + private bool disposed; - Dictionary<HttpListenerContext, HttpListenerContext> registry; // Dictionary<HttpListenerContext,HttpListenerContext> - Dictionary<HttpConnection, HttpConnection> connections; + private Dictionary<HttpListenerContext, HttpListenerContext> registry; + private Dictionary<HttpConnection, HttpConnection> connections; private ILogger _logger; private X509Certificate _certificate; public Action<HttpListenerContext> OnContext { get; set; } - public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, ISocketFactory socketFactory, - INetworkManager networkManager, IStreamHelper streamHelper, IFileSystem fileSystem, + public HttpListener( + ILogger logger, + ICryptoProvider cryptoProvider, + ISocketFactory socketFactory, + IStreamHelper streamHelper, + IFileSystem fileSystem, IEnvironmentInfo environmentInfo) { _logger = logger; CryptoProvider = cryptoProvider; SocketFactory = socketFactory; - NetworkManager = networkManager; StreamHelper = streamHelper; FileSystem = fileSystem; EnvironmentInfo = environmentInfo; + prefixes = new HttpListenerPrefixCollection(logger, this); registry = new Dictionary<HttpListenerContext, HttpListenerContext>(); connections = new Dictionary<HttpConnection, HttpConnection>(); auth_schemes = AuthenticationSchemes.Anonymous; } - public HttpListener(ILogger logger, X509Certificate certificate, ICryptoProvider cryptoProvider, - ISocketFactory socketFactory, INetworkManager networkManager, IStreamHelper streamHelper, - IFileSystem fileSystem, IEnvironmentInfo environmentInfo) - : this(logger, cryptoProvider, socketFactory, networkManager, streamHelper, fileSystem, environmentInfo) + public HttpListener( + ILogger logger, + X509Certificate certificate, + ICryptoProvider cryptoProvider, + ISocketFactory socketFactory, + IStreamHelper streamHelper, + IFileSystem fileSystem, + IEnvironmentInfo environmentInfo) + : this(logger, cryptoProvider, socketFactory, streamHelper, fileSystem, environmentInfo) { _certificate = certificate; } diff --git a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener/Net/HttpListenerPrefixCollection.cs index 97dc6797c..400a1adb6 100644 --- a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs +++ b/SocketHttpListener/Net/HttpListenerPrefixCollection.cs @@ -7,18 +7,18 @@ namespace SocketHttpListener.Net { public class HttpListenerPrefixCollection : ICollection<string>, IEnumerable<string>, IEnumerable { - List<string> prefixes = new List<string>(); - HttpListener listener; + private List<string> _prefixes = new List<string>(); + private HttpListener _listener; private ILogger _logger; internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener) { _logger = logger; - this.listener = listener; + _listener = listener; } - public int Count => prefixes.Count; + public int Count => _prefixes.Count; public bool IsReadOnly => false; @@ -26,61 +26,90 @@ namespace SocketHttpListener.Net public void Add(string uriPrefix) { - listener.CheckDisposed(); + _listener.CheckDisposed(); //ListenerPrefix.CheckUri(uriPrefix); - if (prefixes.Contains(uriPrefix)) + if (_prefixes.Contains(uriPrefix)) + { return; + } - prefixes.Add(uriPrefix); - if (listener.IsListening) - HttpEndPointManager.AddPrefix(_logger, uriPrefix, listener); + _prefixes.Add(uriPrefix); + if (_listener.IsListening) + { + HttpEndPointManager.AddPrefix(_logger, uriPrefix, _listener); + } + } + + public void AddRange(IEnumerable<string> uriPrefixes) + { + _listener.CheckDisposed(); + + foreach (var uriPrefix in uriPrefixes) + { + if (_prefixes.Contains(uriPrefix)) + { + continue; + } + + _prefixes.Add(uriPrefix); + if (_listener.IsListening) + { + HttpEndPointManager.AddPrefix(_logger, uriPrefix, _listener); + } + } } public void Clear() { - listener.CheckDisposed(); - prefixes.Clear(); - if (listener.IsListening) - HttpEndPointManager.RemoveListener(_logger, listener); + _listener.CheckDisposed(); + _prefixes.Clear(); + if (_listener.IsListening) + { + HttpEndPointManager.RemoveListener(_logger, _listener); + } } public bool Contains(string uriPrefix) { - listener.CheckDisposed(); - return prefixes.Contains(uriPrefix); + _listener.CheckDisposed(); + return _prefixes.Contains(uriPrefix); } public void CopyTo(string[] array, int offset) { - listener.CheckDisposed(); - prefixes.CopyTo(array, offset); + _listener.CheckDisposed(); + _prefixes.CopyTo(array, offset); } public void CopyTo(Array array, int offset) { - listener.CheckDisposed(); - ((ICollection)prefixes).CopyTo(array, offset); + _listener.CheckDisposed(); + ((ICollection)_prefixes).CopyTo(array, offset); } public IEnumerator<string> GetEnumerator() { - return prefixes.GetEnumerator(); + return _prefixes.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { - return prefixes.GetEnumerator(); + return _prefixes.GetEnumerator(); } public bool Remove(string uriPrefix) { - listener.CheckDisposed(); + _listener.CheckDisposed(); if (uriPrefix == null) + { throw new ArgumentNullException(nameof(uriPrefix)); + } - bool result = prefixes.Remove(uriPrefix); - if (result && listener.IsListening) - HttpEndPointManager.RemovePrefix(_logger, uriPrefix, listener); + bool result = _prefixes.Remove(uriPrefix); + if (result && _listener.IsListening) + { + HttpEndPointManager.RemovePrefix(_logger, uriPrefix, _listener); + } return result; } diff --git a/SocketHttpListener/WebSocket.cs b/SocketHttpListener/WebSocket.cs index 128bc8b97..0dcb6a64b 100644 --- a/SocketHttpListener/WebSocket.cs +++ b/SocketHttpListener/WebSocket.cs @@ -30,9 +30,9 @@ namespace SocketHttpListener private CookieCollection _cookies; private AutoResetEvent _exitReceiving; private object _forConn; - private object _forEvent; + private readonly SemaphoreSlim _forEvent = new SemaphoreSlim(1, 1); private object _forMessageEventQueue; - private object _forSend; + private readonly SemaphoreSlim _forSend = new SemaphoreSlim(1, 1); private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private Queue<MessageEventArgs> _messageEventQueue; private string _protocol; @@ -109,12 +109,15 @@ namespace SocketHttpListener #region Private Methods - private void close(CloseStatusCode code, string reason, bool wait) + private async Task CloseAsync(CloseStatusCode code, string reason, bool wait) { - close(new PayloadData(((ushort)code).Append(reason)), !code.IsReserved(), wait); + await CloseAsync(new PayloadData( + await ((ushort)code).AppendAsync(reason).ConfigureAwait(false)), + !code.IsReserved(), + wait).ConfigureAwait(false); } - private void close(PayloadData payload, bool send, bool wait) + private async Task CloseAsync(PayloadData payload, bool send, bool wait) { lock (_forConn) { @@ -126,11 +129,12 @@ namespace SocketHttpListener _readyState = WebSocketState.CloseSent; } - var e = new CloseEventArgs(payload); - e.WasClean = - closeHandshake( + var e = new CloseEventArgs(payload) + { + WasClean = await CloseHandshakeAsync( send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null, - wait ? 1000 : 0); + wait ? 1000 : 0).ConfigureAwait(false) + }; _readyState = WebSocketState.Closed; try @@ -143,9 +147,9 @@ namespace SocketHttpListener } } - private bool closeHandshake(byte[] frameAsBytes, int millisecondsTimeout) + private async Task<bool> CloseHandshakeAsync(byte[] frameAsBytes, int millisecondsTimeout) { - var sent = frameAsBytes != null && writeBytes(frameAsBytes); + var sent = frameAsBytes != null && await WriteBytesAsync(frameAsBytes).ConfigureAwait(false); var received = millisecondsTimeout == 0 || (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout)); @@ -189,11 +193,11 @@ namespace SocketHttpListener _context = null; } - private bool concatenateFragmentsInto(Stream dest) + private async Task<bool> ConcatenateFragmentsIntoAsync(Stream dest) { while (true) { - var frame = WebSocketFrame.Read(_stream, true); + var frame = await WebSocketFrame.ReadAsync(_stream, true).ConfigureAwait(false); if (frame.IsFinal) { /* FINAL */ @@ -221,7 +225,7 @@ namespace SocketHttpListener // CLOSE if (frame.IsClose) - return processCloseFrame(frame); + return await ProcessCloseFrameAsync(frame).ConfigureAwait(false); } else { @@ -236,10 +240,10 @@ namespace SocketHttpListener } // ? - return processUnsupportedFrame( + return await ProcessUnsupportedFrameAsync( frame, CloseStatusCode.IncorrectData, - "An incorrect data has been received while receiving fragmented data."); + "An incorrect data has been received while receiving fragmented data.").ConfigureAwait(false); } return true; @@ -299,44 +303,42 @@ namespace SocketHttpListener _compression = CompressionMethod.None; _cookies = new CookieCollection(); _forConn = new object(); - _forEvent = new object(); - _forSend = new object(); _messageEventQueue = new Queue<MessageEventArgs>(); _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; _readyState = WebSocketState.Connecting; } - private void open() + private async Task OpenAsync() { try { startReceiving(); - lock (_forEvent) - { - try - { - if (OnOpen != null) - { - OnOpen(this, EventArgs.Empty); - } - } - catch (Exception ex) - { - processException(ex, "An exception has occurred while OnOpen."); - } - } } catch (Exception ex) { - processException(ex, "An exception has occurred while opening."); + await ProcessExceptionAsync(ex, "An exception has occurred while opening.").ConfigureAwait(false); + } + + await _forEvent.WaitAsync().ConfigureAwait(false); + try + { + OnOpen?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + await ProcessExceptionAsync(ex, "An exception has occurred while OnOpen.").ConfigureAwait(false); + } + finally + { + _forEvent.Release(); } } - private bool processCloseFrame(WebSocketFrame frame) + private async Task<bool> ProcessCloseFrameAsync(WebSocketFrame frame) { var payload = frame.PayloadData; - close(payload, !payload.ContainsReservedCloseStatusCode, false); + await CloseAsync(payload, !payload.ContainsReservedCloseStatusCode, false).ConfigureAwait(false); return false; } @@ -352,7 +354,7 @@ namespace SocketHttpListener return true; } - private void processException(Exception exception, string message) + private async Task ProcessExceptionAsync(Exception exception, string message) { var code = CloseStatusCode.Abnormal; var reason = message; @@ -365,25 +367,31 @@ namespace SocketHttpListener error(message ?? code.GetMessage(), exception); if (_readyState == WebSocketState.Connecting) - Close(HttpStatusCode.BadRequest); + { + await CloseAsync(HttpStatusCode.BadRequest).ConfigureAwait(false); + } else - close(code, reason ?? code.GetMessage(), false); + { + await CloseAsync(code, reason ?? code.GetMessage(), false).ConfigureAwait(false); + } } - private bool processFragmentedFrame(WebSocketFrame frame) + private Task<bool> ProcessFragmentedFrameAsync(WebSocketFrame frame) { return frame.IsContinuation // Not first fragment - ? true - : processFragments(frame); + ? Task.FromResult(true) + : ProcessFragmentsAsync(frame); } - private bool processFragments(WebSocketFrame first) + private async Task<bool> ProcessFragmentsAsync(WebSocketFrame first) { using (var buff = new MemoryStream()) { buff.WriteBytes(first.PayloadData.ApplicationData); - if (!concatenateFragmentsInto(buff)) + if (!await ConcatenateFragmentsIntoAsync(buff).ConfigureAwait(false)) + { return false; + } byte[] data; if (_compression != CompressionMethod.None) @@ -412,36 +420,38 @@ namespace SocketHttpListener return true; } - private bool processUnsupportedFrame(WebSocketFrame frame, CloseStatusCode code, string reason) + private async Task<bool> ProcessUnsupportedFrameAsync(WebSocketFrame frame, CloseStatusCode code, string reason) { - processException(new WebSocketException(code, reason), null); + await ProcessExceptionAsync(new WebSocketException(code, reason), null).ConfigureAwait(false); return false; } - private bool processWebSocketFrame(WebSocketFrame frame) + private Task<bool> ProcessWebSocketFrameAsync(WebSocketFrame frame) { + // TODO: @bond change to if/else chain return frame.IsCompressed && _compression == CompressionMethod.None - ? processUnsupportedFrame( + ? ProcessUnsupportedFrameAsync( frame, CloseStatusCode.IncorrectData, "A compressed data has been received without available decompression method.") : frame.IsFragmented - ? processFragmentedFrame(frame) + ? ProcessFragmentedFrameAsync(frame) : frame.IsData - ? processDataFrame(frame) + ? Task.FromResult(processDataFrame(frame)) : frame.IsPing - ? processPingFrame(frame) + ? Task.FromResult(processPingFrame(frame)) : frame.IsPong - ? processPongFrame(frame) + ? Task.FromResult(processPongFrame(frame)) : frame.IsClose - ? processCloseFrame(frame) - : processUnsupportedFrame(frame, CloseStatusCode.PolicyViolation, null); + ? ProcessCloseFrameAsync(frame) + : ProcessUnsupportedFrameAsync(frame, CloseStatusCode.PolicyViolation, null); } - private bool send(Opcode opcode, Stream stream) + private async Task<bool> SendAsync(Opcode opcode, Stream stream) { - lock (_forSend) + await _forSend.WaitAsync().ConfigureAwait(false); + try { var src = stream; var compressed = false; @@ -454,7 +464,7 @@ namespace SocketHttpListener compressed = true; } - sent = send(opcode, Mask.Unmask, stream, compressed); + sent = await SendAsync(opcode, Mask.Unmask, stream, compressed).ConfigureAwait(false); if (!sent) error("Sending a data has been interrupted."); } @@ -472,16 +482,20 @@ namespace SocketHttpListener return sent; } + finally + { + _forSend.Release(); + } } - private bool send(Opcode opcode, Mask mask, Stream stream, bool compressed) + private async Task<bool> SendAsync(Opcode opcode, Mask mask, Stream stream, bool compressed) { var len = stream.Length; /* Not fragmented */ if (len == 0) - return send(Fin.Final, opcode, mask, new byte[0], compressed); + return await SendAsync(Fin.Final, opcode, mask, new byte[0], compressed).ConfigureAwait(false); var quo = len / FragmentLength; var rem = (int)(len % FragmentLength); @@ -490,26 +504,26 @@ namespace SocketHttpListener if (quo == 0) { buff = new byte[rem]; - return stream.Read(buff, 0, rem) == rem && - send(Fin.Final, opcode, mask, buff, compressed); + return await stream.ReadAsync(buff, 0, rem).ConfigureAwait(false) == rem && + await SendAsync(Fin.Final, opcode, mask, buff, compressed).ConfigureAwait(false); } buff = new byte[FragmentLength]; if (quo == 1 && rem == 0) - return stream.Read(buff, 0, FragmentLength) == FragmentLength && - send(Fin.Final, opcode, mask, buff, compressed); + return await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) == FragmentLength && + await SendAsync(Fin.Final, opcode, mask, buff, compressed).ConfigureAwait(false); /* Send fragmented */ // Begin - if (stream.Read(buff, 0, FragmentLength) != FragmentLength || - !send(Fin.More, opcode, mask, buff, compressed)) + if (await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) != FragmentLength || + !await SendAsync(Fin.More, opcode, mask, buff, compressed).ConfigureAwait(false)) return false; var n = rem == 0 ? quo - 2 : quo - 1; for (long i = 0; i < n; i++) - if (stream.Read(buff, 0, FragmentLength) != FragmentLength || - !send(Fin.More, Opcode.Cont, mask, buff, compressed)) + if (await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) != FragmentLength || + !await SendAsync(Fin.More, Opcode.Cont, mask, buff, compressed).ConfigureAwait(false)) return false; // End @@ -518,98 +532,88 @@ namespace SocketHttpListener else buff = new byte[rem]; - return stream.Read(buff, 0, rem) == rem && - send(Fin.Final, Opcode.Cont, mask, buff, compressed); + return await stream.ReadAsync(buff, 0, rem).ConfigureAwait(false) == rem && + await SendAsync(Fin.Final, Opcode.Cont, mask, buff, compressed).ConfigureAwait(false); } - private bool send(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + private Task<bool> SendAsync(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) { lock (_forConn) { if (_readyState != WebSocketState.Open) { - return false; + return Task.FromResult(false); } - return writeBytes( + return WriteBytesAsync( WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray()); } } - private Task sendAsync(Opcode opcode, Stream stream) - { - var completionSource = new TaskCompletionSource<bool>(); - Task.Run(() => - { - try - { - send(opcode, stream); - completionSource.TrySetResult(true); - } - catch (Exception ex) - { - completionSource.TrySetException(ex); - } - }); - return completionSource.Task; - } - // As server - private bool sendHttpResponse(HttpResponse response) - { - return writeBytes(response.ToByteArray()); - } + private Task<bool> SendHttpResponseAsync(HttpResponse response) + => WriteBytesAsync(response.ToByteArray()); private void startReceiving() { if (_messageEventQueue.Count > 0) + { _messageEventQueue.Clear(); + } _exitReceiving = new AutoResetEvent(false); _receivePong = new AutoResetEvent(false); Action receive = null; - receive = () => WebSocketFrame.ReadAsync( - _stream, - true, - frame => - { - if (processWebSocketFrame(frame) && _readyState != WebSocketState.Closed) - { - receive(); - - if (!frame.IsData) - return; - - lock (_forEvent) - { - try - { - var e = dequeueFromMessageEventQueue(); - if (e != null && _readyState == WebSocketState.Open) - OnMessage.Emit(this, e); - } - catch (Exception ex) - { - processException(ex, "An exception has occurred while OnMessage."); - } - } - } - else if (_exitReceiving != null) - { - _exitReceiving.Set(); - } - }, - ex => processException(ex, "An exception has occurred while receiving a message.")); + receive = async () => await WebSocketFrame.ReadAsync( + _stream, + true, + async frame => + { + if (await ProcessWebSocketFrameAsync(frame).ConfigureAwait(false) && _readyState != WebSocketState.Closed) + { + receive(); + + if (!frame.IsData) + { + return; + } + + await _forEvent.WaitAsync().ConfigureAwait(false); + + try + { + var e = dequeueFromMessageEventQueue(); + if (e != null && _readyState == WebSocketState.Open) + { + OnMessage.Emit(this, e); + } + } + catch (Exception ex) + { + await ProcessExceptionAsync(ex, "An exception has occurred while OnMessage.").ConfigureAwait(false); + } + finally + { + _forEvent.Release(); + } + + } + else if (_exitReceiving != null) + { + _exitReceiving.Set(); + } + }, + async ex => await ProcessExceptionAsync(ex, "An exception has occurred while receiving a message.")).ConfigureAwait(false); receive(); } - private bool writeBytes(byte[] data) + private async Task<bool> WriteBytesAsync(byte[] data) { try { - _stream.Write(data, 0, data.Length); + await _stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); return true; } catch (Exception) @@ -623,10 +627,10 @@ namespace SocketHttpListener #region Internal Methods // As server - internal void Close(HttpResponse response) + internal async Task CloseAsync(HttpResponse response) { _readyState = WebSocketState.CloseSent; - sendHttpResponse(response); + await SendHttpResponseAsync(response).ConfigureAwait(false); closeServerResources(); @@ -634,22 +638,20 @@ namespace SocketHttpListener } // As server - internal void Close(HttpStatusCode code) - { - Close(createHandshakeCloseResponse(code)); - } + internal Task CloseAsync(HttpStatusCode code) + => CloseAsync(createHandshakeCloseResponse(code)); // As server - public void ConnectAsServer() + public async Task ConnectAsServer() { try { _readyState = WebSocketState.Open; - open(); + await OpenAsync().ConfigureAwait(false); } catch (Exception ex) { - processException(ex, "An exception has occurred while connecting."); + await ProcessExceptionAsync(ex, "An exception has occurred while connecting.").ConfigureAwait(false); } } @@ -660,18 +662,18 @@ namespace SocketHttpListener /// <summary> /// Closes the WebSocket connection, and releases all associated resources. /// </summary> - public void Close() + public Task CloseAsync() { var msg = _readyState.CheckIfClosable(); if (msg != null) { error(msg); - return; + return Task.CompletedTask; } var send = _readyState == WebSocketState.Open; - close(new PayloadData(), send, send); + return CloseAsync(new PayloadData(), send, send); } /// <summary> @@ -689,11 +691,11 @@ namespace SocketHttpListener /// <param name="reason"> /// A <see cref="string"/> that represents the reason for the close. /// </param> - public void Close(CloseStatusCode code, string reason) + public async Task CloseAsync(CloseStatusCode code, string reason) { byte[] data = null; var msg = _readyState.CheckIfClosable() ?? - (data = ((ushort)code).Append(reason)).CheckIfValidControlData("reason"); + (data = await ((ushort)code).AppendAsync(reason).ConfigureAwait(false)).CheckIfValidControlData("reason"); if (msg != null) { @@ -703,7 +705,7 @@ namespace SocketHttpListener } var send = _readyState == WebSocketState.Open && !code.IsReserved(); - close(new PayloadData(data), send, send); + await CloseAsync(new PayloadData(data), send, send).ConfigureAwait(false); } /// <summary> @@ -728,7 +730,7 @@ namespace SocketHttpListener throw new Exception(msg); } - return sendAsync(Opcode.Binary, new MemoryStream(data)); + return SendAsync(Opcode.Binary, new MemoryStream(data)); } /// <summary> @@ -753,7 +755,7 @@ namespace SocketHttpListener throw new Exception(msg); } - return sendAsync(Opcode.Text, new MemoryStream(Encoding.UTF8.GetBytes(data))); + return SendAsync(Opcode.Text, new MemoryStream(Encoding.UTF8.GetBytes(data))); } #endregion @@ -768,7 +770,7 @@ namespace SocketHttpListener /// </remarks> void IDisposable.Dispose() { - Close(CloseStatusCode.Away, null); + CloseAsync(CloseStatusCode.Away, null).GetAwaiter().GetResult(); } #endregion diff --git a/SocketHttpListener/WebSocketFrame.cs b/SocketHttpListener/WebSocketFrame.cs index 74ed23c45..8ec64026b 100644 --- a/SocketHttpListener/WebSocketFrame.cs +++ b/SocketHttpListener/WebSocketFrame.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; namespace SocketHttpListener { @@ -177,7 +178,7 @@ namespace SocketHttpListener return opcode == Opcode.Text || opcode == Opcode.Binary; } - private static WebSocketFrame read(byte[] header, Stream stream, bool unmask) + private static async Task<WebSocketFrame> ReadAsync(byte[] header, Stream stream, bool unmask) { /* Header */ @@ -229,7 +230,7 @@ namespace SocketHttpListener ? 2 : 8; - var extPayloadLen = size > 0 ? stream.ReadBytes(size) : new byte[0]; + var extPayloadLen = size > 0 ? await stream.ReadBytesAsync(size).ConfigureAwait(false) : Array.Empty<byte>(); if (size > 0 && extPayloadLen.Length != size) throw new WebSocketException( "The 'Extended Payload Length' of a frame cannot be read from the data source."); @@ -239,7 +240,7 @@ namespace SocketHttpListener /* Masking Key */ var masked = mask == Mask.Mask; - var maskingKey = masked ? stream.ReadBytes(4) : new byte[0]; + var maskingKey = masked ? await stream.ReadBytesAsync(4).ConfigureAwait(false) : Array.Empty<byte>(); if (masked && maskingKey.Length != 4) throw new WebSocketException( "The 'Masking Key' of a frame cannot be read from the data source."); @@ -264,8 +265,8 @@ namespace SocketHttpListener "The length of 'Payload Data' of a frame is greater than the allowable length."); data = payloadLen > 126 - ? stream.ReadBytes((long)len, 1024) - : stream.ReadBytes((int)len); + ? await stream.ReadBytesAsync((long)len, 1024).ConfigureAwait(false) + : await stream.ReadBytesAsync((int)len).ConfigureAwait(false); //if (data.LongLength != (long)len) // throw new WebSocketException( @@ -273,7 +274,7 @@ namespace SocketHttpListener } else { - data = new byte[0]; + data = Array.Empty<byte>(); } var payload = new PayloadData(data, masked); @@ -281,7 +282,7 @@ namespace SocketHttpListener { payload.Mask(maskingKey); frame._mask = Mask.Unmask; - frame._maskingKey = new byte[0]; + frame._maskingKey = Array.Empty<byte>(); } frame._payloadData = payload; @@ -302,10 +303,10 @@ namespace SocketHttpListener return new WebSocketFrame(Opcode.Close, mask, payload); } - internal static WebSocketFrame CreateCloseFrame(Mask mask, CloseStatusCode code, string reason) + internal static async Task<WebSocketFrame> CreateCloseFrameAsync(Mask mask, CloseStatusCode code, string reason) { return new WebSocketFrame( - Opcode.Close, mask, new PayloadData(((ushort)code).Append(reason))); + Opcode.Close, mask, new PayloadData(await ((ushort)code).AppendAsync(reason).ConfigureAwait(false))); } internal static WebSocketFrame CreatePingFrame(Mask mask) @@ -329,41 +330,39 @@ namespace SocketHttpListener return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed); } - internal static WebSocketFrame Read(Stream stream) - { - return Read(stream, true); - } + internal static Task<WebSocketFrame> ReadAsync(Stream stream) + => ReadAsync(stream, true); - internal static WebSocketFrame Read(Stream stream, bool unmask) + internal static async Task<WebSocketFrame> ReadAsync(Stream stream, bool unmask) { - var header = stream.ReadBytes(2); + var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); if (header.Length != 2) + { throw new WebSocketException( "The header part of a frame cannot be read from the data source."); + } - return read(header, stream, unmask); + return await ReadAsync(header, stream, unmask).ConfigureAwait(false); } - internal static async void ReadAsync( + internal static async Task ReadAsync( Stream stream, bool unmask, Action<WebSocketFrame> completed, Action<Exception> error) { try { var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); if (header.Length != 2) + { throw new WebSocketException( "The header part of a frame cannot be read from the data source."); + } - var frame = read(header, stream, unmask); - if (completed != null) - completed(frame); + var frame = await ReadAsync(header, stream, unmask).ConfigureAwait(false); + completed?.Invoke(frame); } catch (Exception ex) { - if (error != null) - { - error(ex); - } + error.Invoke(ex); } } diff --git a/deployment/common.build.sh b/deployment/common.build.sh index c191ec2a1..d028e3a66 100755 --- a/deployment/common.build.sh +++ b/deployment/common.build.sh @@ -15,7 +15,6 @@ DEFAULT_CONFIG="Release" DEFAULT_OUTPUT_DIR="dist/jellyfin-git" DEFAULT_PKG_DIR="pkg-dist" DEFAULT_DOCKERFILE="Dockerfile" -DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD` DEFAULT_ARCHIVE_CMD="tar -xvzf" # Parse the version from the AssemblyVersion @@ -36,9 +35,9 @@ build_jellyfin() echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}" if [[ $DOTNETRUNTIME == 'framework' ]]; then - dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" + dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" else - dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} + dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" fi EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then @@ -53,7 +52,7 @@ build_jellyfin_docker() ( BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT} DOCKERFILE=${2-$DEFAULT_DOCKERFILE} - IMAGE_TAG=${3-$DEFAULT_IMAGE_TAG} + IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"} echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}" docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT} diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/debian-package-armhf/Dockerfile.amd64 new file mode 100644 index 000000000..0d62352e0 --- /dev/null +++ b/deployment/debian-package-armhf/Dockerfile.amd64 @@ -0,0 +1,42 @@ +FROM debian:9 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Prepare the cross-toolchain +RUN dpkg --add-architecture armhf \ + && apt-get update \ + && apt-get install -y cross-gcc-dev \ + && TARGET_LIST="armhf" cross-gcc-gensource 6 \ + && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ + && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf new file mode 100644 index 000000000..eb4152116 --- /dev/null +++ b/deployment/debian-package-armhf/Dockerfile.armhf @@ -0,0 +1,34 @@ +FROM debian:9 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=armhf + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh new file mode 100755 index 000000000..3898110af --- /dev/null +++ b/deployment/debian-package-armhf/clean.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +keep_artifacts="${1}" + +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-debian_armhf-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/debian-package-armhf/dependencies.txt b/deployment/debian-package-armhf/dependencies.txt new file mode 100644 index 000000000..bdb967096 --- /dev/null +++ b/deployment/debian-package-armhf/dependencies.txt @@ -0,0 +1 @@ +docker diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh new file mode 100755 index 000000000..45e68f0c6 --- /dev/null +++ b/deployment/debian-package-armhf/docker-build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Builds the DEB inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image +sed -i '/dotnet-sdk-2.2,/d' debian/control + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -aarmhf + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/deb +mv /jellyfin_* ${ARTIFACT_DIR}/deb/ diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh new file mode 100755 index 000000000..0ec0dc95c --- /dev/null +++ b/deployment/debian-package-armhf/package.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +ARCH="$( arch )" +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-debian_armhf-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Determine which Dockerfile to use +case $ARCH in + 'x86_64') + DOCKERFILE="Dockerfile.amd64" + ;; + 'armv7l') + DOCKERFILE="Dockerfile.armhf" + ;; +esac + +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +# Correct ownership on the DEBs (as current user, then as root if that fails) +chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ + || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-armhf/pkg-src b/deployment/debian-package-armhf/pkg-src new file mode 120000 index 000000000..4c695fea1 --- /dev/null +++ b/deployment/debian-package-armhf/pkg-src @@ -0,0 +1 @@ +../debian-package-x64/pkg-src
\ No newline at end of file diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 869dc4a5e..349e8787f 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,36 @@ +jellyfin (10.2.2-1) unstable; urgency=medium + + * jellyfin: + * PR968 Release 10.2.z copr autobuild + * PR964 Install the dotnet runtime package in Fedora build + * PR979 Build Package releases without debug turned on + * PR990 Fix slow local image validation + * PR991 Fix the ffmpeg compatibility + * PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild + * PR998 Set EnableRaisingEvents to true for processes that require it + * PR1017 Set ffmpeg+ffprobe paths in Docker container + * jellyfin-web: + * PR152 Go back on Media stop + * PR156 Fix volume slider not working on nowplayingbar + + -- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 28 Feb 2019 15:32:16 -0500 + +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/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin index b052b2ec6..58fe79332 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin @@ -21,9 +21,9 @@ JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin" # Restart script for in-app server control JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" -# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values -#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg" -#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe" +# ffmpeg binary paths, overriding the system values +JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg" +JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe" # [OPTIONAL] run Jellyfin as a headless service #JELLYFIN_SERVICE_OPT="--service" diff --git a/deployment/debian-package-x64/pkg-src/control b/deployment/debian-package-x64/pkg-src/control index 88d10438b..d96660590 100644 --- a/deployment/debian-package-x64/pkg-src/control +++ b/deployment/debian-package-x64/pkg-src/control @@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Architecture: any Depends: at, libsqlite3-0, - ffmpeg (<7:4.1) | jellyfin-ffmpeg, + jellyfin-ffmpeg, libfontconfig1, libfreetype6, libssl1.0.0 | libssl1.0.2 diff --git a/deployment/debian-package-x64/pkg-src/rules b/deployment/debian-package-x64/pkg-src/rules index ce98cb8f8..62f75bc6b 100644 --- a/deployment/debian-package-x64/pkg-src/rules +++ b/deployment/debian-package-x64/pkg-src/rules @@ -2,7 +2,23 @@ CONFIG := Release TERM := xterm SHELL := /bin/bash -DOTNETRUNTIME := debian-x64 + +HOST_ARCH := $(shell arch) +BUILD_ARCH := ${DEB_HOST_MULTIARCH} +ifeq ($(HOST_ARCH),x86_64) + ifeq ($(BUILD_ARCH),arm-linux-gnueabihf) + # Cross-building ARM on AMD64 + DOTNETRUNTIME := debian-arm + else + # Building AMD64 + DOTNETRUNTIME := debian-x64 + endif +endif +ifeq ($(HOST_ARCH),armv7l) + # Building ARM + DOTNETRUNTIME := debian-arm +endif + export DH_VERBOSE=1 export DOTNET_CLI_TELEMETRY_OPTOUT=1 @@ -16,7 +32,8 @@ override_dh_auto_test: override_dh_clistrip: override_dh_auto_build: - dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server + dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server override_dh_auto_clean: dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true diff --git a/deployment/debian-x64/build.sh b/deployment/debian-x64/build.sh deleted file mode 100755 index 47cfb5327..000000000 --- a/deployment/debian-x64/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin ../../Jellyfin.Server Release debian-x64 `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile index 8bb1d527d..397c944ea 100644 --- a/deployment/fedora-package-x64/Dockerfile +++ b/deployment/fedora-package-x64/Dockerfile @@ -13,7 +13,7 @@ RUN dnf update -y \ && dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \ && dnf copr enable -y @dotnet-sig/dotnet \ && rpmdev-setuptree \ - && dnf install -y dotnet-sdk-${SDK_VERSION} \ + && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR}/SPECS \ && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 75821cb17..e24bd2fcb 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.2 +Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 URL: https://jellyfin.media @@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, Requires: libcurl, fontconfig, freetype, openssl, glibc libicu # Requirements not packaged in main repos # COPR @dotnet-sig/dotnet -BuildRequires: dotnet-sdk-2.2 +BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2 # RPMfusion free Requires: ffmpeg @@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an %install export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} Jellyfin.Server +dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE %{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json @@ -73,7 +74,6 @@ EOF %{_libdir}/%{name}/jellyfin-web/* %attr(755,root,root) %{_bindir}/%{name} %{_libdir}/%{name}/*.json -%{_libdir}/%{name}/*.pdb %{_libdir}/%{name}/*.dll %{_libdir}/%{name}/*.so %{_libdir}/%{name}/*.a @@ -140,6 +140,31 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org> +- jellyfin: +- PR968 Release 10.2.z copr autobuild +- PR964 Install the dotnet runtime package in Fedora build +- PR979 Build Package releases without debug turned on +- PR990 Fix slow local image validation +- PR991 Fix the ffmpeg compatibility +- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild +- PR998 Set EnableRaisingEvents to true for processes that require it +- PR1017 Set ffmpeg+ffprobe paths in Docker container +- jellyfin-web: +- PR152 Go back on Media stop +- PR156 Fix volume slider not working on nowplayingbar +* 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/osx-x64/build.sh b/deployment/macos/build.sh index d6bfb9f5e..d6bfb9f5e 100755 --- a/deployment/osx-x64/build.sh +++ b/deployment/macos/build.sh diff --git a/deployment/debian-x64/clean.sh b/deployment/macos/clean.sh index 3df2d7796..3df2d7796 100755 --- a/deployment/debian-x64/clean.sh +++ b/deployment/macos/clean.sh diff --git a/deployment/debian-x64/dependencies.txt b/deployment/macos/dependencies.txt index 3d25d1bdf..3d25d1bdf 100644 --- a/deployment/debian-x64/dependencies.txt +++ b/deployment/macos/dependencies.txt diff --git a/deployment/debian-x64/package.sh b/deployment/macos/package.sh index 13b943ea8..13b943ea8 100755 --- a/deployment/debian-x64/package.sh +++ b/deployment/macos/package.sh diff --git a/deployment/osx-x64/clean.sh b/deployment/osx-x64/clean.sh deleted file mode 100755 index 3df2d7796..000000000 --- a/deployment/osx-x64/clean.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/osx-x64/package.sh b/deployment/osx-x64/package.sh deleted file mode 100755 index 13b943ea8..000000000 --- a/deployment/osx-x64/package.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -package_portable ../.. `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/framework/build.sh b/deployment/portable/build.sh index 4f2e6363e..4f2e6363e 100755 --- a/deployment/framework/build.sh +++ b/deployment/portable/build.sh diff --git a/deployment/framework/clean.sh b/deployment/portable/clean.sh index 3df2d7796..3df2d7796 100755 --- a/deployment/framework/clean.sh +++ b/deployment/portable/clean.sh diff --git a/deployment/framework/package.sh b/deployment/portable/package.sh index 13b943ea8..13b943ea8 100755 --- a/deployment/framework/package.sh +++ b/deployment/portable/package.sh diff --git a/deployment/ubuntu-x64/build.sh b/deployment/ubuntu-x64/build.sh deleted file mode 100755 index 870bac780..000000000 --- a/deployment/ubuntu-x64/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin ../../Jellyfin.Server Release ubuntu-x64 `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/ubuntu-x64/clean.sh b/deployment/ubuntu-x64/clean.sh deleted file mode 100755 index 3df2d7796..000000000 --- a/deployment/ubuntu-x64/clean.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/ubuntu-x64/dependencies.txt b/deployment/ubuntu-x64/dependencies.txt deleted file mode 100644 index 3d25d1bdf..000000000 --- a/deployment/ubuntu-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -dotnet diff --git a/deployment/ubuntu-x64/package.sh b/deployment/ubuntu-x64/package.sh deleted file mode 100755 index 13b943ea8..000000000 --- a/deployment/ubuntu-x64/package.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -package_portable ../.. `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/win-generic/dependencies.txt b/deployment/win-generic/dependencies.txt deleted file mode 100644 index 3d25d1bdf..000000000 --- a/deployment/win-generic/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -dotnet diff --git a/deployment/win-generic/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index 1121c3398..2c83f264c 100644 --- a/deployment/win-generic/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 } diff --git a/deployment/osx-x64/dependencies.txt b/deployment/windows/dependencies.txt index 3d25d1bdf..3d25d1bdf 100644 --- a/deployment/osx-x64/dependencies.txt +++ b/deployment/windows/dependencies.txt diff --git a/deployment/win-generic/install-jellyfin.ps1 b/deployment/windows/install-jellyfin.ps1 index b6e00e056..b6e00e056 100644 --- a/deployment/win-generic/install-jellyfin.ps1 +++ b/deployment/windows/install-jellyfin.ps1 diff --git a/deployment/win-generic/install.bat b/deployment/windows/install.bat index e21479a79..e21479a79 100644 --- a/deployment/win-generic/install.bat +++ b/deployment/windows/install.bat |
