Merge branch 'master' into tasks
This commit is contained in:
commit
ce1fa42f9d
|
@ -99,7 +99,7 @@ jobs:
|
|||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
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:
|
||||
matrix:
|
||||
Naming:
|
||||
|
|
|
@ -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
|
||||
COPY . .
|
||||
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"
|
||||
|
||||
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
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
|
@ -21,7 +21,7 @@ RUN apt-get update \
|
|||
COPY --from=ffmpeg / /
|
||||
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 - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
|
|
@ -8,7 +8,7 @@ FROM alpine as qemu_extract
|
|||
COPY --from=qemu /usr/bin 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
|
||||
COPY . .
|
||||
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"
|
||||
|
||||
|
||||
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
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||
|
@ -30,7 +30,7 @@ RUN apt-get update \
|
|||
&& chmod 777 /cache /config /media
|
||||
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 - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
|
|
@ -9,7 +9,7 @@ COPY --from=qemu /usr/bin 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
|
||||
COPY . .
|
||||
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"
|
||||
|
||||
|
||||
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
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||
|
@ -31,7 +31,7 @@ RUN apt-get update \
|
|||
&& chmod 777 /cache /config /media
|
||||
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 - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
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, 160, 160, "png", "PNG_TN");
|
||||
}
|
||||
|
||||
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||
|
||||
}
|
||||
|
||||
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 |
|
@ -102,9 +102,10 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
_sessionManager.ReportSessionEnded(_session.Id);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Could throw if the session is already gone
|
||||
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,22 +113,16 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
var info = e.Argument;
|
||||
|
||||
info.Headers.TryGetValue("NTS", out string nts);
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
|
||||
|
||||
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
|
||||
!_disposed)
|
||||
{
|
||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (!_disposed
|
||||
&& info.Headers.TryGetValue("USN", out string usn)
|
||||
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| (info.Headers.TryGetValue("NT", out string nt)
|
||||
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async void _device_MediaChanged(object sender, MediaChangedEventArgs e)
|
||||
{
|
||||
|
@ -612,22 +607,34 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
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;
|
||||
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
_device = null;
|
||||
|
||||
_device.Dispose();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||
|
||||
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -20,7 +20,10 @@ namespace Emby.Photos
|
|||
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -28,75 +31,55 @@ namespace Emby.Photos
|
|||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
||||
public string Name => "Embedded Information";
|
||||
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = directoryService.GetFile(item.Path);
|
||||
if (file != null && file.LastWriteTimeUtc != item.DateModified)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return (file != null && file.LastWriteTimeUtc != item.DateModified);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// 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
|
||||
{
|
||||
using (var file = TagLib.File.Create(item.Path))
|
||||
{
|
||||
var image = file as TagLib.Image.File;
|
||||
|
||||
var tag = file.GetTag(TagTypes.TiffIFD) as IFDTag;
|
||||
|
||||
if (tag != null)
|
||||
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
|
||||
{
|
||||
var structure = tag.Structure;
|
||||
|
||||
if (structure != null)
|
||||
{
|
||||
var exif = structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) as SubIFDEntry;
|
||||
|
||||
if (exif != null)
|
||||
if (structure != null
|
||||
&& structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
|
||||
{
|
||||
var exifStructure = exif.Structure;
|
||||
|
||||
if (exifStructure != null)
|
||||
{
|
||||
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
double val = entry.Value.Numerator;
|
||||
val /= entry.Value.Denominator;
|
||||
item.Aperture = val;
|
||||
item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||
}
|
||||
|
||||
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
double val = entry.Value.Numerator;
|
||||
val /= entry.Value.Denominator;
|
||||
item.ShutterSpeed = val;
|
||||
}
|
||||
item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (image != null)
|
||||
if (file is TagLib.Image.File image)
|
||||
{
|
||||
item.CameraMake = image.ImageTag.Make;
|
||||
item.CameraModel = image.ImageTag.Model;
|
||||
|
@ -116,13 +99,11 @@ namespace Emby.Photos
|
|||
|
||||
item.Overview = image.ImageTag.Comment;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title))
|
||||
{
|
||||
if (!item.LockedFields.Contains(MetadataFields.Name))
|
||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
|
||||
&& !item.LockedFields.Contains(MetadataFields.Name))
|
||||
{
|
||||
item.Name = image.ImageTag.Title;
|
||||
}
|
||||
}
|
||||
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
if (dateTaken.HasValue)
|
||||
|
@ -140,13 +121,10 @@ namespace Emby.Photos
|
|||
{
|
||||
item.Orientation = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||
{
|
||||
item.Orientation = orientation;
|
||||
}
|
||||
}
|
||||
|
||||
item.ExposureTime = image.ImageTag.ExposureTime;
|
||||
item.FocalLength = image.ImageTag.FocalLength;
|
||||
|
@ -195,7 +173,5 @@ namespace Emby.Photos
|
|||
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public string Name => "Embedded Information";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,8 +83,6 @@ namespace Emby.Server.Implementations.Activity
|
|||
|
||||
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
|
||||
|
||||
_appHost.ApplicationUpdated += OnApplicationUpdated;
|
||||
|
||||
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)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
|
@ -460,8 +448,6 @@ namespace Emby.Server.Implementations.Activity
|
|||
_userManager.UserLockedOut -= OnUserLockedOut;
|
||||
|
||||
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
|
||||
|
||||
_appHost.ApplicationUpdated -= OnApplicationUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -154,11 +154,6 @@ namespace Emby.Server.Implementations
|
|||
/// </summary>
|
||||
public event EventHandler HasPendingRestartChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [application updated].
|
||||
/// </summary>
|
||||
public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||
/// </summary>
|
||||
|
@ -173,11 +168,17 @@ namespace Emby.Server.Implementations
|
|||
/// <value>The logger.</value>
|
||||
protected ILogger Logger { get; set; }
|
||||
|
||||
private IPlugin[] _plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugins.
|
||||
/// </summary>
|
||||
/// <value>The plugins.</value>
|
||||
public IPlugin[] Plugins { get; protected set; }
|
||||
public IPlugin[] Plugins
|
||||
{
|
||||
get => _plugins;
|
||||
protected set => _plugins = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger factory.
|
||||
|
@ -200,7 +201,7 @@ namespace Emby.Server.Implementations
|
|||
/// <summary>
|
||||
/// The disposable parts
|
||||
/// </summary>
|
||||
protected readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration manager.
|
||||
|
@ -216,8 +217,9 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
#if BETA
|
||||
return PackageVersionClass.Beta;
|
||||
#endif
|
||||
#else
|
||||
return PackageVersionClass.Release;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,7 +342,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
protected IProcessFactory ProcessFactory { get; private set; }
|
||||
|
||||
protected ICryptoProvider CryptographyProvider = new CryptographyProvider();
|
||||
protected readonly IXmlSerializer XmlSerializer;
|
||||
|
||||
protected ISocketFactory SocketFactory { get; private set; }
|
||||
|
@ -369,9 +370,6 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
_configuration = configuration;
|
||||
|
||||
// hack alert, until common can target .net core
|
||||
BaseExtensions.CryptographyProvider = CryptographyProvider;
|
||||
|
||||
XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
|
||||
|
||||
NetworkManager = networkManager;
|
||||
|
@ -617,8 +615,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
DiscoverTypes();
|
||||
|
||||
SetHttpLimit();
|
||||
|
||||
await RegisterResources(serviceCollection).ConfigureAwait(false);
|
||||
|
||||
FindParts();
|
||||
|
@ -735,13 +731,12 @@ namespace Emby.Server.Implementations
|
|||
ApplicationHost.StreamHelper = new StreamHelper();
|
||||
serviceCollection.AddSingleton(StreamHelper);
|
||||
|
||||
serviceCollection.AddSingleton(CryptographyProvider);
|
||||
serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider));
|
||||
|
||||
SocketFactory = new SocketFactory();
|
||||
serviceCollection.AddSingleton(SocketFactory);
|
||||
|
||||
InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime);
|
||||
serviceCollection.AddSingleton(InstallationManager);
|
||||
serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
|
||||
|
||||
ZipClient = new ZipClient();
|
||||
serviceCollection.AddSingleton(ZipClient);
|
||||
|
@ -908,8 +903,6 @@ namespace Emby.Server.Implementations
|
|||
_serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
public virtual string PackageRuntime => "netcore";
|
||||
|
||||
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
|
||||
{
|
||||
// Distinct these to prevent users from reporting problems that aren't actually problems
|
||||
|
@ -918,8 +911,7 @@ namespace Emby.Server.Implementations
|
|||
.Distinct();
|
||||
|
||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||
// FIXME: @bond this logs the kernel version, not the OS version
|
||||
logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version);
|
||||
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||
|
@ -929,19 +921,6 @@ namespace Emby.Server.Implementations
|
|||
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)
|
||||
{
|
||||
var certificateLocation = info?.Path;
|
||||
|
@ -1044,11 +1023,47 @@ namespace Emby.Server.Implementations
|
|||
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>
|
||||
/// Finds the parts.
|
||||
/// </summary>
|
||||
protected void FindParts()
|
||||
{
|
||||
InstallationManager = _serviceProvider.GetService<IInstallationManager>();
|
||||
InstallationManager.PluginInstalled += PluginInstalled;
|
||||
|
||||
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||
|
@ -1088,7 +1103,7 @@ namespace Emby.Server.Implementations
|
|||
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
|
||||
|
||||
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||
UserManager.AddParts(GetExports<IAuthenticationProvider>());
|
||||
UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
||||
|
||||
IsoManager.AddParts(GetExports<IIsoMounter>());
|
||||
}
|
||||
|
@ -1131,7 +1146,7 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1145,10 +1160,32 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
Logger.LogInformation("Loading assemblies");
|
||||
|
||||
AllConcreteTypes = GetComposablePartAssemblies()
|
||||
.SelectMany(x => x.ExportedTypes)
|
||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
|
||||
.ToArray();
|
||||
AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).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; }
|
||||
|
@ -1310,10 +1347,21 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
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);
|
||||
yield return Assembly.LoadFrom(file);
|
||||
Assembly plugAss;
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
HasPendingRestart = HasPendingRestart,
|
||||
IsShuttingDown = IsShuttingDown,
|
||||
Version = ApplicationVersion,
|
||||
ProductName = ApplicationProductName,
|
||||
WebSocketPortNumber = HttpPort,
|
||||
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
|
||||
Id = SystemId,
|
||||
|
@ -1422,10 +1479,21 @@ namespace Emby.Server.Implementations
|
|||
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
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
|
||||
{
|
||||
Version = ApplicationVersion,
|
||||
ProductName = ApplicationProductName,
|
||||
Id = SystemId,
|
||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||
WanAddress = wanAddress,
|
||||
|
@ -1460,7 +1528,7 @@ namespace Emby.Server.Implementations
|
|||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> GetWanApiUrl(CancellationToken cancellationToken)
|
||||
public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken)
|
||||
{
|
||||
const string Url = "http://ipv4.icanhazip.com";
|
||||
try
|
||||
|
@ -1476,13 +1544,14 @@ namespace Emby.Server.Implementations
|
|||
CancellationToken = cancellationToken
|
||||
}).ConfigureAwait(false))
|
||||
{
|
||||
return GetLocalApiUrl(response.ReadToEnd().Trim());
|
||||
return GetWanApiUrl(response.ReadToEnd().Trim());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error getting WAN Ip address information");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1498,11 +1567,40 @@ namespace Emby.Server.Implementations
|
|||
|
||||
public string GetLocalApiUrl(string host)
|
||||
{
|
||||
if (EnableHttps)
|
||||
{
|
||||
return string.Format("https://{0}:{1}",
|
||||
host,
|
||||
HttpsPort.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
return string.Format("http://{0}:{1}",
|
||||
host,
|
||||
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)
|
||||
{
|
||||
return GetLocalIpAddressesInternal(true, 0, 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;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration
|
|||
/// </summary>
|
||||
private void UpdateMetadataPath()
|
||||
{
|
||||
string metadataPath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
|
||||
{
|
||||
metadataPath = GetInternalMetadataPath();
|
||||
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
|
||||
}
|
||||
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>
|
||||
|
|
|
@ -4,7 +4,6 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
|
|
|
@ -90,9 +90,10 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(displayPreferences.Id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences.Id));
|
||||
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
|
|
@ -2741,15 +2741,16 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
|
||||
|
||||
int slowThreshold = 100;
|
||||
|
||||
#if DEBUG
|
||||
slowThreshold = 10;
|
||||
const int SlowThreshold = 100;
|
||||
#else
|
||||
const int SlowThreshold = 10;
|
||||
#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,
|
||||
elapsed,
|
||||
commandText);
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
// If the user password is the sha1 hash of the empty string, remove it
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (string.IsNullOrWhiteSpace(rangeHeader))
|
||||
{
|
||||
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
|
||||
StatusCode = HttpStatusCode.OK;
|
||||
}
|
||||
else
|
||||
|
@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
RangeStart = requestedRange.Key;
|
||||
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}";
|
||||
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>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
|
@ -126,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
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);
|
||||
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);
|
||||
|
@ -153,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
QueryString = e.QueryString ?? new QueryCollection()
|
||||
};
|
||||
|
||||
connection.Closed += Connection_Closed;
|
||||
connection.Closed += OnConnectionClosed;
|
||||
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
|
@ -163,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
||||
}
|
||||
|
||||
private void Connection_Closed(object sender, EventArgs e)
|
||||
private void OnConnectionClosed(object sender, EventArgs e)
|
||||
{
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
|
@ -202,6 +201,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
case DirectoryNotFoundException _:
|
||||
case FileNotFoundException _:
|
||||
case ResourceNotFoundException _: return 404;
|
||||
case MethodNotAllowedException _: return 405;
|
||||
case RemoteServiceUnavailableException _: return 502;
|
||||
default: return 500;
|
||||
}
|
||||
|
@ -321,14 +321,14 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
private static string NormalizeConfiguredLocalAddress(string address)
|
||||
{
|
||||
var index = address.Trim('/').IndexOf('/');
|
||||
|
||||
var add = address.AsSpan().Trim('/');
|
||||
int index = add.IndexOf('/');
|
||||
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)
|
||||
|
@ -398,8 +398,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
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
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
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
|
||||
{
|
||||
|
@ -612,21 +612,11 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
{
|
||||
var pathInfo = httpReq.PathInfo;
|
||||
|
||||
var pathParts = pathInfo.TrimStart('/').Split('/');
|
||||
if (pathParts.Length == 0)
|
||||
{
|
||||
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);
|
||||
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
|
||||
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
|
||||
if (restPath != null)
|
||||
{
|
||||
return new ServiceHandler
|
||||
{
|
||||
RestPath = restPath,
|
||||
ResponseContentType = contentType
|
||||
};
|
||||
return new ServiceHandler(restPath, contentType);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var bOutput = Encoding.UTF8.GetBytes(text);
|
||||
response.OriginalResponse.ContentLength = bOutput.Length;
|
||||
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
|
||||
}
|
||||
|
||||
|
@ -654,11 +645,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -683,10 +669,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
UrlPrefixes = urlPrefixes.ToArray();
|
||||
ServiceController = new ServiceController();
|
||||
|
||||
Logger.LogInformation("Calling ServiceStack AppHost.Init");
|
||||
|
||||
var types = services.Select(r => r.GetType()).ToArray();
|
||||
|
||||
var types = services.Select(r => r.GetType());
|
||||
ServiceController.Init(this, types);
|
||||
|
||||
ResponseFilters = new Action<IRequest, IResponse, object>[]
|
||||
|
|
|
@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
content = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(content, contentType);
|
||||
result = new StreamWriter(content, contentType, contentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
bytes = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(bytes, contentType);
|
||||
result = new StreamWriter(bytes, contentType, contentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType);
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new StreamWriter(content, contentType);
|
||||
var result = new StreamWriter(content, contentType, contentLength);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
|
@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
}
|
||||
else
|
||||
{
|
||||
if (totalContentLength.HasValue)
|
||||
{
|
||||
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
using (stream)
|
||||
|
@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (lastModifiedDate.HasValue)
|
||||
{
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString();
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||
|
||||
if (RangeStart > 0 && SourceStream.CanSeek)
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
public void FilterResponse(IRequest req, IResponse res, object dto)
|
||||
{
|
||||
// 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-Origin", "*");
|
||||
|
||||
|
@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (length > 0)
|
||||
{
|
||||
res.OriginalResponse.ContentLength = length;
|
||||
res.SendChunked = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
// This code is executed before the service
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(request);
|
||||
|
||||
if (!IsExemptFromAuthenticationToken(auth, authAttribtues, request))
|
||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
/// </summary>
|
||||
public class StreamWriter : IAsyncStreamWriter, IHasHeaders
|
||||
{
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source stream.
|
||||
/// </summary>
|
||||
|
@ -52,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
SourceStream = source;
|
||||
|
||||
Headers["Content-Type"] = contentType;
|
||||
|
||||
if (source.CanSeek)
|
||||
{
|
||||
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
}
|
||||
|
||||
|
@ -60,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
/// </summary>
|
||||
/// <param name="source">The source.</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))
|
||||
{
|
||||
|
@ -69,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
SourceBytes = source;
|
||||
|
||||
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,6 @@ using System.Threading;
|
|||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.System;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
|
@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
AddAffectedPath(path);
|
||||
}
|
||||
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
|
@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO
|
|||
AddAffectedPath(affectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
|
@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// The file system watchers
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The affected paths
|
||||
/// </summary>
|
||||
|
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO
|
|||
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
|
||||
// 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);
|
||||
|
@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
public void Start()
|
||||
{
|
||||
LibraryManager.ItemAdded += LibraryManager_ItemAdded;
|
||||
LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;
|
||||
LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
||||
LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
|
||||
|
||||
var pathsToWatch = new List<string> { };
|
||||
var pathsToWatch = new List<string>();
|
||||
|
||||
var paths = LibraryManager
|
||||
.RootFolder
|
||||
|
@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</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)
|
||||
{
|
||||
|
@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</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)
|
||||
{
|
||||
|
@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO
|
|||
if (!Directory.Exists(path))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
||||
{
|
||||
if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// not supported
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Already being watched
|
||||
if (_fileSystemWatchers.ContainsKey(path))
|
||||
{
|
||||
|
@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
var newWatcher = new FileSystemWatcher(path, "*")
|
||||
{
|
||||
IncludeSubdirectories = true
|
||||
};
|
||||
|
||||
newWatcher.InternalBufferSize = 65536;
|
||||
|
||||
newWatcher.NotifyFilter = NotifyFilters.CreationTime |
|
||||
IncludeSubdirectories = true,
|
||||
InternalBufferSize = 65536,
|
||||
NotifyFilter = NotifyFilters.CreationTime |
|
||||
NotifyFilters.DirectoryName |
|
||||
NotifyFilters.FileName |
|
||||
NotifyFilters.LastWrite |
|
||||
NotifyFilters.Size |
|
||||
NotifyFilters.Attributes;
|
||||
NotifyFilters.Attributes
|
||||
};
|
||||
|
||||
newWatcher.Created += watcher_Changed;
|
||||
newWatcher.Deleted += watcher_Changed;
|
||||
newWatcher.Renamed += watcher_Changed;
|
||||
newWatcher.Changed += watcher_Changed;
|
||||
newWatcher.Error += watcher_Error;
|
||||
newWatcher.Created += OnWatcherChanged;
|
||||
newWatcher.Deleted += OnWatcherChanged;
|
||||
newWatcher.Renamed += OnWatcherChanged;
|
||||
newWatcher.Changed += OnWatcherChanged;
|
||||
newWatcher.Error += OnWatcherError;
|
||||
|
||||
if (_fileSystemWatchers.TryAdd(path, newWatcher))
|
||||
{
|
||||
|
@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
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.Deleted -= watcher_Changed;
|
||||
watcher.Renamed -= watcher_Changed;
|
||||
watcher.Changed -= watcher_Changed;
|
||||
watcher.Error -= watcher_Error;
|
||||
watcher.Created -= OnWatcherChanged;
|
||||
watcher.Deleted -= OnWatcherChanged;
|
||||
watcher.Renamed -= OnWatcherChanged;
|
||||
watcher.Changed -= OnWatcherChanged;
|
||||
watcher.Error -= OnWatcherError;
|
||||
|
||||
try
|
||||
{
|
||||
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
|
||||
{
|
||||
|
@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <param name="watcher">The watcher.</param>
|
||||
private void RemoveWatcherFromList(FileSystemWatcher watcher)
|
||||
{
|
||||
_fileSystemWatchers.TryRemove(watcher.Path, out var removed);
|
||||
_fileSystemWatchers.TryRemove(watcher.Path, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</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 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);
|
||||
}
|
||||
|
@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</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
|
||||
{
|
||||
//logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
|
||||
|
||||
var path = e.FullPath;
|
||||
|
||||
ReportFileSystemChanged(path);
|
||||
ReportFileSystemChanged(e.FullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -446,26 +414,23 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
if (_fileSystem.AreEqual(i, path))
|
||||
{
|
||||
Logger.LogDebug("Ignoring change to {path}", path);
|
||||
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_fileSystem.ContainsSubPath(i, path))
|
||||
{
|
||||
Logger.LogDebug("Ignoring change to {path}", path);
|
||||
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Go up a level
|
||||
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);
|
||||
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
|
@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
lock (_activeRefreshers)
|
||||
{
|
||||
var refreshers = _activeRefreshers.ToList();
|
||||
foreach (var refresher in refreshers)
|
||||
foreach (var refresher in _activeRefreshers)
|
||||
{
|
||||
// Path is already being refreshed
|
||||
if (_fileSystem.AreEqual(path, refresher.Path))
|
||||
|
@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO
|
|||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
|
||||
LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
|
||||
LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
||||
LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
|
||||
|
||||
foreach (var watcher in _fileSystemWatchers.Values.ToList())
|
||||
{
|
||||
|
@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
refresher.Dispose();
|
||||
}
|
||||
|
||||
_activeRefreshers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -19,8 +19,6 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
protected ILogger Logger;
|
||||
|
||||
private readonly bool _supportsAsyncFileStreams;
|
||||
private char[] _invalidFileNameChars;
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||
|
||||
private readonly string _tempPath;
|
||||
|
@ -32,11 +30,8 @@ namespace Emby.Server.Implementations.IO
|
|||
IApplicationPaths applicationPaths)
|
||||
{
|
||||
Logger = loggerFactory.CreateLogger("FileSystem");
|
||||
_supportsAsyncFileStreams = true;
|
||||
_tempPath = applicationPaths.TempDirectory;
|
||||
|
||||
SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows);
|
||||
|
||||
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
|
||||
}
|
||||
|
||||
|
@ -45,20 +40,6 @@ namespace Emby.Server.Implementations.IO
|
|||
_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>
|
||||
/// Determines whether the specified filename is shortcut.
|
||||
/// </summary>
|
||||
|
@ -92,20 +73,22 @@ namespace Emby.Server.Implementations.IO
|
|||
var extension = Path.GetExtension(filename);
|
||||
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
return handler.Resolve(filename);
|
||||
}
|
||||
|
||||
return null;
|
||||
return handler?.Resolve(filename);
|
||||
}
|
||||
|
||||
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] == '/') return filePath; //absolute local path
|
||||
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
|
||||
{
|
||||
return filePath; // absolute local path
|
||||
}
|
||||
|
||||
// unc path
|
||||
if (filePath.StartsWith("\\\\"))
|
||||
|
@ -125,9 +108,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
try
|
||||
{
|
||||
string path = System.IO.Path.Combine(folderPath, filePath);
|
||||
path = System.IO.Path.GetFullPath(path);
|
||||
return path;
|
||||
return Path.Combine(Path.GetFullPath(folderPath), filePath);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
@ -166,7 +147,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -244,12 +225,13 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
|
||||
{
|
||||
var result = new FileSystemMetadata();
|
||||
|
||||
result.Exists = info.Exists;
|
||||
result.FullName = info.FullName;
|
||||
result.Extension = info.Extension;
|
||||
result.Name = info.Name;
|
||||
var result = new FileSystemMetadata
|
||||
{
|
||||
Exists = info.Exists,
|
||||
FullName = info.FullName,
|
||||
Extension = info.Extension,
|
||||
Name = info.Name
|
||||
};
|
||||
|
||||
if (result.Exists)
|
||||
{
|
||||
|
@ -260,8 +242,7 @@ namespace Emby.Server.Implementations.IO
|
|||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
//}
|
||||
|
||||
var fileInfo = info as FileInfo;
|
||||
if (fileInfo != null)
|
||||
if (info is FileInfo fileInfo)
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
result.DirectoryName = fileInfo.DirectoryName;
|
||||
|
@ -307,7 +288,7 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
var builder = new StringBuilder(filename);
|
||||
|
||||
foreach (var c in _invalidFileNameChars)
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
builder = builder.Replace(c, ' ');
|
||||
}
|
||||
|
@ -394,7 +375,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <returns>FileStream.</returns>
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
|
|||
try
|
||||
{
|
||||
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();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
|
@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
|
|||
if (emptyReadLimit <= 0)
|
||||
{
|
||||
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();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
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)
|
||||
{
|
||||
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
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)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
||||
|
@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
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)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
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)
|
||||
{
|
||||
|
@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
if (!args.IsDirectory)
|
||||
{
|
||||
// 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) ||
|
||||
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|
||||
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|
||||
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|
||||
{
|
||||
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)
|
||||
{
|
||||
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"
|
||||
};
|
||||
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,9 @@ namespace Emby.Server.Implementations.Library
|
|||
private IAuthenticationProvider[] _authenticationProviders;
|
||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||
|
||||
private IPasswordResetProvider[] _passwordResetProviders;
|
||||
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||
|
||||
public UserManager(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerConfigurationManager configurationManager,
|
||||
|
@ -102,8 +105,6 @@ namespace Emby.Server.Implementations.Library
|
|||
_fileSystem = fileSystem;
|
||||
ConfigurationManager = configurationManager;
|
||||
_users = Array.Empty<User>();
|
||||
|
||||
DeletePinFile();
|
||||
}
|
||||
|
||||
public NameIdPair[] GetAuthenticationProviders()
|
||||
|
@ -120,11 +121,29 @@ namespace Emby.Server.Implementations.Library
|
|||
.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();
|
||||
|
||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||
|
||||
_passwordResetProviders = passwordResetProviders.ToArray();
|
||||
|
||||
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||
}
|
||||
|
||||
#region UserUpdated Event
|
||||
|
@ -258,24 +277,35 @@ namespace Emby.Server.Implementations.Library
|
|||
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var success = false;
|
||||
string updatedUsername = null;
|
||||
IAuthenticationProvider authenticationProvider = null;
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
|
||||
authenticationProvider = authResult.Item1;
|
||||
success = authResult.Item2;
|
||||
updatedUsername = authResult.Item2;
|
||||
success = authResult.Item3;
|
||||
}
|
||||
else
|
||||
{
|
||||
// user is null
|
||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
|
||||
authenticationProvider = authResult.Item1;
|
||||
success = authResult.Item2;
|
||||
updatedUsername = authResult.Item2;
|
||||
success = authResult.Item3;
|
||||
|
||||
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;
|
||||
if (hasNewUserPolicy != null)
|
||||
|
@ -342,11 +372,21 @@ namespace Emby.Server.Implementations.Library
|
|||
return provider.GetType().FullName;
|
||||
}
|
||||
|
||||
private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
|
||||
{
|
||||
return provider.GetType().FullName;
|
||||
}
|
||||
|
||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||
{
|
||||
return GetAuthenticationProviders(user).First();
|
||||
}
|
||||
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User user)
|
||||
{
|
||||
return GetPasswordResetProviders(user)[0];
|
||||
}
|
||||
|
||||
private IAuthenticationProvider[] GetAuthenticationProviders(User user)
|
||||
{
|
||||
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
|
||||
|
@ -366,32 +406,59 @@ namespace Emby.Server.Implementations.Library
|
|||
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
|
||||
{
|
||||
var requiresResolvedUser = provider as IRequiresResolvedUser;
|
||||
ProviderAuthenticationResult authenticationResult = null;
|
||||
if (requiresResolvedUser != null)
|
||||
{
|
||||
await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
||||
authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
||||
}
|
||||
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)
|
||||
{
|
||||
_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;
|
||||
IAuthenticationProvider authenticationProvider = null;
|
||||
|
||||
|
@ -410,11 +477,14 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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)
|
||||
{
|
||||
authenticationProvider = provider;
|
||||
username = updatedUsername;
|
||||
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)
|
||||
|
@ -480,7 +550,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? null
|
||||
: user.EasyPassword;
|
||||
: (new PasswordHash(user.EasyPassword)).Hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -526,7 +596,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
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) ?
|
||||
hasConfiguredEasyPassword :
|
||||
|
@ -844,159 +914,51 @@ namespace Emby.Server.Implementations.Library
|
|||
Id = Guid.NewGuid(),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
UsesIdForConfigurationPath = true,
|
||||
//Salt = BCrypt.GenerateSalt()
|
||||
UsesIdForConfigurationPath = true
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
DeletePinFile();
|
||||
|
||||
var user = string.IsNullOrWhiteSpace(enteredUsername) ?
|
||||
null :
|
||||
GetUserByName(enteredUsername);
|
||||
|
||||
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
|
||||
{
|
||||
if (isInNetwork)
|
||||
{
|
||||
action = ForgotPasswordAction.PinCode;
|
||||
}
|
||||
|
||||
var result = await CreatePasswordResetPin().ConfigureAwait(false);
|
||||
pinFile = result.PinFile;
|
||||
expirationDate = result.ExpirationDate;
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = action,
|
||||
PinFile = pinFile,
|
||||
PinExpirationDate = expirationDate
|
||||
PinFile = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
DeletePinFile();
|
||||
|
||||
var usersReset = new List<string>();
|
||||
|
||||
var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
|
||||
string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
|
||||
_lastPasswordPinCreationResult != null &&
|
||||
_lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
|
||||
|
||||
if (valid)
|
||||
foreach (var provider in _passwordResetProviders)
|
||||
{
|
||||
_lastPin = null;
|
||||
_lastPasswordPinCreationResult = null;
|
||||
|
||||
foreach (var user in Users)
|
||||
var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
||||
if (result.Success)
|
||||
{
|
||||
await ResetPassword(user).ConfigureAwait(false);
|
||||
|
||||
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 result;
|
||||
}
|
||||
}
|
||||
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = valid,
|
||||
UsersReset = usersReset.ToArray()
|
||||
Success = false,
|
||||
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)
|
||||
{
|
||||
var path = GetPolicyFilePath(user);
|
||||
|
|
|
@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -1087,8 +1087,8 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
if (coreService != null)
|
||||
{
|
||||
await coreService.RefreshSeriesTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false);
|
||||
await coreService.RefreshTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false);
|
||||
await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
|
||||
await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Load these now which will prefetch metadata
|
||||
|
|
|
@ -10,14 +10,12 @@ using MediaBrowser.Controller;
|
|||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
|
@ -52,9 +50,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
||||
|
||||
var result = await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Cast<ChannelInfo>().ToList();
|
||||
return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
|
@ -73,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
private string[] _disallowedSharedStreamExtensions = new string[]
|
||||
private static readonly string[] _disallowedSharedStreamExtensions = new string[]
|
||||
{
|
||||
".mkv",
|
||||
".mp4",
|
||||
|
@ -88,9 +84,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
if (tunerCount > 0)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
@ -98,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = sources.First();
|
||||
var mediaSource = sources[0];
|
||||
|
||||
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
||||
{
|
||||
|
|
|
@ -11,7 +11,6 @@ using MediaBrowser.Common.Net;
|
|||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
@ -62,12 +61,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
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)
|
||||
{
|
||||
var channels = new List<ChannelInfo>();
|
||||
string line;
|
||||
string extInf = "";
|
||||
string extInf = string.Empty;
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
|
@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
channel.Path = line;
|
||||
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)
|
||||
{
|
||||
var channel = new ChannelInfo();
|
||||
channel.TunerHostId = tunerHostId;
|
||||
var channel = new ChannelInfo()
|
||||
{
|
||||
TunerHostId = tunerHostId
|
||||
};
|
||||
|
||||
extInf = extInf.Trim();
|
||||
|
||||
|
@ -137,13 +139,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
channelIdValues.Add(channelId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tvgId))
|
||||
{
|
||||
channelIdValues.Add(tvgId);
|
||||
}
|
||||
|
||||
if (channelIdValues.Count > 0)
|
||||
{
|
||||
channel.Id = string.Join("_", channelIdValues.ToArray());
|
||||
channel.Id = string.Join("_", channelIdValues);
|
||||
}
|
||||
|
||||
return channel;
|
||||
|
@ -152,7 +156,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
||||
{
|
||||
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 attributeValue;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"Artists": "Umělci",
|
||||
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
|
||||
"Books": "Knihy",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie",
|
||||
"Channels": "Kanály",
|
||||
"ChapterNameValue": "Kapitola {0}",
|
||||
"Collections": "Kolekce",
|
||||
|
@ -16,14 +16,14 @@
|
|||
"Folders": "Složky",
|
||||
"Genres": "Žánry",
|
||||
"HeaderAlbumArtists": "Umělci alba",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderCameraUploads": "Nahrané fotografie",
|
||||
"HeaderContinueWatching": "Pokračovat ve sledování",
|
||||
"HeaderFavoriteAlbums": "Oblíbená alba",
|
||||
"HeaderFavoriteArtists": "Oblíbení umělci",
|
||||
"HeaderFavoriteArtists": "Oblíbení interpreti",
|
||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||
"HeaderFavoriteSongs": "Oblíbené písně",
|
||||
"HeaderLiveTV": "Živá TV",
|
||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
"HeaderNextUp": "Nadcházející",
|
||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||
"HomeVideos": "Domáci videa",
|
||||
|
@ -34,17 +34,17 @@
|
|||
"LabelRunningTimeValue": "Délka média: {0}",
|
||||
"Latest": "Nejnovější",
|
||||
"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",
|
||||
"MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
|
||||
"MixedContent": "Smíšený obsah",
|
||||
"Movies": "Filmy",
|
||||
"Music": "Hudba",
|
||||
"MusicVideos": "Hudební klipy",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameInstallFailed": "Instalace {0} selhala",
|
||||
"NameSeasonNumber": "Sezóna {0}",
|
||||
"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",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována",
|
||||
"NotificationOptionAudioPlayback": "Přehrávání audia zahájeno",
|
||||
|
@ -70,12 +70,12 @@
|
|||
"ProviderValue": "Poskytl: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} selhalo",
|
||||
"ScheduledTaskStartedWithName": "{0} zahájeno",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"ServerNameNeedsToBeRestarted": "{0} vyžaduje restart",
|
||||
"Shows": "Seriály",
|
||||
"Songs": "Skladby",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
|
||||
"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}",
|
||||
"Sync": "Synchronizace",
|
||||
"System": "Systém",
|
||||
|
@ -88,10 +88,10 @@
|
|||
"UserOfflineFromDevice": "{0} se odpojil od {1}",
|
||||
"UserOnlineFromDevice": "{0} se připojil z {1}",
|
||||
"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}",
|
||||
"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}",
|
||||
"VersionNumber": "Verze {0}"
|
||||
}
|
||||
|
|
|
@ -61,8 +61,8 @@
|
|||
"NotificationOptionUserLockedOut": "Bruger låst ude",
|
||||
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
|
||||
"Photos": "Fotos",
|
||||
"Playlists": "Spillelister",
|
||||
"Photos": "Fotoer",
|
||||
"Playlists": "Afspilningslister",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} blev installeret",
|
||||
"PluginUninstalledWithName": "{0} blev afinstalleret",
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"Folders": "Φάκελοι",
|
||||
"Genres": "Είδη",
|
||||
"HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
|
||||
"HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε",
|
||||
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
|
||||
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
|
||||
|
@ -34,7 +34,7 @@
|
|||
"LabelRunningTimeValue": "Διάρκεια: {0}",
|
||||
"Latest": "Πρόσφατα",
|
||||
"MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
||||
"MessageApplicationUpdatedTo": "Ο server Jellyfin αναβαθμίστηκε σε έκδοση {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί",
|
||||
"MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί",
|
||||
"MixedContent": "Ανάμεικτο Περιεχόμενο",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε",
|
||||
"NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε",
|
||||
"NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα",
|
||||
"NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης",
|
||||
"NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο",
|
||||
"NotificationOptionPluginError": "Αποτυχία του plugin",
|
||||
|
@ -75,7 +75,7 @@
|
|||
"Songs": "Τραγούδια",
|
||||
"StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.",
|
||||
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
|
||||
"SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}",
|
||||
"Sync": "Συγχρονισμός",
|
||||
"System": "Σύστημα",
|
||||
|
|
|
@ -1,97 +1,97 @@
|
|||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artists",
|
||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
||||
"Albums": "Albom",
|
||||
"AppDeviceValues": "App: {0}, Grät: {1}",
|
||||
"Application": "Aawändig",
|
||||
"Artists": "Könstler",
|
||||
"AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
|
||||
"Books": "Büecher",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"Channels": "Channels",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
|
||||
"Channels": "Kanäu",
|
||||
"ChapterNameValue": "Kapitu {0}",
|
||||
"Collections": "Sammlige",
|
||||
"DeviceOfflineWithName": "{0} esch offline gange",
|
||||
"DeviceOnlineWithName": "{0} esch online cho",
|
||||
"FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
|
||||
"Favorites": "Favorite",
|
||||
"Folders": "Ordner",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Albuminterprete",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderAlbumArtists": "Albom-Könstler",
|
||||
"HeaderCameraUploads": "Kamera-Uploads",
|
||||
"HeaderContinueWatching": "Wiiterluege",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
"HeaderFavoriteArtists": "Besti Interpret",
|
||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
||||
"HeaderFavoriteShows": "Favorite Shows",
|
||||
"HeaderFavoriteSongs": "Besti Lieder",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
"HeaderNextUp": "Next Up",
|
||||
"HeaderFavoriteAlbums": "Lieblingsalbe",
|
||||
"HeaderFavoriteArtists": "Lieblings-Interprete",
|
||||
"HeaderFavoriteEpisodes": "Lieblingsepisode",
|
||||
"HeaderFavoriteShows": "Lieblingsserie",
|
||||
"HeaderFavoriteSongs": "Lieblingslieder",
|
||||
"HeaderLiveTV": "Live-Färnseh",
|
||||
"HeaderNextUp": "Als nächts",
|
||||
"HeaderRecordingGroups": "Ufnahmegruppe",
|
||||
"HomeVideos": "Heimfilmli",
|
||||
"Inherit": "Hinzuefüege",
|
||||
"ItemAddedWithName": "{0} was added to the library",
|
||||
"ItemRemovedWithName": "{0} was removed from the library",
|
||||
"LabelIpAddressValue": "Ip address: {0}",
|
||||
"LabelRunningTimeValue": "Running time: {0}",
|
||||
"Latest": "Letschte",
|
||||
"MessageApplicationUpdated": "Jellyfin Server has been updated",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
||||
"MessageServerConfigurationUpdated": "Server configuration has been updated",
|
||||
"MixedContent": "Gmischte Inhalt",
|
||||
"Movies": "Movies",
|
||||
"ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
|
||||
"ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
|
||||
"LabelIpAddressValue": "IP-Adrässe: {0}",
|
||||
"LabelRunningTimeValue": "Loufziit: {0}",
|
||||
"Latest": "Nöischti",
|
||||
"MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
|
||||
"MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
|
||||
"MixedContent": "Gmeschti Inhäut",
|
||||
"Movies": "Film",
|
||||
"Music": "Musig",
|
||||
"MusicVideos": "Musigfilm",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionInstallationFailed": "Installation failure",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionPluginInstalled": "Plugin installed",
|
||||
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
|
||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"MusicVideos": "Musigvideos",
|
||||
"NameInstallFailed": "Installation vo {0} fäugschlage",
|
||||
"NameSeasonNumber": "Staffle {0}",
|
||||
"NameSeasonUnknown": "Staffle unbekannt",
|
||||
"NewVersionIsAvailable": "E nöi Version vo Jellyfin Server esch zom Download parat.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Aawändigsupdate verfüegbar",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Aawändigsupdate installiert",
|
||||
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
|
||||
"NotificationOptionCameraImageUploaded": "Foti ueglade",
|
||||
"NotificationOptionInstallationFailed": "Installationsfäuer",
|
||||
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
|
||||
"NotificationOptionPluginError": "Plugin-Fäuer",
|
||||
"NotificationOptionPluginInstalled": "Plugin installiert",
|
||||
"NotificationOptionPluginUninstalled": "Plugin deinstalliert",
|
||||
"NotificationOptionPluginUpdateInstalled": "Pluginupdate installiert",
|
||||
"NotificationOptionServerRestartRequired": "Serverneustart notwändig",
|
||||
"NotificationOptionTaskFailed": "Planti Uufgab fäugschlage",
|
||||
"NotificationOptionUserLockedOut": "Benotzer usgschlosse",
|
||||
"NotificationOptionVideoPlayback": "Videowedergab gstartet",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videowedergab gstoppt",
|
||||
"Photos": "Fotis",
|
||||
"Playlists": "Abspielliste",
|
||||
"Playlists": "Wedergabeliste",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"Shows": "Shows",
|
||||
"Songs": "Songs",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
||||
"PluginInstalledWithName": "{0} esch installiert worde",
|
||||
"PluginUninstalledWithName": "{0} esch deinstalliert worde",
|
||||
"PluginUpdatedWithName": "{0} esch updated worde",
|
||||
"ProviderValue": "Aabieter: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} esch fäugschlage",
|
||||
"ScheduledTaskStartedWithName": "{0} het gstartet",
|
||||
"ServerNameNeedsToBeRestarted": "{0} mues nöi gstartet wärde",
|
||||
"Shows": "Serie",
|
||||
"Songs": "Lieder",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
|
||||
"Sync": "Sync",
|
||||
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
|
||||
"SubtitlesDownloadedForItem": "Ondertetle abeglade för {0}",
|
||||
"Sync": "Synchronisation",
|
||||
"System": "System",
|
||||
"TvShows": "TV Shows",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted",
|
||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueSpecialEpisodeName": "Spezial - {0}",
|
||||
"TvShows": "Färnsehserie",
|
||||
"User": "Benotzer",
|
||||
"UserCreatedWithName": "Benotzer {0} esch erstöut worde",
|
||||
"UserDeletedWithName": "Benotzer {0} esch glösche worde",
|
||||
"UserDownloadingItemWithValues": "{0} ladt {1} abe",
|
||||
"UserLockedOutWithName": "Benotzer {0} esch usgschlosse worde",
|
||||
"UserOfflineFromDevice": "{0} esch vo {1} trennt worde",
|
||||
"UserOnlineFromDevice": "{0} esch online vo {1}",
|
||||
"UserPasswordChangedWithName": "S'Passwort för Benotzer {0} esch gänderet worde",
|
||||
"UserPolicyUpdatedWithName": "Benotzerrechtlinie för {0} esch aktualisiert worde",
|
||||
"UserStartedPlayingItemWithValues": "{0} hed d'Wedergab vo {1} of {2} gstartet",
|
||||
"UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
|
||||
"ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
|
||||
"ValueSpecialEpisodeName": "Extra - {0}",
|
||||
"VersionNumber": "Version {0}"
|
||||
}
|
||||
|
|
1
Emby.Server.Implementations/Localization/Core/ja.json
Normal file
1
Emby.Server.Implementations/Localization/Core/ja.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -5,7 +5,7 @@
|
|||
"Artists": "Oryndaýshylar",
|
||||
"AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
|
||||
"Books": "Kitaptar",
|
||||
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy",
|
||||
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep salyndy",
|
||||
"Channels": "Arnalar",
|
||||
"ChapterNameValue": "{0}-sahna",
|
||||
"Collections": "Jıyntyqtar",
|
||||
|
@ -35,8 +35,8 @@
|
|||
"Latest": "Eń keıingi",
|
||||
"MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy",
|
||||
"MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfıgýrasýasynyń {0} bólimi jańartyldy",
|
||||
"MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy",
|
||||
"MixedContent": "Aralas mazmun",
|
||||
"Movies": "Fılmder",
|
||||
"Music": "Mýzyka",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
|
||||
"NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
|
||||
"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",
|
||||
"NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
|
||||
"NotificationOptionPluginError": "Plagın sátsizdigi",
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"Albums": "Albums",
|
||||
"Albums": "Albumai",
|
||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artists",
|
||||
"Artists": "Atlikėjai",
|
||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
||||
"Books": "Books",
|
||||
"Books": "Knygos",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"Channels": "Channels",
|
||||
"Channels": "Kanalai",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"Collections": "Collections",
|
||||
"Collections": "Kolekcijos",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"Favorites": "Mėgstami",
|
||||
"Folders": "Katalogai",
|
||||
"Genres": "Žanrai",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderAlbumArtists": "Albumo atlikėjai",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderContinueWatching": "Žiūrėti toliau",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavje {0}",
|
||||
"Collections": "Zbirke",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOfflineWithName": "{0} je prekinil povezavo",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
|
||||
"Favorites": "Priljubljeni",
|
||||
|
@ -33,9 +33,9 @@
|
|||
"LabelIpAddressValue": "IP naslov: {0}",
|
||||
"LabelRunningTimeValue": "Čas trajanja: {0}",
|
||||
"Latest": "Najnovejše",
|
||||
"MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
|
||||
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
|
||||
"MixedContent": "Razne vsebine",
|
||||
"Movies": "Filmi",
|
||||
|
@ -57,41 +57,41 @@
|
|||
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
|
||||
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
|
||||
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"Photos": "Photos",
|
||||
"Playlists": "Playlists",
|
||||
"NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
|
||||
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
|
||||
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
|
||||
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
|
||||
"Photos": "Fotografije",
|
||||
"Playlists": "Seznami predvajanja",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"PluginInstalledWithName": "{0} je bil nameščen",
|
||||
"PluginUninstalledWithName": "{0} je bil odstranjen",
|
||||
"PluginUpdatedWithName": "{0} je bil posodobljen",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"ScheduledTaskFailedWithName": "{0} ni uspelo",
|
||||
"ScheduledTaskStartedWithName": "{0} začeto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
|
||||
"Shows": "Serije",
|
||||
"Songs": "Songs",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
||||
"Songs": "Pesmi",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
|
||||
"Sync": "Sync",
|
||||
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
|
||||
"SubtitlesDownloadedForItem": "Podnapisi preneseni za {0}",
|
||||
"Sync": "Sinhroniziraj",
|
||||
"System": "System",
|
||||
"TvShows": "TV Shows",
|
||||
"TvShows": "TV serije",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted",
|
||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
|
||||
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
|
||||
"UserDownloadingItemWithValues": "{0} prenaša {1}",
|
||||
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
|
||||
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
|
||||
"UserOnlineFromDevice": "{0} je aktiven iz {1}",
|
||||
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
|
||||
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}"
|
||||
}
|
||||
|
|
93
Emby.Server.Implementations/Localization/Core/zh-TW.json
Normal file
93
Emby.Server.Implementations/Localization/Core/zh-TW.json
Normal 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}"
|
||||
}
|
|
@ -43,6 +43,11 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
var contentLength = bytesResponse.Length;
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
response.OriginalResponse.ContentLength = contentLength;
|
||||
}
|
||||
|
||||
if (contentLength > 0)
|
||||
{
|
||||
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
@ -20,6 +21,8 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||
}
|
||||
|
||||
response.OriginalResponse.ContentLength = 0;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
@ -39,11 +42,6 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
response.StatusCode = httpResult.Status;
|
||||
response.StatusDescription = httpResult.StatusCode.ToString();
|
||||
//if (string.IsNullOrEmpty(httpResult.ContentType))
|
||||
//{
|
||||
// httpResult.ContentType = defaultContentType;
|
||||
//}
|
||||
//response.ContentType = httpResult.ContentType;
|
||||
}
|
||||
|
||||
var responseOptions = result as IHasHeaders;
|
||||
|
@ -53,6 +51,7 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -72,51 +71,36 @@ namespace Emby.Server.Implementations.Services
|
|||
response.ContentType += "; charset=utf-8";
|
||||
}
|
||||
|
||||
var asyncStreamWriter = result as IAsyncStreamWriter;
|
||||
if (asyncStreamWriter != null)
|
||||
switch (result)
|
||||
{
|
||||
case IAsyncStreamWriter asyncStreamWriter:
|
||||
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
|
||||
}
|
||||
|
||||
var streamWriter = result as IStreamWriter;
|
||||
if (streamWriter != null)
|
||||
{
|
||||
case IStreamWriter streamWriter:
|
||||
streamWriter.WriteTo(response.OutputStream);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var fileWriter = result as FileWriter;
|
||||
if (fileWriter != null)
|
||||
{
|
||||
case FileWriter fileWriter:
|
||||
return fileWriter.WriteToAsync(response, cancellationToken);
|
||||
}
|
||||
|
||||
var stream = result as Stream;
|
||||
if (stream != null)
|
||||
{
|
||||
case Stream stream:
|
||||
return CopyStream(stream, response.OutputStream);
|
||||
}
|
||||
|
||||
var bytes = result as byte[];
|
||||
if (bytes != null)
|
||||
{
|
||||
case byte[] bytes:
|
||||
response.ContentType = "application/octet-stream";
|
||||
response.OriginalResponse.ContentLength = bytes.Length;
|
||||
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
case string responseText:
|
||||
var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
|
||||
response.OriginalResponse.ContentLength = responseTextAsBytes.Length;
|
||||
|
||||
if (responseTextAsBytes.Length > 0)
|
||||
{
|
||||
return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -143,14 +127,13 @@ namespace Emby.Server.Implementations.Services
|
|||
ms.Position = 0;
|
||||
|
||||
var contentLength = ms.Length;
|
||||
response.OriginalResponse.ContentLength = contentLength;
|
||||
|
||||
if (contentLength > 0)
|
||||
{
|
||||
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
//serializer(result, outputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Model.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 void VoidActionInvokerFn(object intance, object request);
|
||||
|
||||
public class ServiceController
|
||||
{
|
||||
public static ServiceController Instance;
|
||||
|
||||
public ServiceController()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void Init(HttpListenerHost appHost, Type[] serviceTypes)
|
||||
public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
|
||||
{
|
||||
foreach (var serviceType in serviceTypes)
|
||||
{
|
||||
|
@ -37,7 +28,11 @@ namespace Emby.Server.Implementations.Services
|
|||
foreach (var mi in serviceType.GetActions())
|
||||
{
|
||||
var requestType = mi.GetParameters()[0].ParameterType;
|
||||
if (processedReqs.Contains(requestType)) continue;
|
||||
if (processedReqs.Contains(requestType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
processedReqs.Add(requestType);
|
||||
|
||||
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 void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
|
||||
|
@ -84,18 +67,25 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
public void RegisterRestPath(RestPath restPath)
|
||||
{
|
||||
if (!restPath.Path.StartsWith("/"))
|
||||
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))
|
||||
if (restPath.Path[0] != '/')
|
||||
{
|
||||
pathsAtFirstMatch = new List<RestPath>();
|
||||
RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch;
|
||||
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.Add(restPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
|
||||
}
|
||||
}
|
||||
|
||||
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
|
||||
{
|
||||
|
@ -155,17 +145,15 @@ namespace Emby.Server.Implementations.Services
|
|||
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;
|
||||
var requestType = requestDto.GetType();
|
||||
req.OperationName = requestType.Name;
|
||||
|
||||
var serviceType = appHost.GetServiceTypeByRequest(requestType);
|
||||
var serviceType = httpHost.GetServiceTypeByRequest(requestType);
|
||||
|
||||
var service = appHost.CreateInstance(serviceType);
|
||||
|
||||
//var service = typeFactory.CreateInstance(serviceType);
|
||||
var service = httpHost.CreateInstance(serviceType);
|
||||
|
||||
var serviceRequiresContext = service as IRequiresRequest;
|
||||
if (serviceRequiresContext != null)
|
||||
|
|
|
@ -11,6 +11,16 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
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)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
|
||||
|
@ -18,24 +28,18 @@ namespace Emby.Server.Implementations.Services
|
|||
var deserializer = RequestHelper.GetRequestReader(host, contentType);
|
||||
if (deserializer != null)
|
||||
{
|
||||
return deserializer(requestType, httpReq.InputStream);
|
||||
return deserializer.Invoke(requestType, httpReq.InputStream);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
contentType = null;
|
||||
var pos = pathInfo.LastIndexOf('.');
|
||||
if (pos >= 0)
|
||||
if (pos != -1)
|
||||
{
|
||||
var format = pathInfo.Substring(pos + 1);
|
||||
contentType = GetFormatContentType(format);
|
||||
|
@ -44,58 +48,38 @@ namespace Emby.Server.Implementations.Services
|
|||
pathInfo = pathInfo.Substring(0, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return pathInfo;
|
||||
}
|
||||
|
||||
private static string GetFormatContentType(string format)
|
||||
{
|
||||
//built-in formats
|
||||
if (format == "json")
|
||||
return "application/json";
|
||||
if (format == "xml")
|
||||
return "application/xml";
|
||||
|
||||
return null;
|
||||
switch (format)
|
||||
{
|
||||
case "json": return "application/json";
|
||||
case "xml": return "application/xml";
|
||||
default: 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)
|
||||
{
|
||||
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);
|
||||
httpReq.Items["__route"] = RestPath;
|
||||
|
||||
if (ResponseContentType != null)
|
||||
{
|
||||
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
|
||||
foreach (var responseFilter in appHost.ResponseFilters)
|
||||
foreach (var responseFilter in httpHost.ResponseFilters)
|
||||
{
|
||||
responseFilter(httpReq, httpRes, response);
|
||||
}
|
||||
|
@ -152,7 +136,11 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
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];
|
||||
if (values.Count == 1)
|
||||
|
@ -175,7 +163,11 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
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);
|
||||
if (values.Count == 1)
|
||||
|
@ -210,7 +202,12 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
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];
|
||||
}
|
||||
|
||||
|
@ -221,7 +218,12 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
@ -229,17 +231,5 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
|
|||
string propertyName = pair.Key;
|
||||
string propertyTextValue = pair.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(propertyTextValue)
|
||||
if (propertyTextValue == null
|
||||
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|
||||
|| propertySerializerEntry.PropertySetFn == null)
|
||||
{
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
|
||||
namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
|
@ -109,10 +109,16 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
public class SwaggerService : IService, IRequiresRequest
|
||||
{
|
||||
private readonly IHttpServer _httpServer;
|
||||
private SwaggerSpec _spec;
|
||||
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
public SwaggerService(IHttpServer httpServer)
|
||||
{
|
||||
_httpServer = httpServer;
|
||||
}
|
||||
|
||||
public object Get(GetSwaggerSpec request)
|
||||
{
|
||||
return _spec ?? (_spec = GetSpec());
|
||||
|
@ -181,7 +187,8 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
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)
|
||||
{
|
||||
|
@ -192,11 +199,8 @@ namespace Emby.Server.Implementations.Services
|
|||
continue;
|
||||
}
|
||||
|
||||
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
||||
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
|
||||
|| info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ using System.IO;
|
|||
using System.Net;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -25,8 +27,6 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||
this.OperationName = operationName;
|
||||
this.request = httpContext;
|
||||
this.Response = new WebSocketSharpResponse(logger, response);
|
||||
|
||||
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
|
||||
}
|
||||
|
||||
public HttpRequest HttpRequest => request;
|
||||
|
@ -40,16 +40,9 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||
public string RawUrl => request.GetEncodedPathAndQuery();
|
||||
|
||||
public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
|
||||
// Header[name] returns "" when undefined
|
||||
|
||||
public string XForwardedFor
|
||||
=> 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 GetHeader(string name) => request.Headers[name].ToString();
|
||||
|
||||
private string remoteIp;
|
||||
public string RemoteIp
|
||||
|
@ -61,107 +54,27 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||
return remoteIp;
|
||||
}
|
||||
|
||||
var temp = CheckBadChars(XForwardedFor.AsSpan());
|
||||
if (temp.Length != 0)
|
||||
{
|
||||
return remoteIp = temp.ToString();
|
||||
}
|
||||
IPAddress ip;
|
||||
|
||||
temp = CheckBadChars(XRealIp.AsSpan());
|
||||
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 = NormalizeIp(temp).ToString();
|
||||
}
|
||||
|
||||
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
|
||||
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
|
||||
{
|
||||
ip = request.HttpContext.Connection.RemoteIpAddress;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
|
||||
|
||||
// CheckBadChars - throws on invalid chars to be not found in header name/value
|
||||
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
|
||||
{
|
||||
if (name.Length == 0)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
return remoteIp = NormalizeIp(ip).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
if (crlf != 0)
|
||||
private static IPAddress NormalizeIp(IPAddress ip)
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
|
||||
if (ip.IsIPv4MappedToIPv6)
|
||||
{
|
||||
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.MapToIPv4();
|
||||
}
|
||||
|
||||
return ip;
|
||||
|
@ -312,97 +225,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||
return pos == -1 ? strVal : strVal.Slice(0, pos);
|
||||
}
|
||||
|
||||
public static string HandlerFactoryPath;
|
||||
|
||||
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 PathInfo => this.request.Path.Value;
|
||||
|
||||
public string UserAgent => request.Headers[HeaderNames.UserAgent];
|
||||
|
||||
|
@ -501,19 +324,5 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,6 @@ namespace Emby.Server.Implementations.Udp
|
|||
_socketFactory = socketFactory;
|
||||
|
||||
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)
|
||||
|
|
|
@ -12,7 +12,6 @@ using MediaBrowser.Common.Plugins;
|
|||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
@ -39,11 +38,10 @@ namespace Emby.Server.Implementations.Updates
|
|||
/// <summary>
|
||||
/// The completed installations
|
||||
/// </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>
|
||||
/// Occurs when [plugin uninstalled].
|
||||
/// </summary>
|
||||
|
@ -57,9 +55,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
{
|
||||
PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin });
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PluginUpdated Event
|
||||
/// <summary>
|
||||
/// Occurs when [plugin updated].
|
||||
/// </summary>
|
||||
|
@ -77,9 +73,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
_applicationHost.NotifyPendingRestart();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PluginInstalled Event
|
||||
/// <summary>
|
||||
/// Occurs when [plugin updated].
|
||||
/// </summary>
|
||||
|
@ -96,7 +90,6 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
_applicationHost.NotifyPendingRestart();
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The _logger
|
||||
|
@ -115,12 +108,8 @@ namespace Emby.Server.Implementations.Updates
|
|||
/// <value>The application host.</value>
|
||||
private readonly IApplicationHost _applicationHost;
|
||||
|
||||
private readonly ICryptoProvider _cryptographyProvider;
|
||||
private readonly IZipClient _zipClient;
|
||||
|
||||
// netframework or netcore
|
||||
private readonly string _packageRuntime;
|
||||
|
||||
public InstallationManager(
|
||||
ILoggerFactory loggerFactory,
|
||||
IApplicationHost appHost,
|
||||
|
@ -129,9 +118,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
IJsonSerializer jsonSerializer,
|
||||
IServerConfigurationManager config,
|
||||
IFileSystem fileSystem,
|
||||
ICryptoProvider cryptographyProvider,
|
||||
IZipClient zipClient,
|
||||
string packageRuntime)
|
||||
IZipClient zipClient)
|
||||
{
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
|
@ -139,18 +126,16 @@ namespace Emby.Server.Implementations.Updates
|
|||
}
|
||||
|
||||
CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>();
|
||||
CompletedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
|
||||
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
|
||||
|
||||
_logger = loggerFactory.CreateLogger(nameof(InstallationManager));
|
||||
_applicationHost = appHost;
|
||||
_appPaths = appPaths;
|
||||
_httpClient = httpClient;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_cryptographyProvider = cryptographyProvider;
|
||||
_zipClient = zipClient;
|
||||
_packageRuntime = packageRuntime;
|
||||
_logger = loggerFactory.CreateLogger(nameof(InstallationManager));
|
||||
}
|
||||
|
||||
private static Version GetPackageVersion(PackageVersionInfo version)
|
||||
|
@ -222,11 +207,6 @@ namespace Emby.Server.Implementations.Updates
|
|||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
versions.Add(version);
|
||||
}
|
||||
|
||||
|
@ -448,7 +428,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
CurrentInstallations.Remove(tuple);
|
||||
}
|
||||
|
||||
CompletedInstallationsInternal.Add(installationInfo);
|
||||
_completedInstallationsInternal.Add(installationInfo);
|
||||
|
||||
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)
|
||||
{
|
||||
// TODO: Remove the `string target` argument as it is not used any longer
|
||||
|
||||
var extension = Path.GetExtension(package.targetFilename);
|
||||
var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
|
@ -538,12 +520,12 @@ namespace Emby.Server.Implementations.Updates
|
|||
return;
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
target = Path.Combine(_appPaths.PluginsPath, Path.GetFileNameWithoutExtension(package.targetFilename));
|
||||
}
|
||||
// Always override the passed-in target (which is a file) and figure it out again
|
||||
target = Path.Combine(_appPaths.PluginsPath, package.name);
|
||||
_logger.LogDebug("Installing plugin to {Filename}.", target);
|
||||
|
||||
// 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
|
||||
{
|
||||
Url = package.sourceUrl,
|
||||
|
@ -556,9 +538,17 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
// 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
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target);
|
||||
using (var stream = File.OpenRead(tempFile))
|
||||
{
|
||||
_zipClient.ExtractAllFromZip(stream, target, true);
|
||||
|
@ -572,6 +562,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Deleting temporary file {Filename}.", tempFile);
|
||||
_fileSystem.DeleteFile(tempFile);
|
||||
}
|
||||
catch (IOException ex)
|
||||
|
@ -594,7 +585,13 @@ namespace Emby.Server.Implementations.Updates
|
|||
_applicationHost.RemovePlugin(plugin);
|
||||
|
||||
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
|
||||
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
|
||||
|
@ -605,7 +602,16 @@ namespace Emby.Server.Implementations.Updates
|
|||
path = file;
|
||||
}
|
||||
|
||||
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 filename = Path.GetFileName(path);
|
||||
|
|
|
@ -19,7 +19,6 @@ using MediaBrowser.Common.Configuration;
|
|||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -119,6 +118,10 @@ namespace Jellyfin.Server
|
|||
|
||||
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
|
||||
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Api.Movies;
|
||||
|
@ -827,9 +829,18 @@ namespace MediaBrowser.Api.Library
|
|||
// Quotes are valid in linux. They'll possibly cause issues here
|
||||
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(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
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Net;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
|
@ -279,10 +281,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
/// <returns>Task{System.Object}.</returns>
|
||||
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
string useragent = null;
|
||||
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
|
||||
|
||||
var trySupportSeek = false;
|
||||
state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent);
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
|
@ -292,29 +291,14 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
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);
|
||||
|
||||
if (trySupportSeek)
|
||||
{
|
||||
foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges })
|
||||
{
|
||||
var val = response.Headers[name];
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
responseHeaders[name] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
||||
|
||||
// Seeing cases of -1 here
|
||||
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
|
||||
{
|
||||
responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
|
@ -356,10 +340,31 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
var contentType = state.GetMimeType(outputPath);
|
||||
|
||||
// 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
|
||||
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);
|
||||
|
@ -397,5 +402,22 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class RevokeKey
|
||||
|
@ -294,6 +300,11 @@ namespace MediaBrowser.Api.Session
|
|||
return _userManager.GetAuthenticationProviders();
|
||||
}
|
||||
|
||||
public object Get(GetPasswordResetProviders request)
|
||||
{
|
||||
return _userManager.GetPasswordResetProviders();
|
||||
}
|
||||
|
||||
public void Delete(RevokeKey request)
|
||||
{
|
||||
_sessionManager.RevokeToken(request.Key);
|
||||
|
|
|
@ -379,10 +379,15 @@ namespace MediaBrowser.Api
|
|||
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
|
||||
{
|
||||
Username = user.Name,
|
||||
Password = request.Password,
|
||||
Password = null, // This should always be null
|
||||
Pw = request.Pw
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace MediaBrowser.Common.Extensions
|
||||
{
|
||||
|
@ -9,8 +10,6 @@ namespace MediaBrowser.Common.Extensions
|
|||
/// </summary>
|
||||
public static class BaseExtensions
|
||||
{
|
||||
public static ICryptoProvider CryptographyProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Strips the HTML.
|
||||
/// </summary>
|
||||
|
@ -31,7 +30,10 @@ namespace MediaBrowser.Common.Extensions
|
|||
/// <returns>Guid.</returns>
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 RemoteServiceUnavailableException()
|
||||
|
|
|
@ -25,11 +25,6 @@ namespace MediaBrowser.Common
|
|||
/// <value>The device identifier.</value>
|
||||
string SystemId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [application updated].
|
||||
/// </summary>
|
||||
event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance has pending kernel reload.
|
||||
/// </summary>
|
||||
|
|
11
MediaBrowser.Common/Net/CustomHeaderNames.cs
Normal file
11
MediaBrowser.Common/Net/CustomHeaderNames.cs
Normal 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";
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -78,10 +78,25 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <summary>
|
||||
/// The trailer folder name
|
||||
/// </summary>
|
||||
public static string TrailerFolderName = "trailers";
|
||||
public static string ThemeSongsFolderName = "theme-music";
|
||||
public static string ThemeSongFilename = "theme";
|
||||
public static string ThemeVideosFolderName = "backdrops";
|
||||
public const string TrailerFolderName = "trailers";
|
||||
public const string ThemeSongsFolderName = "theme-music";
|
||||
public const string ThemeSongFilename = "theme";
|
||||
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]
|
||||
public Guid[] ThemeSongIds { get; set; }
|
||||
|
@ -1276,16 +1291,15 @@ namespace MediaBrowser.Controller.Entities
|
|||
.Select(item =>
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
|
||||
|
||||
if (dbItem != null)
|
||||
if (LibraryManager.GetItemById(item.Id) is Video dbItem)
|
||||
{
|
||||
item = dbItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
// item is new
|
||||
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo;
|
||||
item.ExtraType = Model.Entities.ExtraType.ThemeVideo;
|
||||
}
|
||||
|
||||
return item;
|
||||
|
@ -1296,32 +1310,37 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var files = fileSystemChildren.Where(i => i.IsDirectory)
|
||||
var extras = new List<Video>();
|
||||
|
||||
var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
|
||||
foreach (var extraFolderName in AllExtrasTypesFolderNames)
|
||||
{
|
||||
var files = folders
|
||||
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(i => FileSystem.GetFiles(i.FullName));
|
||||
|
||||
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
||||
extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
||||
.OfType<Video>()
|
||||
.Select(item =>
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
|
||||
|
||||
if (dbItem != null)
|
||||
if (LibraryManager.GetItemById(item.Id) is Video dbItem)
|
||||
{
|
||||
item = dbItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
// item is new
|
||||
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip;
|
||||
}
|
||||
|
||||
// Use some hackery to get the extra type based on foldername
|
||||
Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType);
|
||||
item.ExtraType = extraType;
|
||||
|
||||
return item;
|
||||
|
||||
// Sort them so that the list can be easily compared for changes
|
||||
}).OrderBy(i => i.Path).ToArray();
|
||||
}).OrderBy(i => i.Path));
|
||||
}
|
||||
|
||||
return extras.ToArray();
|
||||
}
|
||||
|
||||
public Task RefreshMetadata(CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -1481,7 +1500,13 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
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();
|
||||
|
||||
|
@ -1493,7 +1518,15 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
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);
|
||||
|
|
|
@ -64,21 +64,31 @@ namespace MediaBrowser.Controller.Entities
|
|||
where T : 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);
|
||||
p.SetValue(dest, sourceProp.GetValue(source, null), null);
|
||||
continue;
|
||||
}
|
||||
|
||||
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>
|
||||
|
@ -93,7 +103,5 @@ namespace MediaBrowser.Controller.Entities
|
|||
source.DeepCopy(dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,8 +83,6 @@ namespace MediaBrowser.Controller
|
|||
|
||||
void EnableLoopback(string appName);
|
||||
|
||||
string PackageRuntime { get; }
|
||||
|
||||
WakeOnLanInfo[] GetWakeOnLanInfo();
|
||||
|
||||
string ExpandVirtualPath(string path);
|
||||
|
|
|
@ -200,8 +200,9 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns>System.String.</returns>
|
||||
string MakeValidUsername(string username);
|
||||
|
||||
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders);
|
||||
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
|
||||
|
||||
NameIdPair[] GetAuthenticationProviders();
|
||||
NameIdPair[] GetPasswordResetProviders();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
private readonly int DefaultImageExtractionTimeoutMs;
|
||||
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 ILocalizationManager _localization;
|
||||
|
||||
|
@ -230,6 +230,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
/// <returns></returns>
|
||||
private string ExistsOnSystemPath(string filename)
|
||||
{
|
||||
string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, filename);
|
||||
if (!string.IsNullOrEmpty(inJellyfinPath))
|
||||
{
|
||||
return inJellyfinPath;
|
||||
}
|
||||
var values = Environment.GetEnvironmentVariable("PATH");
|
||||
|
||||
foreach (var path in values.Split(Path.PathSeparator))
|
||||
|
@ -577,6 +582,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
bool ranToCompletion;
|
||||
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
StartProcess(processWrapper);
|
||||
|
||||
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||
|
@ -591,6 +599,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
StopProcess(processWrapper, 1000);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_thumbnailResourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
var file = FileSystem.GetFileInfo(tempExtractPath);
|
||||
|
@ -620,7 +633,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
|
||||
}
|
||||
|
||||
public async Task ExtractVideoImagesOnInterval(string[] inputFiles,
|
||||
public async Task ExtractVideoImagesOnInterval(
|
||||
string[] inputFiles,
|
||||
string container,
|
||||
MediaStream videoStream,
|
||||
MediaProtocol protocol,
|
||||
|
@ -631,8 +645,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
int? maxWidth,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resourcePool = _thumbnailResourcePool;
|
||||
|
||||
var inputArgument = GetInputArgument(inputFiles, protocol);
|
||||
|
||||
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);
|
||||
|
||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
bool ranToCompletion = false;
|
||||
|
||||
|
@ -737,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
finally
|
||||
{
|
||||
resourcePool.Release();
|
||||
_thumbnailResourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
|
|
|
@ -7,26 +7,6 @@ namespace MediaBrowser.Model.Services
|
|||
/// </summary>
|
||||
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>
|
||||
/// The value of the Accept HTTP Request Header
|
||||
/// </summary>
|
||||
|
|
|
@ -26,6 +26,11 @@ namespace MediaBrowser.Model.System
|
|||
/// <value>The version.</value>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The product name. This is the AssemblyProduct name.
|
||||
/// </summary>
|
||||
public string ProductName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operating system.
|
||||
/// </summary>
|
||||
|
|
|
@ -32,10 +32,6 @@ namespace MediaBrowser.Model.System
|
|||
/// <value>The display name of the operating system.</value>
|
||||
public string OperatingSystemDisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The product name. This is the AssemblyProduct name.
|
||||
/// </summary>
|
||||
public string ProductName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or sets the package name.
|
||||
|
|
|
@ -75,6 +75,7 @@ namespace MediaBrowser.Model.Users
|
|||
|
||||
public int RemoteClientBitrateLimit { get; set; }
|
||||
public string AuthenticationProviderId { get; set; }
|
||||
public string PasswordResetProviderId { get; set; }
|
||||
|
||||
public UserPolicy()
|
||||
{
|
||||
|
|
|
@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Music
|
|||
}
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
return ParseReleaseList(subReader);
|
||||
return ParseReleaseList(subReader).ToList();
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Music
|
|||
}
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
return ParseArtistList(subReader);
|
||||
return ParseArtistList(subReader).ToList();
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
|
|||
/// <summary>
|
||||
/// Class RemoteEpisodeProvider
|
||||
/// </summary>
|
||||
class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||
public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d
|
||||
Subproject commit b0f7a9b67cc72de98dc357425e9d5c3894c7f377
|
|
@ -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!
|
||||
|
||||
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/).
|
||||
|
||||
<p align="center">
|
||||
<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 align="center">
|
||||
<strong>Want to contribute?</strong>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.2.2")]
|
||||
[assembly: AssemblyFileVersion("10.2.2")]
|
||||
[assembly: AssemblyVersion("10.3.3")]
|
||||
[assembly: AssemblyFileVersion("10.3.3")]
|
||||
|
|
2
build
2
build
|
@ -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 "To build all platforms, 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
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.2.2"
|
||||
version: "10.3.3"
|
||||
packages:
|
||||
- debian-package-x64
|
||||
- debian-package-armhf
|
||||
- debian-package-arm64
|
||||
- ubuntu-package-x64
|
||||
- ubuntu-package-armhf
|
||||
- ubuntu-package-arm64
|
||||
- fedora-package-x64
|
||||
- centos-package-x64
|
||||
- linux-x64
|
||||
|
|
137
bump_version
137
bump_version
|
@ -9,7 +9,7 @@ usage() {
|
|||
echo -e "bump_version - increase the shared version and generate changelogs"
|
||||
echo -e ""
|
||||
echo -e "Usage:"
|
||||
echo -e " $ bump_version [-b/--web-branch <web_branch>] <new_version>"
|
||||
echo -e " $ bump_version <new_version>"
|
||||
echo -e ""
|
||||
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"
|
||||
|
@ -22,14 +22,9 @@ if [[ -z $1 ]]; then
|
|||
fi
|
||||
|
||||
shared_version_file="./SharedVersion.cs"
|
||||
build_file="./build.yaml"
|
||||
|
||||
# Parse branch option
|
||||
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
|
||||
git submodule update --init --recursive
|
||||
|
@ -47,22 +42,11 @@ if ! git diff-index --quiet HEAD --; then
|
|||
fi
|
||||
|
||||
git fetch --all
|
||||
# If this is an official branch name, fetch it from origin
|
||||
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
|
||||
git checkout origin/${web_branch}
|
||||
popd
|
||||
|
||||
git add MediaBrowser.WebDashboard/jellyfin-web
|
||||
|
||||
new_version="$1"
|
||||
|
||||
# Parse the version from the AssemblyVersion
|
||||
|
@ -70,94 +54,47 @@ old_version="$(
|
|||
grep "AssemblyVersion" ${shared_version_file} \
|
||||
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
|
||||
)"
|
||||
echo $old_version
|
||||
|
||||
# Set the shared version to the specified new_version
|
||||
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
|
||||
declare changelog_string_github
|
||||
declare changelog_string_deb
|
||||
declare changelog_string_yum
|
||||
|
||||
# Build up a changelog from merge commits
|
||||
for repo in ./ MediaBrowser.WebDashboard/jellyfin-web/; do
|
||||
last_master_merge_commit=""
|
||||
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
|
||||
*jellyfin-web*)
|
||||
repo_name="jellyfin-web"
|
||||
;;
|
||||
*)
|
||||
repo_name="jellyfin"
|
||||
;;
|
||||
esac
|
||||
|
||||
pushd ${repo}
|
||||
|
||||
# Find the last release commit, so we know what's happened since
|
||||
last_master_branch="release-${old_version}"
|
||||
last_master_merge_commit="$(
|
||||
git log --merges --pretty=oneline \
|
||||
| grep -F "${last_master_branch}" \
|
||||
| awk '{ print $1 }' \
|
||||
|| true # Don't die here with errexit
|
||||
old_version="$(
|
||||
grep "version:" ${build_file} \
|
||||
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
|
||||
)"
|
||||
if [[ -z ${last_master_merge_commit} ]]; then
|
||||
# This repo has no last proper commit, so just skip it
|
||||
popd
|
||||
continue
|
||||
echo $old_version
|
||||
|
||||
# Set the build.yaml version to the specified new_version
|
||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||
sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
|
||||
|
||||
if [[ ${new_version} == *"-"* ]]; then
|
||||
new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
|
||||
else
|
||||
new_version_deb="${new_version}-1"
|
||||
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}"
|
||||
# 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/'
|
||||
)"
|
||||
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
|
||||
echo $old_version
|
||||
|
||||
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
|
||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||
sed -i "s/${old_version_sed}/${new_version}/g" Dockerfile*
|
||||
|
||||
# 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_temp="$( mktemp )"
|
||||
# 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.
|
||||
jellyfin (${new_version}-1) unstable; urgency=medium
|
||||
${changelog_string_deb}
|
||||
jellyfin (${new_version_deb}) unstable; urgency=medium
|
||||
|
||||
* 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 )
|
||||
" >> ${debian_changelog_temp}
|
||||
|
@ -180,13 +117,14 @@ pushd ${fedora_spec_temp_dir}
|
|||
# Split out the stuff before and after changelog
|
||||
csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
|
||||
# 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
|
||||
sed -i '/^%changelog/d' xx01
|
||||
# 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.
|
||||
%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}
|
||||
# Edit the file to verify
|
||||
$EDITOR ${fedora_changelog_temp}
|
||||
|
@ -199,10 +137,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file}
|
|||
rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
|
||||
|
||||
# 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
|
||||
|
||||
# Write out the GitHub-formatted changelog for the merge request/release pages
|
||||
echo ""
|
||||
echo "=== The GitHub-formatted changelog follows ==="
|
||||
echo -e "${changelog_string_github}"
|
||||
|
|
|
@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
|
|||
# Move the artifacts out
|
||||
mkdir -p ${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}
|
||||
|
|
|
@ -72,9 +72,6 @@ fi
|
|||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||
# Build the RPMs and copy out to ${package_temporary_dir}
|
||||
${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
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
|
||||
|
|
|
@ -17,12 +17,12 @@ DEFAULT_PKG_DIR="pkg-dist"
|
|||
DEFAULT_DOCKERFILE="Dockerfile"
|
||||
DEFAULT_ARCHIVE_CMD="tar -xvzf"
|
||||
|
||||
# Parse the version from the AssemblyVersion
|
||||
# Parse the version from the build.yaml version
|
||||
get_version()
|
||||
(
|
||||
local ROOT=${1-$DEFAULT_ROOT}
|
||||
grep "AssemblyVersion" ${ROOT}/SharedVersion.cs \
|
||||
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
|
||||
grep "version:" ${ROOT}/build.yaml \
|
||||
| sed -E 's/version: "([0-9\.]+.*)"/\1/'
|
||||
)
|
||||
|
||||
# Run a build
|
||||
|
|
43
deployment/debian-package-arm64/Dockerfile.amd64
Normal file
43
deployment/debian-package-arm64/Dockerfile.amd64
Normal 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"]
|
34
deployment/debian-package-arm64/Dockerfile.arm64
Normal file
34
deployment/debian-package-arm64/Dockerfile.arm64
Normal 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"]
|
29
deployment/debian-package-arm64/clean.sh
Executable file
29
deployment/debian-package-arm64/clean.sh
Executable 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
|
1
deployment/debian-package-arm64/dependencies.txt
Normal file
1
deployment/debian-package-arm64/dependencies.txt
Normal file
|
@ -0,0 +1 @@
|
|||
docker
|
21
deployment/debian-package-arm64/docker-build.sh
Executable file
21
deployment/debian-package-arm64/docker-build.sh
Executable 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}
|
41
deployment/debian-package-arm64/package.sh
Executable file
41
deployment/debian-package-arm64/package.sh
Executable 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}"
|
1
deployment/debian-package-arm64/pkg-src
Symbolic link
1
deployment/debian-package-arm64/pkg-src
Symbolic link
|
@ -0,0 +1 @@
|
|||
../debian-package-x64/pkg-src
|
|
@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf
|
|||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
|
|
@ -30,13 +30,12 @@ case $ARCH in
|
|||
;;
|
||||
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}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
|
|
|
@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
|
|||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
|
|
@ -19,13 +19,12 @@ else
|
|||
docker_sudo=""
|
||||
fi
|
||||
|
||||
# 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}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -22,7 +22,7 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
|
|||
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
||||
|
||||
# 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
|
||||
#JELLYFIN_SERVICE_OPT="--service"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user