Merge branch 'master' into tasks

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

View File

@ -99,7 +99,7 @@ jobs:
pool:
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:

View File

@ -1,6 +1,6 @@
ARG DOTNET_VERSION=2
ARG DOTNET_VERSION=2.2
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
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

View File

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

View File

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

View File

@ -920,8 +920,6 @@ namespace Emby.Dlna.Didl
}
}
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{
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

View File

@ -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,20 +113,14 @@ 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 (!_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)))
{
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
{
OnDeviceUnavailable();
}
OnDeviceUnavailable();
}
}
@ -612,22 +607,34 @@ namespace Emby.Dlna.PlayTo
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_device.PlaybackStart -= _device_PlaybackStart;
_device.PlaybackProgress -= _device_PlaybackProgress;
_device.PlaybackStopped -= _device_PlaybackStopped;
_device.MediaChanged -= _device_MediaChanged;
//_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
_device.OnDeviceUnavailable = null;
_device.Dispose();
}
Dispose(true);
GC.SuppressFinalize(this);
}
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_device.Dispose();
}
_device.PlaybackStart -= _device_PlaybackStart;
_device.PlaybackProgress -= _device_PlaybackProgress;
_device.PlaybackStopped -= _device_PlaybackStopped;
_device.MediaChanged -= _device_MediaChanged;
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
_device.OnDeviceUnavailable = null;
_device = null;
_disposed = true;
}
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{

View File

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

View File

@ -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)
if (structure != null
&& structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{
var exif = structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) as SubIFDEntry;
if (exif != null)
var exifStructure = exif.Structure;
if (exifStructure != null)
{
var exifStructure = exif.Structure;
if (exifStructure != null)
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
if (entry != null)
{
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
}
if (entry != null)
{
double val = entry.Value.Numerator;
val /= entry.Value.Denominator;
item.Aperture = val;
}
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
if (entry != null)
{
double val = entry.Value.Numerator;
val /= entry.Value.Denominator;
item.ShutterSpeed = val;
}
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
if (entry != null)
{
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,12 +99,10 @@ namespace Emby.Photos
item.Overview = image.ImageTag.Comment;
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title))
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
&& !item.LockedFields.Contains(MetadataFields.Name))
{
if (!item.LockedFields.Contains(MetadataFields.Name))
{
item.Name = image.ImageTag.Title;
}
item.Name = image.ImageTag.Title;
}
var dateTaken = image.ImageTag.DateTime;
@ -140,12 +121,9 @@ namespace Emby.Photos
{
item.Orientation = null;
}
else
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
{
if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
{
item.Orientation = orientation;
}
item.Orientation = orientation;
}
item.ExposureTime = image.ImageTag.ExposureTime;
@ -195,7 +173,5 @@ namespace Emby.Photos
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
return Task.FromResult(result);
}
public string Name => "Embedded Information";
}
}

View File

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

View File

@ -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,9 +1567,38 @@ 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));
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)
@ -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>

View File

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

View File

@ -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
@ -136,7 +135,7 @@ namespace Emby.Server.Implementations.Cryptography
{
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
}
public byte[] ComputeHash(PasswordHash hash)
{
int iterations = _defaultIterations;

View File

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

View File

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

View File

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

View File

@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(),
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>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
{
@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO
return lst.Any(str =>
{
//this should be a little quicker than examining each actual parent folder...
// this should be a little quicker than examining each actual parent folder...
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO
if (!Directory.Exists(path))
{
// 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
IncludeSubdirectories = true,
InternalBufferSize = 65536,
NotifyFilter = NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes
};
newWatcher.InternalBufferSize = 65536;
newWatcher.NotifyFilter = NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes;
newWatcher.Created += watcher_Changed;
newWatcher.Deleted += watcher_Changed;
newWatcher.Renamed += watcher_Changed;
newWatcher.Changed += watcher_Changed;
newWatcher.Error += watcher_Error;
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.
}
watcher.EnableRaisingEvents = false;
}
}
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,25 +414,22 @@ 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);
return true;
}
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>

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library
public string Name => "Default";
public bool IsEnabled => true;
// This is dumb and an artifact of the backwards way auth providers were designed.
// This version of authenticate was never meant to be called, but needs to be here for interface compat
// Only the providers that don't provide local user support use this
@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library
{
throw new NotImplementedException();
}
// This is the verson that we need to use for local users. Because reasons.
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{
@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
string hash = user.Password;
user.Password = string.Format("$SHA1${0}", hash);
}
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
{
string hash = user.EasyPassword;

View File

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

View File

@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
private readonly IImageProcessor _imageProcessor;
private readonly 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);
}
}
}

