Merge branch 'master' into tasks

This commit is contained in:
Bond-009 2019-06-01 17:06:01 +02:00 committed by GitHub
commit ce1fa42f9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 1990 additions and 1792 deletions

View File

@ -99,7 +99,7 @@ jobs:
pool: pool:
vmImage: ubuntu-16.04 vmImage: ubuntu-16.04
dependsOn: main_build dependsOn: main_build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) condition: false #and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
strategy: strategy:
matrix: matrix:
Naming: Naming:

View File

@ -1,6 +1,6 @@
ARG DOTNET_VERSION=2 ARG DOTNET_VERSION=2.2
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
@ -8,7 +8,7 @@ RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin" build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
FROM jellyfin/ffmpeg as ffmpeg FROM jellyfin/ffmpeg as ffmpeg
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}
# libfontconfig1 is required for Skia # libfontconfig1 is required for Skia
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \ && apt-get install --no-install-recommends --no-install-suggests -y \
@ -21,7 +21,7 @@ RUN apt-get update \
COPY --from=ffmpeg / / COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2 ARG JELLYFIN_WEB_VERSION=10.3.3
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web

View File

@ -8,7 +8,7 @@ FROM alpine as qemu_extract
COPY --from=qemu /usr/bin qemu-arm-static.tar.gz COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
RUN tar -xzvf qemu-arm-static.tar.gz RUN tar -xzvf qemu-arm-static.tar.gz
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
@ -21,7 +21,7 @@ RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin" build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7 FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
COPY --from=qemu_extract qemu-arm-static /usr/bin COPY --from=qemu_extract qemu-arm-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
@ -30,7 +30,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2 ARG JELLYFIN_WEB_VERSION=10.3.3
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web

View File

@ -9,7 +9,7 @@ COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz
RUN tar -xzvf qemu-aarch64-static.tar.gz RUN tar -xzvf qemu-aarch64-static.tar.gz
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
@ -22,7 +22,7 @@ RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin" build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8 FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8
COPY --from=qemu_extract qemu-aarch64-static /usr/bin COPY --from=qemu_extract qemu-aarch64-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
@ -31,7 +31,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2 ARG JELLYFIN_WEB_VERSION=10.3.3
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web

View File

@ -920,8 +920,6 @@ namespace Emby.Dlna.Didl
} }
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{ {
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
@ -930,6 +928,9 @@ namespace Emby.Dlna.Didl
AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG"); AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG");
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN"); AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
} }
private void AddEmbeddedImageAsCover(string name, XmlWriter writer) private void AddEmbeddedImageAsCover(string name, XmlWriter writer)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -102,9 +102,10 @@ namespace Emby.Dlna.PlayTo
{ {
_sessionManager.ReportSessionEnded(_session.Id); _sessionManager.ReportSessionEnded(_session.Id);
} }
catch catch (Exception ex)
{ {
// Could throw if the session is already gone // Could throw if the session is already gone
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
} }
} }
@ -112,20 +113,14 @@ namespace Emby.Dlna.PlayTo
{ {
var info = e.Argument; var info = e.Argument;
info.Headers.TryGetValue("NTS", out string nts); if (!_disposed
&& info.Headers.TryGetValue("USN", out string usn)
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty; && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty; || (info.Headers.TryGetValue("NT", out string nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
!_disposed)
{ {
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 || OnDeviceUnavailable();
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
{
OnDeviceUnavailable();
}
} }
} }
@ -612,22 +607,34 @@ namespace Emby.Dlna.PlayTo
public void Dispose() public void Dispose()
{ {
if (!_disposed) Dispose(true);
{ GC.SuppressFinalize(this);
_disposed = true;
_device.PlaybackStart -= _device_PlaybackStart;
_device.PlaybackProgress -= _device_PlaybackProgress;
_device.PlaybackStopped -= _device_PlaybackStopped;
_device.MediaChanged -= _device_MediaChanged;
//_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
_device.OnDeviceUnavailable = null;
_device.Dispose();
}
} }
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_device.Dispose();
}
_device.PlaybackStart -= _device_PlaybackStart;
_device.PlaybackProgress -= _device_PlaybackProgress;
_device.PlaybackStopped -= _device_PlaybackStopped;
_device.MediaChanged -= _device_MediaChanged;
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
_device.OnDeviceUnavailable = null;
_device = null;
_disposed = true;
}
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{ {

View File

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@ -20,7 +20,10 @@ namespace Emby.Photos
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
// These are causing taglib to hang
private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
public PhotoProvider(ILogger logger, IImageProcessor imageProcessor) public PhotoProvider(ILogger logger, IImageProcessor imageProcessor)
{ {
@ -28,75 +31,55 @@ namespace Emby.Photos
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
} }
public string Name => "Embedded Information";
public bool HasChanged(BaseItem item, IDirectoryService directoryService) public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
var file = directoryService.GetFile(item.Path); var file = directoryService.GetFile(item.Path);
if (file != null && file.LastWriteTimeUtc != item.DateModified) return (file != null && file.LastWriteTimeUtc != item.DateModified);
{
return true;
}
} }
return false; return false;
} }
// These are causing taglib to hang
private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
item.SetImagePath(ImageType.Primary, item.Path); item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (_includextensions.Contains(Path.GetExtension(item.Path) ?? string.Empty, StringComparer.OrdinalIgnoreCase)) if (_includextensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
{ {
try try
{ {
using (var file = TagLib.File.Create(item.Path)) using (var file = TagLib.File.Create(item.Path))
{ {
var image = file as TagLib.Image.File; if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
var tag = file.GetTag(TagTypes.TiffIFD) as IFDTag;
if (tag != null)
{ {
var structure = tag.Structure; var structure = tag.Structure;
if (structure != null
if (structure != null) && structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{ {
var exif = structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) as SubIFDEntry; var exifStructure = exif.Structure;
if (exifStructure != null)
if (exif != null)
{ {
var exifStructure = exif.Structure; var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
if (entry != null)
if (exifStructure != null)
{ {
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry; item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
}
if (entry != null) entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
{ if (entry != null)
double val = entry.Value.Numerator; {
val /= entry.Value.Denominator; item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
item.Aperture = val;
}
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
if (entry != null)
{
double val = entry.Value.Numerator;
val /= entry.Value.Denominator;
item.ShutterSpeed = val;
}
} }
} }
} }
} }
if (image != null) if (file is TagLib.Image.File image)
{ {
item.CameraMake = image.ImageTag.Make; item.CameraMake = image.ImageTag.Make;
item.CameraModel = image.ImageTag.Model; item.CameraModel = image.ImageTag.Model;
@ -116,12 +99,10 @@ namespace Emby.Photos
item.Overview = image.ImageTag.Comment; item.Overview = image.ImageTag.Comment;
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)) if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
&& !item.LockedFields.Contains(MetadataFields.Name))
{ {
if (!item.LockedFields.Contains(MetadataFields.Name)) item.Name = image.ImageTag.Title;
{
item.Name = image.ImageTag.Title;
}
} }
var dateTaken = image.ImageTag.DateTime; var dateTaken = image.ImageTag.DateTime;
@ -140,12 +121,9 @@ namespace Emby.Photos
{ {
item.Orientation = null; item.Orientation = null;
} }
else else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
{ {
if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation)) item.Orientation = orientation;
{
item.Orientation = orientation;
}
} }
item.ExposureTime = image.ImageTag.ExposureTime; item.ExposureTime = image.ImageTag.ExposureTime;
@ -195,7 +173,5 @@ namespace Emby.Photos
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport; const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
return Task.FromResult(result); return Task.FromResult(result);
} }
public string Name => "Embedded Information";
} }
} }

View File

@ -83,8 +83,6 @@ namespace Emby.Server.Implementations.Activity
_deviceManager.CameraImageUploaded += OnCameraImageUploaded; _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
_appHost.ApplicationUpdated += OnApplicationUpdated;
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -275,16 +273,6 @@ namespace Emby.Server.Implementations.Activity
}); });
} }
private void OnApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
{
CreateLogEntry(new ActivityLogEntry
{
Name = string.Format(_localization.GetLocalizedString("MessageApplicationUpdatedTo"), e.Argument.versionStr),
Type = NotificationType.ApplicationUpdateInstalled.ToString(),
Overview = e.Argument.description
});
}
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
{ {
CreateLogEntry(new ActivityLogEntry CreateLogEntry(new ActivityLogEntry
@ -460,8 +448,6 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserLockedOut -= OnUserLockedOut; _userManager.UserLockedOut -= OnUserLockedOut;
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded; _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
_appHost.ApplicationUpdated -= OnApplicationUpdated;
} }
/// <summary> /// <summary>

View File

@ -154,11 +154,6 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
public event EventHandler HasPendingRestartChanged; public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Occurs when [application updated].
/// </summary>
public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
/// <summary> /// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart. /// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary> /// </summary>
@ -173,11 +168,17 @@ namespace Emby.Server.Implementations
/// <value>The logger.</value> /// <value>The logger.</value>
protected ILogger Logger { get; set; } protected ILogger Logger { get; set; }
private IPlugin[] _plugins;
/// <summary> /// <summary>
/// Gets the plugins. /// Gets the plugins.
/// </summary> /// </summary>
/// <value>The plugins.</value> /// <value>The plugins.</value>
public IPlugin[] Plugins { get; protected set; } public IPlugin[] Plugins
{
get => _plugins;
protected set => _plugins = value;
}
/// <summary> /// <summary>
/// Gets or sets the logger factory. /// Gets or sets the logger factory.
@ -200,7 +201,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// The disposable parts /// The disposable parts
/// </summary> /// </summary>
protected readonly List<IDisposable> _disposableParts = new List<IDisposable>(); private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
/// <summary> /// <summary>
/// Gets the configuration manager. /// Gets the configuration manager.
@ -216,8 +217,9 @@ namespace Emby.Server.Implementations
{ {
#if BETA #if BETA
return PackageVersionClass.Beta; return PackageVersionClass.Beta;
#endif #else
return PackageVersionClass.Release; return PackageVersionClass.Release;
#endif
} }
} }
@ -340,7 +342,6 @@ namespace Emby.Server.Implementations
protected IProcessFactory ProcessFactory { get; private set; } protected IProcessFactory ProcessFactory { get; private set; }
protected ICryptoProvider CryptographyProvider = new CryptographyProvider();
protected readonly IXmlSerializer XmlSerializer; protected readonly IXmlSerializer XmlSerializer;
protected ISocketFactory SocketFactory { get; private set; } protected ISocketFactory SocketFactory { get; private set; }
@ -369,9 +370,6 @@ namespace Emby.Server.Implementations
{ {
_configuration = configuration; _configuration = configuration;
// hack alert, until common can target .net core
BaseExtensions.CryptographyProvider = CryptographyProvider;
XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory); XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
NetworkManager = networkManager; NetworkManager = networkManager;
@ -617,8 +615,6 @@ namespace Emby.Server.Implementations
DiscoverTypes(); DiscoverTypes();
SetHttpLimit();
await RegisterResources(serviceCollection).ConfigureAwait(false); await RegisterResources(serviceCollection).ConfigureAwait(false);
FindParts(); FindParts();
@ -735,13 +731,12 @@ namespace Emby.Server.Implementations
ApplicationHost.StreamHelper = new StreamHelper(); ApplicationHost.StreamHelper = new StreamHelper();
serviceCollection.AddSingleton(StreamHelper); serviceCollection.AddSingleton(StreamHelper);
serviceCollection.AddSingleton(CryptographyProvider); serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider));
SocketFactory = new SocketFactory(); SocketFactory = new SocketFactory();
serviceCollection.AddSingleton(SocketFactory); serviceCollection.AddSingleton(SocketFactory);
InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime); serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
serviceCollection.AddSingleton(InstallationManager);
ZipClient = new ZipClient(); ZipClient = new ZipClient();
serviceCollection.AddSingleton(ZipClient); serviceCollection.AddSingleton(ZipClient);
@ -908,8 +903,6 @@ namespace Emby.Server.Implementations
_serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider = serviceCollection.BuildServiceProvider();
} }
public virtual string PackageRuntime => "netcore";
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
{ {
// Distinct these to prevent users from reporting problems that aren't actually problems // Distinct these to prevent users from reporting problems that aren't actually problems
@ -918,8 +911,7 @@ namespace Emby.Server.Implementations
.Distinct(); .Distinct();
logger.LogInformation("Arguments: {Args}", commandLineArgs); logger.LogInformation("Arguments: {Args}", commandLineArgs);
// FIXME: @bond this logs the kernel version, not the OS version logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess); logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
@ -929,19 +921,6 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
} }
private void SetHttpLimit()
{
try
{
// Increase the max http request limit
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error setting http limit");
}
}
private X509Certificate2 GetCertificate(CertificateInfo info) private X509Certificate2 GetCertificate(CertificateInfo info)
{ {
var certificateLocation = info?.Path; var certificateLocation = info?.Path;
@ -1044,11 +1023,47 @@ namespace Emby.Server.Implementations
AuthenticatedAttribute.AuthService = AuthService; AuthenticatedAttribute.AuthService = AuthService;
} }
private async void PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> args)
{
string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name);
var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
.Select(x => Assembly.LoadFrom(x))
.SelectMany(x => x.ExportedTypes)
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType)
.ToList();
types.AddRange(types);
var plugins = types.Where(x => x.IsAssignableFrom(typeof(IPlugin)))
.Select(CreateInstanceSafe)
.Where(x => x != null)
.Cast<IPlugin>()
.Select(LoadPlugin)
.Where(x => x != null)
.ToArray();
int oldLen = _plugins.Length;
Array.Resize<IPlugin>(ref _plugins, _plugins.Length + plugins.Length);
plugins.CopyTo(_plugins, oldLen);
var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint)))
.Select(CreateInstanceSafe)
.Where(x => x != null)
.Cast<IServerEntryPoint>()
.ToList();
await Task.WhenAll(StartEntryPoints(entries, true));
await Task.WhenAll(StartEntryPoints(entries, false));
}
/// <summary> /// <summary>
/// Finds the parts. /// Finds the parts.
/// </summary> /// </summary>
protected void FindParts() protected void FindParts()
{ {
InstallationManager = _serviceProvider.GetService<IInstallationManager>();
InstallationManager.PluginInstalled += PluginInstalled;
if (!ServerConfigurationManager.Configuration.IsPortAuthorized) if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
{ {
ServerConfigurationManager.Configuration.IsPortAuthorized = true; ServerConfigurationManager.Configuration.IsPortAuthorized = true;
@ -1088,7 +1103,7 @@ namespace Emby.Server.Implementations
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>()); MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
UserManager.AddParts(GetExports<IAuthenticationProvider>()); UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
IsoManager.AddParts(GetExports<IIsoMounter>()); IsoManager.AddParts(GetExports<IIsoMounter>());
} }
@ -1131,7 +1146,7 @@ namespace Emby.Server.Implementations
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error loading plugin {pluginName}", plugin.GetType().FullName); Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
return null; return null;
} }
@ -1145,10 +1160,32 @@ namespace Emby.Server.Implementations
{ {
Logger.LogInformation("Loading assemblies"); Logger.LogInformation("Loading assemblies");
AllConcreteTypes = GetComposablePartAssemblies() AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
.SelectMany(x => x.ExportedTypes) }
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
.ToArray(); private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
{
Type[] exportedTypes;
try
{
exportedTypes = ass.GetExportedTypes();
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue;
}
foreach (Type type in exportedTypes)
{
if (type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
{
yield return type;
}
}
}
} }
private CertificateInfo CertificateInfo { get; set; } private CertificateInfo CertificateInfo { get; set; }
@ -1310,10 +1347,21 @@ namespace Emby.Server.Implementations
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) if (Directory.Exists(ApplicationPaths.PluginsPath))
{ {
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly)) foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
{ {
Logger.LogInformation("Loading assembly {Path}", file); Assembly plugAss;
yield return Assembly.LoadFrom(file); try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
} }
} }
@ -1372,14 +1420,23 @@ namespace Emby.Server.Implementations
public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken) public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
{ {
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
var wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false);
string wanAddress;
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
{
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
}
else
{
wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
}
return new SystemInfo return new SystemInfo
{ {
HasPendingRestart = HasPendingRestart, HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown, IsShuttingDown = IsShuttingDown,
Version = ApplicationVersion, Version = ApplicationVersion,
ProductName = ApplicationProductName,
WebSocketPortNumber = HttpPort, WebSocketPortNumber = HttpPort,
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
Id = SystemId, Id = SystemId,
@ -1422,10 +1479,21 @@ namespace Emby.Server.Implementations
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken) public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
{ {
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
var wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false);
string wanAddress;
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
{
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
}
else
{
wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
}
return new PublicSystemInfo return new PublicSystemInfo
{ {
Version = ApplicationVersion, Version = ApplicationVersion,
ProductName = ApplicationProductName,
Id = SystemId, Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystem = OperatingSystem.Id.ToString(),
WanAddress = wanAddress, WanAddress = wanAddress,
@ -1460,7 +1528,7 @@ namespace Emby.Server.Implementations
return null; return null;
} }
public async Task<string> GetWanApiUrl(CancellationToken cancellationToken) public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken)
{ {
const string Url = "http://ipv4.icanhazip.com"; const string Url = "http://ipv4.icanhazip.com";
try try
@ -1476,13 +1544,14 @@ namespace Emby.Server.Implementations
CancellationToken = cancellationToken CancellationToken = cancellationToken
}).ConfigureAwait(false)) }).ConfigureAwait(false))
{ {
return GetLocalApiUrl(response.ReadToEnd().Trim()); return GetWanApiUrl(response.ReadToEnd().Trim());
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error getting WAN Ip address information"); Logger.LogError(ex, "Error getting WAN Ip address information");
} }
return null; return null;
} }
@ -1498,9 +1567,38 @@ namespace Emby.Server.Implementations
public string GetLocalApiUrl(string host) public string GetLocalApiUrl(string host)
{ {
if (EnableHttps)
{
return string.Format("https://{0}:{1}",
host,
HttpsPort.ToString(CultureInfo.InvariantCulture));
}
return string.Format("http://{0}:{1}", return string.Format("http://{0}:{1}",
host, host,
HttpPort.ToString(CultureInfo.InvariantCulture)); HttpPort.ToString(CultureInfo.InvariantCulture));
}
public string GetWanApiUrl(IpAddressInfo ipAddress)
{
if (ipAddress.AddressFamily == IpAddressFamily.InterNetworkV6)
{
return GetWanApiUrl("[" + ipAddress.Address + "]");
}
return GetWanApiUrl(ipAddress.Address);
}
public string GetWanApiUrl(string host)
{
if (EnableHttps)
{
return string.Format("https://{0}:{1}",
host,
ServerConfigurationManager.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture));
}
return string.Format("http://{0}:{1}",
host,
ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture));
} }
public Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken) public Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken)
@ -1756,24 +1854,6 @@ namespace Emby.Server.Implementations
{ {
} }
/// <summary>
/// Called when [application updated].
/// </summary>
/// <param name="package">The package.</param>
protected void OnApplicationUpdated(PackageVersionInfo package)
{
Logger.LogInformation("Application has been updated to version {0}", package.versionStr);
ApplicationUpdated?.Invoke(
this,
new GenericEventArgs<PackageVersionInfo>()
{
Argument = package
});
NotifyPendingRestart();
}
private bool _disposed = false; private bool _disposed = false;
/// <summary> /// <summary>

View File

@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration
/// </summary> /// </summary>
private void UpdateMetadataPath() private void UpdateMetadataPath()
{ {
string metadataPath;
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
{ {
metadataPath = GetInternalMetadataPath(); ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
} }
else else
{ {
metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath;
} }
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath;
}
private string GetInternalMetadataPath()
{
return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
} }
/// <summary> /// <summary>

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Linq;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Cryptography namespace Emby.Server.Implementations.Cryptography

View File

@ -90,9 +90,10 @@ namespace Emby.Server.Implementations.Data
{ {
throw new ArgumentNullException(nameof(displayPreferences)); throw new ArgumentNullException(nameof(displayPreferences));
} }
if (string.IsNullOrEmpty(displayPreferences.Id)) if (string.IsNullOrEmpty(displayPreferences.Id))
{ {
throw new ArgumentNullException(nameof(displayPreferences.Id)); throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
} }
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();

View File

@ -2741,15 +2741,16 @@ namespace Emby.Server.Implementations.Data
{ {
var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
int slowThreshold = 100;
#if DEBUG #if DEBUG
slowThreshold = 10; const int SlowThreshold = 100;
#else
const int SlowThreshold = 10;
#endif #endif
if (elapsed >= slowThreshold) if (elapsed >= SlowThreshold)
{ {
Logger.LogWarning("{0} query time (slow): {1:g}. Query: {2}", Logger.LogWarning(
"{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
methodName, methodName,
elapsed, elapsed,
commandText); commandText);

View File

@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Data
{ {
// If the user password is the sha1 hash of the empty string, remove it // If the user password is the sha1 hash of the empty string, remove it
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
{ {
continue; continue;
} }

View File

@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(),
CollectionFolders = GetTopParentIds(newAndRemoved, user, allUserRootChildren).ToArray() CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
}; };
} }
@ -407,7 +407,7 @@ namespace Emby.Server.Implementations.EntryPoints
return item.SourceType == SourceType.Library; return item.SourceType == SourceType.Library;
} }
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, User user, List<Folder> allUserRootChildren) private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
{ {
var list = new List<string>(); var list = new List<string>();

View File

@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
if (string.IsNullOrWhiteSpace(rangeHeader)) if (string.IsNullOrWhiteSpace(rangeHeader))
{ {
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
StatusCode = HttpStatusCode.OK; StatusCode = HttpStatusCode.OK;
} }
else else
@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
RangeStart = requestedRange.Key; RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart; RangeLength = 1 + RangeEnd - RangeStart;
// Content-Length is the length of what we're serving, not the original content
var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentLength] = lengthString;
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString; Headers[HeaderNames.ContentRange] = rangeString;
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString); Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
} }
/// <summary> /// <summary>

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
@ -126,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType) private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
{ {
var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList(); var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
var serviceType = GetServiceTypeByRequest(requestDtoType); var serviceType = GetServiceTypeByRequest(requestDtoType);
if (serviceType != null) if (serviceType != null)
{ {
attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>()); attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
} }
attributes.Sort((x, y) => x.Priority - y.Priority); attributes.Sort((x, y) => x.Priority - y.Priority);
@ -153,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer
QueryString = e.QueryString ?? new QueryCollection() QueryString = e.QueryString ?? new QueryCollection()
}; };
connection.Closed += Connection_Closed; connection.Closed += OnConnectionClosed;
lock (_webSocketConnections) lock (_webSocketConnections)
{ {
@ -163,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
} }
private void Connection_Closed(object sender, EventArgs e) private void OnConnectionClosed(object sender, EventArgs e)
{ {
lock (_webSocketConnections) lock (_webSocketConnections)
{ {
@ -202,6 +201,7 @@ namespace Emby.Server.Implementations.HttpServer
case DirectoryNotFoundException _: case DirectoryNotFoundException _:
case FileNotFoundException _: case FileNotFoundException _:
case ResourceNotFoundException _: return 404; case ResourceNotFoundException _: return 404;
case MethodNotAllowedException _: return 405;
case RemoteServiceUnavailableException _: return 502; case RemoteServiceUnavailableException _: return 502;
default: return 500; default: return 500;
} }
@ -321,14 +321,14 @@ namespace Emby.Server.Implementations.HttpServer
private static string NormalizeConfiguredLocalAddress(string address) private static string NormalizeConfiguredLocalAddress(string address)
{ {
var index = address.Trim('/').IndexOf('/'); var add = address.AsSpan().Trim('/');
int index = add.IndexOf('/');
if (index != -1) if (index != -1)
{ {
address = address.Substring(index + 1); add = add.Slice(index + 1);
} }
return address.Trim('/'); return add.TrimStart('/').ToString();
} }
private bool ValidateHost(string host) private bool ValidateHost(string host)
@ -398,8 +398,8 @@ namespace Emby.Server.Implementations.HttpServer
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
{ {
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 || if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
{ {
return true; return true;
} }
@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.HttpServer
if (handler != null) if (handler != null)
{ {
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false); await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
@ -612,21 +612,11 @@ namespace Emby.Server.Implementations.HttpServer
{ {
var pathInfo = httpReq.PathInfo; var pathInfo = httpReq.PathInfo;
var pathParts = pathInfo.TrimStart('/').Split('/'); pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
if (pathParts.Length == 0) var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
{
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) if (restPath != null)
{ {
return new ServiceHandler return new ServiceHandler(restPath, contentType);
{
RestPath = restPath,
ResponseContentType = contentType
};
} }
Logger.LogError("Could not find handler for {PathInfo}", pathInfo); Logger.LogError("Could not find handler for {PathInfo}", pathInfo);
@ -636,6 +626,7 @@ namespace Emby.Server.Implementations.HttpServer
private static Task Write(IResponse response, string text) private static Task Write(IResponse response, string text)
{ {
var bOutput = Encoding.UTF8.GetBytes(text); var bOutput = Encoding.UTF8.GetBytes(text);
response.OriginalResponse.ContentLength = bOutput.Length;
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
} }
@ -654,11 +645,6 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
// TODO what is this?
var httpsUrl = url
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
RedirectToUrl(httpRes, url); RedirectToUrl(httpRes, url);
} }
} }
@ -683,10 +669,7 @@ namespace Emby.Server.Implementations.HttpServer
UrlPrefixes = urlPrefixes.ToArray(); UrlPrefixes = urlPrefixes.ToArray();
ServiceController = new ServiceController(); ServiceController = new ServiceController();
Logger.LogInformation("Calling ServiceStack AppHost.Init"); var types = services.Select(r => r.GetType());
var types = services.Select(r => r.GetType()).ToArray();
ServiceController.Init(this, types); ServiceController.Init(this, types);
ResponseFilters = new Action<IRequest, IResponse, object>[] ResponseFilters = new Action<IRequest, IResponse, object>[]

View File

@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
content = Array.Empty<byte>(); content = Array.Empty<byte>();
} }
result = new StreamWriter(content, contentType); result = new StreamWriter(content, contentType, contentLength);
} }
else else
{ {
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
bytes = Array.Empty<byte>(); bytes = Array.Empty<byte>();
} }
result = new StreamWriter(bytes, contentType); result = new StreamWriter(bytes, contentType, contentLength);
} }
else else
{ {
@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
if (isHeadRequest) if (isHeadRequest)
{ {
var result = new StreamWriter(Array.Empty<byte>(), contentType); var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
else else
{ {
var result = new StreamWriter(content, contentType); var result = new StreamWriter(content, contentType, contentLength);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
}
if (isHeadRequest) if (isHeadRequest)
{ {
using (stream) using (stream)
@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
if (lastModifiedDate.HasValue) if (lastModifiedDate.HasValue)
{ {
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString(); responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
} }
} }

View File

@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
RangeStart = requestedRange.Key; RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart; RangeLength = 1 + RangeEnd - RangeStart;
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
if (RangeStart > 0 && SourceStream.CanSeek) if (RangeStart > 0 && SourceStream.CanSeek)

View File

@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
public void FilterResponse(IRequest req, IResponse res, object dto) public void FilterResponse(IRequest req, IResponse res, object dto)
{ {
// Try to prevent compatibility view // Try to prevent compatibility view
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.AddHeader("Access-Control-Allow-Origin", "*"); res.AddHeader("Access-Control-Allow-Origin", "*");
@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
if (length > 0) if (length > 0)
{ {
res.OriginalResponse.ContentLength = length;
res.SendChunked = false; res.SendChunked = false;
} }
} }

View File

@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
// This code is executed before the service // This code is executed before the service
var auth = AuthorizationContext.GetAuthorizationInfo(request); var auth = AuthorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(auth, authAttribtues, request)) if (!IsExemptFromAuthenticationToken(authAttribtues, request))
{ {
ValidateSecurityToken(request, auth.Token); ValidateSecurityToken(request, auth.Token);
} }
@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
} }
} }
private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request) private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
{ {
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
{ {

View File

@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
public class StreamWriter : IAsyncStreamWriter, IHasHeaders public class StreamWriter : IAsyncStreamWriter, IHasHeaders
{ {
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary> /// <summary>
/// Gets or sets the source stream. /// Gets or sets the source stream.
/// </summary> /// </summary>
@ -52,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
SourceStream = source; SourceStream = source;
Headers["Content-Type"] = contentType;
if (source.CanSeek)
{
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
}
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
} }
@ -60,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
/// <param name="source">The source.</param> /// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param> /// <param name="contentType">Type of the content.</param>
public StreamWriter(byte[] source, string contentType) public StreamWriter(byte[] source, string contentType, int contentLength)
{ {
if (string.IsNullOrEmpty(contentType)) if (string.IsNullOrEmpty(contentType))
{ {
@ -69,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceBytes = source; SourceBytes = source;
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
} }

View File

@ -6,10 +6,6 @@ using System.Threading;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO
{ {
AddAffectedPath(path); AddAffectedPath(path);
} }
RestartTimer(); RestartTimer();
} }
@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO
AddAffectedPath(affectedFile); AddAffectedPath(affectedFile);
} }
} }
RestartTimer(); RestartTimer();
} }

View File