View File

@ -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)
return new ForgotPasswordResult
{
action = ForgotPasswordAction.PinCode;
}
var result = await CreatePasswordResetPin().ConfigureAwait(false);
pinFile = result.PinFile;
expirationDate = result.ExpirationDate;
Action = action,
PinFile = string.Empty
};
}
return new ForgotPasswordResult
{
Action = action,
PinFile = pinFile,
PinExpirationDate = expirationDate
};
}
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "Σύστημα",

View File

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

View File

@ -0,0 +1 @@
{}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,52 +71,37 @@ namespace Emby.Server.Implementations.Services
response.ContentType += "; charset=utf-8";
}
var asyncStreamWriter = result as IAsyncStreamWriter;
if (asyncStreamWriter != null)
switch (result)
{
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
}
case IAsyncStreamWriter asyncStreamWriter:
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
case IStreamWriter streamWriter:
streamWriter.WriteTo(response.OutputStream);
return Task.CompletedTask;
case FileWriter fileWriter:
return fileWriter.WriteToAsync(response, cancellationToken);
case Stream stream:
return CopyStream(stream, response.OutputStream);
case byte[] bytes:
response.ContentType = "application/octet-stream";
response.OriginalResponse.ContentLength = bytes.Length;
var streamWriter = result as IStreamWriter;
if (streamWriter != null)
{
streamWriter.WriteTo(response.OutputStream);
return Task.CompletedTask;
}
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
var fileWriter = result as FileWriter;
if (fileWriter != null)
{
return fileWriter.WriteToAsync(response, cancellationToken);
}
return Task.CompletedTask;
case string responseText:
var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
response.OriginalResponse.ContentLength = responseTextAsBytes.Length;
var stream = result as Stream;
if (stream != null)
{
return CopyStream(stream, response.OutputStream);
}
if (responseTextAsBytes.Length > 0)
{
return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
}
var bytes = result as byte[];
if (bytes != null)
{
response.ContentType = "application/octet-stream";
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
}
var responseText = result as string;
if (responseText != null)
{
bytes = Encoding.UTF8.GetBytes(responseText);
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
return Task.CompletedTask;
}
return WriteObject(request, result, response);
@ -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);
}
}
}

View File

@ -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,17 +67,24 @@ 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 };
}
pathsAtFirstMatch.Add(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)

View File

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

View File

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

View File

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

View File

@ -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)
IPAddress ip;
// "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
// (if the server is behind a reverse proxy for example)
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
{
return remoteIp = temp.ToString();
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
{
ip = request.HttpContext.Connection.RemoteIpAddress;
}
}
temp = CheckBadChars(XRealIp.AsSpan());
if (temp.Length != 0)
{
return remoteIp = NormalizeIp(temp).ToString();
}
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
return remoteIp = NormalizeIp(ip).ToString();
}
}
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
// CheckBadChars - throws on invalid chars to be not found in header name/value
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
private static IPAddress NormalizeIp(IPAddress ip)
{
if (name.Length == 0)
if (ip.IsIPv4MappedToIPv6)
{
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));
}
}
}
if (crlf != 0)
{
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
return name;
}
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
{
if (ip.Length != 0 && !ip.IsWhiteSpace())
{
// Handle ipv4 mapped to ipv6
const string srch = "::ffff:";
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0)
{
ip = ip.Slice(srch.Length);
}
return ip.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();
}
}
}

View File

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

View File