@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
{ {
@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO
/// The file system watchers /// The file system watchers
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
/// <summary> /// <summary>
/// The affected paths /// The affected paths
/// </summary> /// </summary>
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
// This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. // This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
await Task.Delay(45000).ConfigureAwait(false); await Task.Delay(45000).ConfigureAwait(false);
@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO
public void Start() public void Start()
{ {
LibraryManager.ItemAdded += LibraryManager_ItemAdded; LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
var pathsToWatch = new List<string> { }; var pathsToWatch = new List<string>();
var paths = LibraryManager var paths = LibraryManager
.RootFolder .RootFolder
@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
{ {
if (e.Parent is AggregateFolder) if (e.Parent is AggregateFolder)
{ {
@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
{ {
if (e.Parent is AggregateFolder) if (e.Parent is AggregateFolder)
{ {
@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO
return lst.Any(str => return lst.Any(str =>
{ {
//this should be a little quicker than examining each actual parent folder... // this should be a little quicker than examining each actual parent folder...
var compare = str.TrimEnd(Path.DirectorySeparatorChar); var compare = str.TrimEnd(Path.DirectorySeparatorChar);
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO
if (!Directory.Exists(path)) if (!Directory.Exists(path))
{ {
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
Logger.LogInformation("Skipping realtime monitor for {0} because the path does not exist", path); Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
return; return;
} }
if (OperatingSystem.Id != OperatingSystemId.Windows)
{
if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
{
// not supported
return;
}
}
// Already being watched // Already being watched
if (_fileSystemWatchers.ContainsKey(path)) if (_fileSystemWatchers.ContainsKey(path))
{ {
@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO
{ {
var newWatcher = new FileSystemWatcher(path, "*") var newWatcher = new FileSystemWatcher(path, "*")
{ {
IncludeSubdirectories = true IncludeSubdirectories = true,
InternalBufferSize = 65536,
NotifyFilter = NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes
}; };
newWatcher.InternalBufferSize = 65536; newWatcher.Created += OnWatcherChanged;
newWatcher.Deleted += OnWatcherChanged;
newWatcher.NotifyFilter = NotifyFilters.CreationTime | newWatcher.Renamed += OnWatcherChanged;
NotifyFilters.DirectoryName | newWatcher.Changed += OnWatcherChanged;
NotifyFilters.FileName | newWatcher.Error += OnWatcherError;
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes;
newWatcher.Created += watcher_Changed;
newWatcher.Deleted += watcher_Changed;
newWatcher.Renamed += watcher_Changed;
newWatcher.Changed += watcher_Changed;
newWatcher.Error += watcher_Error;
if (_fileSystemWatchers.TryAdd(path, newWatcher)) if (_fileSystemWatchers.TryAdd(path, newWatcher))
{ {
@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO
{ {
using (watcher) using (watcher)
{ {
Logger.LogInformation("Stopping directory watching for path {path}", watcher.Path); Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
watcher.Created -= watcher_Changed; watcher.Created -= OnWatcherChanged;
watcher.Deleted -= watcher_Changed; watcher.Deleted -= OnWatcherChanged;
watcher.Renamed -= watcher_Changed; watcher.Renamed -= OnWatcherChanged;
watcher.Changed -= watcher_Changed; watcher.Changed -= OnWatcherChanged;
watcher.Error -= watcher_Error; watcher.Error -= OnWatcherError;
try watcher.EnableRaisingEvents = false;
{
watcher.EnableRaisingEvents = false;
}
catch (InvalidOperationException)
{
// Seeing this under mono on linux sometimes
// Collection was modified; enumeration operation may not execute.
}
} }
}
catch (NotImplementedException)
{
// the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android
}
catch
{
} }
finally finally
{ {
@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="watcher">The watcher.</param> /// <param name="watcher">The watcher.</param>
private void RemoveWatcherFromList(FileSystemWatcher watcher) private void RemoveWatcherFromList(FileSystemWatcher watcher)
{ {
_fileSystemWatchers.TryRemove(watcher.Path, out var removed); _fileSystemWatchers.TryRemove(watcher.Path, out _);
} }
/// <summary> /// <summary>
@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param> /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
void watcher_Error(object sender, ErrorEventArgs e) private void OnWatcherError(object sender, ErrorEventArgs e)
{ {
var ex = e.GetException(); var ex = e.GetException();
var dw = (FileSystemWatcher)sender; var dw = (FileSystemWatcher)sender;
Logger.LogError(ex, "Error in Directory watcher for: {path}", dw.Path); Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
DisposeWatcher(dw, true); DisposeWatcher(dw, true);
} }
@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param> /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
void watcher_Changed(object sender, FileSystemEventArgs e) private void OnWatcherChanged(object sender, FileSystemEventArgs e)
{ {
try try
{ {
//logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); ReportFileSystemChanged(e.FullPath);
var path = e.FullPath;
ReportFileSystemChanged(path);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO
{ {
if (_fileSystem.AreEqual(i, path)) if (_fileSystem.AreEqual(i, path))
{ {
Logger.LogDebug("Ignoring change to {path}", path); Logger.LogDebug("Ignoring change to {Path}", path);
return true; return true;
} }
if (_fileSystem.ContainsSubPath(i, path)) if (_fileSystem.ContainsSubPath(i, path))
{ {
Logger.LogDebug("Ignoring change to {path}", path); Logger.LogDebug("Ignoring change to {Path}", path);
return true; return true;
} }
// Go up a level // Go up a level
var parent = Path.GetDirectoryName(i); var parent = Path.GetDirectoryName(i);
if (!string.IsNullOrEmpty(parent)) if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{ {
if (_fileSystem.AreEqual(parent, path)) Logger.LogDebug("Ignoring change to {Path}", path);
{ return true;
Logger.LogDebug("Ignoring change to {path}", path);
return true;
}
} }
return false; return false;
@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO
lock (_activeRefreshers) lock (_activeRefreshers)
{ {
var refreshers = _activeRefreshers.ToList(); foreach (var refresher in _activeRefreshers)
foreach (var refresher in refreshers)
{ {
// Path is already being refreshed // Path is already being refreshed
if (_fileSystem.AreEqual(path, refresher.Path)) if (_fileSystem.AreEqual(path, refresher.Path))
@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
public void Stop() public void Stop()
{ {
LibraryManager.ItemAdded -= LibraryManager_ItemAdded; LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
foreach (var watcher in _fileSystemWatchers.Values.ToList()) foreach (var watcher in _fileSystemWatchers.Values.ToList())
{ {
@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO
{ {
refresher.Dispose(); refresher.Dispose();
} }
_activeRefreshers.Clear(); _activeRefreshers.Clear();
} }
} }
private bool _disposed; private bool _disposed = false;
/// <summary> /// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this);
} }
/// <summary> /// <summary>

View File

@ -19,8 +19,6 @@ namespace Emby.Server.Implementations.IO
{ {
protected ILogger Logger; protected ILogger Logger;
private readonly bool _supportsAsyncFileStreams;
private char[] _invalidFileNameChars;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath; private readonly string _tempPath;
@ -32,11 +30,8 @@ namespace Emby.Server.Implementations.IO
IApplicationPaths applicationPaths) IApplicationPaths applicationPaths)
{ {
Logger = loggerFactory.CreateLogger("FileSystem"); Logger = loggerFactory.CreateLogger("FileSystem");
_supportsAsyncFileStreams = true;
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows);
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
} }
@ -45,20 +40,6 @@ namespace Emby.Server.Implementations.IO
_shortcutHandlers.Add(handler); _shortcutHandlers.Add(handler);
} }
protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars)
{
if (enableManagedInvalidFileNameChars)
{
_invalidFileNameChars = Path.GetInvalidFileNameChars();
}
else
{
// Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions
// https://referencesource.microsoft.com/#mscorlib/system/io/path.cs
_invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' };
}
}
/// <summary> /// <summary>
/// Determines whether the specified filename is shortcut. /// Determines whether the specified filename is shortcut.
/// </summary> /// </summary>
@ -92,20 +73,22 @@ namespace Emby.Server.Implementations.IO
var extension = Path.GetExtension(filename); var extension = Path.GetExtension(filename);
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
if (handler != null) return handler?.Resolve(filename);
{
return handler.Resolve(filename);
}
return null;
} }
public virtual string MakeAbsolutePath(string folderPath, string filePath) public virtual string MakeAbsolutePath(string folderPath, string filePath)
{ {
if (string.IsNullOrWhiteSpace(filePath)) return filePath; if (string.IsNullOrWhiteSpace(filePath)
// stream
|| filePath.Contains("://"))
{
return filePath;
}
if (filePath.Contains(@"://")) return filePath; //stream if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path {
return filePath; // absolute local path
}
// unc path // unc path
if (filePath.StartsWith("\\\\")) if (filePath.StartsWith("\\\\"))
@ -125,9 +108,7 @@ namespace Emby.Server.Implementations.IO
} }
try try
{ {
string path = System.IO.Path.Combine(folderPath, filePath); return Path.Combine(Path.GetFullPath(folderPath), filePath);
path = System.IO.Path.GetFullPath(path);
return path;
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@ -166,7 +147,7 @@ namespace Emby.Server.Implementations.IO
} }
var extension = Path.GetExtension(shortcutPath); var extension = Path.GetExtension(shortcutPath);
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
if (handler != null) if (handler != null)
{ {
@ -244,12 +225,13 @@ namespace Emby.Server.Implementations.IO
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info) private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
{ {
var result = new FileSystemMetadata(); var result = new FileSystemMetadata
{
result.Exists = info.Exists; Exists = info.Exists,
result.FullName = info.FullName; FullName = info.FullName,
result.Extension = info.Extension; Extension = info.Extension,
result.Name = info.Name; Name = info.Name
};
if (result.Exists) if (result.Exists)
{ {
@ -260,8 +242,7 @@ namespace Emby.Server.Implementations.IO
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
//} //}
var fileInfo = info as FileInfo; if (info is FileInfo fileInfo)
if (fileInfo != null)
{ {
result.Length = fileInfo.Length; result.Length = fileInfo.Length;
result.DirectoryName = fileInfo.DirectoryName; result.DirectoryName = fileInfo.DirectoryName;
@ -307,7 +288,7 @@ namespace Emby.Server.Implementations.IO
{ {
var builder = new StringBuilder(filename); var builder = new StringBuilder(filename);
foreach (var c in _invalidFileNameChars) foreach (var c in Path.GetInvalidFileNameChars())
{ {
builder = builder.Replace(c, ' '); builder = builder.Replace(c, ' ');
} }
@ -394,7 +375,7 @@ namespace Emby.Server.Implementations.IO
/// <returns>FileStream.</returns> /// <returns>FileStream.</returns>
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
{ {
if (_supportsAsyncFileStreams && isAsync) if (isAsync)
{ {
return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous); return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
} }

View File

@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
try try
{ {
int read; int read;
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
if (onStarted != null) if (onStarted != null)
{ {
@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
if (emptyReadLimit <= 0) if (emptyReadLimit <= 0)
{ {
int read; int read;
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
} }
return; return;
@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
if (bytesRead == 0) if (bytesRead == 0)
{ {
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
{ {
eofCount = 0; eofCount = 0;
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
} }
} }
} }
@ -109,64 +109,6 @@ namespace Emby.Server.Implementations.IO
} }
} }
public async Task<int> CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
try
{
int bytesRead;
int totalBytesRead = 0;
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
{
var bytesToWrite = bytesRead;
if (bytesToWrite > 0)
{
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
}
}
return totalBytesRead;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
try
{
int bytesRead;
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
{
var bytesToWrite = Math.Min(bytesRead, copyLength);
if (bytesToWrite > 0)
{
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
}
copyLength -= bytesToWrite;
if (copyLength <= 0)
{
break;
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{ {
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize); byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO
if (bytesRead == 0) if (bytesRead == 0)
{ {
await Task.Delay(100).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false);
} }
} }
} }
@ -225,7 +167,7 @@ namespace Emby.Server.Implementations.IO
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{ {
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
} }

View File

@ -1,355 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Emby.Server.Implementations.IO
{
/// <summary>
/// Class for streaming data with throttling support.
/// </summary>
public class ThrottledStream : Stream
{
/// <summary>
/// A constant used to specify an infinite number of bytes that can be transferred per second.
/// </summary>
public const long Infinite = 0;
#region Private members
/// <summary>
/// The base stream.
/// </summary>
private readonly Stream _baseStream;
/// <summary>
/// The maximum bytes per second that can be transferred through the base stream.
/// </summary>
private long _maximumBytesPerSecond;
/// <summary>
/// The number of bytes that has been transferred since the last throttle.
/// </summary>
private long _byteCount;
/// <summary>
/// The start time in milliseconds of the last throttle.
/// </summary>
private long _start;
#endregion
#region Properties
/// <summary>
/// Gets the current milliseconds.
/// </summary>
/// <value>The current milliseconds.</value>
protected long CurrentMilliseconds => Environment.TickCount;
/// <summary>
/// Gets or sets the maximum bytes per second that can be transferred through the base stream.
/// </summary>
/// <value>The maximum bytes per second.</value>
public long MaximumBytesPerSecond
{
get => _maximumBytesPerSecond;
set
{
if (MaximumBytesPerSecond != value)
{
_maximumBytesPerSecond = value;
Reset();
}
}
}
/// <summary>
/// Gets a value indicating whether the current stream supports reading.
/// </summary>
/// <returns>true if the stream supports reading; otherwise, false.</returns>
public override bool CanRead => _baseStream.CanRead;
/// <summary>
/// Gets a value indicating whether the current stream supports seeking.
/// </summary>
/// <value></value>
/// <returns>true if the stream supports seeking; otherwise, false.</returns>
public override bool CanSeek => _baseStream.CanSeek;
/// <summary>
/// Gets a value indicating whether the current stream supports writing.
/// </summary>
/// <value></value>
/// <returns>true if the stream supports writing; otherwise, false.</returns>
public override bool CanWrite => _baseStream.CanWrite;
/// <summary>
/// Gets the length in bytes of the stream.
/// </summary>
/// <value></value>
/// <returns>A long value representing the length of the stream in bytes.</returns>
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
public override long Length => _baseStream.Length;
/// <summary>
/// Gets or sets the position within the current stream.
/// </summary>
/// <value></value>
/// <returns>The current position within the stream.</returns>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
public override long Position
{
get => _baseStream.Position;
set => _baseStream.Position = value;
}
#endregion
public long MinThrottlePosition;
#region Ctor
/// <summary>
/// Initializes a new instance of the <see cref="T:ThrottledStream"/> class.
/// </summary>
/// <param name="baseStream">The base stream.</param>
/// <param name="maximumBytesPerSecond">The maximum bytes per second that can be transferred through the base stream.</param>
/// <exception cref="ArgumentNullException">Thrown when <see cref="baseStream"/> is a null reference.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="maximumBytesPerSecond"/> is a negative value.</exception>
public ThrottledStream(Stream baseStream, long maximumBytesPerSecond)
{
if (baseStream == null)
{
throw new ArgumentNullException(nameof(baseStream));
}
if (maximumBytesPerSecond < 0)
{
throw new ArgumentOutOfRangeException(nameof(maximumBytesPerSecond),
maximumBytesPerSecond, "The maximum number of bytes per second can't be negative.");
}
_baseStream = baseStream;
_maximumBytesPerSecond = maximumBytesPerSecond;
_start = CurrentMilliseconds;
_byteCount = 0;
}
#endregion
#region Public methods
/// <summary>
/// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
/// </summary>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
public override void Flush()
{
_baseStream.Flush();
}
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
/// </summary>
/// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
/// <returns>
/// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
/// </returns>
/// <exception cref="T:System.ArgumentException">The sum of offset and count is larger than the buffer length. </exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
/// <exception cref="T:System.NotSupportedException">The base stream does not support reading. </exception>
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
public override int Read(byte[] buffer, int offset, int count)
{
Throttle(count);
return _baseStream.Read(buffer, offset, count);
}
/// <summary>
/// Sets the position within the current stream.
/// </summary>
/// <param name="offset">A byte offset relative to the origin parameter.</param>
/// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"></see> indicating the reference point used to obtain the new position.</param>
/// <returns>
/// The new position within the current stream.
/// </returns>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. </exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
public override long Seek(long offset, SeekOrigin origin)
{
return _baseStream.Seek(offset, origin);
}
/// <summary>
/// Sets the length of the current stream.
/// </summary>
/// <param name="value">The desired length of the current stream in bytes.</param>
/// <exception cref="T:System.NotSupportedException">The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
public override void SetLength(long value)
{
_baseStream.SetLength(value);
}
private long _bytesWritten;
/// <summary>
/// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
/// </summary>
/// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
/// <param name="count">The number of bytes to be written to the current stream.</param>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <exception cref="T:System.NotSupportedException">The base stream does not support writing. </exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
/// <exception cref="T:System.ArgumentException">The sum of offset and count is greater than the buffer length. </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
public override void Write(byte[] buffer, int offset, int count)
{
Throttle(count);
_baseStream.Write(buffer, offset, count);
_bytesWritten += count;
}
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
await ThrottleAsync(count, cancellationToken).ConfigureAwait(false);
await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
_bytesWritten += count;
}
/// <summary>
/// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
/// </summary>
/// <returns>
/// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
/// </returns>
public override string ToString()
{
return _baseStream.ToString();
}
#endregion
private bool ThrottleCheck(int bufferSizeInBytes)
{
if (_bytesWritten < MinThrottlePosition)
{
return false;
}
// Make sure the buffer isn't empty.
if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0)
{
return false;
}
return true;
}
#region Protected methods
/// <summary>
/// Throttles for the specified buffer size in bytes.
/// </summary>
/// <param name="bufferSizeInBytes">The buffer size in bytes.</param>
protected void Throttle(int bufferSizeInBytes)
{
if (!ThrottleCheck(bufferSizeInBytes))
{
return;
}
_byteCount += bufferSizeInBytes;
long elapsedMilliseconds = CurrentMilliseconds - _start;
if (elapsedMilliseconds > 0)
{
// Calculate the current bps.
long bps = _byteCount * 1000L / elapsedMilliseconds;
// If the bps are more then the maximum bps, try to throttle.
if (bps > _maximumBytesPerSecond)
{
// Calculate the time to sleep.
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
if (toSleep > 1)
{
try
{
// The time to sleep is more then a millisecond, so sleep.
var task = Task.Delay(toSleep);
Task.WaitAll(task);
}
catch
{
// Eatup ThreadAbortException.
}
// A sleep has been done, reset.
Reset();
}
}
}
}
protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken)
{
if (!ThrottleCheck(bufferSizeInBytes))
{
return;
}
_byteCount += bufferSizeInBytes;
long elapsedMilliseconds = CurrentMilliseconds - _start;
if (elapsedMilliseconds > 0)
{
// Calculate the current bps.
long bps = _byteCount * 1000L / elapsedMilliseconds;
// If the bps are more then the maximum bps, try to throttle.
if (bps > _maximumBytesPerSecond)
{
// Calculate the time to sleep.
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
if (toSleep > 1)
{
// The time to sleep is more then a millisecond, so sleep.
await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false);
// A sleep has been done, reset.
Reset();
}
}
}
}
/// <summary>
/// Will reset the bytecount to 0 and reset the start time to the current time.
/// </summary>
protected void Reset()
{
long difference = CurrentMilliseconds - _start;
// Only reset counters when a known history is available of more then 1 second.
if (difference > 1000)
{
_byteCount = 0;
_start = CurrentMilliseconds;
}
}
#endregion
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
namespace Emby.Server.Implementations.Library
{
public class DefaultPasswordResetProvider : IPasswordResetProvider
{
public string Name => "Default Password Reset Provider";
public bool IsEnabled => true;
private readonly string _passwordResetFileBase;
private readonly string _passwordResetFileBaseDir;
private readonly string _passwordResetFileBaseName = "passwordreset";
private readonly IJsonSerializer _jsonSerializer;
private readonly IUserManager _userManager;
private readonly ICryptoProvider _crypto;
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
{
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
_jsonSerializer = jsonSerializer;
_userManager = userManager;
_crypto = cryptoProvider;
}
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
SerializablePasswordReset spr;
HashSet<string> usersreset = new HashSet<string>();
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
{
using (var str = File.OpenRead(resetfile))
{
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
}
if (spr.ExpirationDate < DateTime.Now)
{
File.Delete(resetfile);
}
else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
{
var resetUser = _userManager.GetUserByName(spr.UserName);
if (resetUser == null)
{
throw new Exception($"User with a username of {spr.UserName} not found");
}
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
usersreset.Add(resetUser.Name);
File.Delete(resetfile);
}
}
if (usersreset.Count < 1)
{
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
}
else
{
return new PinRedeemResult
{
Success = true,
UsersReset = usersreset.ToArray()
};
}
}
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
{
string pin = string.Empty;
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
{
byte[] bytes = new byte[4];
cryptoRandom.GetBytes(bytes);
pin = BitConverter.ToString(bytes);
}
DateTime expireTime = DateTime.Now.AddMinutes(30);
string filePath = _passwordResetFileBase + user.InternalId + ".json";
SerializablePasswordReset spr = new SerializablePasswordReset
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = filePath,
UserName = user.Name
};
try
{
using (FileStream fileStream = File.OpenWrite(filePath))
{
_jsonSerializer.SerializeToStream(spr, fileStream);
await fileStream.FlushAsync().ConfigureAwait(false);
}
}
catch (Exception e)
{
throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e);
}
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
PinFile = filePath
};
}
private class SerializablePasswordReset : PasswordPinCreationResult
{
public string Pin { get; set; }
public string UserName { get; set; }
}
}
}

View File

@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"folder",
"thumb",
"landscape",
"fanart",
"backdrop",
"poster",
"cover",
"logo",
"default"
};
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
{ {
@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (!args.IsDirectory) if (!args.IsDirectory)
{ {
// Must be an image file within a photo collection // Must be an image file within a photo collection
var collectionType = args.GetCollectionType(); var collectionType = args.CollectionType;
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) || if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
{ {
if (IsImageFile(args.Path, _imageProcessor)) if (IsImageFile(args.Path, _imageProcessor))
{ {
@ -74,43 +86,29 @@ namespace Emby.Server.Implementations.Library.Resolvers
} }
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename) internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
{ => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
if (imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
private static readonly HashSet<string> IgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"folder",
"thumb",
"landscape",
"fanart",
"backdrop",
"poster",
"cover",
"logo",
"default"
};
internal static bool IsImageFile(string path, IImageProcessor imageProcessor) internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{ {
var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty; if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (IgnoreFiles.Contains(filename)) var filename = Path.GetFileNameWithoutExtension(path);
if (_ignoreFiles.Contains(filename))
{ {
return false; return false;
} }
if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
{ {
return false; return false;
} }
return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal); string extension = Path.GetExtension(path).TrimStart('.');
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
} }
} }
} }

View File

@ -79,6 +79,9 @@ namespace Emby.Server.Implementations.Library
private IAuthenticationProvider[] _authenticationProviders; private IAuthenticationProvider[] _authenticationProviders;
private DefaultAuthenticationProvider _defaultAuthenticationProvider; private DefaultAuthenticationProvider _defaultAuthenticationProvider;
private IPasswordResetProvider[] _passwordResetProviders;
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
public UserManager( public UserManager(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
@ -102,8 +105,6 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem; _fileSystem = fileSystem;
ConfigurationManager = configurationManager; ConfigurationManager = configurationManager;
_users = Array.Empty<User>(); _users = Array.Empty<User>();
DeletePinFile();
} }
public NameIdPair[] GetAuthenticationProviders() public NameIdPair[] GetAuthenticationProviders()
@ -120,11 +121,29 @@ namespace Emby.Server.Implementations.Library
.ToArray(); .ToArray();
} }
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders) public NameIdPair[] GetPasswordResetProviders()
{
return _passwordResetProviders
.Where(i => i.IsEnabled)
.OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
.ThenBy(i => i.Name)
.Select(i => new NameIdPair
{
Name = i.Name,
Id = GetPasswordResetProviderId(i)
})
.ToArray();
}
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders)
{ {
_authenticationProviders = authenticationProviders.ToArray(); _authenticationProviders = authenticationProviders.ToArray();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_passwordResetProviders = passwordResetProviders.ToArray();
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
} }
#region UserUpdated Event #region UserUpdated Event
@ -258,24 +277,35 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var success = false; var success = false;
string updatedUsername = null;
IAuthenticationProvider authenticationProvider = null; IAuthenticationProvider authenticationProvider = null;
if (user != null) if (user != null)
{ {
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.Item1; authenticationProvider = authResult.Item1;
success = authResult.Item2; updatedUsername = authResult.Item2;
success = authResult.Item3;
} }
else else
{ {
// user is null // user is null
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.Item1; authenticationProvider = authResult.Item1;
success = authResult.Item2; updatedUsername = authResult.Item2;
success = authResult.Item3;
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
{ {
user = await CreateUser(username).ConfigureAwait(false); // We should trust the user that the authprovider says, not what was typed
if (updatedUsername != username)
{
username = updatedUsername;
}
// Search the database for the user again; the authprovider might have created it
user = Users
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
if (hasNewUserPolicy != null) if (hasNewUserPolicy != null)
@ -342,11 +372,21 @@ namespace Emby.Server.Implementations.Library
return provider.GetType().FullName; return provider.GetType().FullName;
} }
private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
{
return provider.GetType().FullName;
}
private IAuthenticationProvider GetAuthenticationProvider(User user) private IAuthenticationProvider GetAuthenticationProvider(User user)
{ {
return GetAuthenticationProviders(user).First(); return GetAuthenticationProviders(user).First();
} }
private IPasswordResetProvider GetPasswordResetProvider(User user)
{
return GetPasswordResetProviders(user)[0];
}
private IAuthenticationProvider[] GetAuthenticationProviders(User user) private IAuthenticationProvider[] GetAuthenticationProviders(User user)
{ {
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
@ -366,32 +406,59 @@ namespace Emby.Server.Implementations.Library
return providers; return providers;
} }
private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) private IPasswordResetProvider[] GetPasswordResetProviders(User user)
{
var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
if (!string.IsNullOrEmpty(passwordResetProviderId))
{
providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
}
if (providers.Length == 0)
{
providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
}
return providers;
}
private async Task<Tuple<string, bool>> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
{ {
try try
{ {
var requiresResolvedUser = provider as IRequiresResolvedUser; var requiresResolvedUser = provider as IRequiresResolvedUser;
ProviderAuthenticationResult authenticationResult = null;
if (requiresResolvedUser != null) if (requiresResolvedUser != null)
{ {
await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
} }
else else
{ {
await provider.Authenticate(username, password).ConfigureAwait(false); authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false);
} }
return true; if(authenticationResult.Username != username)
{
_logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
username = authenticationResult.Username;
}
return new Tuple<string, bool>(username, true);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
return false; return new Tuple<string, bool>(username, false);
} }
} }
private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) private async Task<Tuple<IAuthenticationProvider, string, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
{ {
string updatedUsername = null;
bool success = false; bool success = false;
IAuthenticationProvider authenticationProvider = null; IAuthenticationProvider authenticationProvider = null;
@ -410,11 +477,14 @@ namespace Emby.Server.Implementations.Library
{ {
foreach (var provider in GetAuthenticationProviders(user)) foreach (var provider in GetAuthenticationProviders(user))
{ {
success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
updatedUsername = providerAuthResult.Item1;
success = providerAuthResult.Item2;
if (success) if (success)
{ {
authenticationProvider = provider; authenticationProvider = provider;
username = updatedUsername;
break; break;
} }
} }
@ -436,7 +506,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success); return new Tuple<IAuthenticationProvider, string, bool>(authenticationProvider, username, success);
} }
private void UpdateInvalidLoginAttemptCount(User user, int newValue) private void UpdateInvalidLoginAttemptCount(User user, int newValue)
@ -480,7 +550,7 @@ namespace Emby.Server.Implementations.Library
{ {
return string.IsNullOrEmpty(user.EasyPassword) return string.IsNullOrEmpty(user.EasyPassword)
? null ? null
: user.EasyPassword; : (new PasswordHash(user.EasyPassword)).Hash;
} }
/// <summary> /// <summary>
@ -526,7 +596,7 @@ namespace Emby.Server.Implementations.Library
} }
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetLocalPasswordHash(user));
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
hasConfiguredEasyPassword : hasConfiguredEasyPassword :
@ -844,159 +914,51 @@ namespace Emby.Server.Implementations.Library
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DateCreated = DateTime.UtcNow, DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow, DateModified = DateTime.UtcNow,
UsesIdForConfigurationPath = true, UsesIdForConfigurationPath = true
//Salt = BCrypt.GenerateSalt()
}; };
} }
private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt");
private string _lastPin;
private PasswordPinCreationResult _lastPasswordPinCreationResult;
private int _pinAttempts;
private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
{
var num = new Random().Next(1, 9999);
var path = PasswordResetFile;
var pin = num.ToString("0000", CultureInfo.InvariantCulture);
_lastPin = pin;
var time = TimeSpan.FromMinutes(5);
var expiration = DateTime.UtcNow.Add(time);
var text = new StringBuilder();
var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
text.AppendLine("Use your web browser to visit:");
text.AppendLine(string.Empty);
text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html");
text.AppendLine(string.Empty);
text.AppendLine("Enter the following pin code:");
text.AppendLine(string.Empty);
text.AppendLine(pin);
text.AppendLine(string.Empty);
var localExpirationTime = expiration.ToLocalTime();
// Tuesday, 22 August 2006 06:30 AM
text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture));
File.WriteAllText(path, text.ToString(), Encoding.UTF8);
var result = new PasswordPinCreationResult
{
PinFile = path,
ExpirationDate = expiration
};
_lastPasswordPinCreationResult = result;
_pinAttempts = 0;
return result;
}
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{ {
DeletePinFile();
var user = string.IsNullOrWhiteSpace(enteredUsername) ? var user = string.IsNullOrWhiteSpace(enteredUsername) ?
null : null :
GetUserByName(enteredUsername); GetUserByName(enteredUsername);
var action = ForgotPasswordAction.InNetworkRequired; var action = ForgotPasswordAction.InNetworkRequired;
string pinFile = null;
DateTime? expirationDate = null;
if (user != null && !user.Policy.IsAdministrator) if (user != null && isInNetwork)
{ {
action = ForgotPasswordAction.ContactAdmin; var passwordResetProvider = GetPasswordResetProvider(user);
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
} }
else else
{ {
if (isInNetwork) return new ForgotPasswordResult
{ {
action = ForgotPasswordAction.PinCode; Action = action,
} PinFile = string.Empty
};
var result = await CreatePasswordResetPin().ConfigureAwait(false);
pinFile = result.PinFile;
expirationDate = result.ExpirationDate;
} }
return new ForgotPasswordResult
{
Action = action,
PinFile = pinFile,
PinExpirationDate = expirationDate
};
} }
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{ {
DeletePinFile(); foreach (var provider in _passwordResetProviders)
var usersReset = new List<string>();
var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
_lastPasswordPinCreationResult != null &&
_lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
if (valid)
{ {
_lastPin = null; var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
_lastPasswordPinCreationResult = null; if (result.Success)
foreach (var user in Users)
{ {
await ResetPassword(user).ConfigureAwait(false); return result;
if (user.Policy.IsDisabled)
{
user.Policy.IsDisabled = false;
UpdateUserPolicy(user, user.Policy, true);
}
usersReset.Add(user.Name);
}
}
else
{
_pinAttempts++;
if (_pinAttempts >= 3)
{
_lastPin = null;
_lastPasswordPinCreationResult = null;
} }
} }
return new PinRedeemResult return new PinRedeemResult
{ {
Success = valid, Success = false,
UsersReset = usersReset.ToArray() UsersReset = Array.Empty<string>()
}; };
} }
private void DeletePinFile()
{
try
{
_fileSystem.DeleteFile(PasswordResetFile);
}
catch
{
}
}
class PasswordPinCreationResult
{
public string PinFile { get; set; }
public DateTime ExpirationDate { get; set; }
}
public UserPolicy GetUserPolicy(User user) public UserPolicy GetUserPolicy(User user)
{ {
var path = GetPolicyFilePath(user); var path = GetPolicyFilePath(user);

View File

@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public string HomePageUrl => "https://github.com/jellyfin/jellyfin"; public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress) public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
{ {
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
@ -271,7 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
} }
public async Task RefreshTimers(CancellationToken cancellationToken, IProgress<double> progress) public async Task RefreshTimers(CancellationToken cancellationToken)
{ {
var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false); var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);

View File

@ -1087,8 +1087,8 @@ namespace Emby.Server.Implementations.LiveTv
if (coreService != null) if (coreService != null)
{ {
await coreService.RefreshSeriesTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false); await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
await coreService.RefreshTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false); await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
} }
// Load these now which will prefetch metadata // Load these now which will prefetch metadata

View File

@ -10,14 +10,12 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -52,9 +50,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var channelIdPrefix = GetFullChannelIdPrefix(info); var channelIdPrefix = GetFullChannelIdPrefix(info);
var result = await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
return result.Cast<ChannelInfo>().ToList();
} }
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@ -73,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list); return Task.FromResult(list);
} }
private string[] _disallowedSharedStreamExtensions = new string[] private static readonly string[] _disallowedSharedStreamExtensions = new string[]
{ {
".mkv", ".mkv",
".mp4", ".mp4",
@ -88,9 +84,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (tunerCount > 0) if (tunerCount > 0)
{ {
var tunerHostId = info.Id; var tunerHostId = info.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)).ToList(); var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count >= tunerCount) if (liveStreams.Count() >= tunerCount)
{ {
throw new LiveTvConflictException("M3U simultaneous stream limit has been reached."); throw new LiveTvConflictException("M3U simultaneous stream limit has been reached.");
} }
@ -98,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false); var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
var mediaSource = sources.First(); var mediaSource = sources[0];
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
{ {

View File

@ -11,7 +11,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
@ -62,12 +61,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult((Stream)File.OpenRead(url)); return Task.FromResult((Stream)File.OpenRead(url));
} }
const string ExtInfPrefix = "#EXTINF:"; private const string ExtInfPrefix = "#EXTINF:";
private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId) private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
{ {
var channels = new List<ChannelInfo>(); var channels = new List<ChannelInfo>();
string line; string line;
string extInf = ""; string extInf = string.Empty;
while ((line = reader.ReadLine()) != null) while ((line = reader.ReadLine()) != null)
{ {
@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.Path = line; channel.Path = line;
channels.Add(channel); channels.Add(channel);
extInf = ""; extInf = string.Empty;
} }
} }
@ -110,8 +110,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl) private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
{ {
var channel = new ChannelInfo(); var channel = new ChannelInfo()
channel.TunerHostId = tunerHostId; {
TunerHostId = tunerHostId
};
extInf = extInf.Trim(); extInf = extInf.Trim();
@ -137,13 +139,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
channelIdValues.Add(channelId); channelIdValues.Add(channelId);
} }
if (!string.IsNullOrWhiteSpace(tvgId)) if (!string.IsNullOrWhiteSpace(tvgId))
{ {
channelIdValues.Add(tvgId); channelIdValues.Add(tvgId);
} }
if (channelIdValues.Count > 0) if (channelIdValues.Count > 0)
{ {
channel.Id = string.Join("_", channelIdValues.ToArray()); channel.Id = string.Join("_", channelIdValues);
} }
return channel; return channel;
@ -152,7 +156,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
{ {
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null; var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null;
string numberString = null; string numberString = null;
string attributeValue; string attributeValue;

View File

@ -5,7 +5,7 @@
"Artists": "Umělci", "Artists": "Umělci",
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen", "AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
"Books": "Knihy", "Books": "Knihy",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie",
"Channels": "Kanály", "Channels": "Kanály",
"ChapterNameValue": "Kapitola {0}", "ChapterNameValue": "Kapitola {0}",
"Collections": "Kolekce", "Collections": "Kolekce",
@ -16,14 +16,14 @@
"Folders": "Složky", "Folders": "Složky",
"Genres": "Žánry", "Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba", "HeaderAlbumArtists": "Umělci alba",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování", "HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení umělci", "HeaderFavoriteArtists": "Oblíbení interpreti",
"HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbené písně", "HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "Živá TV", "HeaderLiveTV": "Live TV",
"HeaderNextUp": "Nadcházející", "HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek", "HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domáci videa", "HomeVideos": "Domáci videa",
@ -34,17 +34,17 @@
"LabelRunningTimeValue": "Délka média: {0}", "LabelRunningTimeValue": "Délka média: {0}",
"Latest": "Nejnovější", "Latest": "Nejnovější",
"MessageApplicationUpdated": "Jellyfin Server byl aktualizován", "MessageApplicationUpdated": "Jellyfin Server byl aktualizován",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin server byl aktualizován na verzi {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována", "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována",
"MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována", "MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
"MixedContent": "Smíšený obsah", "MixedContent": "Smíšený obsah",
"Movies": "Filmy", "Movies": "Filmy",
"Music": "Hudba", "Music": "Hudba",
"MusicVideos": "Hudební klipy", "MusicVideos": "Hudební klipy",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "Instalace {0} selhala",
"NameSeasonNumber": "Sezóna {0}", "NameSeasonNumber": "Sezóna {0}",
"NameSeasonUnknown": "Neznámá sezóna", "NameSeasonUnknown": "Neznámá sezóna",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "Nová verze Jellyfin serveru je k dispozici ke stažení.",
"NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace", "NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace",
"NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována", "NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována",
"NotificationOptionAudioPlayback": "Přehrávání audia zahájeno", "NotificationOptionAudioPlayback": "Přehrávání audia zahájeno",
@ -70,12 +70,12 @@
"ProviderValue": "Poskytl: {0}", "ProviderValue": "Poskytl: {0}",
"ScheduledTaskFailedWithName": "{0} selhalo", "ScheduledTaskFailedWithName": "{0} selhalo",
"ScheduledTaskStartedWithName": "{0} zahájeno", "ScheduledTaskStartedWithName": "{0} zahájeno",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} vyžaduje restart",
"Shows": "Seriály", "Shows": "Seriály",
"Songs": "Skladby", "Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.", "StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
"SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}", "SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
"SubtitlesDownloadedForItem": "Staženy titulky pro {0}", "SubtitlesDownloadedForItem": "Staženy titulky pro {0}",
"Sync": "Synchronizace", "Sync": "Synchronizace",
"System": "Systém", "System": "Systém",
@ -88,10 +88,10 @@
"UserOfflineFromDevice": "{0} se odpojil od {1}", "UserOfflineFromDevice": "{0} se odpojil od {1}",
"UserOnlineFromDevice": "{0} se připojil z {1}", "UserOnlineFromDevice": "{0} se připojil z {1}",
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}", "UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}", "UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",
"UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}", "UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií",
"ValueSpecialEpisodeName": "Speciál - {0}", "ValueSpecialEpisodeName": "Speciál - {0}",
"VersionNumber": "Verze {0}" "VersionNumber": "Verze {0}"
} }

View File

@ -61,8 +61,8 @@
"NotificationOptionUserLockedOut": "Bruger låst ude", "NotificationOptionUserLockedOut": "Bruger låst ude",
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt", "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet", "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
"Photos": "Fotos", "Photos": "Fotoer",
"Playlists": "Spillelister", "Playlists": "Afspilningslister",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} blev installeret", "PluginInstalledWithName": "{0} blev installeret",
"PluginUninstalledWithName": "{0} blev afinstalleret", "PluginUninstalledWithName": "{0} blev afinstalleret",

View File

@ -16,7 +16,7 @@
"Folders": "Φάκελοι", "Folders": "Φάκελοι",
"Genres": "Είδη", "Genres": "Είδη",
"HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών", "HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε", "HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@ -34,7 +34,7 @@
"LabelRunningTimeValue": "Διάρκεια: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα", "Latest": "Πρόσφατα",
"MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί", "MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Ο server Jellyfin αναβαθμίστηκε σε έκδοση {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί", "MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί",
"MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί", "MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί",
"MixedContent": "Ανάμεικτο Περιεχόμενο", "MixedContent": "Ανάμεικτο Περιεχόμενο",
@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε", "NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε",
"NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε", "NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε",
"NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε", "NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε",
"NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα",
"NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης", "NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης",
"NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο", "NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο",
"NotificationOptionPluginError": "Αποτυχία του plugin", "NotificationOptionPluginError": "Αποτυχία του plugin",
@ -75,7 +75,7 @@
"Songs": "Τραγούδια", "Songs": "Τραγούδια",
"StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.", "StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.",
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}", "SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
"SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}", "SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}",
"Sync": "Συγχρονισμός", "Sync": "Συγχρονισμός",
"System": "Σύστημα", "System": "Σύστημα",

View File

@ -1,97 +1,97 @@
{ {
"Albums": "Albums", "Albums": "Albom",
"AppDeviceValues": "App: {0}, Device: {1}", "AppDeviceValues": "App: {0}, Grät: {1}",
"Application": "Application", "Application": "Aawändig",
"Artists": "Artists", "Artists": "Könstler",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated", "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
"Books": "Büecher", "Books": "Büecher",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
"Channels": "Channels", "Channels": "Kanäu",
"ChapterNameValue": "Chapter {0}", "ChapterNameValue": "Kapitu {0}",
"Collections": "Collections", "Collections": "Sammlige",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} esch offline gange",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} esch online cho",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
"Favorites": "Favorites", "Favorites": "Favorite",
"Folders": "Folders", "Folders": "Ordner",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Albuminterprete", "HeaderAlbumArtists": "Albom-Könstler",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Wiiterluege", "HeaderContinueWatching": "Wiiterluege",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Lieblingsalbe",
"HeaderFavoriteArtists": "Besti Interpret", "HeaderFavoriteArtists": "Lieblings-Interprete",
"HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteEpisodes": "Lieblingsepisode",
"HeaderFavoriteShows": "Favorite Shows", "HeaderFavoriteShows": "Lieblingsserie",
"HeaderFavoriteSongs": "Besti Lieder", "HeaderFavoriteSongs": "Lieblingslieder",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "Live-Färnseh",
"HeaderNextUp": "Next Up", "HeaderNextUp": "Als nächts",
"HeaderRecordingGroups": "Ufnahmegruppe", "HeaderRecordingGroups": "Ufnahmegruppe",
"HomeVideos": "Heimfilmli", "HomeVideos": "Heimfilmli",
"Inherit": "Hinzuefüege", "Inherit": "Hinzuefüege",
"ItemAddedWithName": "{0} was added to the library", "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
"ItemRemovedWithName": "{0} was removed from the library", "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
"LabelIpAddressValue": "Ip address: {0}", "LabelIpAddressValue": "IP-Adrässe: {0}",
"LabelRunningTimeValue": "Running time: {0}", "LabelRunningTimeValue": "Loufziit: {0}",
"Latest": "Letschte", "Latest": "Nöischti",
"MessageApplicationUpdated": "Jellyfin Server has been updated", "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
"MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
"MixedContent": "Gmischte Inhalt", "MixedContent": "Gmeschti Inhäut",
"Movies": "Movies", "Movies": "Film",
"Music": "Musig", "Music": "Musig",
"MusicVideos": "Musigfilm", "MusicVideos": "Musigvideos",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "Installation vo {0} fäugschlage",
"NameSeasonNumber": "Season {0}", "NameSeasonNumber": "Staffle {0}",
"NameSeasonUnknown": "Season Unknown", "NameSeasonUnknown": "Staffle unbekannt",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "E nöi Version vo Jellyfin Server esch zom Download parat.",
"NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateAvailable": "Aawändigsupdate verfüegbar",
"NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionApplicationUpdateInstalled": "Aawändigsupdate installiert",
"NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
"NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionCameraImageUploaded": "Foti ueglade",
"NotificationOptionInstallationFailed": "Installation failure", "NotificationOptionInstallationFailed": "Installationsfäuer",
"NotificationOptionNewLibraryContent": "New content added", "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
"NotificationOptionPluginError": "Plugin failure", "NotificationOptionPluginError": "Plugin-Fäuer",
"NotificationOptionPluginInstalled": "Plugin installed", "NotificationOptionPluginInstalled": "Plugin installiert",
"NotificationOptionPluginUninstalled": "Plugin uninstalled", "NotificationOptionPluginUninstalled": "Plugin deinstalliert",
"NotificationOptionPluginUpdateInstalled": "Plugin update installed", "NotificationOptionPluginUpdateInstalled": "Pluginupdate installiert",
"NotificationOptionServerRestartRequired": "Server restart required", "NotificationOptionServerRestartRequired": "Serverneustart notwändig",
"NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionTaskFailed": "Planti Uufgab fäugschlage",
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "Benotzer usgschlosse",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "Videowedergab gstartet",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped", "NotificationOptionVideoPlaybackStopped": "Videowedergab gstoppt",
"Photos": "Fotis", "Photos": "Fotis",
"Playlists": "Abspielliste", "Playlists": "Wedergabeliste",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed", "PluginInstalledWithName": "{0} esch installiert worde",
"PluginUninstalledWithName": "{0} was uninstalled", "PluginUninstalledWithName": "{0} esch deinstalliert worde",
"PluginUpdatedWithName": "{0} was updated", "PluginUpdatedWithName": "{0} esch updated worde",
"ProviderValue": "Provider: {0}", "ProviderValue": "Aabieter: {0}",
"ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskFailedWithName": "{0} esch fäugschlage",
"ScheduledTaskStartedWithName": "{0} started", "ScheduledTaskStartedWithName": "{0} het gstartet",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} mues nöi gstartet wärde",
"Shows": "Shows", "Shows": "Serie",
"Songs": "Songs", "Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "SubtitlesDownloadedForItem": "Ondertetle abeglade för {0}",
"Sync": "Sync", "Sync": "Synchronisation",
"System": "System", "System": "System",
"TvShows": "TV Shows", "TvShows": "Färnsehserie",
"User": "User", "User": "Benotzer",
"UserCreatedWithName": "User {0} has been created", "UserCreatedWithName": "Benotzer {0} esch erstöut worde",
"UserDeletedWithName": "User {0} has been deleted", "UserDeletedWithName": "Benotzer {0} esch glösche worde",
"UserDownloadingItemWithValues": "{0} is downloading {1}", "UserDownloadingItemWithValues": "{0} ladt {1} abe",
"UserLockedOutWithName": "User {0} has been locked out", "UserLockedOutWithName": "Benotzer {0} esch usgschlosse worde",
"UserOfflineFromDevice": "{0} has disconnected from {1}", "UserOfflineFromDevice": "{0} esch vo {1} trennt worde",
"UserOnlineFromDevice": "{0} is online from {1}", "UserOnlineFromDevice": "{0} esch online vo {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}", "UserPasswordChangedWithName": "S'Passwort för Benotzer {0} esch gänderet worde",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "Benotzerrechtlinie för {0} esch aktualisiert worde",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStartedPlayingItemWithValues": "{0} hed d'Wedergab vo {1} of {2} gstartet",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
"ValueSpecialEpisodeName": "Spezial - {0}", "ValueSpecialEpisodeName": "Extra - {0}",
"VersionNumber": "Version {0}" "VersionNumber": "Version {0}"
} }

View File

@ -0,0 +1 @@
{}

View File

@ -5,7 +5,7 @@
"Artists": "Oryndaýshylar", "Artists": "Oryndaýshylar",
"AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy", "AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
"Books": "Kitaptar", "Books": "Kitaptar",
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy", "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep salyndy",
"Channels": "Arnalar", "Channels": "Arnalar",
"ChapterNameValue": "{0}-sahna", "ChapterNameValue": "{0}-sahna",
"Collections": "Jıyntyqtar", "Collections": "Jıyntyqtar",
@ -35,8 +35,8 @@
"Latest": "Eń keıingi", "Latest": "Eń keıingi",
"MessageApplicationUpdated": "Jellyfin Serveri jańartyldy", "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy", "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
"MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy", "MessageNamedServerConfigurationUpdatedWithValue": "Server konfıgýrasýasynyń {0} bólimi jańartyldy",
"MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy", "MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy",
"MixedContent": "Aralas mazmun", "MixedContent": "Aralas mazmun",
"Movies": "Fılmder", "Movies": "Fılmder",
"Music": "Mýzyka", "Music": "Mýzyka",
@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy", "NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
"NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy", "NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
"NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy", "NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy",
"NotificationOptionCameraImageUploaded": "Kameradan fotosýret keri qotarylǵan", "NotificationOptionCameraImageUploaded": "Kameradan fotosýret júktep salynǵan",
"NotificationOptionInstallationFailed": "Ornatý sátsizdigi", "NotificationOptionInstallationFailed": "Ornatý sátsizdigi",
"NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen", "NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
"NotificationOptionPluginError": "Plagın sátsizdigi", "NotificationOptionPluginError": "Plagın sátsizdigi",