@ -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;
}
_fileSystem.DeleteFile(path);
if (isDirectory)
{
_logger.LogInformation("Deleting plugin directory {0}", path);
Directory.Delete(path, true);
}
else
{
_logger.LogInformation("Deleting plugin file {0}", path);
_fileSystem.DeleteFile(path);
}
var list = _config.Configuration.UninstalledPlugins.ToList();
var filename = Path.GetFileName(path);

View File

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

View File

@ -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;
@ -828,7 +830,16 @@ namespace MediaBrowser.Api.Library
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
if (!string.IsNullOrWhiteSpace(filename))
{
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
// Kestrel doesn't support non-ASCII characters in headers
if (Regex.IsMatch(filename, "[^[:ascii:]]"))
{
// Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
}
else
{
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
}
}
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions

View File

@ -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)
responseHeaders[HeaderNames.AcceptRanges] = "none";
// Seeing cases of -1 here
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
{
foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges })
{
var val = response.Headers[name];
if (!string.IsNullOrWhiteSpace(val))
{
responseHeaders[name] = val;
}
}
}
else
{
responseHeaders[HeaderNames.AcceptRanges] = "none";
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;
}
}
}

View File

@ -245,6 +245,12 @@ namespace MediaBrowser.Api.Session
{
}
[Route("/Auth/PasswordResetProviders", "GET")]
[Authenticated(Roles = "Admin")]
public class GetPasswordResetProviders : IReturn<NameIdPair[]>
{
}
[Route("/Auth/Keys/{Key}", "DELETE")]
[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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,10 +78,25 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// 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,33 +1310,38 @@ namespace MediaBrowser.Controller.Entities
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
var files = fileSystemChildren.Where(i => i.IsDirectory)
.SelectMany(i => FileSystem.GetFiles(i.FullName));
var extras = new List<Video>();
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Video>()
.Select(item =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
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));
if (dbItem != null)
extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Video>()
.Select(item =>
{
item = dbItem;
}
else
{
// item is new
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip;
}
// Try to retrieve it from the db. If we don't find it, use the resolved version
if (LibraryManager.GetItemById(item.Id) is Video dbItem)
{
item = dbItem;
}
return item;
// Use some hackery to get the extra type based on foldername
Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType);
item.ExtraType = extraType;
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path).ToArray();
return item;
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path));
}
return extras.ToArray();
}
public Task RefreshMetadata(CancellationToken cancellationToken)
{
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
@ -1481,7 +1500,13 @@ namespace MediaBrowser.Controller.Entities
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
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);

View File

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

View File

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

View File

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

View File

@ -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,19 +582,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
bool ranToCompletion;
StartProcess(processWrapper);
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
if (timeoutMs <= 0)
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
timeoutMs = DefaultImageExtractionTimeoutMs;
StartProcess(processWrapper);
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
if (timeoutMs <= 0)
{
timeoutMs = DefaultImageExtractionTimeoutMs;
}
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
if (!ranToCompletion)
{
StopProcess(processWrapper, 1000);
}
}
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
if (!ranToCompletion)
finally
{
StopProcess(processWrapper, 1000);
_thumbnailResourcePool.Release();
}
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
@ -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;

View File

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

View File

@ -25,6 +25,11 @@ namespace MediaBrowser.Model.System
/// </summary>
/// <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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,13 +22,13 @@
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
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>

View File

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