View File

@ -1,21 +1,21 @@
{ {
"Albums": "Albums", "Albums": "Albumai",
"AppDeviceValues": "App: {0}, Device: {1}", "AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application", "Application": "Application",
"Artists": "Artists", "Artists": "Atlikėjai",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated", "AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books", "Books": "Knygos",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels", "Channels": "Kanalai",
"ChapterNameValue": "Chapter {0}", "ChapterNameValue": "Chapter {0}",
"Collections": "Collections", "Collections": "Kolekcijos",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites", "Favorites": "Mėgstami",
"Folders": "Folders", "Folders": "Katalogai",
"Genres": "Žanrai", "Genres": "Žanrai",
"HeaderAlbumArtists": "Album Artists", "HeaderAlbumArtists": "Albumo atlikėjai",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Žiūrėti toliau", "HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Favorite Albums",

View File

@ -9,7 +9,7 @@
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}", "ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke", "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeni", "Favorites": "Priljubljeni",
@ -33,9 +33,9 @@
"LabelIpAddressValue": "IP naslov: {0}", "LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Čas trajanja: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Najnovejše", "Latest": "Najnovejše",
"MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen", "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
"MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}", "MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
"MixedContent": "Razne vsebine", "MixedContent": "Razne vsebine",
"Movies": "Filmi", "Movies": "Filmi",
@ -57,41 +57,41 @@
"NotificationOptionPluginUninstalled": "Dodatek odstranjen", "NotificationOptionPluginUninstalled": "Dodatek odstranjen",
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
"NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped", "NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
"Photos": "Photos", "Photos": "Fotografije",
"Playlists": "Playlists", "Playlists": "Seznami predvajanja",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed", "PluginInstalledWithName": "{0} je bil nameščen",
"PluginUninstalledWithName": "{0} was uninstalled", "PluginUninstalledWithName": "{0} je bil odstranjen",
"PluginUpdatedWithName": "{0} was updated", "PluginUpdatedWithName": "{0} je bil posodobljen",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskFailedWithName": "{0} ni uspelo",
"ScheduledTaskStartedWithName": "{0} started", "ScheduledTaskStartedWithName": "{0} začeto",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
"Shows": "Serije", "Shows": "Serije",
"Songs": "Songs", "Songs": "Pesmi",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "SubtitlesDownloadedForItem": "Podnapisi preneseni za {0}",
"Sync": "Sync", "Sync": "Sinhroniziraj",
"System": "System", "System": "System",
"TvShows": "TV Shows", "TvShows": "TV serije",
"User": "User", "User": "User",
"UserCreatedWithName": "User {0} has been created", "UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
"UserDeletedWithName": "User {0} has been deleted", "UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
"UserDownloadingItemWithValues": "{0} is downloading {1}", "UserDownloadingItemWithValues": "{0} prenaša {1}",
"UserLockedOutWithName": "User {0} has been locked out", "UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
"UserOfflineFromDevice": "{0} has disconnected from {1}", "UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
"UserOnlineFromDevice": "{0} is online from {1}", "UserOnlineFromDevice": "{0} je aktiven iz {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}", "UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
"ValueSpecialEpisodeName": "Special - {0}", "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}" "VersionNumber": "Version {0}"
} }

View File

@ -0,0 +1,93 @@
{
"Albums": "專輯",
"AppDeviceValues": "應用: {0}, 裝置: {1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
"Books": "圖書",
"CameraImageUploadedFrom": "{0} 已經成功上傳一張相片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷線",
"DeviceOnlineWithName": "{0} 已經連線",
"FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
"HeaderCameraUploads": "相機上傳",
"HeaderContinueWatching": "繼續觀賞",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
"HeaderFavoriteEpisodes": "最愛級數",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
"HomeVideos": "自製影片",
"ItemAddedWithName": "{0} 已新增至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
"LabelIpAddressValue": "IP 位置: {0}",
"LabelRunningTimeValue": "運行時間: {0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
"MusicVideos": "音樂MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionAudioPlayback": "音樂開始播放",
"NotificationOptionAudioPlaybackStopped": "音樂停止播放",
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
"NotificationOptionPluginError": "外掛失敗",
"NotificationOptionPluginInstalled": "外掛已安裝",
"NotificationOptionPluginUninstalled": "外掛已移除",
"NotificationOptionPluginUpdateInstalled": "已更新外掛",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
"NotificationOptionVideoPlayback": "影片開始播放",
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "外掛",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
"ProviderValue": "提供商: {0}",
"ScheduledTaskFailedWithName": "{0} 已失敗",
"ScheduledTaskStartedWithName": "{0} 已開始",
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin Server正在啟動請稍後再試一次。",
"SubtitlesDownloadedForItem": "已為 {0} 下載字幕",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已建立",
"UserDeletedWithName": "使用者 {0} 已移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
"UserOfflineFromDevice": "{0} 已從 {1} 斷線",
"UserOnlineFromDevice": "{0} 已連線,來自 {1}",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本 {0}"
}

View File

@ -43,6 +43,11 @@ namespace Emby.Server.Implementations.Services
{ {
var contentLength = bytesResponse.Length; var contentLength = bytesResponse.Length;
if (response != null)
{
response.OriginalResponse.ContentLength = contentLength;
}
if (contentLength > 0) if (contentLength > 0)
{ {
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Text; using System.Text;
@ -20,6 +21,8 @@ namespace Emby.Server.Implementations.Services
{ {
response.StatusCode = (int)HttpStatusCode.NoContent; response.StatusCode = (int)HttpStatusCode.NoContent;
} }
response.OriginalResponse.ContentLength = 0;
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -39,11 +42,6 @@ namespace Emby.Server.Implementations.Services
response.StatusCode = httpResult.Status; response.StatusCode = httpResult.Status;
response.StatusDescription = httpResult.StatusCode.ToString(); response.StatusDescription = httpResult.StatusCode.ToString();
//if (string.IsNullOrEmpty(httpResult.ContentType))
//{
// httpResult.ContentType = defaultContentType;
//}
//response.ContentType = httpResult.ContentType;
} }
var responseOptions = result as IHasHeaders; var responseOptions = result as IHasHeaders;
@ -53,6 +51,7 @@ namespace Emby.Server.Implementations.Services
{ {
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{ {
response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
continue; continue;
} }
@ -72,52 +71,37 @@ namespace Emby.Server.Implementations.Services
response.ContentType += "; charset=utf-8"; response.ContentType += "; charset=utf-8";
} }
var asyncStreamWriter = result as IAsyncStreamWriter; switch (result)
if (asyncStreamWriter != null)
{ {
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken); case IAsyncStreamWriter asyncStreamWriter:
} return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
case IStreamWriter streamWriter:
streamWriter.WriteTo(response.OutputStream);
return Task.CompletedTask;
case FileWriter fileWriter:
return fileWriter.WriteToAsync(response, cancellationToken);
case Stream stream:
return CopyStream(stream, response.OutputStream);
case byte[] bytes:
response.ContentType = "application/octet-stream";
response.OriginalResponse.ContentLength = bytes.Length;
var streamWriter = result as IStreamWriter; if (bytes.Length > 0)
if (streamWriter != null) {
{ return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
streamWriter.WriteTo(response.OutputStream); }
return Task.CompletedTask;
}
var fileWriter = result as FileWriter; return Task.CompletedTask;
if (fileWriter != null) case string responseText:
{ var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
return fileWriter.WriteToAsync(response, cancellationToken); response.OriginalResponse.ContentLength = responseTextAsBytes.Length;
}
var stream = result as Stream; if (responseTextAsBytes.Length > 0)
if (stream != null) {
{ return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
return CopyStream(stream, response.OutputStream); }
}
var bytes = result as byte[]; return Task.CompletedTask;
if (bytes != null)
{
response.ContentType = "application/octet-stream";
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
}
var responseText = result as string;
if (responseText != null)
{
bytes = Encoding.UTF8.GetBytes(responseText);
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
} }
return WriteObject(request, result, response); return WriteObject(request, result, response);
@ -143,14 +127,13 @@ namespace Emby.Server.Implementations.Services
ms.Position = 0; ms.Position = 0;
var contentLength = ms.Length; var contentLength = ms.Length;
response.OriginalResponse.ContentLength = contentLength;
if (contentLength > 0) if (contentLength > 0)
{ {
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false); await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
} }
} }
//serializer(result, outputStream);
} }
} }
} }

View File

@ -1,26 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
namespace Emby.Server.Implementations.Services namespace Emby.Server.Implementations.Services
{ {
public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
public delegate object ActionInvokerFn(object intance, object request); public delegate object ActionInvokerFn(object intance, object request);
public delegate void VoidActionInvokerFn(object intance, object request); public delegate void VoidActionInvokerFn(object intance, object request);
public class ServiceController public class ServiceController
{ {
public static ServiceController Instance; public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
public ServiceController()
{
Instance = this;
}
public void Init(HttpListenerHost appHost, Type[] serviceTypes)
{ {
foreach (var serviceType in serviceTypes) foreach (var serviceType in serviceTypes)
{ {
@ -37,7 +28,11 @@ namespace Emby.Server.Implementations.Services
foreach (var mi in serviceType.GetActions()) foreach (var mi in serviceType.GetActions())
{ {
var requestType = mi.GetParameters()[0].ParameterType; var requestType = mi.GetParameters()[0].ParameterType;
if (processedReqs.Contains(requestType)) continue; if (processedReqs.Contains(requestType))
{
continue;
}
processedReqs.Add(requestType); processedReqs.Add(requestType);
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
@ -55,18 +50,6 @@ namespace Emby.Server.Implementations.Services
} }
} }
public static Type FirstGenericType(Type type)
{
while (type != null)
{
if (type.GetTypeInfo().IsGenericType)
return type;
type = type.GetTypeInfo().BaseType;
}
return null;
}
public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
@ -84,17 +67,24 @@ namespace Emby.Server.Implementations.Services
public void RegisterRestPath(RestPath restPath) public void RegisterRestPath(RestPath restPath)
{ {
if (!restPath.Path.StartsWith("/")) if (restPath.Path[0] != '/')
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
{ {
pathsAtFirstMatch = new List<RestPath>(); throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch; }
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
{
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
}
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
{
pathsAtFirstMatch.Add(restPath);
}
else
{
RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
} }
pathsAtFirstMatch.Add(restPath);
} }
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
@ -155,17 +145,15 @@ namespace Emby.Server.Implementations.Services
return null; return null;
} }
public Task<object> Execute(HttpListenerHost appHost, object requestDto, IRequest req) public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
{ {
req.Dto = requestDto; req.Dto = requestDto;
var requestType = requestDto.GetType(); var requestType = requestDto.GetType();
req.OperationName = requestType.Name; req.OperationName = requestType.Name;
var serviceType = appHost.GetServiceTypeByRequest(requestType); var serviceType = httpHost.GetServiceTypeByRequest(requestType);
var service = appHost.CreateInstance(serviceType); var service = httpHost.CreateInstance(serviceType);
//var service = typeFactory.CreateInstance(serviceType);
var serviceRequiresContext = service as IRequiresRequest; var serviceRequiresContext = service as IRequiresRequest;
if (serviceRequiresContext != null) if (serviceRequiresContext != null)

View File

@ -11,6 +11,16 @@ namespace Emby.Server.Implementations.Services
{ {
public class ServiceHandler public class ServiceHandler
{ {
public RestPath RestPath { get; }
public string ResponseContentType { get; }
internal ServiceHandler(RestPath restPath, string responseContentType)
{
RestPath = restPath;
ResponseContentType = responseContentType;
}
protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
{ {
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
@ -18,24 +28,18 @@ namespace Emby.Server.Implementations.Services
var deserializer = RequestHelper.GetRequestReader(host, contentType); var deserializer = RequestHelper.GetRequestReader(host, contentType);
if (deserializer != null) if (deserializer != null)
{ {
return deserializer(requestType, httpReq.InputStream); return deserializer.Invoke(requestType, httpReq.InputStream);
} }
} }
return Task.FromResult(host.CreateInstance(requestType)); return Task.FromResult(host.CreateInstance(requestType));
} }
public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType)
{
pathInfo = GetSanitizedPathInfo(pathInfo, out contentType);
return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo);
}
public static string GetSanitizedPathInfo(string pathInfo, out string contentType) public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
{ {
contentType = null; contentType = null;
var pos = pathInfo.LastIndexOf('.'); var pos = pathInfo.LastIndexOf('.');
if (pos >= 0) if (pos != -1)
{ {
var format = pathInfo.Substring(pos + 1); var format = pathInfo.Substring(pos + 1);
contentType = GetFormatContentType(format); contentType = GetFormatContentType(format);
@ -44,58 +48,38 @@ namespace Emby.Server.Implementations.Services
pathInfo = pathInfo.Substring(0, pos); pathInfo = pathInfo.Substring(0, pos);
} }
} }
return pathInfo; return pathInfo;
} }
private static string GetFormatContentType(string format) private static string GetFormatContentType(string format)
{ {
//built-in formats //built-in formats
if (format == "json") switch (format)
return "application/json"; {
if (format == "xml") case "json": return "application/json";
return "application/xml"; case "xml": return "application/xml";
default: return null;
return null; }
} }
public RestPath GetRestPath(string httpMethod, string pathInfo) public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, IResponse httpRes, ILogger logger, CancellationToken cancellationToken)
{ {
if (this.RestPath == null) httpReq.Items["__route"] = RestPath;
{
this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out string contentType);
if (contentType != null)
ResponseContentType = contentType;
}
return this.RestPath;
}
public RestPath RestPath { get; set; }
// Set from SSHHF.GetHandlerForPathInfo()
public string ResponseContentType { get; set; }
public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName, CancellationToken cancellationToken)
{
var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo);
if (restPath == null)
{
throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo);
}
SetRoute(httpReq, restPath);
if (ResponseContentType != null) if (ResponseContentType != null)
{
httpReq.ResponseContentType = ResponseContentType; httpReq.ResponseContentType = ResponseContentType;
}
var request = httpReq.Dto = await CreateRequest(appHost, httpReq, restPath, logger).ConfigureAwait(false); var request = httpReq.Dto = await CreateRequest(httpHost, httpReq, RestPath, logger).ConfigureAwait(false);
appHost.ApplyRequestFilters(httpReq, httpRes, request); httpHost.ApplyRequestFilters(httpReq, httpRes, request);
var response = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false); var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters // Apply response filters
foreach (var responseFilter in appHost.ResponseFilters) foreach (var responseFilter in httpHost.ResponseFilters)
{ {
responseFilter(httpReq, httpRes, response); responseFilter(httpReq, httpRes, response);
} }
@ -152,7 +136,11 @@ namespace Emby.Server.Implementations.Services
foreach (var name in request.QueryString.Keys) foreach (var name in request.QueryString.Keys)
{ {
if (name == null) continue; //thank you ASP.NET if (name == null)
{
// thank you ASP.NET
continue;
}
var values = request.QueryString[name]; var values = request.QueryString[name];
if (values.Count == 1) if (values.Count == 1)
@ -175,7 +163,11 @@ namespace Emby.Server.Implementations.Services
{ {
foreach (var name in formData.Keys) foreach (var name in formData.Keys)
{ {
if (name == null) continue; //thank you ASP.NET if (name == null)
{
// thank you ASP.NET
continue;
}
var values = formData.GetValues(name); var values = formData.GetValues(name);
if (values.Count == 1) if (values.Count == 1)
@ -210,7 +202,12 @@ namespace Emby.Server.Implementations.Services
foreach (var name in request.QueryString.Keys) foreach (var name in request.QueryString.Keys)
{ {
if (name == null) continue; //thank you ASP.NET if (name == null)
{
// thank you ASP.NET
continue;
}
map[name] = request.QueryString[name]; map[name] = request.QueryString[name];
} }
@ -221,7 +218,12 @@ namespace Emby.Server.Implementations.Services
{ {
foreach (var name in formData.Keys) foreach (var name in formData.Keys)
{ {
if (name == null) continue; //thank you ASP.NET if (name == null)
{
// thank you ASP.NET
continue;
}
map[name] = formData[name]; map[name] = formData[name];
} }
} }
@ -229,17 +231,5 @@ namespace Emby.Server.Implementations.Services
return map; return map;
} }
private static void SetRoute(IRequest req, RestPath route)
{
req.Items["__route"] = route;
}
private static RestPath GetRoute(IRequest req)
{
req.Items.TryGetValue("__route", out var route);
return route as RestPath;
}
} }
} }

View File

@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
string propertyName = pair.Key; string propertyName = pair.Key;
string propertyTextValue = pair.Value; string propertyTextValue = pair.Value;
if (string.IsNullOrEmpty(propertyTextValue) if (propertyTextValue == null
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|| propertySerializerEntry.PropertySetFn == null) || propertySerializerEntry.PropertySetFn == null)
{ {

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using MediaBrowser.Controller.Net;
using System.Threading.Tasks;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Emby.Server.Implementations.HttpServer;
namespace Emby.Server.Implementations.Services namespace Emby.Server.Implementations.Services
{ {
@ -109,10 +109,16 @@ namespace Emby.Server.Implementations.Services
public class SwaggerService : IService, IRequiresRequest public class SwaggerService : IService, IRequiresRequest
{ {
private readonly IHttpServer _httpServer;
private SwaggerSpec _spec; private SwaggerSpec _spec;
public IRequest Request { get; set; } public IRequest Request { get; set; }
public SwaggerService(IHttpServer httpServer)
{
_httpServer = httpServer;
}
public object Get(GetSwaggerSpec request) public object Get(GetSwaggerSpec request)
{ {
return _spec ?? (_spec = GetSpec()); return _spec ?? (_spec = GetSpec());
@ -181,7 +187,8 @@ namespace Emby.Server.Implementations.Services
{ {
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>(); var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
var all = ServiceController.Instance.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); // REVIEW: this can be done better
var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
foreach (var current in all) foreach (var current in all)
{ {
@ -192,11 +199,8 @@ namespace Emby.Server.Implementations.Services
continue; continue;
} }
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)) if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
{ || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
continue;
}
if (info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
{ {
continue; continue;
} }

View File

@ -5,6 +5,8 @@ using System.IO;
using System.Net; using System.Net;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -25,8 +27,6 @@ namespace Emby.Server.Implementations.SocketSharp
this.OperationName = operationName; this.OperationName = operationName;
this.request = httpContext; this.request = httpContext;
this.Response = new WebSocketSharpResponse(logger, response); this.Response = new WebSocketSharpResponse(logger, response);
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
} }
public HttpRequest HttpRequest => request; public HttpRequest HttpRequest => request;
@ -40,16 +40,9 @@ namespace Emby.Server.Implementations.SocketSharp
public string RawUrl => request.GetEncodedPathAndQuery(); public string RawUrl => request.GetEncodedPathAndQuery();
public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
// Header[name] returns "" when undefined
public string XForwardedFor private string GetHeader(string name) => request.Headers[name].ToString();
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString();
public int? XForwardedPort
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString();
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
private string remoteIp; private string remoteIp;
public string RemoteIp public string RemoteIp
@ -61,107 +54,27 @@ namespace Emby.Server.Implementations.SocketSharp
return remoteIp; return remoteIp;
} }
var temp = CheckBadChars(XForwardedFor.AsSpan()); IPAddress ip;
if (temp.Length != 0)
// "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
// (if the server is behind a reverse proxy for example)
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
{ {
return remoteIp = temp.ToString(); if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
{
ip = request.HttpContext.Connection.RemoteIpAddress;
}
} }
temp = CheckBadChars(XRealIp.AsSpan()); return remoteIp = NormalizeIp(ip).ToString();
if (temp.Length != 0)
{
return remoteIp = NormalizeIp(temp).ToString();
}
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
} }
} }
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; private static IPAddress NormalizeIp(IPAddress ip)
// CheckBadChars - throws on invalid chars to be not found in header name/value
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
{ {
if (name.Length == 0) if (ip.IsIPv4MappedToIPv6)
{ {
return name; return ip.MapToIPv4();
}
// VALUE check
// Trim spaces from both ends
name = name.Trim(HttpTrimCharacters);
// First, check for correctly formed multi-line value
// Second, check for absence of CTL characters
int crlf = 0;
for (int i = 0; i < name.Length; ++i)
{
char c = (char)(0x000000ff & (uint)name[i]);
switch (crlf)
{
case 0:
{
if (c == '\r')
{
crlf = 1;
}
else if (c == '\n')
{
// Technically this is bad HTTP. But it would be a breaking change to throw here.
// Is there an exploit?
crlf = 2;
}
else if (c == 127 || (c < ' ' && c != '\t'))
{
throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
}
break;
}
case 1:
{
if (c == '\n')
{
crlf = 2;
break;
}
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
case 2:
{
if (c == ' ' || c == '\t')
{
crlf = 0;
break;
}
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
}
}
if (crlf != 0)
{
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
return name;
}
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
{
if (ip.Length != 0 && !ip.IsWhiteSpace())
{
// Handle ipv4 mapped to ipv6
const string srch = "::ffff:";
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0)
{
ip = ip.Slice(srch.Length);
}
} }
return ip; return ip;
@ -312,97 +225,7 @@ namespace Emby.Server.Implementations.SocketSharp
return pos == -1 ? strVal : strVal.Slice(0, pos); return pos == -1 ? strVal : strVal.Slice(0, pos);
} }
public static string HandlerFactoryPath; public string PathInfo => this.request.Path.Value;
private string pathInfo;
public string PathInfo
{
get
{
if (this.pathInfo == null)
{
var mode = HandlerFactoryPath;
var pos = RawUrl.IndexOf("?", StringComparison.Ordinal);
if (pos != -1)
{
var path = RawUrl.Substring(0, pos);
this.pathInfo = GetPathInfo(
path,
mode,
mode ?? string.Empty);
}
else
{
this.pathInfo = RawUrl;
}
this.pathInfo = WebUtility.UrlDecode(pathInfo);
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
}
return this.pathInfo;
}
}
private static string GetPathInfo(string fullPath, string mode, string appPath)
{
var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode);
if (!string.IsNullOrEmpty(pathInfo))
{
return pathInfo;
}
// Wildcard mode relies on this to work out the handlerPath
pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath);
if (!string.IsNullOrEmpty(pathInfo))
{
return pathInfo;
}
return fullPath;
}
private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot)
{
if (mappedPathRoot == null)
{
return null;
}
var sbPathInfo = new StringBuilder();
var fullPathParts = fullPath.Split('/');
var mappedPathRootParts = mappedPathRoot.Split('/');
var fullPathIndexOffset = mappedPathRootParts.Length - 1;
var pathRootFound = false;
for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++)
{
if (pathRootFound)
{
sbPathInfo.Append("/" + fullPathParts[fullPathIndex]);
}
else if (fullPathIndex - fullPathIndexOffset >= 0)
{
pathRootFound = true;
for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++)
{
if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase))
{
pathRootFound = false;
break;
}
}
}
}
if (!pathRootFound)
{
return null;
}
return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/";
}
public string UserAgent => request.Headers[HeaderNames.UserAgent]; public string UserAgent => request.Headers[HeaderNames.UserAgent];
@ -501,19 +324,5 @@ namespace Emby.Server.Implementations.SocketSharp
return httpFiles; return httpFiles;
} }
} }
public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
{
if (handlerPath != null)
{
var trimmed = pathInfo.AsSpan().TrimStart('/');
if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
}
}
return pathInfo.AsSpan();
}
} }
} }

View File

@ -41,8 +41,6 @@ namespace Emby.Server.Implementations.Udp
_socketFactory = socketFactory; _socketFactory = socketFactory;
AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message); AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message);
AddMessageResponder("who is EmbyServer?", true, RespondToV2Message);
AddMessageResponder("who is MediaBrowserServer_v2?", false, RespondToV2Message);
} }
private void AddMessageResponder(string message, bool isSubstring, Func<string, IpEndPointInfo, Encoding, CancellationToken, Task> responder) private void AddMessageResponder(string message, bool isSubstring, Func<string, IpEndPointInfo, Encoding, CancellationToken, Task> responder)

View File