@ -29,7 +29,7 @@ usage() {
echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
echo -e "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

View File

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

View File

@ -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
web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
# 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
old_version="$(
grep "version:" ${build_file} \
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
)"
echo $old_version
# 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=""
# 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}
case $repo in
*jellyfin-web*)
repo_name="jellyfin-web"
;;
*)
repo_name="jellyfin"
;;
esac
if [[ ${new_version} == *"-"* ]]; then
new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
else
new_version_deb="${new_version}-1"
fi
pushd ${repo}
# Set the Dockerfile web version to the specified new_version
old_version="$(
grep "JELLYFIN_WEB_VERSION=" Dockerfile \
| sed -E 's/ARG JELLYFIN_WEB_VERSION=([0-9\.]+[-a-z0-9]*)/\1/'
)"
echo $old_version
# Find the last release commit, so we know what's happened since
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
)"
if [[ -z ${last_master_merge_commit} ]]; then
# This repo has no last proper commit, so just skip it
popd
continue
fi
# Get all the PR merge commits since the last master merge commit in `jellyfin`
pr_merges_since_last_master+=( $(
git log --merges --pretty=oneline ${last_master_merge_commit}..HEAD \
| grep -F "Merge pull request" \
| awk '{ print $1 }'
) )
for commit_hash in ${pr_merges_since_last_master[@]}; do
git_show_details="$( git show ${commit_hash} )"
pull_request_id="$(
awk '
/Merge pull request/{ print $4 }
{ next }
' <<<"${git_show_details}"
)"
pull_request_description="$(
awk '
/^[a-zA-Z]/{ next }
/^ Merge/{ next }
/^$/{ next }
{ print $0 }
' <<<"${git_show_details}"
)"
pull_request_description="$( sed ':a;N;$!ba;s/\n//g; s/ \+//g' <<<"${pull_request_description}" )"
changelog_strings_repo_github="${changelog_strings_repo_github}\n* ${pull_request_id}: ${pull_request_description}"
changelog_strings_repo_deb="${changelog_strings_repo_deb}\n * $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}"
changelog_strings_repo_yum="${changelog_strings_repo_yum}\n- $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}"
done
changelog_string_github="${changelog_string_github}\n#### ${repo_name}:\n$( echo -e "${changelog_strings_repo_github}" | sort -nk2 )\n"
changelog_string_deb="${changelog_string_deb}\n * ${repo_name}:$( echo -e "${changelog_strings_repo_deb}" | sort -nk2 )"
changelog_string_yum="${changelog_string_yum}\n- ${repo_name}:$( echo -e "${changelog_strings_repo_yum}" | sort -nk2 )"
popd
done
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}"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Prepare the cross-toolchain
RUN dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
# Link to Debian source dir; mkdir needed or it fails, can't force dest
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
ENTRYPOINT ["/docker-build.sh"]
#ENTRYPOINT ["/bin/bash"]

View File

@ -0,0 +1,34 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=arm64
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
# Link to Debian source dir; mkdir needed or it fails, can't force dest
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
ENTRYPOINT ["/docker-build.sh"]

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
source ../common.build.sh
keep_artifacts="${1}"
WORKDIR="$( pwd )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
current_user="$( whoami )"
image_name="jellyfin-debian_arm64-build"
rm -rf "${package_temporary_dir}" &>/dev/null \
|| sudo rm -rf "${package_temporary_dir}" &>/dev/null
rm -rf "${output_dir}" &>/dev/null \
|| sudo rm -rf "${output_dir}" &>/dev/null
if [[ ${keep_artifacts} == 'n' ]]; then
docker_sudo=""
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
&& [[ ! ${EUID:-1000} -eq 0 ]] \
&& [[ ! ${USER} == "root" ]] \
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
docker_sudo=sudo
fi
${docker_sudo} docker image rm ${image_name} --force
fi

View File

@ -0,0 +1 @@
docker

View File

@ -0,0 +1,21 @@
#!/bin/bash
# Builds the DEB inside the Docker container
set -o errexit
set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarm64
# Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}

View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
source ../common.build.sh
ARCH="$( arch )"
WORKDIR="$( pwd )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
current_user="$( whoami )"
image_name="jellyfin-debian_arm64-build"
# Determine if sudo should be used for Docker
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
&& [[ ! ${EUID:-1000} -eq 0 ]] \
&& [[ ! ${USER} == "root" ]] \
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
docker_sudo="sudo"
else
docker_sudo=""
fi
# Determine which Dockerfile to use
case $ARCH in
'x86_64')
DOCKERFILE="Dockerfile.amd64"
;;
'armv7l')
DOCKERFILE="Dockerfile.arm64"
;;
esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"

View File

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

View File

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

View File

@ -30,13 +30,12 @@ case $ARCH in
;;
esac
# 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}"

View File

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

View File

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

View File

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

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