@ -12,7 +12,6 @@ using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
@ -39,11 +38,10 @@ namespace Emby.Server.Implementations.Updates
/// <summary> /// <summary>
/// The completed installations /// The completed installations
/// </summary> /// </summary>
private ConcurrentBag<InstallationInfo> CompletedInstallationsInternal { get; set; } private ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
public IEnumerable<InstallationInfo> CompletedInstallations => CompletedInstallationsInternal; public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
#region PluginUninstalled Event
/// <summary> /// <summary>
/// Occurs when [plugin uninstalled]. /// Occurs when [plugin uninstalled].
/// </summary> /// </summary>
@ -57,9 +55,7 @@ namespace Emby.Server.Implementations.Updates
{ {
PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin }); PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin });
} }
#endregion
#region PluginUpdated Event
/// <summary> /// <summary>
/// Occurs when [plugin updated]. /// Occurs when [plugin updated].
/// </summary> /// </summary>
@ -77,9 +73,7 @@ namespace Emby.Server.Implementations.Updates
_applicationHost.NotifyPendingRestart(); _applicationHost.NotifyPendingRestart();
} }
#endregion
#region PluginInstalled Event
/// <summary> /// <summary>
/// Occurs when [plugin updated]. /// Occurs when [plugin updated].
/// </summary> /// </summary>
@ -96,7 +90,6 @@ namespace Emby.Server.Implementations.Updates
_applicationHost.NotifyPendingRestart(); _applicationHost.NotifyPendingRestart();
} }
#endregion
/// <summary> /// <summary>
/// The _logger /// The _logger
@ -115,12 +108,8 @@ namespace Emby.Server.Implementations.Updates
/// <value>The application host.</value> /// <value>The application host.</value>
private readonly IApplicationHost _applicationHost; private readonly IApplicationHost _applicationHost;
private readonly ICryptoProvider _cryptographyProvider;
private readonly IZipClient _zipClient; private readonly IZipClient _zipClient;
// netframework or netcore
private readonly string _packageRuntime;
public InstallationManager( public InstallationManager(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IApplicationHost appHost, IApplicationHost appHost,
@ -129,9 +118,7 @@ namespace Emby.Server.Implementations.Updates
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IFileSystem fileSystem,
ICryptoProvider cryptographyProvider, IZipClient zipClient)
IZipClient zipClient,
string packageRuntime)
{ {
if (loggerFactory == null) if (loggerFactory == null)
{ {
@ -139,18 +126,16 @@ namespace Emby.Server.Implementations.Updates
} }
CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>(); CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>();
CompletedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
_logger = loggerFactory.CreateLogger(nameof(InstallationManager));
_applicationHost = appHost; _applicationHost = appHost;
_appPaths = appPaths; _appPaths = appPaths;
_httpClient = httpClient; _httpClient = httpClient;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_config = config; _config = config;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_cryptographyProvider = cryptographyProvider;
_zipClient = zipClient; _zipClient = zipClient;
_packageRuntime = packageRuntime;
_logger = loggerFactory.CreateLogger(nameof(InstallationManager));
} }
private static Version GetPackageVersion(PackageVersionInfo version) private static Version GetPackageVersion(PackageVersionInfo version)
@ -222,11 +207,6 @@ namespace Emby.Server.Implementations.Updates
continue; continue;
} }
if (string.IsNullOrEmpty(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1)
{
continue;
}
versions.Add(version); versions.Add(version);
} }
@ -448,7 +428,7 @@ namespace Emby.Server.Implementations.Updates
CurrentInstallations.Remove(tuple); CurrentInstallations.Remove(tuple);
} }
CompletedInstallationsInternal.Add(installationInfo); _completedInstallationsInternal.Add(installationInfo);
PackageInstallationCompleted?.Invoke(this, installationEventArgs); PackageInstallationCompleted?.Invoke(this, installationEventArgs);
} }
@ -529,6 +509,8 @@ namespace Emby.Server.Implementations.Updates
private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken) private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken)
{ {
// TODO: Remove the `string target` argument as it is not used any longer
var extension = Path.GetExtension(package.targetFilename); var extension = Path.GetExtension(package.targetFilename);
var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase); var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
@ -538,12 +520,12 @@ namespace Emby.Server.Implementations.Updates
return; return;
} }
if (target == null) // Always override the passed-in target (which is a file) and figure it out again
{ target = Path.Combine(_appPaths.PluginsPath, package.name);
target = Path.Combine(_appPaths.PluginsPath, Path.GetFileNameWithoutExtension(package.targetFilename)); _logger.LogDebug("Installing plugin to {Filename}.", target);
}
// Download to temporary file so that, if interrupted, it won't destroy the existing installation // Download to temporary file so that, if interrupted, it won't destroy the existing installation
_logger.LogDebug("Downloading ZIP.");
var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
{ {
Url = package.sourceUrl, Url = package.sourceUrl,
@ -556,9 +538,17 @@ namespace Emby.Server.Implementations.Updates
// TODO: Validate with a checksum, *properly* // TODO: Validate with a checksum, *properly*
// Check if the target directory already exists, and remove it if so
if (Directory.Exists(target))
{
_logger.LogDebug("Deleting existing plugin at {Filename}.", target);
Directory.Delete(target, true);
}
// Success - move it to the real target // Success - move it to the real target
try try
{ {
_logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target);
using (var stream = File.OpenRead(tempFile)) using (var stream = File.OpenRead(tempFile))
{ {
_zipClient.ExtractAllFromZip(stream, target, true); _zipClient.ExtractAllFromZip(stream, target, true);
@ -572,6 +562,7 @@ namespace Emby.Server.Implementations.Updates
try try
{ {
_logger.LogDebug("Deleting temporary file {Filename}.", tempFile);
_fileSystem.DeleteFile(tempFile); _fileSystem.DeleteFile(tempFile);
} }
catch (IOException ex) catch (IOException ex)
@ -594,7 +585,13 @@ namespace Emby.Server.Implementations.Updates
_applicationHost.RemovePlugin(plugin); _applicationHost.RemovePlugin(plugin);
var path = plugin.AssemblyFilePath; var path = plugin.AssemblyFilePath;
_logger.LogInformation("Deleting plugin file {0}", path); bool isDirectory = false;
// Check if we have a plugin directory we should remove too
if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
{
path = Path.GetDirectoryName(plugin.AssemblyFilePath);
isDirectory = true;
}
// Make this case-insensitive to account for possible incorrect assembly naming // Make this case-insensitive to account for possible incorrect assembly naming
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path)) var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
@ -605,7 +602,16 @@ namespace Emby.Server.Implementations.Updates
path = file; path = file;
} }
_fileSystem.DeleteFile(path); if (isDirectory)
{
_logger.LogInformation("Deleting plugin directory {0}", path);
Directory.Delete(path, true);
}
else
{
_logger.LogInformation("Deleting plugin file {0}", path);
_fileSystem.DeleteFile(path);
}
var list = _config.Configuration.UninstalledPlugins.ToList(); var list = _config.Configuration.UninstalledPlugins.ToList();
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);

View File

@ -19,7 +19,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -119,6 +118,10 @@ namespace Jellyfin.Server
SQLitePCL.Batteries_V2.Init(); SQLitePCL.Batteries_V2.Init();
// Increase the max http request limit
// The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
// Allow all https requests // Allow all https requests
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Api.Movies; using MediaBrowser.Api.Movies;
@ -828,7 +830,16 @@ namespace MediaBrowser.Api.Library
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty); var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
if (!string.IsNullOrWhiteSpace(filename)) if (!string.IsNullOrWhiteSpace(filename))
{ {
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\""; // Kestrel doesn't support non-ASCII characters in headers
if (Regex.IsMatch(filename, "[^[:ascii:]]"))
{
// Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
}
else
{
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
}
} }
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace MediaBrowser.Api.Playback.Progressive namespace MediaBrowser.Api.Playback.Progressive
@ -279,10 +281,7 @@ namespace MediaBrowser.Api.Playback.Progressive
/// <returns>Task{System.Object}.</returns> /// <returns>Task{System.Object}.</returns>
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource) private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
{ {
string useragent = null; state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent);
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
var trySupportSeek = false;
var options = new HttpRequestOptions var options = new HttpRequestOptions
{ {
@ -292,29 +291,14 @@ namespace MediaBrowser.Api.Playback.Progressive
CancellationToken = cancellationTokenSource.Token CancellationToken = cancellationTokenSource.Token
}; };
if (trySupportSeek)
{
if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range]))
{
options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range];
}
}
var response = await HttpClient.GetResponse(options).ConfigureAwait(false); var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
if (trySupportSeek) responseHeaders[HeaderNames.AcceptRanges] = "none";
// Seeing cases of -1 here
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
{ {
foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges }) responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
{
var val = response.Headers[name];
if (!string.IsNullOrWhiteSpace(val))
{
responseHeaders[name] = val;
}
}
}
else
{
responseHeaders[HeaderNames.AcceptRanges] = "none";
} }
if (isHeadRequest) if (isHeadRequest)
@ -356,10 +340,31 @@ namespace MediaBrowser.Api.Playback.Progressive
var contentType = state.GetMimeType(outputPath); var contentType = state.GetMimeType(outputPath);
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
if (contentLength.HasValue)
{
responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
}
// Headers only // Headers only
if (isHeadRequest) if (isHeadRequest)
{ {
return ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders); var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
if (streamResult is IHasHeaders hasHeaders)
{
if (contentLength.HasValue)
{
hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
hasHeaders.Headers.Remove(HeaderNames.ContentLength);
}
}
return streamResult;
} }
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
@ -397,5 +402,22 @@ namespace MediaBrowser.Api.Playback.Progressive
transcodingLock.Release(); transcodingLock.Release();
} }
} }
/// <summary>
/// Gets the length of the estimated content.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.Nullable{System.Int64}.</returns>
private long? GetEstimatedContentLength(StreamState state)
{
var totalBitrate = state.TotalOutputBitrate ?? 0;
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
{
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
}
return null;
}
} }
} }

View File

@ -245,6 +245,12 @@ namespace MediaBrowser.Api.Session
{ {
} }
[Route("/Auth/PasswordResetProviders", "GET")]
[Authenticated(Roles = "Admin")]
public class GetPasswordResetProviders : IReturn<NameIdPair[]>
{
}
[Route("/Auth/Keys/{Key}", "DELETE")] [Route("/Auth/Keys/{Key}", "DELETE")]
[Authenticated(Roles = "Admin")] [Authenticated(Roles = "Admin")]
public class RevokeKey public class RevokeKey
@ -294,6 +300,11 @@ namespace MediaBrowser.Api.Session
return _userManager.GetAuthenticationProviders(); return _userManager.GetAuthenticationProviders();
} }
public object Get(GetPasswordResetProviders request)
{
return _userManager.GetPasswordResetProviders();
}
public void Delete(RevokeKey request) public void Delete(RevokeKey request)
{ {
_sessionManager.RevokeToken(request.Key); _sessionManager.RevokeToken(request.Key);

View File

@ -379,10 +379,15 @@ namespace MediaBrowser.Api
throw new ResourceNotFoundException("User not found"); throw new ResourceNotFoundException("User not found");
} }
if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw))
{
throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API.");
}
return Post(new AuthenticateUserByName return Post(new AuthenticateUserByName
{ {
Username = user.Name, Username = user.Name,
Password = request.Password, Password = null, // This should always be null
Pw = request.Pw Pw = request.Pw
}); });
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MediaBrowser.Model.Cryptography; using System.Security.Cryptography;
namespace MediaBrowser.Common.Extensions namespace MediaBrowser.Common.Extensions
{ {
@ -9,8 +10,6 @@ namespace MediaBrowser.Common.Extensions
/// </summary> /// </summary>
public static class BaseExtensions public static class BaseExtensions
{ {
public static ICryptoProvider CryptographyProvider { get; set; }
/// <summary> /// <summary>
/// Strips the HTML. /// Strips the HTML.
/// </summary> /// </summary>
@ -31,7 +30,10 @@ namespace MediaBrowser.Common.Extensions
/// <returns>Guid.</returns> /// <returns>Guid.</returns>
public static Guid GetMD5(this string str) public static Guid GetMD5(this string str)
{ {
return CryptographyProvider.GetMD5(str); using (var provider = MD5.Create())
{
return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str)));
}
} }
} }
} }

View File

@ -26,6 +26,30 @@ namespace MediaBrowser.Common.Extensions
} }
} }
/// <summary>
/// Class MethodNotAllowedException
/// </summary>
public class MethodNotAllowedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
/// </summary>
public MethodNotAllowedException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
/// </summary>
/// <param name="message">The message.</param>
public MethodNotAllowedException(string message)
: base(message)
{
}
}
public class RemoteServiceUnavailableException : Exception public class RemoteServiceUnavailableException : Exception
{ {
public RemoteServiceUnavailableException() public RemoteServiceUnavailableException()

View File

@ -25,11 +25,6 @@ namespace MediaBrowser.Common
/// <value>The device identifier.</value> /// <value>The device identifier.</value>
string SystemId { get; } string SystemId { get; }
/// <summary>
/// Occurs when [application updated].
/// </summary>
event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance has pending kernel reload. /// Gets or sets a value indicating whether this instance has pending kernel reload.
/// </summary> /// </summary>

View File

@ -0,0 +1,11 @@
namespace MediaBrowser.Common.Net
{
public static class CustomHeaderNames
{
// Other Headers
public const string XForwardedFor = "X-Forwarded-For";
public const string XForwardedPort = "X-Forwarded-Port";
public const string XForwardedProto = "X-Forwarded-Proto";
public const string XRealIP = "X-Real-IP";
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication
{
public interface IPasswordResetProvider
{
string Name { get; }
bool IsEnabled { get; }
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
}
public class PasswordPinCreationResult
{
public string PinFile { get; set; }
public DateTime ExpirationDate { get; set; }
}
}

View File

@ -78,10 +78,25 @@ namespace MediaBrowser.Controller.Entities
/// <summary> /// <summary>
/// The trailer folder name /// The trailer folder name
/// </summary> /// </summary>
public static string TrailerFolderName = "trailers"; public const string TrailerFolderName = "trailers";
public static string ThemeSongsFolderName = "theme-music"; public const string ThemeSongsFolderName = "theme-music";
public static string ThemeSongFilename = "theme"; public const string ThemeSongFilename = "theme";
public static string ThemeVideosFolderName = "backdrops"; public const string ThemeVideosFolderName = "backdrops";
public const string ExtrasFolderName = "extras";
public const string BehindTheScenesFolderName = "behind the scenes";
public const string DeletedScenesFolderName = "deleted scenes";
public const string InterviewFolderName = "interviews";
public const string SceneFolderName = "scenes";
public const string SampleFolderName = "samples";
public static readonly string[] AllExtrasTypesFolderNames = {
ExtrasFolderName,
BehindTheScenesFolderName,
DeletedScenesFolderName,
InterviewFolderName,
SceneFolderName,
SampleFolderName
};
[IgnoreDataMember] [IgnoreDataMember]
public Guid[] ThemeSongIds { get; set; } public Guid[] ThemeSongIds { get; set; }
@ -1276,16 +1291,15 @@ namespace MediaBrowser.Controller.Entities
.Select(item => .Select(item =>
{ {
// Try to retrieve it from the db. If we don't find it, use the resolved version // 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) if (LibraryManager.GetItemById(item.Id) is Video dbItem)
{ {
item = dbItem; item = dbItem;
} }
else else
{ {
// item is new // item is new
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; item.ExtraType = Model.Entities.ExtraType.ThemeVideo;
} }
return item; return item;
@ -1296,33 +1310,38 @@ namespace MediaBrowser.Controller.Entities
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{ {
var files = fileSystemChildren.Where(i => i.IsDirectory) var extras = new List<Video>();
.SelectMany(i => FileSystem.GetFiles(i.FullName));
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
.OfType<Video>() foreach (var extraFolderName in AllExtrasTypesFolderNames)
.Select(item => {
{ var files = folders
// Try to retrieve it from the db. If we don't find it, use the resolved version .Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
var dbItem = LibraryManager.GetItemById(item.Id) as Video; .SelectMany(i => FileSystem.GetFiles(i.FullName));
if (dbItem != null) extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Video>()
.Select(item =>
{ {
item = dbItem; // Try to retrieve it from the db. If we don't find it, use the resolved version
} if (LibraryManager.GetItemById(item.Id) is Video dbItem)
else {
{ item = dbItem;
// item is new }
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip;
}
return item; // Use some hackery to get the extra type based on foldername
Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType);
item.ExtraType = extraType;
// Sort them so that the list can be easily compared for changes return item;
}).OrderBy(i => i.Path).ToArray();
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path));
}
return extras.ToArray();
} }
public Task RefreshMetadata(CancellationToken cancellationToken) public Task RefreshMetadata(CancellationToken cancellationToken)
{ {
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
@ -1481,7 +1500,13 @@ namespace MediaBrowser.Controller.Entities
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) 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 extras = LoadExtras(fileSystemChildren, options.DirectoryService);
var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length];
extras.CopyTo(newExtras, 0);
themeVideos.CopyTo(newExtras, extras.Length);
themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length);
var newExtraIds = newExtras.Select(i => i.Id).ToArray(); var newExtraIds = newExtras.Select(i => i.Id).ToArray();
@ -1493,7 +1518,15 @@ namespace MediaBrowser.Controller.Entities
var tasks = newExtras.Select(i => var tasks = newExtras.Select(i =>
{ {
return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken); var subOptions = new MetadataRefreshOptions(options);
if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
}
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
}); });
await Task.WhenAll(tasks).ConfigureAwait(false); await Task.WhenAll(tasks).ConfigureAwait(false);

View File

@ -64,21 +64,31 @@ namespace MediaBrowser.Controller.Entities
where T : BaseItem where T : BaseItem
where TU : BaseItem where TU : BaseItem
{ {
var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList(); var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList();
var destProps = typeof(TU).GetProperties()
.Where(x => x.CanWrite)
.ToList();
foreach (var sourceProp in sourceProps) foreach (var sourceProp in typeof(T).GetProperties())
{ {
if (destProps.Any(x => x.Name == sourceProp.Name)) // We should be able to write to the property
// for both the source and destination type
// This is only false when the derived type hides the base member
// (which we shouldn't copy anyway)
if (!sourceProp.CanRead || !sourceProp.CanWrite)
{ {
var p = destProps.First(x => x.Name == sourceProp.Name); continue;
p.SetValue(dest, sourceProp.GetValue(source, null), null);
} }
} var v = sourceProp.GetValue(source);
if (v == null)
{
continue;
}
var p = destProps.Find(x => x.Name == sourceProp.Name);
if (p != null)
{
p.SetValue(dest, v);
}
}
} }
/// <summary> /// <summary>
@ -93,7 +103,5 @@ namespace MediaBrowser.Controller.Entities
source.DeepCopy(dest); source.DeepCopy(dest);
return dest; return dest;
} }
} }
} }

View File

@ -83,8 +83,6 @@ namespace MediaBrowser.Controller
void EnableLoopback(string appName); void EnableLoopback(string appName);
string PackageRuntime { get; }
WakeOnLanInfo[] GetWakeOnLanInfo(); WakeOnLanInfo[] GetWakeOnLanInfo();
string ExpandVirtualPath(string path); string ExpandVirtualPath(string path);

View File

@ -200,8 +200,9 @@ namespace MediaBrowser.Controller.Library
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
string MakeValidUsername(string username); string MakeValidUsername(string username);
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders); void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
NameIdPair[] GetAuthenticationProviders(); NameIdPair[] GetAuthenticationProviders();
NameIdPair[] GetPasswordResetProviders();
} }
} }

View File

@ -53,7 +53,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly int DefaultImageExtractionTimeoutMs; private readonly int DefaultImageExtractionTimeoutMs;
private readonly string StartupOptionFFmpegPath; private readonly string StartupOptionFFmpegPath;
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
@ -230,6 +230,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <returns></returns> /// <returns></returns>
private string ExistsOnSystemPath(string filename) private string ExistsOnSystemPath(string filename)
{ {
string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, filename);
if (!string.IsNullOrEmpty(inJellyfinPath))
{
return inJellyfinPath;
}
var values = Environment.GetEnvironmentVariable("PATH"); var values = Environment.GetEnvironmentVariable("PATH");
foreach (var path in values.Split(Path.PathSeparator)) foreach (var path in values.Split(Path.PathSeparator))
@ -577,19 +582,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
bool ranToCompletion; bool ranToCompletion;
StartProcess(processWrapper); await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
if (timeoutMs <= 0)
{ {
timeoutMs = DefaultImageExtractionTimeoutMs; StartProcess(processWrapper);
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
if (timeoutMs <= 0)
{
timeoutMs = DefaultImageExtractionTimeoutMs;
}
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
if (!ranToCompletion)
{
StopProcess(processWrapper, 1000);
}
} }
finally
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
if (!ranToCompletion)
{ {
StopProcess(processWrapper, 1000); _thumbnailResourcePool.Release();
} }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
@ -620,7 +633,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
return time.ToString(@"hh\:mm\:ss\.fff", UsCulture); return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
} }
public async Task ExtractVideoImagesOnInterval(string[] inputFiles, public async Task ExtractVideoImagesOnInterval(
string[] inputFiles,
string container, string container,
MediaStream videoStream, MediaStream videoStream,
MediaProtocol protocol, MediaProtocol protocol,
@ -631,8 +645,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
int? maxWidth, int? maxWidth,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var resourcePool = _thumbnailResourcePool;
var inputArgument = GetInputArgument(inputFiles, protocol); var inputArgument = GetInputArgument(inputFiles, protocol);
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture); var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
@ -696,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
bool ranToCompletion = false; bool ranToCompletion = false;
@ -737,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
} }
finally finally
{ {
resourcePool.Release(); _thumbnailResourcePool.Release();
} }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;

View File

@ -7,26 +7,6 @@ namespace MediaBrowser.Model.Services
/// </summary> /// </summary>
string HttpMethod { get; } string HttpMethod { get; }
/// <summary>
/// The IP Address of the X-Forwarded-For header, null if null or empty
/// </summary>
string XForwardedFor { get; }
/// <summary>
/// The Port number of the X-Forwarded-Port header, null if null or empty
/// </summary>
int? XForwardedPort { get; }
/// <summary>
/// The http or https scheme of the X-Forwarded-Proto header, null if null or empty
/// </summary>
string XForwardedProtocol { get; }
/// <summary>
/// The value of the X-Real-IP header, null if null or empty
/// </summary>
string XRealIp { get; }
/// <summary> /// <summary>
/// The value of the Accept HTTP Request Header /// The value of the Accept HTTP Request Header
/// </summary> /// </summary>

View File

@ -26,6 +26,11 @@ namespace MediaBrowser.Model.System
/// <value>The version.</value> /// <value>The version.</value>
public string Version { get; set; } public string Version { get; set; }
/// <summary>
/// The product name. This is the AssemblyProduct name.
/// </summary>
public string ProductName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the operating system. /// Gets or sets the operating system.
/// </summary> /// </summary>

View File

@ -32,10 +32,6 @@ namespace MediaBrowser.Model.System
/// <value>The display name of the operating system.</value> /// <value>The display name of the operating system.</value>
public string OperatingSystemDisplayName { get; set; } public string OperatingSystemDisplayName { get; set; }
/// <summary>
/// The product name. This is the AssemblyProduct name.
/// </summary>
public string ProductName { get; set; }
/// <summary> /// <summary>
/// Get or sets the package name. /// Get or sets the package name.

View File

@ -75,6 +75,7 @@ namespace MediaBrowser.Model.Users
public int RemoteClientBitrateLimit { get; set; } public int RemoteClientBitrateLimit { get; set; }
public string AuthenticationProviderId { get; set; } public string AuthenticationProviderId { get; set; }
public string PasswordResetProviderId { get; set; }
public UserPolicy() public UserPolicy()
{ {

View File

@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Music
} }
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
return ParseReleaseList(subReader); return ParseReleaseList(subReader).ToList();
} }
} }
default: default:

View File

@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Music
} }
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
return ParseArtistList(subReader); return ParseArtistList(subReader).ToList();
} }
} }
default: default:

View File

@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
/// <summary> /// <summary>
/// Class RemoteEpisodeProvider /// Class RemoteEpisodeProvider
/// </summary> /// </summary>
class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILogger _logger; private readonly ILogger _logger;

@ -1 +1 @@
Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d Subproject commit b0f7a9b67cc72de98dc357425e9d5c3894c7f377

View File

@ -22,13 +22,13 @@
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help). For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/getting-help/).
For more information about the project, please see our [about page](https://jellyfin.readthedocs.io/en/latest/about/). For more information about the project, please see our [about page](https://jellyfin.readthedocs.io/en/latest/about/).
<p align="center"> <p align="center">
<strong>Want to get started?</strong> <strong>Want to get started?</strong>
<em>Choose from <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/installing/">Prebuilt Packages</a> or <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/building/">Build from Source</a>, then see our <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/first-time/">first-time setup guide</a>.</em> <em>Choose from <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/installing/">Prebuilt Packages</a> or <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/building/">Build from Source</a>, then see our <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/quick-start/">quick start guide</a>.</em>
</p> </p>
<p align="center"> <p align="center">
<strong>Want to contribute?</strong> <strong>Want to contribute?</strong>

View File

@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.2.2")] [assembly: AssemblyVersion("10.3.3")]
[assembly: AssemblyFileVersion("10.2.2")] [assembly: AssemblyFileVersion("10.3.3")]

2
build
View File

@ -29,7 +29,7 @@ usage() {
echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching." echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
echo -e "To build all platforms, use 'all'." echo -e "To build all platforms, use 'all'."
echo -e "To perform all build actions, use 'all'." echo -e "To perform all build actions, use 'all'."
echo -e "Build output files are collected at '../jellyfin-build/<platform>'." echo -e "Build output files are collected at '../bin/<platform>'."
} }
# Show usage on stderr with exit 1 on argless # Show usage on stderr with exit 1 on argless

View File

@ -1,11 +1,14 @@
--- ---
# We just wrap `build` so this is really it # We just wrap `build` so this is really it
name: "jellyfin" name: "jellyfin"
version: "10.2.2" version: "10.3.3"
packages: packages:
- debian-package-x64 - debian-package-x64
- debian-package-armhf - debian-package-armhf
- debian-package-arm64
- ubuntu-package-x64 - ubuntu-package-x64
- ubuntu-package-armhf
- ubuntu-package-arm64
- fedora-package-x64 - fedora-package-x64
- centos-package-x64 - centos-package-x64
- linux-x64 - linux-x64

View File

@ -9,7 +9,7 @@ usage() {
echo -e "bump_version - increase the shared version and generate changelogs" echo -e "bump_version - increase the shared version and generate changelogs"
echo -e "" echo -e ""
echo -e "Usage:" echo -e "Usage:"
echo -e " $ bump_version [-b/--web-branch <web_branch>] <new_version>" echo -e " $ bump_version <new_version>"
echo -e "" echo -e ""
echo -e "The web_branch defaults to the same branch name as the current main branch." echo -e "The web_branch defaults to the same branch name as the current main branch."
echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z" echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z"
@ -22,14 +22,9 @@ if [[ -z $1 ]]; then
fi fi
shared_version_file="./SharedVersion.cs" shared_version_file="./SharedVersion.cs"
build_file="./build.yaml"
# Parse branch option web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
if [[ $1 == '-b' || $1 == '--web-branch' ]]; then
web_branch="$2"
shift 2
else
web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
fi
# Initialize submodules # Initialize submodules
git submodule update --init --recursive git submodule update --init --recursive
@ -47,22 +42,11 @@ if ! git diff-index --quiet HEAD --; then
fi fi
git fetch --all git fetch --all
# If this is an official branch name, fetch it from origin git checkout origin/${web_branch}
official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$"
if [[ ${web_branch} =~ ${official_branches_regex} ]]; then
git checkout origin/${web_branch} || {
echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid."
exit 1
}
# Otherwise, just check out the local branch (for testing, etc.)
else
git checkout ${web_branch} || {
echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid."
exit 1
}
fi
popd popd
git add MediaBrowser.WebDashboard/jellyfin-web
new_version="$1" new_version="$1"
# Parse the version from the AssemblyVersion # Parse the version from the AssemblyVersion
@ -70,94 +54,47 @@ old_version="$(
grep "AssemblyVersion" ${shared_version_file} \ grep "AssemblyVersion" ${shared_version_file} \
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/' | sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
)" )"
echo $old_version
# Set the shared version to the specified new_version # Set the shared version to the specified new_version
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
sed -i "s/${old_version_sed}/${new_version}/g" ${shared_version_file} new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file}
declare -a pr_merges_since_last_master old_version="$(
declare changelog_string_github grep "version:" ${build_file} \
declare changelog_string_deb | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
declare changelog_string_yum )"
echo $old_version
# Build up a changelog from merge commits # Set the build.yaml version to the specified new_version
for repo in ./ MediaBrowser.WebDashboard/jellyfin-web/; do old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
last_master_merge_commit="" sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
pr_merges_since_last_master=()
git_show_details=""
pull_request_id=""
pull_request_description=""
changelog_strings_repo_github=""
changelog_strings_repo_deb=""
changelog_strings_repo_yum=""
case $repo in if [[ ${new_version} == *"-"* ]]; then
*jellyfin-web*) new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
repo_name="jellyfin-web" else
;; new_version_deb="${new_version}-1"
*) fi
repo_name="jellyfin"
;;
esac
pushd ${repo} # Set the Dockerfile web version to the specified new_version
old_version="$(
grep "JELLYFIN_WEB_VERSION=" Dockerfile \
| sed -E 's/ARG JELLYFIN_WEB_VERSION=([0-9\.]+[-a-z0-9]*)/\1/'
)"
echo $old_version
# Find the last release commit, so we know what's happened since old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
last_master_branch="release-${old_version}" sed -i "s/${old_version_sed}/${new_version}/g" Dockerfile*
last_master_merge_commit="$(
git log --merges --pretty=oneline \
| grep -F "${last_master_branch}" \
| awk '{ print $1 }' \
|| true # Don't die here with errexit
)"
if [[ -z ${last_master_merge_commit} ]]; then
# This repo has no last proper commit, so just skip it
popd
continue
fi
# Get all the PR merge commits since the last master merge commit in `jellyfin`
pr_merges_since_last_master+=( $(
git log --merges --pretty=oneline ${last_master_merge_commit}..HEAD \
| grep -F "Merge pull request" \
| awk '{ print $1 }'
) )
for commit_hash in ${pr_merges_since_last_master[@]}; do
git_show_details="$( git show ${commit_hash} )"
pull_request_id="$(
awk '
/Merge pull request/{ print $4 }
{ next }
' <<<"${git_show_details}"
)"
pull_request_description="$(
awk '
/^[a-zA-Z]/{ next }
/^ Merge/{ next }
/^$/{ next }
{ print $0 }
' <<<"${git_show_details}"
)"
pull_request_description="$( sed ':a;N;$!ba;s/\n//g; s/ \+//g' <<<"${pull_request_description}" )"
changelog_strings_repo_github="${changelog_strings_repo_github}\n* ${pull_request_id}: ${pull_request_description}"
changelog_strings_repo_deb="${changelog_strings_repo_deb}\n * $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}"
changelog_strings_repo_yum="${changelog_strings_repo_yum}\n- $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}"
done
changelog_string_github="${changelog_string_github}\n#### ${repo_name}:\n$( echo -e "${changelog_strings_repo_github}" | sort -nk2 )\n"
changelog_string_deb="${changelog_string_deb}\n * ${repo_name}:$( echo -e "${changelog_strings_repo_deb}" | sort -nk2 )"
changelog_string_yum="${changelog_string_yum}\n- ${repo_name}:$( echo -e "${changelog_strings_repo_yum}" | sort -nk2 )"
popd
done
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting # Write out a temporary Debian changelog with our new stuff appended and some templated formatting
debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog" debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog"
debian_changelog_temp="$( mktemp )" debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog # Create new temp file with our changelog
echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit. echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
jellyfin (${new_version}-1) unstable; urgency=medium jellyfin (${new_version_deb}) unstable; urgency=medium
${changelog_string_deb}
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 ) -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
" >> ${debian_changelog_temp} " >> ${debian_changelog_temp}
@ -180,13 +117,14 @@ pushd ${fedora_spec_temp_dir}
# Split out the stuff before and after changelog # Split out the stuff before and after changelog
csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01 csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
# Update the version in xx00 # Update the version in xx00
sed -i "s/${old_version_sed}/${new_version}/g" xx00 sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
# Remove the header from xx01 # Remove the header from xx01
sed -i '/^%changelog/d' xx01 sed -i '/^%changelog/d' xx01
# Create new temp file with our changelog # Create new temp file with our changelog
echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit. echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
%changelog %changelog
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>${changelog_string_yum}" >> ${fedora_changelog_temp} * $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
cat xx01 >> ${fedora_changelog_temp} cat xx01 >> ${fedora_changelog_temp}
# Edit the file to verify # Edit the file to verify
$EDITOR ${fedora_changelog_temp} $EDITOR ${fedora_changelog_temp}
@ -199,10 +137,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file}
rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir} rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
# Stage the changed files for commit # Stage the changed files for commit
git add ${shared_version_file} ${debian_changelog_file} ${fedora_spec_file} git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} Dockerfile*
git status git status
# Write out the GitHub-formatted changelog for the merge request/release pages
echo ""
echo "=== The GitHub-formatted changelog follows ==="
echo -e "${changelog_string_github}"

View File

@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/rpm mkdir -p ${ARTIFACT_DIR}/rpm
mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}

View File

@ -72,9 +72,6 @@ fi
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the RPMs and copy out to ${package_temporary_dir} # Build the RPMs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the RPMs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" \
|| sudo chown -R "${current_user}" "${package_temporary_dir}"
# Move the RPMs to the output directory # Move the RPMs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/rpm/* "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}"

View File

@ -17,12 +17,12 @@ DEFAULT_PKG_DIR="pkg-dist"
DEFAULT_DOCKERFILE="Dockerfile" DEFAULT_DOCKERFILE="Dockerfile"
DEFAULT_ARCHIVE_CMD="tar -xvzf" DEFAULT_ARCHIVE_CMD="tar -xvzf"
# Parse the version from the AssemblyVersion # Parse the version from the build.yaml version
get_version() get_version()
( (
local ROOT=${1-$DEFAULT_ROOT} local ROOT=${1-$DEFAULT_ROOT}
grep "AssemblyVersion" ${ROOT}/SharedVersion.cs \ grep "version:" ${ROOT}/build.yaml \
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/' | sed -E 's/version: "([0-9\.]+.*)"/\1/'
) )
# Run a build # Run a build

View File

@ -0,0 +1,43 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
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 arm64 \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu 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:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
# 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"]
#ENTRYPOINT ["/bin/bash"]

View File

@ -0,0 +1,34 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
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=arm64
# 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"]

View File

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

View File

@ -0,0 +1 @@
docker

View File

@ -0,0 +1,21 @@
#!/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 -aarm64
# Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}

View File

@ -0,0 +1,41 @@
#!/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_arm64-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.arm64"
;;
esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# 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}"
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"

View File

@ -0,0 +1 @@
../debian-package-x64/pkg-src

View File

@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/ mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}

View File

@ -30,13 +30,12 @@ case $ARCH in
;; ;;
esac esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir} # Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${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 # Move the DEBs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}"

View File

@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/ mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}

View File

@ -19,13 +19,12 @@ else
docker_sudo="" docker_sudo=""
fi fi
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the DEBs and copy out to ${package_temporary_dir} # Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${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 # Move the DEBs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}"

View File

@ -1,3 +1,27 @@
jellyfin (10.3.3-1) unstable; urgency=medium
* New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 17 May 2019 23:12:08 -0400
jellyfin (10.3.2-1) unstable; urgency=medium
* New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 30 Apr 2019 20:18:44 -0400
jellyfin (10.3.1-1) unstable; urgency=medium
* New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 20 Apr 2019 14:24:07 -0400
jellyfin (10.3.0-1) unstable; urgency=medium
* New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
jellyfin (10.2.2-1) unstable; urgency=medium jellyfin (10.2.2-1) unstable; urgency=medium
* jellyfin: * jellyfin:

View File

@ -22,7 +22,7 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
# ffmpeg binary paths, overriding the system values # ffmpeg binary paths, overriding the system values
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg" JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# [OPTIONAL] run Jellyfin as a headless service # [OPTIONAL] run Jellyfin as a headless service
#JELLYFIN_SERVICE_OPT="--service" #JELLYFIN_SERVICE_OPT="--service"

Some files were not shown because too many files have changed in this diff Show More