Merge branch 'master' into xml

This commit is contained in:
Bond-009 2019-03-12 16:37:18 +01:00 committed by GitHub
commit 3ddbda9aca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
211 changed files with 2431 additions and 13716 deletions

183
.ci/azure-pipelines.yml Normal file
View File

@ -0,0 +1,183 @@
name: $(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: TestProjects
value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
pr:
autoCancel: true
trigger:
batch: true
branches:
include:
- master
jobs:
- job: main_build
displayName: Main Build
pool:
vmImage: ubuntu-16.04
strategy:
matrix:
release:
BuildConfiguration: Release
debug:
BuildConfiguration: Debug
maxParallel: 2
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: false
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: restore
projects: '$(RestoreBuildProjects)'
- task: DotNetCoreCLI@2
displayName: Build
inputs:
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration)'
- task: DotNetCoreCLI@2
displayName: Test
inputs:
command: test
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration)'
enabled: false
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: publish
publishWebProjects: false
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
zipAfterPublish: false
# - task: PublishBuildArtifacts@1
# displayName: 'Publish Artifact'
# inputs:
# PathtoPublish: '$(build.artifactstagingdirectory)'
# artifactName: 'jellyfin-build-$(BuildConfiguration)'
# zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Naming'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Controller'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Model'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Common'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
- job: dotnet_compat
displayName: Compatibility Check
pool:
vmImage: ubuntu-16.04
dependsOn: main_build
condition: succeeded()
strategy:
matrix:
Naming:
NugetPackageName: Jellyfin.Naming
AssemblyFileName: Emby.Naming.dll
Controller:
NugetPackageName: Jellyfin.Controller
AssemblyFileName: MediaBrowser.Controller.dll
Model:
NugetPackageName: Jellyfin.Model
AssemblyFileName: MediaBrowser.Model.dll
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
maxParallel: 2
steps:
- checkout: none
- task: NuGetCommand@2
displayName: 'Download $(NugetPackageName)'
inputs:
command: custom
arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload'
- task: CopyFiles@2
displayName: Copy Nuget Assembly to current-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadBuildArtifacts@0
displayName: Download the Assembly Build Artifact
inputs:
buildType: 'current' # Options: current, specific
allowPartiallySucceededBuilds: false # Optional
downloadType: 'single' # Options: single, specific
artifactName: '$(NugetPackageName)' # Required when downloadType == Single
downloadPath: '$(System.ArtifactsDirectory)/new-artifacts'
- task: CopyFiles@2
displayName: Copy Artifact Assembly to new-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadGitHubRelease@0
displayName: Download ABI compatibility check tool from GitHub
inputs:
connection: Jellyfin GitHub
userRepository: EraYaN/dotnet-compatibility
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
#version: # Required when defaultVersionType != Latest
itemPattern: '**-ci.zip' # Optional
downloadPath: '$(System.ArtifactsDirectory)'
- task: ExtractFiles@1
displayName: Extract ABI compatibility check tool
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
- task: CmdLine@2
displayName: Execute ABI compatibility check tool
inputs:
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)'
workingDirectory: $(System.ArtifactsDirectory) # Optional
#failOnStderr: false # Optional

View File

@ -23,6 +23,7 @@
- [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng)
- [ploughpuff](https://github.com/ploughpuff)
# Emby Contributors

View File

@ -1,36 +0,0 @@
namespace DvdLib.Ifo
{
public enum AudioCodec
{
AC3 = 0,
MPEG1 = 2,
MPEG2ext = 3,
LPCM = 4,
DTS = 6,
}
public enum ApplicationMode
{
Unspecified = 0,
Karaoke = 1,
Surround = 2,
}
public class AudioAttributes
{
public readonly AudioCodec Codec;
public readonly bool MultichannelExtensionPresent;
public readonly ApplicationMode Mode;
public readonly byte QuantDRC;
public readonly byte SampleRate;
public readonly byte Channels;
public readonly ushort LanguageCode;
public readonly byte LanguageExtension;
public readonly byte CodeExtension;
}
public class MultiChannelExtension
{
}
}

View File

@ -1,17 +0,0 @@
using System.Collections.Generic;
namespace DvdLib.Ifo
{
public class ProgramChainCommandTable
{
public readonly ushort LastByteAddress;
public readonly List<VirtualMachineCommand> PreCommands;
public readonly List<VirtualMachineCommand> PostCommands;
public readonly List<VirtualMachineCommand> CellCommands;
}
public class VirtualMachineCommand
{
public readonly byte[] Command;
}
}

View File

@ -25,13 +25,10 @@ namespace DvdLib.Ifo
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
private ushort _nextProgramNumber;
public readonly ProgramChain Next;
private ushort _prevProgramNumber;
public readonly ProgramChain Previous;
private ushort _goupProgramNumber;
public readonly ProgramChain Goup; // ?? maybe Group
public ProgramPlaybackMode PlaybackMode { get; private set; }
public uint ProgramCount { get; private set; }
@ -40,7 +37,6 @@ namespace DvdLib.Ifo
public byte[] Palette { get; private set; } // 16*4 entries
private ushort _commandTableOffset;
public readonly ProgramChainCommandTable CommandTable;
private ushort _programMapOffset;
private ushort _cellPlaybackOffset;

View File

@ -1,46 +0,0 @@
namespace DvdLib.Ifo
{
public enum VideoCodec
{
MPEG1 = 0,
MPEG2 = 1,
}
public enum VideoFormat
{
NTSC = 0,
PAL = 1,
}
public enum AspectRatio
{
ar4to3 = 0,
ar16to9 = 3
}
public enum FilmMode
{
None = -1,
Camera = 0,
Film = 1,
}
public class VideoAttributes
{
public readonly VideoCodec Codec;
public readonly VideoFormat Format;
public readonly AspectRatio Aspect;
public readonly bool AutomaticPanScan;
public readonly bool AutomaticLetterBox;
public readonly bool Line21CCField1;
public readonly bool Line21CCField2;
public readonly int Width;
public readonly int Height;
public readonly bool Letterboxed;
public readonly FilmMode FilmMode;
public VideoAttributes()
{
}
}
}

View File

@ -136,7 +136,7 @@ namespace Emby.Dlna.Api
{
var url = Request.AbsoluteUri;
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers.ToDictionary(), request.UuId, serverAddress);
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
var cacheLength = TimeSpan.FromDays(1);
var cacheKey = Request.RawUrl.GetMD5();
@ -147,21 +147,21 @@ namespace Emby.Dlna.Api
public object Get(GetContentDirectory request)
{
var xml = ContentDirectory.GetServiceXml(Request.Headers.ToDictionary());
var xml = ContentDirectory.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
public object Get(GetMediaReceiverRegistrar request)
{
var xml = MediaReceiverRegistrar.GetServiceXml(Request.Headers.ToDictionary());
var xml = MediaReceiverRegistrar.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
public object Get(GetConnnectionManager request)
{
var xml = ConnectionManager.GetServiceXml(Request.Headers.ToDictionary());
var xml = ConnectionManager.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
@ -193,7 +193,7 @@ namespace Emby.Dlna.Api
return service.ProcessControlRequest(new ControlRequest
{
Headers = Request.Headers.ToDictionary(),
Headers = Request.Headers,
InputXml = requestStream,
TargetServerUuId = id,
RequestedUrl = Request.AbsoluteUri

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@ -21,7 +20,7 @@ namespace Emby.Dlna.ConnectionManager
_logger = logger;
}
public string GetServiceXml(IDictionary<string, string> headers)
public string GetServiceXml()
{
return new ConnectionManagerXmlBuilder().GetXml();
}

View File

@ -67,7 +67,7 @@ namespace Emby.Dlna.ContentDirectory
}
}
public string GetServiceXml(IDictionary<string, string> headers)
public string GetServiceXml()
{
return new ContentDirectoryXmlBuilder().GetXml();
}

View File

@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna
{
public class ControlRequest
{
public IDictionary<string, string> Headers { get; set; }
public IHeaderDictionary Headers { get; set; }
public Stream InputXml { get; set; }
@ -15,7 +15,7 @@ namespace Emby.Dlna
public ControlRequest()
{
Headers = new Dictionary<string, string>();
Headers = new HeaderDictionary();
}
}
}

View File

@ -17,7 +17,9 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Emby.Dlna
{
@ -203,16 +205,13 @@ namespace Emby.Dlna
}
}
public DeviceProfile GetProfile(IDictionary<string, string> headers)
public DeviceProfile GetProfile(IHeaderDictionary headers)
{
if (headers == null)
{
throw new ArgumentNullException(nameof(headers));
}
// Convert to case insensitive
headers = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
if (profile != null)
@ -228,12 +227,12 @@ namespace Emby.Dlna
return profile;
}
private bool IsMatch(IDictionary<string, string> headers, DeviceIdentification profileInfo)
private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
{
return profileInfo.Headers.Any(i => IsMatch(headers, i));
}
private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header)
private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
{
// Handle invalid user setup
if (string.IsNullOrEmpty(header.Name))
@ -241,14 +240,14 @@ namespace Emby.Dlna
return false;
}
if (headers.TryGetValue(header.Name, out string value))
if (headers.TryGetValue(header.Name, out StringValues value))
{
switch (header.Match)
{
case HeaderMatchType.Equals:
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
case HeaderMatchType.Substring:
var isMatch = value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
//_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
return isMatch;
case HeaderMatchType.Regex:
@ -494,7 +493,7 @@ namespace Emby.Dlna
internal string Path { get; set; }
}
public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress)
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetProfile(headers) ??
GetDefaultProfile();

View File

@ -58,4 +58,9 @@
<EmbeddedResource Include="Profiles\Xml\Xbox One.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace Emby.Dlna
{
public interface IUpnpService
@ -7,9 +5,8 @@ namespace Emby.Dlna
/// <summary>
/// Gets the content directory XML.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>System.String.</returns>
string GetServiceXml(IDictionary<string, string> headers);
string GetServiceXml();
/// <summary>
/// Processes the control request.

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@ -16,7 +15,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
_config = config;
}
public string GetServiceXml(IDictionary<string, string> headers)
public string GetServiceXml()
{
return new MediaReceiverRegistrarXmlBuilder().GetXml();
}

View File

@ -1,9 +0,0 @@
using System;
namespace Emby.Dlna.PlayTo
{
public class CurrentIdEventArgs : EventArgs
{
public string Id { get; set; }
}
}

View File

@ -1126,6 +1126,11 @@ namespace Emby.Dlna.PlayTo
private void OnPlaybackStart(uBaseObject mediaInfo)
{
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
return;
}
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
{
MediaInfo = mediaInfo
@ -1134,8 +1139,7 @@ namespace Emby.Dlna.PlayTo
private void OnPlaybackProgress(uBaseObject mediaInfo)
{
var mediaUrl = mediaInfo.Url;
if (string.IsNullOrWhiteSpace(mediaUrl))
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
return;
}
@ -1148,7 +1152,6 @@ namespace Emby.Dlna.PlayTo
private void OnPlaybackStop(uBaseObject mediaInfo)
{
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{
MediaInfo = mediaInfo

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@ -17,8 +18,8 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo
@ -847,13 +848,13 @@ namespace Emby.Dlna.PlayTo
if (index == -1) return request;
var query = url.Substring(index + 1);
QueryParamCollection values = MyHttpUtility.ParseQueryString(query);
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
request.DeviceProfileId = values.Get("DeviceProfileId");
request.DeviceId = values.Get("DeviceId");
request.MediaSourceId = values.Get("MediaSourceId");
request.LiveStreamId = values.Get("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.Get("Static"), StringComparison.OrdinalIgnoreCase);
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
@ -867,9 +868,9 @@ namespace Emby.Dlna.PlayTo
}
}
private static int? GetIntValue(QueryParamCollection values, string name)
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.Get(name);
var value = values.GetValueOrDefault(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
@ -879,9 +880,9 @@ namespace Emby.Dlna.PlayTo
return null;
}
private static long GetLongValue(QueryParamCollection values, string name)
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.Get(name);
var value = values.GetValueOrDefault(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{

View File

@ -9,8 +9,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaylistItemFactory
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public PlaylistItem Create(Photo item, DeviceProfile profile)
{
var playlistItem = new PlaylistItem

View File

@ -1,9 +0,0 @@
using System;
namespace Emby.Dlna.PlayTo
{
public class TransportStateEventArgs : EventArgs
{
public TRANSPORTSTATE State { get; set; }
}
}

View File

@ -1,47 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Emby.Dlna.PlayTo
{
public class uParser
{
public static IList<uBaseObject> ParseBrowseXml(XDocument doc)
{
if (doc == null)
{
throw new ArgumentException("doc");
}
var list = new List<uBaseObject>();
var document = doc.Document;
if (document == null)
return list;
var item = (from result in document.Descendants("Result") select result).FirstOrDefault();
if (item == null)
return list;
var uPnpResponse = XElement.Parse((string)item);
var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers)
select new uParserObject { Element = container };
var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items)
select new uParserObject { Element = container };
list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null));
return list;
}
public static uBaseObject CreateObjectFromXML(uParserObject uItem)
{
return UpnpContainer.Create(uItem.Element);
}
}
}

View File

@ -1,9 +0,0 @@
using System.Xml.Linq;
namespace Emby.Dlna.PlayTo
{
public class uParserObject
{
public XElement Element { get; set; }
}
}

View File

@ -30,13 +30,10 @@ namespace Emby.Server.Implementations.Activity
public class ActivityLogEntryPoint : IServerEntryPoint
{
private readonly IInstallationManager _installationManager;
//private readonly ILogger _logger;
private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager;
@ -61,41 +58,37 @@ namespace Emby.Server.Implementations.Activity
public Task RunAsync()
{
_taskManager.TaskCompleted += _taskManager_TaskCompleted;
_taskManager.TaskCompleted += OnTaskCompleted;
_installationManager.PluginInstalled += _installationManager_PluginInstalled;
_installationManager.PluginUninstalled += _installationManager_PluginUninstalled;
_installationManager.PluginUpdated += _installationManager_PluginUpdated;
_installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed;
_installationManager.PluginInstalled += OnPluginInstalled;
_installationManager.PluginUninstalled += OnPluginUninstalled;
_installationManager.PluginUpdated += OnPluginUpdated;
_installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
_sessionManager.SessionStarted += _sessionManager_SessionStarted;
_sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed;
_sessionManager.AuthenticationSucceeded += _sessionManager_AuthenticationSucceeded;
_sessionManager.SessionEnded += _sessionManager_SessionEnded;
_sessionManager.SessionStarted += OnSessionStarted;
_sessionManager.AuthenticationFailed += OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
_sessionManager.SessionEnded += OnSessionEnded;
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
_sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
_sessionManager.PlaybackStart += OnPlaybackStart;
_sessionManager.PlaybackStopped += OnPlaybackStopped;
//_subManager.SubtitlesDownloaded += _subManager_SubtitlesDownloaded;
_subManager.SubtitleDownloadFailure += _subManager_SubtitleDownloadFailure;
_subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
_userManager.UserCreated += _userManager_UserCreated;
_userManager.UserPasswordChanged += _userManager_UserPasswordChanged;
_userManager.UserDeleted += _userManager_UserDeleted;
_userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
_userManager.UserLockedOut += _userManager_UserLockedOut;
_userManager.UserCreated += OnUserCreated;
_userManager.UserPasswordChanged += OnUserPasswordChanged;
_userManager.UserDeleted += OnUserDeleted;
_userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserLockedOut += OnUserLockedOut;
//_config.ConfigurationUpdated += _config_ConfigurationUpdated;
//_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
_deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded;
_appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
_appHost.ApplicationUpdated += OnApplicationUpdated;
return Task.CompletedTask;
}
void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -104,7 +97,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e)
private void OnUserLockedOut(object sender, GenericEventArgs<User> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -114,7 +107,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _subManager_SubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -125,7 +118,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
var item = e.MediaInfo;
@ -146,7 +139,7 @@ namespace Emby.Server.Implementations.Activity
return;
}
var user = e.Users.First();
var user = e.Users[0];
CreateLogEntry(new ActivityLogEntry
{
@ -156,7 +149,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
var item = e.MediaInfo;
@ -232,7 +225,7 @@ namespace Emby.Server.Implementations.Activity
return null;
}
void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
private void OnSessionEnded(object sender, SessionEventArgs e)
{
string name;
var session = e.SessionInfo;
@ -258,7 +251,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
{
var user = e.Argument.User;
@ -271,7 +264,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _sessionManager_AuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -282,7 +275,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _appHost_ApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
private void OnApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -292,25 +285,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
CreateLogEntry(new ActivityLogEntry
{
Name = string.Format(_localization.GetLocalizedString("MessageNamedServerConfigurationUpdatedWithValue"), e.Key),
Type = "NamedConfigurationUpdated"
});
}
void _config_ConfigurationUpdated(object sender, EventArgs e)
{
CreateLogEntry(new ActivityLogEntry
{
Name = _localization.GetLocalizedString("MessageServerConfigurationUpdated"),
Type = "ServerConfigurationUpdated"
});
}
void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -320,7 +295,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _userManager_UserDeleted(object sender, GenericEventArgs<User> e)
private void OnUserDeleted(object sender, GenericEventArgs<User> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -329,7 +304,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _userManager_UserPasswordChanged(object sender, GenericEventArgs<User> e)
private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -339,7 +314,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _userManager_UserCreated(object sender, GenericEventArgs<User> e)
private void OnUserCreated(object sender, GenericEventArgs<User> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -349,18 +324,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _subManager_SubtitlesDownloaded(object sender, SubtitleDownloadEventArgs e)
{
CreateLogEntry(new ActivityLogEntry
{
Name = string.Format(_localization.GetLocalizedString("SubtitlesDownloadedForItem"), Notifications.Notifications.GetItemName(e.Item)),
Type = "SubtitlesDownloaded",
ItemId = e.Item.Id.ToString("N"),
ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider)
});
}
void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
private void OnSessionStarted(object sender, SessionEventArgs e)
{
string name;
var session = e.SessionInfo;
@ -386,7 +350,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _installationManager_PluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e)
private void OnPluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -397,7 +361,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _installationManager_PluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -406,7 +370,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _installationManager_PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
private void OnPluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
{
CreateLogEntry(new ActivityLogEntry
{
@ -416,7 +380,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e)
private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{
var installationInfo = e.InstallationInfo;
@ -429,7 +393,7 @@ namespace Emby.Server.Implementations.Activity
});
}
void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
{
var result = e.Result;
var task = e.Task;
@ -468,48 +432,36 @@ namespace Emby.Server.Implementations.Activity
}
private void CreateLogEntry(ActivityLogEntry entry)
{
try
{
_activityManager.Create(entry);
}
catch
{
// Logged at lower levels
}
}
=> _activityManager.Create(entry);
public void Dispose()
{
_taskManager.TaskCompleted -= _taskManager_TaskCompleted;
_taskManager.TaskCompleted -= OnTaskCompleted;
_installationManager.PluginInstalled -= _installationManager_PluginInstalled;
_installationManager.PluginUninstalled -= _installationManager_PluginUninstalled;
_installationManager.PluginUpdated -= _installationManager_PluginUpdated;
_installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed;
_installationManager.PluginInstalled -= OnPluginInstalled;
_installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PluginUpdated -= OnPluginUpdated;
_installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
_sessionManager.SessionStarted -= _sessionManager_SessionStarted;
_sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed;
_sessionManager.AuthenticationSucceeded -= _sessionManager_AuthenticationSucceeded;
_sessionManager.SessionEnded -= _sessionManager_SessionEnded;
_sessionManager.SessionStarted -= OnSessionStarted;
_sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
_sessionManager.SessionEnded -= OnSessionEnded;
_sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
_sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
_sessionManager.PlaybackStart -= OnPlaybackStart;
_sessionManager.PlaybackStopped -= OnPlaybackStopped;
_subManager.SubtitleDownloadFailure -= _subManager_SubtitleDownloadFailure;
_subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
_userManager.UserCreated -= _userManager_UserCreated;
_userManager.UserPasswordChanged -= _userManager_UserPasswordChanged;
_userManager.UserDeleted -= _userManager_UserDeleted;
_userManager.UserPolicyUpdated -= _userManager_UserPolicyUpdated;
_userManager.UserLockedOut -= _userManager_UserLockedOut;
_userManager.UserCreated -= OnUserCreated;
_userManager.UserPasswordChanged -= OnUserPasswordChanged;
_userManager.UserDeleted -= OnUserDeleted;
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserLockedOut -= OnUserLockedOut;
_config.ConfigurationUpdated -= _config_ConfigurationUpdated;
_config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated;
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
_deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded;
_appHost.ApplicationUpdated -= _appHost_ApplicationUpdated;
_appHost.ApplicationUpdated -= OnApplicationUpdated;
}
/// <summary>
@ -531,6 +483,7 @@ namespace Emby.Server.Implementations.Activity
values.Add(CreateValueString(years, "year"));
days = days % DaysInYear;
}
// Number of months
if (days >= DaysInMonth)
{
@ -538,25 +491,39 @@ namespace Emby.Server.Implementations.Activity
values.Add(CreateValueString(months, "month"));
days = days % DaysInMonth;
}
// Number of days
if (days >= 1)
{
values.Add(CreateValueString(days, "day"));
}
// Number of hours
if (span.Hours >= 1)
{
values.Add(CreateValueString(span.Hours, "hour"));
}
// Number of minutes
if (span.Minutes >= 1)
{
values.Add(CreateValueString(span.Minutes, "minute"));
}
// Number of seconds (include when 0 if no other components included)
if (span.Seconds >= 1 || values.Count == 0)
{
values.Add(CreateValueString(span.Seconds, "second"));
}
// Combine values into string
var builder = new StringBuilder();
for (int i = 0; i < values.Count; i++)
{
if (builder.Length > 0)
{
builder.Append(i == values.Count - 1 ? " and " : ", ");
}
builder.Append(values[i]);
}
// Return result

View File

@ -28,20 +28,20 @@ using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Diagnostics;
using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.FFMpeg;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Middleware;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Reflection;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using MediaBrowser.Api;
@ -91,7 +91,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
@ -103,11 +102,15 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.Providers.TV.TheTVDB;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using ServiceStack;
using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate;
namespace Emby.Server.Implementations
{
@ -533,7 +536,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
MediaEncoder.Init();
MediaEncoder.SetFFmpegPath();
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
//{
@ -610,6 +613,68 @@ namespace Emby.Server.Implementations
await RegisterResources(serviceCollection);
FindParts();
string contentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath;
if (string.IsNullOrEmpty(contentRoot))
{
contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "jellyfin-web", "src");
}
var host = new WebHostBuilder()
.UseKestrel(options =>
{
options.ListenAnyIP(HttpPort);
if (EnableHttps && Certificate != null)
{
options.ListenAnyIP(HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); });
}
})
.UseContentRoot(contentRoot)
.ConfigureServices(services =>
{
services.AddResponseCompression();
services.AddHttpContextAccessor();
})
.Configure(app =>
{
app.UseWebSockets();
app.UseResponseCompression();
// TODO app.UseMiddleware<WebSocketMiddleware>();
app.Use(ExecuteWebsocketHandlerAsync);
app.Use(ExecuteHttpHandlerAsync);
})
.Build();
await host.StartAsync();
}
private async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await next().ConfigureAwait(false);
return;
}
await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
}
private async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
{
if (context.WebSockets.IsWebSocketRequest)
{
await next().ConfigureAwait(false);
return;
}
var request = context.Request;
var response = context.Response;
var localPath = context.Request.Path.ToString();
var req = new WebSocketSharpRequest(request, response, request.Path, Logger);
await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false);
}
protected virtual IHttpClient CreateHttpClient()
@ -631,6 +696,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IConfiguration>(_configuration);
serviceCollection.AddSingleton(JsonSerializer);
serviceCollection.AddSingleton(LoggerFactory);
@ -672,7 +739,7 @@ namespace Emby.Server.Implementations
ZipClient = new ZipClient();
serviceCollection.AddSingleton(ZipClient);
HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, CreateBrotliCompressor());
HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper);
serviceCollection.AddSingleton(HttpResultFactory);
serviceCollection.AddSingleton<IServerApplicationHost>(this);
@ -680,9 +747,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(ServerConfigurationManager);
var assemblyInfo = new AssemblyInfo();
serviceCollection.AddSingleton<IAssemblyInfo>(assemblyInfo);
LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LoggerFactory);
await LocalizationManager.LoadAll();
serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager);
@ -699,7 +763,7 @@ namespace Emby.Server.Implementations
var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager);
serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo);
ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, assemblyInfo);
ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory);
serviceCollection.AddSingleton<IItemRepository>(ItemRepository);
AuthenticationRepository = GetAuthenticationRepository();
@ -729,7 +793,8 @@ namespace Emby.Server.Implementations
_configuration,
NetworkManager,
JsonSerializer,
XmlSerializer);
XmlSerializer,
CreateHttpListener());
HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
serviceCollection.AddSingleton(HttpServer);
@ -786,7 +851,18 @@ namespace Emby.Server.Implementations
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
serviceCollection.AddSingleton(ChapterManager);
RegisterMediaEncoder(serviceCollection);
MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
LoggerFactory,
JsonSerializer,
StartupOptions.FFmpegPath,
StartupOptions.FFprobePath,
ServerConfigurationManager,
FileSystemManager,
() => SubtitleEncoder,
() => MediaSourceManager,
ProcessFactory,
5000);
serviceCollection.AddSingleton(MediaEncoder);
EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
serviceCollection.AddSingleton(EncodingManager);
@ -822,11 +898,6 @@ namespace Emby.Server.Implementations
_serviceProvider = serviceCollection.BuildServiceProvider();
}
protected virtual IBrotliCompressor CreateBrotliCompressor()
{
return null;
}
public virtual string PackageRuntime => "netcore";
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, EnvironmentInfo.EnvironmentInfo environmentInfo)
@ -859,11 +930,9 @@ namespace Emby.Server.Implementations
}
}
protected virtual bool SupportsDualModeSockets => true;
private X509Certificate GetCertificate(CertificateInfo info)
private X509Certificate2 GetCertificate(CertificateInfo info)
{
var certificateLocation = info == null ? null : info.Path;
var certificateLocation = info?.Path;
if (string.IsNullOrWhiteSpace(certificateLocation))
{
@ -902,85 +971,6 @@ namespace Emby.Server.Implementations
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
}
protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()
{
var info = new FFMpegInstallInfo();
// Windows builds: http://ffmpeg.zeranoe.com/builds/
// Linux builds: http://johnvansickle.com/ffmpeg/
// OS X builds: http://ffmpegmac.net/
// OS X x64: http://www.evermeet.cx/ffmpeg/
if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux)
{
info.FFMpegFilename = "ffmpeg";
info.FFProbeFilename = "ffprobe";
info.ArchiveType = "7z";
info.Version = "20170308";
}
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
{
info.FFMpegFilename = "ffmpeg.exe";
info.FFProbeFilename = "ffprobe.exe";
info.Version = "20170308";
info.ArchiveType = "7z";
}
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
{
info.FFMpegFilename = "ffmpeg";
info.FFProbeFilename = "ffprobe";
info.ArchiveType = "7z";
info.Version = "20170308";
}
return info;
}
protected virtual FFMpegInfo GetFFMpegInfo()
{
return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
.GetFFMpegInfo(StartupOptions);
}
/// <summary>
/// Registers the media encoder.
/// </summary>
/// <returns>Task.</returns>
private void RegisterMediaEncoder(IServiceCollection serviceCollection)
{
string encoderPath = null;
string probePath = null;
var info = GetFFMpegInfo();
encoderPath = info.EncoderPath;
probePath = info.ProbePath;
var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
LoggerFactory,
JsonSerializer,
encoderPath,
probePath,
hasExternalEncoder,
ServerConfigurationManager,
FileSystemManager,
LiveTvManager,
IsoManager,
LibraryManager,
ChannelManager,
SessionManager,
() => SubtitleEncoder,
() => MediaSourceManager,
HttpClient,
ZipClient,
ProcessFactory,
5000);
MediaEncoder = mediaEncoder;
serviceCollection.AddSingleton(MediaEncoder);
}
/// <summary>
/// Gets the user repository.
/// </summary>
@ -1017,7 +1007,7 @@ namespace Emby.Server.Implementations
/// </summary>
private void SetStaticProperties()
{
((SqliteItemRepository)ItemRepository).ImageProcessor = ImageProcessor;
ItemRepository.ImageProcessor = ImageProcessor;
// For now there's no real way to inject these properly
BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem");
@ -1059,9 +1049,7 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>());
StartServer();
HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>(), GetUrlPrefixes());
LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(),
@ -1145,15 +1133,12 @@ namespace Emby.Server.Implementations
AllConcreteTypes = GetComposablePartAssemblies()
.SelectMany(x => x.ExportedTypes)
.Where(type =>
{
return type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType;
})
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
.ToArray();
}
private CertificateInfo CertificateInfo { get; set; }
protected X509Certificate Certificate { get; private set; }
protected X509Certificate2 Certificate { get; private set; }
private IEnumerable<string> GetUrlPrefixes()
{
@ -1175,45 +1160,7 @@ namespace Emby.Server.Implementations
});
}
protected abstract IHttpListener CreateHttpListener();
/// <summary>
/// Starts the server.
/// </summary>
private void StartServer()
{
try
{
((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener());
return;
}
catch (Exception ex)
{
var msg = string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase)
? "The http server is unable to start due to a Socket error. This can occasionally happen when the operating system takes longer than usual to release the IP bindings from the previous session. This can take up to five minutes. Please try waiting or rebooting the system."
: "Error starting Http Server";
Logger.LogError(ex, msg);
if (HttpPort == ServerConfiguration.DefaultHttpPort)
{
throw;
}
}
HttpPort = ServerConfiguration.DefaultHttpPort;
try
{
((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener());
}
catch (Exception ex)
{
Logger.LogError(ex, "Error starting http server");
throw;
}
}
protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger);
private CertificateInfo GetCertificateInfo(bool generateCertificate)
{
@ -1456,7 +1403,7 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName,
LocalAddress = localAddress,
SupportsLibraryMonitor = true,
EncoderLocationType = MediaEncoder.EncoderLocationType,
EncoderLocation = MediaEncoder.EncoderLocation,
SystemArchitecture = EnvironmentInfo.SystemArchitecture,
SystemUpdateLevel = SystemUpdateLevel,
PackageName = StartupOptions.PackageName
@ -1650,7 +1597,7 @@ namespace Emby.Server.Implementations
LogErrorResponseBody = false,
LogErrors = logPing,
LogRequest = logPing,
TimeoutMs = 30000,
TimeoutMs = 5000,
BufferContent = false,
CancellationToken = cancellationToken

View File

@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Collections
return base.Supports(item);
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var playlist = (BoxSet)item;
@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Collections
.ToList();
}
protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
}

View File

@ -6,7 +6,8 @@ namespace Emby.Server.Implementations
{
public static readonly Dictionary<string, string> Configuration = new Dictionary<string, string>
{
{"HttpListenerHost:DefaultRedirectPath", "web/index.html"}
{"HttpListenerHost:DefaultRedirectPath", "web/index.html"},
{"MusicBrainz:BaseUrl", "https://www.musicbrainz.org"}
};
}
}

View File

@ -1,13 +1,49 @@
using System;
using System.Collections.Generic;
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
{
public class CryptographyProvider : ICryptoProvider
{
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
"System.Security.Cryptography.MD5",
"SHA",
"SHA1",
"System.Security.Cryptography.SHA1",
"SHA256",
"SHA-256",
"System.Security.Cryptography.SHA256",
"SHA384",
"SHA-384",
"System.Security.Cryptography.SHA384",
"SHA512",
"SHA-512",
"System.Security.Cryptography.SHA512"
};
public string DefaultHashMethod => "PBKDF2";
private RandomNumberGenerator _randomNumberGenerator;
private const int _defaultIterations = 1000;
public CryptographyProvider()
{
//FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
//Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
//there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
//Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
_randomNumberGenerator = RandomNumberGenerator.Create();
}
public Guid GetMD5(string str)
{
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
return provider.ComputeHash(bytes);
}
}
public IEnumerable<string> GetSupportedHashMethods()
{
return _supportedHashMethods;
}
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
{
//downgrading for now as we need this library to be dotnetstandard compliant
//with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
if (method == DefaultHashMethod)
{
using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
{
return r.GetBytes(32);
}
}
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
}
public byte[] ComputeHash(string hashMethod, byte[] bytes)
{
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
}
public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
{
return ComputeHash(DefaultHashMethod, bytes);
}
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
{
if (hashMethod == DefaultHashMethod)
{
return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
}
else if (_supportedHashMethods.Contains(hashMethod))
{
using (var h = HashAlgorithm.Create(hashMethod))
{
if (salt.Length == 0)
{
return h.ComputeHash(bytes);
}
else
{
byte[] salted = new byte[bytes.Length + salt.Length];
Array.Copy(bytes, salted, bytes.Length);
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
return h.ComputeHash(salted);
}
}
}
else
{
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
}
}
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
{
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
}
public byte[] ComputeHash(PasswordHash hash)
{
int iterations = _defaultIterations;
if (!hash.Parameters.ContainsKey("iterations"))
{
hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture));
}
else
{
try
{
iterations = int.Parse(hash.Parameters["iterations"]);
}
catch (Exception e)
{
throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
}
}
return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
}
public byte[] GenerateSalt()
{
byte[] salt = new byte[64];
_randomNumberGenerator.GetBytes(salt);
return salt;
}
}
}

View File

@ -24,7 +24,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@ -65,8 +64,7 @@ namespace Emby.Server.Implementations.Data
IServerConfigurationManager config,
IServerApplicationHost appHost,
IJsonSerializer jsonSerializer,
ILoggerFactory loggerFactory,
IAssemblyInfo assemblyInfo)
ILoggerFactory loggerFactory)
: base(loggerFactory.CreateLogger(nameof(SqliteItemRepository)))
{
if (config == null)
@ -82,7 +80,7 @@ namespace Emby.Server.Implementations.Data
_appHost = appHost;
_config = config;
_jsonSerializer = jsonSerializer;
_typeMapper = new TypeMapper(assemblyInfo);
_typeMapper = new TypeMapper();
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
}

View File

@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
{
TryMigrateToLocalUsersTable(connection);
}
RemoveEmptyPasswordHashes();
}
}
@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
}
}
private void RemoveEmptyPasswordHashes()
{
foreach (var user in RetrieveAllUsers())
{
// 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))
{
continue;
}
user.Password = null;
var serialized = _jsonSerializer.SerializeToBytes(user);
using (WriteLock.Write())
using (var connection = CreateConnection())
{
connection.RunInTransaction(db =>
{
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
}
}
/// <summary>
/// Save a user in the repo
/// </summary>

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using MediaBrowser.Model.Reflection;
namespace Emby.Server.Implementations.Data
{
@ -10,16 +9,13 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public class TypeMapper
{
private readonly IAssemblyInfo _assemblyInfo;
/// <summary>
/// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types
/// </summary>
private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
public TypeMapper(IAssemblyInfo assemblyInfo)
public TypeMapper()
{
_assemblyInfo = assemblyInfo;
}
/// <summary>
@ -45,8 +41,7 @@ namespace Emby.Server.Implementations.Data
/// <returns>Type.</returns>
private Type LookupType(string typeName)
{
return _assemblyInfo
.GetCurrentAssemblies()
return AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(typeName))
.FirstOrDefault(t => t != null);
}

View File

@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Diagnostics
public void Dispose()
{
_process.Dispose();
_process?.Dispose();
}
}
}

View File

@ -9,7 +9,6 @@
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="..\SocketHttpListener\SocketHttpListener.csproj" />
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj" />
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
@ -22,6 +21,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
@ -40,6 +47,21 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />

View File

@ -1,24 +0,0 @@
namespace Emby.Server.Implementations.FFMpeg
{
/// <summary>
/// Class FFMpegInfo
/// </summary>
public class FFMpegInfo
{
/// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
public string EncoderPath { get; set; }
/// <summary>
/// Gets or sets the probe path.
/// </summary>
/// <value>The probe path.</value>
public string ProbePath { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
public string Version { get; set; }
}
}

View File

@ -1,17 +0,0 @@
namespace Emby.Server.Implementations.FFMpeg
{
public class FFMpegInstallInfo
{
public string Version { get; set; }
public string FFMpegFilename { get; set; }
public string FFProbeFilename { get; set; }
public string ArchiveType { get; set; }
public FFMpegInstallInfo()
{
Version = "Path";
FFMpegFilename = "ffmpeg";
FFProbeFilename = "ffprobe";
}
}
}

View File

@ -1,132 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.FFMpeg
{
public class FFMpegLoader
{
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly FFMpegInstallInfo _ffmpegInstallInfo;
public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo)
{
_appPaths = appPaths;
_fileSystem = fileSystem;
_ffmpegInstallInfo = ffmpegInstallInfo;
}
public FFMpegInfo GetFFMpegInfo(IStartupOptions options)
{
var customffMpegPath = options.FFmpegPath;
var customffProbePath = options.FFprobePath;
if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath))
{
return new FFMpegInfo
{
ProbePath = customffProbePath,
EncoderPath = customffMpegPath,
Version = "external"
};
}
var downloadInfo = _ffmpegInstallInfo;
var prebuiltFolder = _appPaths.ProgramSystemPath;
var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename);
var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename);
if (File.Exists(prebuiltffmpeg) && File.Exists(prebuiltffprobe))
{
return new FFMpegInfo
{
ProbePath = prebuiltffprobe,
EncoderPath = prebuiltffmpeg,
Version = "external"
};
}
var version = downloadInfo.Version;
if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase))
{
return new FFMpegInfo();
}
var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
var versionedDirectoryPath = Path.Combine(rootEncoderPath, version);
var info = new FFMpegInfo
{
ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename),
EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename),
Version = version
};
Directory.CreateDirectory(versionedDirectoryPath);
var excludeFromDeletions = new List<string> { versionedDirectoryPath };
if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath))
{
// ffmpeg not present. See if there's an older version we can start with
var existingVersion = GetExistingVersion(info, rootEncoderPath);
// No older version. Need to download and block until complete
if (existingVersion == null)
{
return new FFMpegInfo();
}
else
{
info = existingVersion;
versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
excludeFromDeletions.Add(versionedDirectoryPath);
}
}
// Allow just one of these to be overridden, if desired.
if (!string.IsNullOrWhiteSpace(customffMpegPath))
{
info.EncoderPath = customffMpegPath;
}
if (!string.IsNullOrWhiteSpace(customffProbePath))
{
info.ProbePath = customffProbePath;
}
return info;
}
private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
{
var encoderFilename = Path.GetFileName(info.EncoderPath);
var probeFilename = Path.GetFileName(info.ProbePath);
foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath))
{
var allFiles = _fileSystem.GetFilePaths(directory, true).ToList();
var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(encoder) &&
!string.IsNullOrWhiteSpace(probe))
{
return new FFMpegInfo
{
EncoderPath = encoder,
ProbePath = probe,
Version = Path.GetFileName(Path.GetDirectoryName(probe))
};
}
}
return null;
}
}
}

View File

@ -15,6 +15,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpClientManager
{
@ -179,11 +180,11 @@ namespace Emby.Server.Implementations.HttpClientManager
foreach (var header in options.RequestHeaders)
{
if (string.Equals(header.Key, "Accept", StringComparison.OrdinalIgnoreCase))
if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase))
{
request.Accept = header.Value;
}
else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
{
SetUserAgent(request, header.Value);
hasUserAgent = true;
@ -327,7 +328,6 @@ namespace Emby.Server.Implementations.HttpClientManager
}
httpWebRequest.ContentType = contentType;
httpWebRequest.ContentLength = bytes.Length;
(await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
}
catch (Exception ex)

View File

@ -5,15 +5,19 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.IO;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
public class FileWriter : IHttpResult
{
private readonly IStreamHelper _streamHelper;
private ILogger Logger { get; set; }
private readonly IFileSystem _fileSystem;
private string RangeHeader { get; set; }
private bool IsHeadRequest { get; set; }
@ -42,25 +46,27 @@ namespace Emby.Server.Implementations.HttpServer
public string Path { get; set; }
public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem)
public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
_streamHelper = streamHelper;
_fileSystem = fileSystem;
Path = path;
Logger = logger;
RangeHeader = rangeHeader;
Headers["Content-Type"] = contentType;
Headers[HeaderNames.ContentType] = contentType;
TotalContentLength = fileSystem.GetFileInfo(path).Length;
Headers["Accept-Ranges"] = "bytes";
Headers[HeaderNames.AcceptRanges] = "bytes";
if (string.IsNullOrWhiteSpace(rangeHeader))
{
Headers["Content-Length"] = TotalContentLength.ToString(UsCulture);
StatusCode = HttpStatusCode.OK;
}
else
@ -93,13 +99,10 @@ 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(UsCulture);
Headers["Content-Length"] = lengthString;
var rangeString = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
Headers["Content-Range"] = rangeString;
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString;
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString);
}
/// <summary>
@ -145,8 +148,7 @@ namespace Emby.Server.Implementations.HttpServer
}
}
private string[] SkipLogExtensions = new string[]
{
private static readonly string[] SkipLogExtensions = {
".js",
".html",
".css"
@ -163,8 +165,10 @@ namespace Emby.Server.Implementations.HttpServer
}
var path = Path;
var offset = RangeStart;
var count = RangeLength;
if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1))
if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
{
var extension = System.IO.Path.GetExtension(path);
@ -173,20 +177,15 @@ namespace Emby.Server.Implementations.HttpServer
Logger.LogDebug("Transmit file {0}", path);
}
//var count = FileShare == FileShareMode.ReadWrite ? TotalContentLength : 0;
await response.TransmitFile(path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false);
return;
offset = 0;
count = 0;
}
await response.TransmitFile(path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false);
await response.TransmitFile(path, offset, count, FileShare, _fileSystem, _streamHelper, cancellationToken).ConfigureAwait(false);
}
finally
{
if (OnComplete != null)
{
OnComplete();
}
OnComplete?.Invoke();
}
}
@ -203,8 +202,5 @@ namespace Emby.Server.Implementations.HttpServer
get => (HttpStatusCode)Status;
set => Status = (int)value;
}
public string StatusDescription { get; set; }
}
}

View File

@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@ -20,6 +21,9 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ServiceStack.Text.Jsv;
@ -29,12 +33,8 @@ namespace Emby.Server.Implementations.HttpServer
public class HttpListenerHost : IHttpServer, IDisposable
{
private string DefaultRedirectPath { get; set; }
private readonly ILogger _logger;
public string[] UrlPrefixes { get; private set; }
private IHttpListener _listener;
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
private readonly IServerConfigurationManager _config;
@ -42,6 +42,7 @@ namespace Emby.Server.Implementations.HttpServer
private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer;
private readonly IHttpListener _socketListener;
private readonly Func<Type, Func<string, object>> _funcParseFn;
public Action<IRequest, IResponse, object>[] ResponseFilters { get; set; }
@ -59,15 +60,18 @@ namespace Emby.Server.Implementations.HttpServer
IConfiguration configuration,
INetworkManager networkManager,
IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer)
IXmlSerializer xmlSerializer,
IHttpListener socketListener)
{
_appHost = applicationHost;
_logger = loggerFactory.CreateLogger("HttpServer");
Logger = loggerFactory.CreateLogger("HttpServer");
_config = config;
DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"];
_networkManager = networkManager;
_jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer;
_socketListener = socketListener;
_socketListener.WebSocketConnected = OnWebSocketConnected;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
@ -77,7 +81,7 @@ namespace Emby.Server.Implementations.HttpServer
public string GlobalResponse { get; set; }
protected ILogger Logger => _logger;
protected ILogger Logger { get; }
public object CreateInstance(Type type)
{
@ -143,11 +147,11 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, Logger)
{
OnReceive = ProcessWebSocketMessageReceived,
Url = e.Url,
QueryString = e.QueryString ?? new QueryParamCollection()
QueryString = e.QueryString ?? new QueryCollection()
};
connection.Closed += Connection_Closed;
@ -212,16 +216,16 @@ namespace Emby.Server.Implementations.HttpServer
if (logExceptionStackTrace)
{
_logger.LogError(ex, "Error processing request");
Logger.LogError(ex, "Error processing request");
}
else if (logExceptionMessage)
{
_logger.LogError(ex.Message);
Logger.LogError(ex.Message);
}
var httpRes = httpReq.Response;
if (httpRes.IsClosed)
if (httpRes.OriginalResponse.HasStarted)
{
return;
}
@ -234,7 +238,7 @@ namespace Emby.Server.Implementations.HttpServer
}
catch (Exception errorEx)
{
_logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)");
Logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)");
}
}
@ -277,14 +281,6 @@ namespace Emby.Server.Implementations.HttpServer
}
}
if (_listener != null)
{
_logger.LogInformation("Stopping HttpListener...");
var task = _listener.Stop();
Task.WaitAll(task);
_logger.LogInformation("HttpListener stopped");
}
}
public static string RemoveQueryStringByKey(string url, string key)
@ -292,7 +288,7 @@ namespace Emby.Server.Implementations.HttpServer
var uri = new Uri(url);
// this gets all the query string key value pairs as a collection
var newQueryString = MyHttpUtility.ParseQueryString(uri.Query);
var newQueryString = QueryHelpers.ParseQuery(uri.Query);
var originalCount = newQueryString.Count;
@ -313,7 +309,7 @@ namespace Emby.Server.Implementations.HttpServer
string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
return newQueryString.Count > 0
? string.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString)
? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
: pagePathWithoutQueryString;
}
@ -422,7 +418,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Overridable method that can be used to implement a custom hnandler
/// </summary>
protected async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
@ -599,17 +595,15 @@ namespace Emby.Server.Implementations.HttpServer
}
finally
{
httpRes.Close();
stopWatch.Stop();
var elapsed = stopWatch.Elapsed;
if (elapsed.TotalMilliseconds > 500)
{
_logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
Logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
}
else
{
_logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
Logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
}
}
}
@ -622,7 +616,7 @@ namespace Emby.Server.Implementations.HttpServer
var pathParts = pathInfo.TrimStart('/').Split('/');
if (pathParts.Length == 0)
{
_logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl);
Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl);
return null;
}
@ -636,15 +630,13 @@ namespace Emby.Server.Implementations.HttpServer
};
}
_logger.LogError("Could not find handler for {PathInfo}", pathInfo);
Logger.LogError("Could not find handler for {PathInfo}", pathInfo);
return null;
}
private static Task Write(IResponse response, string text)
{
var bOutput = Encoding.UTF8.GetBytes(text);
response.SetContentLength(bOutput.Length);
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
}
@ -663,6 +655,7 @@ 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);
@ -683,13 +676,15 @@ namespace Emby.Server.Implementations.HttpServer
/// Adds the rest handlers.
/// </summary>
/// <param name="services">The services.</param>
public void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listeners)
/// <param name="listeners"></param>
/// <param name="urlPrefixes"></param>
public void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
{
_webSocketListeners = listeners.ToArray();
UrlPrefixes = urlPrefixes.ToArray();
ServiceController = new ServiceController();
_logger.LogInformation("Calling ServiceStack AppHost.Init");
Logger.LogInformation("Calling ServiceStack AppHost.Init");
var types = services.Select(r => r.GetType()).ToArray();
@ -697,7 +692,7 @@ namespace Emby.Server.Implementations.HttpServer
ResponseFilters = new Action<IRequest, IResponse, object>[]
{
new ResponseFilter(_logger).FilterResponse
new ResponseFilter(Logger).FilterResponse
};
}
@ -759,8 +754,12 @@ namespace Emby.Server.Implementations.HttpServer
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
}
//TODO Add Jellyfin Route Path Normalizer
public Task ProcessWebSocketRequest(HttpContext context)
{
return _socketListener.ProcessWebSocketRequest(context);
}
//TODO Add Jellyfin Route Path Normalizer
private static string NormalizeEmbyRoutePath(string path)
{
if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
@ -793,6 +792,7 @@ namespace Emby.Server.Implementations.HttpServer
private bool _disposed;
private readonly object _disposeLock = new object();
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
@ -821,7 +821,7 @@ namespace Emby.Server.Implementations.HttpServer
return Task.CompletedTask;
}
_logger.LogDebug("Websocket message received: {0}", result.MessageType);
Logger.LogDebug("Websocket message received: {0}", result.MessageType);
var tasks = _webSocketListeners.Select(i => Task.Run(async () =>
{
@ -831,7 +831,7 @@ namespace Emby.Server.Implementations.HttpServer
}
catch (Exception ex)
{
_logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty);
Logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty);
}
}));
@ -842,18 +842,5 @@ namespace Emby.Server.Implementations.HttpServer
{
Dispose(true);
}
public void StartServer(string[] urlPrefixes, IHttpListener httpListener)
{
UrlPrefixes = urlPrefixes;
_listener = httpListener;
_listener.WebSocketConnected = OnWebSocketConnected;
_listener.ErrorHandler = ErrorHandler;
_listener.RequestHandler = RequestHandler;
_listener.Start(UrlPrefixes);
}
}
}

View File

@ -16,6 +16,8 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
@ -32,17 +34,16 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private IBrotliCompressor _brotliCompressor;
private readonly IStreamHelper _streamHelper;
/// <summary>
/// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
/// </summary>
public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor)
public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
{
_fileSystem = fileSystem;
_jsonSerializer = jsonSerializer;
_brotliCompressor = brotliCompressor;
_streamHelper = streamHelper;
_logger = loggerfactory.CreateLogger("HttpResultFactory");
}
@ -76,7 +77,7 @@ namespace Emby.Server.Implementations.HttpServer
public object GetRedirectResult(string url)
{
var responseHeaders = new Dictionary<string, string>();
responseHeaders["Location"] = url;
responseHeaders[HeaderNames.Location] = url;
var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
@ -97,9 +98,9 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires))
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
{
responseHeaders["Expires"] = "-1";
responseHeaders[HeaderNames.Expires] = "-1";
}
AddResponseHeaders(result, responseHeaders);
@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
content = Array.Empty<byte>();
}
result = new StreamWriter(content, contentType, contentLength);
result = new StreamWriter(content, contentType);
}
else
{
@ -143,9 +144,9 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{
responseHeaders["Expires"] = "-1";
responseHeaders[HeaderNames.Expires] = "-1";
}
AddResponseHeaders(result, responseHeaders);
@ -175,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
bytes = Array.Empty<byte>();
}
result = new StreamWriter(bytes, contentType, contentLength);
result = new StreamWriter(bytes, contentType);
}
else
{
@ -187,9 +188,9 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{
responseHeaders["Expires"] = "-1";
responseHeaders[HeaderNames.Expires] = "-1";
}
AddResponseHeaders(result, responseHeaders);
@ -214,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
responseHeaders["Expires"] = "-1";
responseHeaders[HeaderNames.Expires] = "-1";
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
}
@ -246,9 +247,9 @@ namespace Emby.Server.Implementations.HttpServer
private static string GetCompressionType(IRequest request)
{
var acceptEncoding = request.Headers["Accept-Encoding"];
var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
if (acceptEncoding != null)
if (string.IsNullOrEmpty(acceptEncoding))
{
//if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
// return "br";
@ -326,21 +327,21 @@ namespace Emby.Server.Implementations.HttpServer
}
content = Compress(content, requestedCompressionType);
responseHeaders["Content-Encoding"] = requestedCompressionType;
responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
responseHeaders["Vary"] = "Accept-Encoding";
responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
var contentLength = content.Length;
if (isHeadRequest)
{
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
var result = new StreamWriter(Array.Empty<byte>(), contentType);
AddResponseHeaders(result, responseHeaders);
return result;
}
else
{
var result = new StreamWriter(content, contentType, contentLength);
var result = new StreamWriter(content, contentType);
AddResponseHeaders(result, responseHeaders);
return result;
}
@ -348,11 +349,6 @@ namespace Emby.Server.Implementations.HttpServer
private byte[] Compress(byte[] bytes, string compressionType)
{
if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
{
return CompressBrotli(bytes);
}
if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
{
return Deflate(bytes);
@ -366,11 +362,6 @@ namespace Emby.Server.Implementations.HttpServer
throw new NotSupportedException(compressionType);
}
private byte[] CompressBrotli(byte[] bytes)
{
return _brotliCompressor.Compress(bytes);
}
private static byte[] Deflate(byte[] bytes)
{
// In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
@ -424,12 +415,12 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary>
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
{
bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
if (!noCache)
{
DateTime.TryParse(requestContext.Headers.Get("If-Modified-Since"), out var ifModifiedSinceHeader);
DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
{
@ -530,7 +521,7 @@ namespace Emby.Server.Implementations.HttpServer
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var contentType = options.ContentType;
if (!string.IsNullOrEmpty(requestContext.Headers.Get("If-Modified-Since")))
if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
{
// See if the result is already cached in the browser
var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
@ -548,11 +539,11 @@ namespace Emby.Server.Implementations.HttpServer
AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
AddAgeHeader(responseHeaders, options.DateLastModified);
var rangeHeader = requestContext.Headers.Get("Range");
var rangeHeader = requestContext.Headers[HeaderNames.Range];
if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
{
var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem)
var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
{
OnComplete = options.OnComplete,
OnError = options.OnError,
@ -590,11 +581,6 @@ namespace Emby.Server.Implementations.HttpServer
}
else
{
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture);
}
if (isHeadRequest)
{
using (stream)
@ -614,11 +600,6 @@ namespace Emby.Server.Implementations.HttpServer
}
}
/// <summary>
/// The us culture
/// </summary>
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary>
/// Adds the caching responseHeaders.
/// </summary>
@ -627,23 +608,23 @@ namespace Emby.Server.Implementations.HttpServer
{
if (noCache)
{
responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate";
responseHeaders["pragma"] = "no-cache, no-store, must-revalidate";
responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
return;
}
if (cacheDuration.HasValue)
{
responseHeaders["Cache-Control"] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
}
else
{
responseHeaders["Cache-Control"] = "public";
responseHeaders[HeaderNames.CacheControl] = "public";
}
if (lastModifiedDate.HasValue)
{
responseHeaders["Last-Modified"] = lastModifiedDate.ToString();
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString();
}
}
@ -656,7 +637,7 @@ namespace Emby.Server.Implementations.HttpServer
{
if (lastDateModified.HasValue)
{
responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
}
}
@ -714,9 +695,4 @@ namespace Emby.Server.Implementations.HttpServer
}
}
}
public interface IBrotliCompressor
{
byte[] Compress(byte[] content);
}
}

View File

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer
{
@ -28,21 +27,11 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The web socket handler.</value>
Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
/// <summary>
/// Gets or sets the web socket connecting.
/// </summary>
/// <value>The web socket connecting.</value>
Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; }
/// <summary>
/// Starts this instance.
/// </summary>
/// <param name="urlPrefixes">The URL prefixes.</param>
void Start(IEnumerable<string> urlPrefixes);
/// <summary>
/// Stops this instance.
/// </summary>
Task Stop();
Task ProcessWebSocketRequest(HttpContext ctx);
}
}

View File

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
@ -66,8 +67,8 @@ namespace Emby.Server.Implementations.HttpServer
this._logger = logger;
ContentType = contentType;
Headers["Content-Type"] = contentType;
Headers["Accept-Ranges"] = "bytes";
Headers[HeaderNames.ContentType] = contentType;
Headers[HeaderNames.AcceptRanges] = "bytes";
StatusCode = HttpStatusCode.PartialContent;
SetRangeValues(contentLength);
@ -95,9 +96,7 @@ 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
Headers["Content-Length"] = RangeLength.ToString(UsCulture);
Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
if (RangeStart > 0 && SourceStream.CanSeek)
{

View File

@ -3,6 +3,7 @@ using System.Globalization;
using System.Text;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
@ -25,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-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-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-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.AddHeader("Access-Control-Allow-Origin", "*");
@ -44,20 +45,19 @@ namespace Emby.Server.Implementations.HttpServer
if (dto is IHasHeaders hasHeaders)
{
if (!hasHeaders.Headers.ContainsKey("Server"))
if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
{
hasHeaders.Headers["Server"] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
}
// Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
if (hasHeaders.Headers.TryGetValue("Content-Length", out string contentLength)
if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
&& !string.IsNullOrEmpty(contentLength))
{
var length = long.Parse(contentLength, UsCulture);
if (length > 0)
{
res.SetContentLength(length);
res.SendChunked = false;
}
}

View File

@ -5,6 +5,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
{
@ -176,7 +177,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers["Authorization"];
auth = httpReq.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
@ -52,12 +53,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceStream = source;
Headers["Content-Type"] = contentType;
if (source.CanSeek)
{
Headers["Content-Length"] = source.Length.ToString(UsCulture);
}
Headers[HeaderNames.ContentType] = contentType;
}
/// <summary>
@ -65,8 +61,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
/// <param name="logger">The logger.</param>
public StreamWriter(byte[] source, string contentType, int contentLength)
public StreamWriter(byte[] source, string contentType)
{
if (string.IsNullOrEmpty(contentType))
{
@ -75,9 +70,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceBytes = source;
Headers["Content-Type"] = contentType;
Headers["Content-Length"] = contentLength.ToString(UsCulture);
Headers[HeaderNames.ContentType] = contentType;
}
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)

View File

@ -8,6 +8,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using UtfUnknown;
@ -67,7 +68,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
public QueryParamCollection QueryString { get; set; }
public IQueryCollection QueryString { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
@ -101,12 +102,6 @@ namespace Emby.Server.Implementations.HttpServer
_socket = socket;
_socket.OnReceiveBytes = OnReceiveInternal;
var memorySocket = socket as IMemoryWebSocket;
if (memorySocket != null)
{
memorySocket.OnReceiveMemoryBytes = OnReceiveInternal;
}
RemoteEndPoint = remoteEndPoint;
_logger = logger;
@ -142,34 +137,6 @@ namespace Emby.Server.Implementations.HttpServer
}
}
/// <summary>
/// Called when [receive].
/// </summary>
/// <param name="memory">The memory block.</param>
/// <param name="length">The length of the memory block.</param>
private void OnReceiveInternal(Memory<byte> memory, int length)
{
LastActivityDate = DateTime.UtcNow;
if (OnReceive == null)
{
return;
}
var bytes = memory.Slice(0, length).ToArray();
var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
{
OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
}
else
{
OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
}
}
private void OnReceiveInternal(string message)
{
LastActivityDate = DateTime.UtcNow;
@ -193,7 +160,7 @@ namespace Emby.Server.Implementations.HttpServer
var info = new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data == null ? null : stub.Data.ToString(),
Data = stub.Data?.ToString(),
Connection = this
};

View File

@ -7,7 +7,6 @@ using System.Text;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
@ -711,20 +710,20 @@ namespace Emby.Server.Implementations.IO
return GetFiles(path, null, false, recursive);
}
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
{
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
}
var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
if (extensions != null && extensions.Length > 0)
if (extensions != null && extensions.Count > 0)
{
files = files.Where(i =>
{

View File

@ -20,6 +20,9 @@ namespace Emby.Server.Implementations.Images
public abstract class BaseDynamicImageProvider<T> : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder
where T : BaseItem
{
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
= new ImageType[] { ImageType.Primary };
protected IFileSystem FileSystem { get; private set; }
protected IProviderManager ProviderManager { get; private set; }
protected IApplicationPaths ApplicationPaths { get; private set; }
@ -33,18 +36,7 @@ namespace Emby.Server.Implementations.Images
ImageProcessor = imageProcessor;
}
protected virtual bool Supports(BaseItem item)
{
return true;
}
public virtual ImageType[] GetSupportedImages(BaseItem item)
{
return new ImageType[]
{
ImageType.Primary
};
}
protected virtual bool Supports(BaseItem _) => true;
public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
@ -54,15 +46,14 @@ namespace Emby.Server.Implementations.Images
}
var updateType = ItemUpdateType.None;
var supportedImages = GetSupportedImages(item);
if (supportedImages.Contains(ImageType.Primary))
if (SupportedImages.Contains(ImageType.Primary))
{
var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | primaryResult;
}
if (supportedImages.Contains(ImageType.Thumb))
if (SupportedImages.Contains(ImageType.Thumb))
{
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | thumbResult;
@ -94,7 +85,7 @@ namespace Emby.Server.Implementations.Images
}
protected async Task<ItemUpdateType> FetchToFileInternal(BaseItem item,
List<BaseItem> itemsWithImages,
IReadOnlyList<BaseItem> itemsWithImages,
ImageType imageType,
CancellationToken cancellationToken)
{
@ -119,9 +110,9 @@ namespace Emby.Server.Implementations.Images
return ItemUpdateType.ImageUpdate;
}
protected abstract List<BaseItem> GetItemsWithImages(BaseItem item);
protected abstract IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item);
protected string CreateThumbCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath)
protected string CreateThumbCollage(BaseItem primaryItem, IEnumerable<BaseItem> items, string outputPath)
{
return CreateCollage(primaryItem, items, outputPath, 640, 360);
}
@ -132,38 +123,38 @@ namespace Emby.Server.Implementations.Images
.Select(i =>
{
var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile)
{
return image.Path;
}
image = i.GetImageInfo(ImageType.Thumb, 0);
if (image != null && image.IsLocalFile)
{
return image.Path;
}
return null;
})
.Where(i => !string.IsNullOrEmpty(i));
}
protected string CreatePosterCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath)
protected string CreatePosterCollage(BaseItem primaryItem, IEnumerable<BaseItem> items, string outputPath)
{
return CreateCollage(primaryItem, items, outputPath, 400, 600);
}
protected string CreateSquareCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath)
protected string CreateSquareCollage(BaseItem primaryItem, IEnumerable<BaseItem> items, string outputPath)
{
return CreateCollage(primaryItem, items, outputPath, 600, 600);
}
protected string CreateThumbCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath, int width, int height)
protected string CreateThumbCollage(BaseItem primaryItem, IEnumerable<BaseItem> items, string outputPath, int width, int height)
{
return CreateCollage(primaryItem, items, outputPath, width, height);
}
private string CreateCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath, int width, int height)
private string CreateCollage(BaseItem primaryItem, IEnumerable<BaseItem> items, string outputPath, int width, int height)
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
@ -192,7 +183,7 @@ namespace Emby.Server.Implementations.Images
public string Name => "Dynamic Image Provider";
protected virtual string CreateImage(BaseItem item,
List<BaseItem> itemsWithImages,
IReadOnlyCollection<BaseItem> itemsWithImages,
string outputPathWithoutExtension,
ImageType imageType,
int imageIndex)
@ -211,18 +202,15 @@ namespace Emby.Server.Implementations.Images
if (imageType == ImageType.Primary)
{
if (item is UserView)
{
return CreateSquareCollage(item, itemsWithImages, outputPath);
}
if (item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum)
if (item is UserView || item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum)
{
return CreateSquareCollage(item, itemsWithImages, outputPath);
}
return CreatePosterCollage(item, itemsWithImages, outputPath);
}
throw new ArgumentException("Unexpected image type");
throw new ArgumentException("Unexpected image type", nameof(imageType));
}
protected virtual int MaxImageAgeDays => 7;
@ -234,13 +222,11 @@ namespace Emby.Server.Implementations.Images
return false;
}
var supportedImages = GetSupportedImages(item);
if (supportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary))
if (SupportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary))
{
return true;
}
if (supportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb))
if (SupportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb))
{
return true;
}
@ -285,7 +271,7 @@ namespace Emby.Server.Implementations.Images
public int Order => 0;
protected string CreateSingleImage(List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
{
var image = itemsWithImages
.Where(i => i.HasImage(imageType) && i.GetImageInfo(imageType, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(imageType)))

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
@ -18,20 +19,64 @@ 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
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
{
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)
{
bool success = false;
if (resolvedUser == null)
{
throw new Exception("Invalid username or password");
}
var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
// As long as jellyfin supports passwordless users, we need this little block here to accomodate
if (IsPasswordEmpty(resolvedUser, password))
{
return Task.FromResult(new ProviderAuthenticationResult
{
Username = username
});
}
ConvertPasswordFormat(resolvedUser);
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
byte[] calculatedHash;
string calculatedHashString;
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
{
if (string.IsNullOrEmpty(readyHash.Salt))
{
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
}
else
{
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
}
if (calculatedHashString == readyHash.Hash)
{
success = true;
// throw new Exception("Invalid username or password");
}
}
else
{
throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
}
// var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
if (!success)
{
@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
});
}
// This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
// but at least they are in the new format.
private void ConvertPasswordFormat(User user)
{
if (string.IsNullOrEmpty(user.Password))
{
return;
}
if (!user.Password.Contains("$"))
{
string hash = user.Password;
user.Password = string.Format("$SHA1${0}", hash);
}
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
{
string hash = user.EasyPassword;
user.EasyPassword = string.Format("$SHA1${0}", hash);
}
}
public Task<bool> HasPassword(User user)
{
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
return Task.FromResult(hasConfiguredPassword);
}
private bool IsPasswordEmpty(User user, string passwordHash)
private bool IsPasswordEmpty(User user, string password)
{
return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
}
public Task ChangePassword(User user, string newPassword)
{
string newPasswordHash = null;
if (newPassword != null)
ConvertPasswordFormat(user);
// This is needed to support changing a no password user to a password user
if (string.IsNullOrEmpty(user.Password))
{
newPasswordHash = GetHashedString(user, newPassword);
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
user.Password = newPasswordHash.ToString();
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
PasswordHash passwordHash = new PasswordHash(user.Password);
if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
{
throw new ArgumentNullException(nameof(newPasswordHash));
passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
}
else if (newPassword != null)
{
passwordHash.Hash = GetHashedString(user, newPassword);
}
user.Password = newPasswordHash;
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
{
throw new ArgumentNullException(nameof(passwordHash.Hash));
}
user.Password = passwordHash.ToString();
return Task.CompletedTask;
}
public string GetPasswordHash(User user)
{
return string.IsNullOrEmpty(user.Password)
? GetEmptyHashedString(user)
: user.Password;
return user.Password;
}
public string GetEmptyHashedString(User user)
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
{
return GetHashedString(user, string.Empty);
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
}
/// <summary>
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public string GetHashedString(User user, string str)
{
var salt = user.Salt;
if (salt != null)
PasswordHash passwordHash;
if (string.IsNullOrEmpty(user.Password))
{
// return BCrypt.HashPassword(str, salt);
passwordHash = new PasswordHash(_cryptographyProvider);
}
else
{
ConvertPasswordFormat(user);
passwordHash = new PasswordHash(user.Password);
}
// legacy
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
if (passwordHash.SaltBytes != null)
{
// the password is modern format with PBKDF and we should take advantage of that
passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
}
else
{
// the password has no salt and should be called with the older method for safety
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
}
}
}
}

View File

@ -278,6 +278,7 @@ namespace Emby.Server.Implementations.Library
{
throw new ArgumentNullException(nameof(item));
}
if (item is IItemByName)
{
if (!(item is MusicArtist))
@ -285,18 +286,7 @@ namespace Emby.Server.Implementations.Library
return;
}
}
else if (item.IsFolder)
{
//if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder))
//{
// if (item.SourceType != SourceType.Library)
// {
// return;
// }
//}
}
else
else if (!item.IsFolder)
{
if (!(item is Video) && !(item is LiveTvChannel))
{
@ -371,19 +361,20 @@ namespace Emby.Server.Implementations.Library
foreach (var metadataPath in GetMetadataPaths(item, children))
{
_logger.LogDebug("Deleting path {0}", metadataPath);
if (!Directory.Exists(metadataPath))
{
continue;
}
_logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
try
{
Directory.Delete(metadataPath, true);
}
catch (IOException)
{
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting {metadataPath}", metadataPath);
_logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
}
}

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Events;
@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
}
}
public bool IsValidUsername(string username)
public static bool IsValidUsername(string username)
{
// Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
foreach (var currentChar in username)
{
if (!IsValidUsernameCharacter(currentChar))
{
return false;
}
}
return true;
//This is some regex that matches only on unicode "word" characters, as well as -, _ and @
//In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
return Regex.IsMatch(username, "^[\\w-'._@]*$");
}
private static bool IsValidUsernameCharacter(char i)
{
return !char.Equals(i, '<') && !char.Equals(i, '>');
return IsValidUsername(i.ToString());
}
public string MakeValidUsername(string username)
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
private string GetLocalPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
? _defaultAuthenticationProvider.GetEmptyHashedString(user)
? null
: user.EasyPassword;
}
private bool IsPasswordEmpty(User user, string passwordHash)
{
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Loads the users from the repository
/// </summary>
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user));
}
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
hasConfiguredEasyPassword :
hasConfiguredPassword;
var dto = new UserDto
UserDto dto = new UserDto
{
Id = user.Id,
Name = user.Name,
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
dto.EnableAutoLogin = true;
}
var image = user.GetImageInfo(ImageType.Primary, 0);
ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
if (image != null)
{
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
if (!IsValidUsername(name))
{
throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))

View File

@ -33,7 +33,6 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IProviderManager _providerManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IProcessFactory _processFactory;
private readonly IAssemblyInfo _assemblyInfo;
private IMediaSourceManager _mediaSourceManager;
public static EmbyTV Current;
@ -74,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public EmbyTV(IServerApplicationHost appHost,
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
IAssemblyInfo assemblyInfo,
ILogger logger,
IJsonSerializer jsonSerializer,
IHttpClient httpClient,
@ -101,12 +98,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_processFactory = processFactory;
_liveTvManager = (LiveTvManager)liveTvManager;
_jsonSerializer = jsonSerializer;
_assemblyInfo = assemblyInfo;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"), _logger);
_timerProvider.TimerFired += _timerProvider_TimerFired;
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@ -32,32 +31,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (_items == null)
{
if (!File.Exists(_dataPath))
{
return new List<T>();
}
Logger.LogInformation("Loading live tv data from {0}", _dataPath);
_items = GetItemsFromFile(_dataPath);
}
return _items.ToList();
}
}
private List<T> GetItemsFromFile(string path)
{
var jsonFile = path + ".json";
if (!File.Exists(jsonFile))
{
return new List<T>();
}
try
{
return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile) ?? new List<T>();
}
catch (IOException)
{
return _jsonSerializer.DeserializeFromFile<List<T>>(path);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error deserializing {jsonFile}", jsonFile);
Logger.LogError(ex, "Error deserializing {Path}", path);
}
return new List<T>();
@ -70,12 +65,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(newList));
}
var file = _dataPath + ".json";
Directory.CreateDirectory(Path.GetDirectoryName(file));
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
lock (_fileDataLock)
{
_jsonSerializer.SerializeToFile(newList, file);
_jsonSerializer.SerializeToFile(newList, _dataPath);
_items = newList;
}
}

View File

@ -17,6 +17,7 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.LiveTv.Listings
{
@ -638,7 +639,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
#if NETSTANDARD2_0
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
options.RequestHeaders["Accept-Encoding"] = "deflate";
options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate";
}
#endif
@ -676,7 +677,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
#if NETSTANDARD2_0
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
options.RequestHeaders["Accept-Encoding"] = "deflate";
options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate";
}
#endif

View File

@ -19,6 +19,7 @@ using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (protocol == MediaProtocol.Http)
{
// Use user-defined user-agent. If there isn't one, make it look like a browser.
httpHeaders["User-Agent"] = string.IsNullOrWhiteSpace(info.UserAgent) ?
httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ?
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" :
info.UserAgent;
}

View File

@ -1,97 +1,97 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"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",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "Favorite Shows",
"HeaderFavoriteSongs": "Favorite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "Ip address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
"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": "Mixed content",
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music videos",
"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",
"Photos": "Photos",
"Playlists": "Playlists",
"Albums": "Álbumes",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas de álbumes",
"HeaderCameraUploads": "Subidas de cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "Continuar Viendo",
"HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"Latest": "Últimos",
"MessageApplicationUpdated": "El servidor Jellyfin fue actualizado",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Fue actualizada la sección {0} de la configuración del servidor",
"MessageServerConfigurationUpdated": "Fue actualizada la configuración del servidor",
"MixedContent": "Contenido mixto",
"Movies": "Películas",
"Music": "Música",
"MusicVideos": "Videos musicales",
"NameInstallFailed": "{0} error de instalación",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconocida",
"NewVersionIsAvailable": "Disponible una nueva versión de Jellyfin para descargar.",
"NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
"NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
"NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada",
"NotificationOptionInstallationFailed": "Error de instalación",
"NotificationOptionNewLibraryContent": "Nuevo contenido añadido",
"NotificationOptionPluginError": "Error en plugin",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginUpdateInstalled": "Actualización del complemento instalada",
"NotificationOptionServerRestartRequired": "Se requiere reinicio del servidor",
"NotificationOptionTaskFailed": "Error de tarea programada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
"NotificationOptionVideoPlayback": "Se inició la reproducción de video",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"Photos": "Fotos",
"Playlists": "Listas de reproducción",
"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",
"PluginInstalledWithName": "{0} fue instalado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginUpdatedWithName": "{0} fue actualizado",
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series",
"Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
"Sync": "Sync",
"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": "Special - {0}",
"VersionNumber": "Version {0}"
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
"SubtitlesDownloadedForItem": "Descargar subtítulos para {0}",
"Sync": "Sincronizar",
"System": "Sistema",
"TvShows": "Series de TV",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
"UserOfflineFromDevice": "{0} se ha desconectado de {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}"
}

View File

@ -1,97 +1,97 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"AppDeviceValues": "Application : {0}, Appareil : {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
"Books": "Livres",
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
"FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderAlbumArtists": "Artistes de l'album",
"HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "Favorite Shows",
"HeaderFavoriteSongs": "Favorite Songs",
"HeaderLiveTV": "Live TV",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
"HeaderFavoriteSongs": "Chansons favorites",
"HeaderLiveTV": "TV en direct",
"HeaderNextUp": "À Suivre",
"HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "Ip address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
"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": "Mixed content",
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music videos",
"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",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hériter",
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
"MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
"MusicVideos": "Vidéos musicales",
"NameInstallFailed": "{0} échec d'installation",
"NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue",
"NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.",
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
"NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
"NotificationOptionInstallationFailed": "Échec d'installation",
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
"NotificationOptionPluginError": "Erreur d'extension",
"NotificationOptionPluginInstalled": "Extension installée",
"NotificationOptionPluginUninstalled": "Extension désinstallée",
"NotificationOptionPluginUpdateInstalled": "Mise à jour d'extension installée",
"NotificationOptionServerRestartRequired": "Un redémarrage du serveur est requis",
"NotificationOptionTaskFailed": "Échec de tâche planifiée",
"NotificationOptionUserLockedOut": "Utilisateur verrouillé",
"NotificationOptionVideoPlayback": "Lecture vidéo démarrée",
"NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée",
"Photos": "Photos",
"Playlists": "Playlists",
"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": "Series",
"Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"Playlists": "Listes de lecture",
"Plugin": "Extension",
"PluginInstalledWithName": "{0} a été installé",
"PluginUninstalledWithName": "{0} a été désinstallé",
"PluginUpdatedWithName": "{0} a été mis à jour",
"ProviderValue": "Fournisseur : {0}",
"ScheduledTaskFailedWithName": "{0} a échoué",
"ScheduledTaskStartedWithName": "{0} a commencé",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Émissions",
"Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
"Sync": "Sync",
"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",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"SubtitlesDownloadedForItem": "Les sous-titres de {0} ont été téléchargés",
"Sync": "Synchroniser",
"System": "Système",
"TvShows": "Séries Télé",
"User": "Utilisateur",
"UserCreatedWithName": "L'utilisateur {0} a été créé",
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}"
}

View File

@ -44,7 +44,7 @@
"NameInstallFailed": "{0} échec d'installation",
"NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue",
"NewVersionIsAvailable": "Une nouvelle version d'Jellyfin Serveur est disponible au téléchargement.",
"NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.",
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
@ -89,7 +89,7 @@
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
"UserStartedPlayingItemWithValues": "{0} est entrain de lire {1} sur {2}",
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie",
"ValueSpecialEpisodeName": "Spécial - {0}",

View File

@ -1,8 +1,8 @@
{
"Albums": "Albums",
"Albums": "אלבומים",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"Application": "אפליקציה",
"Artists": "אמנים",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "ספרים",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",

View File

@ -34,17 +34,17 @@
"LabelRunningTimeValue": "Durata: {0}",
"Latest": "Più recenti",
"MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto",
"Movies": "Film",
"Music": "Musica",
"MusicVideos": "Video musicali",
"NameInstallFailed": "{0} installation failed",
"NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuto",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
"NotificationOptionApplicationUpdateAvailable": "Aggiornamento dell'applicazione disponibile",
"NotificationOptionApplicationUpdateInstalled": "Aggiornamento dell'applicazione installato",
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
@ -70,12 +70,12 @@
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviati",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Programmi",
"Songs": "Canzoni",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
"SubtitlesDownloadedForItem": "Sottotitoli scaricati per {0}",
"Sync": "Sincronizza",
"System": "Sistema",
@ -91,7 +91,7 @@
"UserPolicyUpdatedWithName": "La politica dell'utente è stata aggiornata per {0}",
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
"ValueSpecialEpisodeName": "Speciale - {0}",
"VersionNumber": "Versione {0}"
}

View File

@ -3,15 +3,15 @@
"AppDeviceValues": "Qoldanba: {0}, Qurylǵy: {1}",
"Application": "Qoldanba",
"Artists": "Oryndaýshylar",
"AuthenticationSucceededWithUserName": "{0} túpnusqalyǵyn rastalýy sátti",
"AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
"Books": "Kitaptar",
"CameraImageUploadedFrom": "Jańa sýret {0} kamerasynan júktep alyndy",
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy",
"Channels": "Arnalar",
"ChapterNameValue": "{0}-sahna",
"Collections": "Jıyntyqtar",
"DeviceOfflineWithName": "{0} ajyratylǵan",
"DeviceOnlineWithName": "{0} qosylǵan",
"FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz",
"FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz aıaqtaldy",
"Favorites": "Tańdaýlylar",
"Folders": "Qaltalar",
"Genres": "Janrlar",
@ -28,13 +28,13 @@
"HeaderRecordingGroups": "Jazba toptary",
"HomeVideos": ılik beıneler",
"Inherit": "Muraǵa ıelený",
"ItemAddedWithName": "{0} tasyǵyshhanaǵa ústelindi",
"ItemAddedWithName": "{0} tasyǵyshhanaǵa ústeldi",
"ItemRemovedWithName": "{0} tasyǵyshhanadan alastaldy",
"LabelIpAddressValue": "IP-mekenjaıy: {0}",
"LabelRunningTimeValue": "Oınatý ýaqyty: {0}",
"Latest": "Eń keıingi",
"MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} deńgeıge jańartyldy",
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
"MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy",
"MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy",
"MixedContent": "Aralas mazmun",

View File

@ -5,7 +5,7 @@
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"CameraImageUploadedFrom": "Uma nova imagem da câmera foi submetida de {0}",
"Channels": "Canais",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Coletâneas",
@ -30,21 +30,21 @@
"Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
"LabelIpAddressValue": "Endereço ip: {0}",
"LabelIpAddressValue": "Endereço IP: {0}",
"LabelRunningTimeValue": "Tempo de execução: {0}",
"Latest": "Recente",
"MessageApplicationUpdated": "O servidor Jellyfin foi atualizado",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageApplicationUpdatedTo": "O Servidor Jellyfin foi atualizado para {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "A seção {0} da configuração do servidor foi atualizada",
"MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
"MixedContent": "Conteúdo misto",
"Movies": "Filmes",
"Music": "Música",
"MusicVideos": "Vídeos musicais",
"NameInstallFailed": "{0} installation failed",
"NameInstallFailed": "A instalação de {0} falhou",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconhecida",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicativo disponível",
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicativo instalada",
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
@ -70,12 +70,12 @@
"ProviderValue": "Provedor: {0}",
"ScheduledTaskFailedWithName": "{0} falhou",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"ServerNameNeedsToBeRestarted": "O servidor {0} precisa ser reiniciado",
"Shows": "Séries",
"Songs": "Músicas",
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor tente novamente em breve.",
"SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}",
"SubtitlesDownloadedForItem": "Legendas baixadas para {0}",
"Sync": "Sincronizar",
"System": "Sistema",
@ -91,7 +91,7 @@
"UserPolicyUpdatedWithName": "A política de usuário foi atualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} iniciou a reprodução de {1}",
"UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueHasBeenAddedToLibrary": "{0} foi adicionado a sua biblioteca",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versão {0}"
}

View File

@ -1,62 +1,62 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"Albums": "Albumi",
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
"Application": "Aplikacija",
"Artists": "Izvajalci",
"AuthenticationSucceededWithUserName": "{0} preverjanje uspešno",
"Books": "Knjige",
"CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "Favorite Shows",
"HeaderFavoriteSongs": "Favorite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "Ip address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
"MessageApplicationUpdated": "Jellyfin Server has been updated",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeni",
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
"HeaderCameraUploads": "Posnetki kamere",
"HeaderContinueWatching": "Nadaljuj gledanje",
"HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Priljubljene epizode",
"HeaderFavoriteShows": "Priljubljene serije",
"HeaderFavoriteSongs": "Priljubljene pesmi",
"HeaderLiveTV": "TV v živo",
"HeaderNextUp": "Sledi",
"HeaderRecordingGroups": "Zbirke posnetkov",
"HomeVideos": "Domači posnetki",
"Inherit": "Podeduj",
"ItemAddedWithName": "{0} je dodan v knjižnico",
"ItemRemovedWithName": "{0} je bil odstranjen iz knjižnice",
"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",
"MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music videos",
"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",
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
"MixedContent": "Razne vsebine",
"Movies": "Filmi",
"Music": "Glasba",
"MusicVideos": "Glasbeni posnetki",
"NameInstallFailed": "{0} namestitev neuspešna",
"NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Season neznana",
"NewVersionIsAvailable": "Nova razničica Jellyfin strežnika je na voljo za prenos.",
"NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
"NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
"NotificationOptionAudioPlayback": "Predvajanje zvoka začeto",
"NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno",
"NotificationOptionCameraImageUploaded": "Posnetek kamere naložen",
"NotificationOptionInstallationFailed": "Napaka pri nameščanju",
"NotificationOptionNewLibraryContent": "Nove vsebine dodane",
"NotificationOptionPluginError": "Napaka dodatka",
"NotificationOptionPluginInstalled": "Dodatek nameščen",
"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",

View File

@ -1,12 +1,12 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"Albums": "Albümler",
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
"Application": "Uygulama",
"Artists": "Sanatçılar",
"AuthenticationSucceededWithUserName": "{0} başarı ile giriş yaptı",
"Books": "Kitaplar",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"Channels": "Kanallar",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} has disconnected",
@ -17,8 +17,8 @@
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "Favori Showlar",
@ -30,21 +30,21 @@
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "Ip address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"LabelIpAddressValue": "Ip adresi: {0}",
"LabelRunningTimeValue": "Çalışma süresi: {0}",
"Latest": "Latest",
"MessageApplicationUpdated": "Jellyfin Server has been updated",
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
"MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
"Music": "Müzik",
"MusicVideos": "Müzik videoları",
"NameInstallFailed": "{0} kurulum başarısız",
"NameSeasonNumber": "Sezon {0}",
"NameSeasonUnknown": "Bilinmeyen Sezon",
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir versiyonu indirmek için hazır.",
"NotificationOptionApplicationUpdateAvailable": "Application update available",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started",

View File

@ -34,14 +34,14 @@
"LabelRunningTimeValue": "运行时间:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 服务器已更新",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageApplicationUpdatedTo": "Jellyfin Server 的版本已更新为 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "服务器配置 {0} 部分已更新",
"MessageServerConfigurationUpdated": "服务器配置已更新",
"MixedContent": "混合内容",
"Movies": "电影",
"Music": "音乐",
"MusicVideos": "音乐视频",
"NameInstallFailed": "{0} installation failed",
"NameInstallFailed": "{0} 安装失败",
"NameSeasonNumber": "季 {0}",
"NameSeasonUnknown": "未知季",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
@ -70,7 +70,7 @@
"ProviderValue": "提供商:{0}",
"ScheduledTaskFailedWithName": "{0} 已失败",
"ScheduledTaskStartedWithName": "{0} 已开始",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"ServerNameNeedsToBeRestarted": "{0} 需要重新启动",
"Shows": "节目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 服务器加载中。请稍后再试。",

View File

@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization
{
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
Directory.CreateDirectory(LocalizationPath);
var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName);
// Extract from the assembly
foreach (var resource in _assembly.GetManifestResourceNames())
{
@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization
continue;
}
string filename = "ratings-" + resource.Substring(ratingsResource.Length);
string countryCode = resource.Substring(ratingsResource.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
if (existingFiles.Contains(filename))
using (var str = _assembly.GetManifestResourceStream(resource))
using (var reader = new StreamReader(str))
{
continue;
}
using (var stream = _assembly.GetManifestResourceStream(resource))
{
string target = Path.Combine(LocalizationPath, filename);
_logger.LogInformation("Extracting ratings to {0}", target);
using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
await stream.CopyToAsync(fs);
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
string[] parts = line.Split(',');
if (parts.Length == 2
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
{
dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
}
#if DEBUG
else
{
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
#endif
}
}
}
foreach (var file in GetRatingsFiles(LocalizationPath))
{
await LoadRatings(file);
_allParentalRatings[countryCode] = dict;
}
LoadAdditionalRatings();
await LoadCultures();
}
private void LoadAdditionalRatings()
{
LoadRatings("au", new[]
{
new ParentalRating("AU-G", 1),
new ParentalRating("AU-PG", 5),
new ParentalRating("AU-M", 6),
new ParentalRating("AU-MA15+", 7),
new ParentalRating("AU-M15+", 8),
new ParentalRating("AU-R18+", 9),
new ParentalRating("AU-X18+", 10),
new ParentalRating("AU-RC", 11)
});
LoadRatings("be", new[]
{
new ParentalRating("BE-AL", 1),
new ParentalRating("BE-MG6", 2),
new ParentalRating("BE-6", 3),
new ParentalRating("BE-9", 5),
new ParentalRating("BE-12", 6),
new ParentalRating("BE-16", 8)
});
LoadRatings("de", new[]
{
new ParentalRating("DE-0", 1),
new ParentalRating("FSK-0", 1),
new ParentalRating("DE-6", 5),
new ParentalRating("FSK-6", 5),
new ParentalRating("DE-12", 7),
new ParentalRating("FSK-12", 7),
new ParentalRating("DE-16", 8),
new ParentalRating("FSK-16", 8),
new ParentalRating("DE-18", 9),
new ParentalRating("FSK-18", 9)
});
LoadRatings("ru", new[]
{
new ParentalRating("RU-0+", 1),
new ParentalRating("RU-6+", 3),
new ParentalRating("RU-12+", 7),
new ParentalRating("RU-16+", 9),
new ParentalRating("RU-18+", 10)
});
}
private void LoadRatings(string country, ParentalRating[] ratings)
{
_allParentalRatings[country] = ratings.ToDictionary(i => i.Name);
}
private IEnumerable<string> GetRatingsFiles(string directory)
=> _fileSystem.GetFilePaths(directory, false)
.Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase))
.Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase));
/// <summary>
/// Gets the localization path.
/// </summary>
/// <value>The localization path.</value>
public string LocalizationPath
=> Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization");
public string NormalizeFormKD(string text)
=> text.Normalize(NormalizationForm.FormKD);
@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization
return value;
}
/// <summary>
/// Loads the ratings.
/// </summary>
/// <param name="file">The file.</param>
/// <returns>Dictionary{System.StringParentalRating}.</returns>
private async Task LoadRatings(string file)
{
Dictionary<string, ParentalRating> dict
= new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
using (var str = File.OpenRead(file))
using (var reader = new StreamReader(str))
{
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
string[] parts = line.Split(',');
if (parts.Length == 2
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
{
dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value }));
}
#if DEBUG
else
{
_logger.LogWarning("Misformed line in {Path}", file);
}
#endif
}
}
var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1];
_allParentalRatings[countryCode] = dict;
}
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
/// <summary>

View File

@ -0,0 +1,8 @@
AU-G,1
AU-PG,5
AU-M,6
AU-MA15+,7
AU-M15+,8
AU-R18+,9
AU-X18+,10
AU-RC,11
1 AU-G 1
2 AU-PG 5
3 AU-M 6
4 AU-MA15+ 7
5 AU-M15+ 8
6 AU-R18+ 9
7 AU-X18+ 10
8 AU-RC 11

View File

@ -0,0 +1,6 @@
BE-AL,1
BE-MG6,2
BE-6,3
BE-9,5
BE-12,6
BE-16,8
1 BE-AL 1
2 BE-MG6 2
3 BE-6 3
4 BE-9 5
5 BE-12 6
6 BE-16 8

View File

@ -0,0 +1,10 @@
DE-0,1
FSK-0,1
DE-6,5
FSK-6,5
DE-12,7
FSK-12,7
DE-16,8
FSK-16,8
DE-18,9
FSK-18,9
1 DE-0 1
2 FSK-0 1
3 DE-6 5
4 FSK-6 5
5 DE-12 7
6 FSK-12 7
7 DE-16 8
8 FSK-16 8
9 DE-18 9
10 FSK-18 9

View File

@ -0,0 +1,5 @@
RU-0+,1
RU-6+,3
RU-12+,7
RU-16+,9
RU-18+,10
1 RU-0+ 1
2 RU-6+ 3
3 RU-12+ 7
4 RU-16+ 9
5 RU-18+ 10

View File

@ -202,6 +202,10 @@ namespace Emby.Server.Implementations.MediaEncoder
private static List<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
{
var path = GetChapterImagesPath(video);
if (!Directory.Exists(path))
{
return new List<string>();
}
try
{

View File

@ -0,0 +1,39 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
namespace Emby.Server.Implementations.Middleware
{
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<WebSocketMiddleware> _logger;
private readonly WebSocketManager _webSocketManager;
public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
{
_next = next;
_logger = logger;
_webSocketManager = webSocketManager;
}
public async Task Invoke(HttpContext httpContext)
{
_logger.LogInformation("Handling request: " + httpContext.Request.Path);
if (httpContext.WebSockets.IsWebSocketRequest)
{
var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
if (webSocketContext != null)
{
await _webSocketManager.OnWebSocketConnected(webSocketContext);
}
}
else
{
await _next.Invoke(httpContext);
}
}
}
}

View File

@ -45,9 +45,4 @@ namespace Emby.Server.Implementations.Net
/// <returns>Task.</returns>
Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
}
public interface IMemoryWebSocket
{
Action<Memory<byte>, int> OnReceiveMemoryBytes { get; set; }
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Net.WebSockets;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.Net
{
@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Net
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
public QueryParamCollection QueryString { get; set; }
public IQueryCollection QueryString { get; set; }
/// <summary>
/// Gets or sets the web socket.
/// </summary>

View File

@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Playlists
{
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var playlist = (Playlist)item;
@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Playlists
_libraryManager = libraryManager;
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
@ -89,7 +89,6 @@ namespace Emby.Server.Implementations.Playlists
Recursive = true,
ImageTypes = new[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
});
}
}
@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.Playlists
_libraryManager = libraryManager;
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
@ -116,11 +115,5 @@ namespace Emby.Server.Implementations.Playlists
DtoOptions = new DtoOptions(false)
});
}
//protected override Task<string> CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
//{
// return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
//}
}
}

View File

@ -1,25 +0,0 @@
using System;
using System.IO;
using System.Reflection;
using MediaBrowser.Model.Reflection;
namespace Emby.Server.Implementations.Reflection
{
public class AssemblyInfo : IAssemblyInfo
{
public Stream GetManifestResourceStream(Type type, string resource)
{
return type.Assembly.GetManifestResourceStream(resource);
}
public string[] GetManifestResourceNames(Type type)
{
return type.Assembly.GetManifestResourceNames();
}
public Assembly[] GetCurrentAssemblies()
{
return AppDomain.CurrentDomain.GetAssemblies();
}
}
}

View File

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

View File

@ -20,8 +20,6 @@ namespace Emby.Server.Implementations.Services
{
response.StatusCode = (int)HttpStatusCode.NoContent;
}
response.SetContentLength(0);
return Task.CompletedTask;
}
@ -55,7 +53,6 @@ namespace Emby.Server.Implementations.Services
{
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
response.SetContentLength(long.Parse(responseHeaders.Value));
continue;
}
@ -104,7 +101,6 @@ namespace Emby.Server.Implementations.Services
if (bytes != null)
{
response.ContentType = "application/octet-stream";
response.SetContentLength(bytes.Length);
if (bytes.Length > 0)
{
@ -117,7 +113,6 @@ namespace Emby.Server.Implementations.Services
if (responseText != null)
{
bytes = Encoding.UTF8.GetBytes(responseText);
response.SetContentLength(bytes.Length);
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
@ -149,8 +144,6 @@ namespace Emby.Server.Implementations.Services
var contentLength = ms.Length;
response.SetContentLength(contentLength);
if (contentLength > 0)
{
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);

View File

@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Services
foreach (var requestFilter in actionContext.RequestFilters)
{
requestFilter.RequestFilter(request, request.Response, requestDto);
if (request.Response.IsClosed)
if (request.Response.OriginalResponse.HasStarted)
{
Task.FromResult<object>(null);
}

View File

@ -154,7 +154,7 @@ namespace Emby.Server.Implementations.Services
{
if (name == null) continue; //thank you ASP.NET
var values = request.QueryString.GetValues(name);
var values = request.QueryString[name];
if (values.Count == 1)
{
map[name] = values[0];

View File

@ -5,6 +5,7 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Session
}
}
private SessionInfo GetSession(QueryParamCollection queryString, string remoteEndpoint)
private SessionInfo GetSession(IQueryCollection queryString, string remoteEndpoint)
{
if (queryString == null)
{

View File

@ -1,7 +1,7 @@
using System.IO;
using MediaBrowser.Model.Services;
namespace Jellyfin.Server.SocketSharp
namespace Emby.Server.Implementations.SocketSharp
{
public class HttpFile : IHttpFile
{

View File

@ -63,6 +63,28 @@ public sealed class HttpPostedFile : IDisposable
_position = offset;
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _end - _offset;
public override long Position
{
get => _position - _offset;
set
{
if (value > Length)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_position = Seek(value, SeekOrigin.Begin);
}
}
public override void Flush()
{
}
@ -178,27 +200,5 @@ public sealed class HttpPostedFile : IDisposable
{
throw new NotSupportedException();
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _end - _offset;
public override long Position
{
get => _position - _offset;
set
{
if (value > Length)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_position = Seek(value, SeekOrigin.Begin);
}
}
}
}

View File

@ -6,14 +6,16 @@ using System.Net;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Server.SocketSharp
namespace Emby.Server.Implementations.SocketSharp
{
public partial class WebSocketSharpRequest : IHttpRequest
{
internal static string GetParameter(ReadOnlySpan<char> header, string attr)
{
int ap = header.IndexOf(attr, StringComparison.Ordinal);
int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal);
if (ap == -1)
{
return null;
@ -43,7 +45,7 @@ namespace Jellyfin.Server.SocketSharp
private async Task LoadMultiPart(WebROCollection form)
{
string boundary = GetParameter(ContentType, "; boundary=");
string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
if (boundary == null)
{
return;
@ -77,7 +79,7 @@ namespace Jellyfin.Server.SocketSharp
byte[] copy = new byte[e.Length];
input.Position = e.Start;
input.Read(copy, 0, (int)e.Length);
await input.ReadAsync(copy, 0, (int)e.Length).ConfigureAwait(false);
form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length));
}
@ -96,23 +98,15 @@ namespace Jellyfin.Server.SocketSharp
var form = new WebROCollection();
files = new Dictionary<string, HttpPostedFile>();
if (IsContentType("multipart/form-data", true))
if (IsContentType("multipart/form-data"))
{
await LoadMultiPart(form).ConfigureAwait(false);
}
else if (IsContentType("application/x-www-form-urlencoded", true))
else if (IsContentType("application/x-www-form-urlencoded"))
{
await LoadWwwForm(form).ConfigureAwait(false);
}
#if NET_4_0
if (validateRequestNewMode && !checked_form) {
// Setting this before calling the validator prevents
// possible endless recursion
checked_form = true;
ValidateNameValueCollection("Form", query_string_nvc, RequestValidationSource.Form);
} else
#endif
if (validate_form && !checked_form)
{
checked_form = true;
@ -122,15 +116,11 @@ namespace Jellyfin.Server.SocketSharp
return form;
}
public string Accept => string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"];
public string Accept => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Accept]) ? null : request.Headers[HeaderNames.Accept].ToString();
public string Authorization => string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"];
public string Authorization => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]) ? null : request.Headers[HeaderNames.Authorization].ToString();
protected bool validate_cookies { get; set; }
protected bool validate_query_string { get; set; }
protected bool validate_form { get; set; }
protected bool checked_cookies { get; set; }
protected bool checked_query_string { get; set; }
protected bool checked_form { get; set; }
private static void ThrowValidationException(string name, string key, string value)
@ -210,26 +200,14 @@ namespace Jellyfin.Server.SocketSharp
return false;
}
public void ValidateInput()
private bool IsContentType(string ct)
{
validate_cookies = true;
validate_query_string = true;
validate_form = true;
}
private bool IsContentType(string ct, bool starts_with)
{
if (ct == null || ContentType == null)
if (ContentType == null)
{
return false;
}
if (starts_with)
{
return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase);
}
return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase);
return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase);
}
private async Task LoadWwwForm(WebROCollection form)
@ -396,14 +374,14 @@ namespace Jellyfin.Server.SocketSharp
var elem = new Element();
ReadOnlySpan<char> header;
while ((header = ReadHeaders()) != null)
while ((header = ReadHeaders().AsSpan()) != null)
{
if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase))
if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
elem.Name = GetContentDispositionAttribute(header, "name");
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
}
else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
elem.Encoding = GetEncoding(elem.ContentType);
@ -455,7 +433,7 @@ namespace Jellyfin.Server.SocketSharp
private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name)
{
int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
if (idx < 0)
{
return null;
@ -478,7 +456,7 @@ namespace Jellyfin.Server.SocketSharp
private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name)
{
int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
if (idx < 0)
{
return null;

View File

@ -1,11 +1,12 @@
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.SocketSharp
namespace Emby.Server.Implementations.SocketSharp
{
public class SharpWebSocket : IWebSocket
{
@ -20,67 +21,22 @@ namespace Jellyfin.Server.SocketSharp
/// Gets or sets the web socket.
/// </summary>
/// <value>The web socket.</value>
private SocketHttpListener.WebSocket WebSocket { get; set; }
private readonly WebSocket _webSocket;
private TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false;
private bool _disposed;
public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger)
public SharpWebSocket(WebSocket socket, ILogger logger)
{
if (socket == null)
{
throw new ArgumentNullException(nameof(socket));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger;
WebSocket = socket;
socket.OnMessage += OnSocketMessage;
socket.OnClose += OnSocketClose;
socket.OnError += OnSocketError;
}
public Task ConnectAsServerAsync()
=> WebSocket.ConnectAsServer();
public Task StartReceive()
{
return _taskCompletionSource.Task;
}
private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e)
{
_logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty);
// Closed?.Invoke(this, EventArgs.Empty);
}
private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e)
{
_taskCompletionSource.TrySetResult(true);
Closed?.Invoke(this, EventArgs.Empty);
}
private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e)
{
if (OnReceiveBytes != null)
{
OnReceiveBytes(e.RawData);
}
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
}
/// <summary>
/// Gets or sets the state.
/// </summary>
/// <value>The state.</value>
public WebSocketState State => WebSocket.ReadyState;
public WebSocketState State => _webSocket.State;
/// <summary>
/// Sends the async.
@ -91,7 +47,7 @@ namespace Jellyfin.Server.SocketSharp
/// <returns>Task.</returns>
public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
{
return WebSocket.SendAsync(bytes);
return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
}
/// <summary>
@ -103,7 +59,7 @@ namespace Jellyfin.Server.SocketSharp
/// <returns>Task.</returns>
public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
{
return WebSocket.SendAsync(text);
return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
}
/// <summary>
@ -128,13 +84,13 @@ namespace Jellyfin.Server.SocketSharp
if (dispose)
{
WebSocket.OnMessage -= OnSocketMessage;
WebSocket.OnClose -= OnSocketClose;
WebSocket.OnError -= OnSocketError;
_cancellationTokenSource.Cancel();
WebSocket.CloseAsync().GetAwaiter().GetResult();
if (_webSocket.State == WebSocketState.Open)
{
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
CancellationToken.None);
}
Closed?.Invoke(this, EventArgs.Empty);
}
_disposed = true;

View File

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.SocketSharp
{
public class WebSocketSharpListener : IHttpListener
{
private readonly ILogger _logger;
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private CancellationToken _disposeCancellationToken;
public WebSocketSharpListener(
ILogger logger)
{
_logger = logger;
_disposeCancellationToken = _disposeCancellationTokenSource.Token;
}
public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
private static void LogRequest(ILogger logger, HttpRequest request)
{
var url = request.GetDisplayUrl();
logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
}
public async Task ProcessWebSocketRequest(HttpContext ctx)
{
try
{
LogRequest(_logger, ctx.Request);
var endpoint = ctx.Connection.RemoteIpAddress.ToString();
var url = ctx.Request.GetDisplayUrl();
var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
var socket = new SharpWebSocket(webSocketContext, _logger);
WebSocketConnected(new WebSocketConnectEventArgs
{
Url = url,
QueryString = ctx.Request.Query,
WebSocket = socket,
Endpoint = endpoint
});
WebSocketReceiveResult result;
var message = new List<byte>();
do
{
var buffer = WebSocket.CreateServerBuffer(4096);
result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
message.AddRange(buffer.Array.Take(result.Count));
if (result.EndOfMessage)
{
socket.OnReceiveBytes(message.ToArray());
message.Clear();
}
} while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
if (webSocketContext.State == WebSocketState.Open)
{
await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
result.CloseStatusDescription, _disposeCancellationToken);
}
socket.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "AcceptWebSocketAsync error");
if (!ctx.Response.HasStarted)
{
ctx.Response.StatusCode = 500;
}
}
}
public Task Stop()
{
_disposeCancellationTokenSource.Cancel();
return Task.CompletedTask;
}
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed;
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
/// <param name="disposing">Whether or not the managed resources should be disposed</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
Stop().GetAwaiter().GetResult();
}
_disposed = true;
}
}
}

View File

@ -2,59 +2,54 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using SocketHttpListener.Net;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IHttpFile = MediaBrowser.Model.Services.IHttpFile;
using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
using IResponse = MediaBrowser.Model.Services.IResponse;
namespace Jellyfin.Server.SocketSharp
namespace Emby.Server.Implementations.SocketSharp
{
public partial class WebSocketSharpRequest : IHttpRequest
{
private readonly HttpListenerRequest request;
private readonly IHttpResponse response;
private readonly HttpRequest request;
public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger)
public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger)
{
this.OperationName = operationName;
this.request = httpContext.Request;
this.response = new WebSocketSharpResponse(logger, httpContext.Response, this);
this.request = httpContext;
this.Response = new WebSocketSharpResponse(logger, response);
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
}
public HttpListenerRequest HttpRequest => request;
public HttpRequest HttpRequest => request;
public object OriginalRequest => request;
public IResponse Response => response;
public IHttpResponse HttpResponse => response;
public IResponse Response { get; }
public string OperationName { get; set; }
public object Dto { get; set; }
public string RawUrl => request.RawUrl;
public string RawUrl => request.GetEncodedPathAndQuery();
public string AbsoluteUri => request.Url.AbsoluteUri.TrimEnd('/');
public string UserHostAddress => request.UserHostAddress;
public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
public string XForwardedFor
=> string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"];
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString();
public int? XForwardedPort
=> string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
public string XForwardedProtocol => string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"];
public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString();
public string XRealIp => string.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"];
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
private string remoteIp;
public string RemoteIp
@ -66,19 +61,19 @@ namespace Jellyfin.Server.SocketSharp
return remoteIp;
}
var temp = CheckBadChars(XForwardedFor);
var temp = CheckBadChars(XForwardedFor.AsSpan());
if (temp.Length != 0)
{
return remoteIp = temp.ToString();
}
temp = CheckBadChars(XRealIp);
temp = CheckBadChars(XRealIp.AsSpan());
if (temp.Length != 0)
{
return remoteIp = NormalizeIp(temp).ToString();
}
return remoteIp = NormalizeIp(request.RemoteEndPoint?.Address.ToString()).ToString();
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
}
}
@ -156,26 +151,13 @@ namespace Jellyfin.Server.SocketSharp
return name;
}
internal static bool ContainsNonAsciiChars(string token)
{
for (int i = 0; i < token.Length; ++i)
{
if ((token[i] < 0x20) || (token[i] > 0x7e))
{
return true;
}
}
return false;
}
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, StringComparison.OrdinalIgnoreCase);
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0)
{
ip = ip.Slice(srch.Length);
@ -185,9 +167,7 @@ namespace Jellyfin.Server.SocketSharp
return ip;
}
public bool IsSecureConnection => request.IsSecureConnection || XForwardedProtocol == "https";
public string[] AcceptTypes => request.AcceptTypes;
public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
private Dictionary<string, object> items;
public Dictionary<string, object> Items => items ?? (items = new Dictionary<string, object>());
@ -197,13 +177,13 @@ namespace Jellyfin.Server.SocketSharp
{
get =>
responseContentType
?? (responseContentType = GetResponseContentType(this));
?? (responseContentType = GetResponseContentType(HttpRequest));
set => this.responseContentType = value;
}
public const string FormUrlEncoded = "application/x-www-form-urlencoded";
public const string MultiPartFormData = "multipart/form-data";
public static string GetResponseContentType(IRequest httpReq)
public static string GetResponseContentType(HttpRequest httpReq)
{
var specifiedContentType = GetQueryStringContentType(httpReq);
if (!string.IsNullOrEmpty(specifiedContentType))
@ -213,7 +193,7 @@ namespace Jellyfin.Server.SocketSharp
const string serverDefaultContentType = "application/json";
var acceptContentTypes = httpReq.AcceptTypes;
var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
string defaultContentType = null;
if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
{
@ -261,7 +241,7 @@ namespace Jellyfin.Server.SocketSharp
public const string Soap11 = "text/xml; charset=utf-8";
public static bool HasAnyOfContentTypes(IRequest request, params string[] contentTypes)
public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
{
if (contentTypes == null || request.ContentType == null)
{
@ -279,18 +259,18 @@ namespace Jellyfin.Server.SocketSharp
return false;
}
public static bool IsContentType(IRequest request, string contentType)
public static bool IsContentType(HttpRequest request, string contentType)
{
return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
}
private static string GetQueryStringContentType(IRequest httpReq)
private static string GetQueryStringContentType(HttpRequest httpReq)
{
ReadOnlySpan<char> format = httpReq.QueryString["format"];
ReadOnlySpan<char> format = httpReq.Query["format"].ToString().AsSpan();
if (format == null)
{
const int formatMaxLength = 4;
ReadOnlySpan<char> pi = httpReq.PathInfo;
ReadOnlySpan<char> pi = httpReq.Path.ToString().AsSpan();
if (pi == null || pi.Length <= formatMaxLength)
{
return null;
@ -309,11 +289,11 @@ namespace Jellyfin.Server.SocketSharp
}
format = LeftPart(format, '.');
if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
if (format.Contains("json".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return "application/json";
}
else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
else if (format.Contains("xml".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return "application/xml";
}
@ -343,10 +323,10 @@ namespace Jellyfin.Server.SocketSharp
{
var mode = HandlerFactoryPath;
var pos = request.RawUrl.IndexOf('?', StringComparison.Ordinal);
var pos = RawUrl.IndexOf("?", StringComparison.Ordinal);
if (pos != -1)
{
var path = request.RawUrl.Substring(0, pos);
var path = RawUrl.Substring(0, pos);
this.pathInfo = GetPathInfo(
path,
mode,
@ -354,10 +334,10 @@ namespace Jellyfin.Server.SocketSharp
}
else
{
this.pathInfo = request.RawUrl;
this.pathInfo = RawUrl;
}
this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo);
this.pathInfo = WebUtility.UrlDecode(pathInfo);
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
}
@ -421,59 +401,55 @@ namespace Jellyfin.Server.SocketSharp
return null;
}
var path = sbPathInfo.ToString();
return path.Length > 1 ? path.TrimEnd('/') : "/";
return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/";
}
private Dictionary<string, System.Net.Cookie> cookies;
public IDictionary<string, System.Net.Cookie> Cookies
{
get
{
if (cookies == null)
{
cookies = new Dictionary<string, System.Net.Cookie>();
foreach (var cookie in this.request.Cookies)
{
var httpCookie = (System.Net.Cookie)cookie;
cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain);
}
}
public string UserAgent => request.Headers[HeaderNames.UserAgent];
return cookies;
}
}
public IHeaderDictionary Headers => request.Headers;
public string UserAgent => request.UserAgent;
public IQueryCollection QueryString => request.Query;
public QueryParamCollection Headers => request.Headers;
private QueryParamCollection queryString;
public QueryParamCollection QueryString => queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query));
public bool IsLocal => request.IsLocal;
public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString());
private string httpMethod;
public string HttpMethod =>
httpMethod
?? (httpMethod = request.HttpMethod);
?? (httpMethod = request.Method);
public string Verb => HttpMethod;
public string ContentType => request.ContentType;
private Encoding contentEncoding;
public Encoding ContentEncoding
private Encoding ContentEncoding
{
get => contentEncoding ?? request.ContentEncoding;
set => contentEncoding = value;
get
{
// TODO is this necessary?
if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP"))
{
string postDataCharset = Headers["x-up-devcap-post-charset"];
if (!string.IsNullOrEmpty(postDataCharset))
{
try
{
return Encoding.GetEncoding(postDataCharset);
}
catch (ArgumentException)
{
}
}
}
return request.GetTypedHeaders().ContentType.Encoding ?? Encoding.UTF8;
}
}
public Uri UrlReferrer => request.UrlReferrer;
public Uri UrlReferrer => request.GetTypedHeaders().Referer;
public static Encoding GetEncoding(string contentTypeHeader)
{
var param = GetParameter(contentTypeHeader, "charset=");
var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
if (param == null)
{
return null;
@ -489,9 +465,9 @@ namespace Jellyfin.Server.SocketSharp
}
}
public Stream InputStream => request.InputStream;
public Stream InputStream => request.Body;
public long ContentLength => request.ContentLength64;
public long ContentLength => request.ContentLength ?? 0;
private IHttpFile[] httpFiles;
public IHttpFile[] Files
@ -530,13 +506,13 @@ namespace Jellyfin.Server.SocketSharp
if (handlerPath != null)
{
var trimmed = pathInfo.AsSpan().TrimStart('/');
if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return trimmed.Slice(handlerPath.Length).ToString();
return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
}
}
return pathInfo;
return pathInfo.AsSpan();
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using IRequest = MediaBrowser.Model.Services.IRequest;
namespace Emby.Server.Implementations.SocketSharp
{
public class WebSocketSharpResponse : IResponse
{
private readonly ILogger _logger;
public WebSocketSharpResponse(ILogger logger, HttpResponse response)
{
_logger = logger;
OriginalResponse = response;
}
public HttpResponse OriginalResponse { get; }
public int StatusCode
{
get => OriginalResponse.StatusCode;
set => OriginalResponse.StatusCode = value;
}
public string StatusDescription { get; set; }
public string ContentType
{
get => OriginalResponse.ContentType;
set => OriginalResponse.ContentType = value;
}
public void AddHeader(string name, string value)
{
if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
{
ContentType = value;
return;
}
OriginalResponse.Headers.Add(name, value);
}
public void Redirect(string url)
{
OriginalResponse.Redirect(url);
}
public Stream OutputStream => OriginalResponse.Body;
public bool SendChunked { get; set; }
const int StreamCopyToBufferSize = 81920;
public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken)
{
var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
//if (count <= 0)
//{
// allowAsync = true;
//}
var fileOpenOptions = FileOpenOptions.SequentialScan;
if (allowAsync)
{
fileOpenOptions |= FileOpenOptions.Asynchronous;
}
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions))
{
if (offset > 0)
{
fs.Position = offset;
}
if (count > 0)
{
await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false);
}
else
{
await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false);
}
}
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
@ -20,7 +19,7 @@ namespace Emby.Server.Implementations.UserViews
{
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var view = (CollectionFolder)item;
var viewType = view.CollectionType;
@ -56,7 +55,7 @@ namespace Emby.Server.Implementations.UserViews
includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" };
}
var recursive = !new[] { CollectionType.Playlists }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
return view.GetItemList(new InternalItemsQuery
{
@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.UserViews
},
IncludeItemTypes = includeItemTypes
}).ToList();
});
}
protected override bool Supports(BaseItem item)
@ -79,7 +78,7 @@ namespace Emby.Server.Implementations.UserViews
return item is CollectionFolder;
}
protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png");

View File

@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.UserViews
_libraryManager = libraryManager;
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var view = (UserView)item;
@ -46,8 +46,7 @@ namespace Emby.Server.Implementations.UserViews
var items = result.Select(i =>
{
var episode = i as Episode;
if (episode != null)
if (i is Episode episode)
{
var series = episode.Series;
if (series != null)
@ -58,8 +57,7 @@ namespace Emby.Server.Implementations.UserViews
return episode;
}
var season = i as Season;
if (season != null)
if (i is Season season)
{
var series = season.Series;
if (series != null)
@ -70,8 +68,7 @@ namespace Emby.Server.Implementations.UserViews
return season;
}
var audio = i as Audio;
if (audio != null)
if (i is Audio audio)
{
var album = audio.AlbumEntity;
if (album != null && album.HasImage(ImageType.Primary))
@ -122,7 +119,7 @@ namespace Emby.Server.Implementations.UserViews
return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty);
}
protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
if (itemsWithImages.Count == 0)
{

View File

@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.UserViews
_libraryManager = libraryManager;
}
protected override List<BaseItem> GetItemsWithImages(BaseItem item)
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.UserViews
});
}
protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.WebSockets
{
public interface IWebSocketHandler
{
Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using UtfUnknown;
namespace Emby.Server.Implementations.WebSockets
{
public class WebSocketManager
{
private readonly IWebSocketHandler[] _webSocketHandlers;
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<WebSocketManager> _logger;
private const int BufferSize = 4096;
public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
{
_webSocketHandlers = webSocketHandlers;
_jsonSerializer = jsonSerializer;
_logger = logger;
}
public async Task OnWebSocketConnected(WebSocket webSocket)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
var cancellationToken = new CancellationTokenSource().Token;
WebSocketReceiveResult result;
var message = new List<byte>();
// Keep listening for incoming messages, otherwise the socket closes automatically
do
{
var buffer = WebSocket.CreateServerBuffer(BufferSize);
result = await webSocket.ReceiveAsync(buffer, cancellationToken);
message.AddRange(buffer.Array.Take(result.Count));
if (result.EndOfMessage)
{
await ProcessMessage(message.ToArray(), taskCompletionSource);
message.Clear();
}
} while (!taskCompletionSource.Task.IsCompleted &&
webSocket.State == WebSocketState.Open &&
result.MessageType != WebSocketMessageType.Close);
if (webSocket.State == WebSocketState.Open)
{
await webSocket.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
result.CloseStatusDescription, cancellationToken);
}
}
private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
{
var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
: Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
// All messages are expected to be valid JSON objects
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
return;
}
try
{
var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
_logger.LogDebug("Websocket message received: {0}", info.MessageType);
var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
{
try
{
handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
handler.GetType().Name, info.MessageType ?? string.Empty);
}
}));
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing web socket message");
}
}
}
}

View File

@ -77,21 +77,18 @@ namespace Jellyfin.Drawing.Skia
{
canvas.Clear(SKColors.Black);
// number of images used in the thumbnail
var iCount = 3;
// determine sizes for each image that will composited into the final image
var iSlice = Convert.ToInt32(width * 0.23475);
int iTrans = Convert.ToInt32(height * .25);
int iHeight = Convert.ToInt32(height * .70);
var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
var iSlice = Convert.ToInt32(width / iCount);
int iHeight = Convert.ToInt32(height * 1.00);
int imageIndex = 0;
for (int i = 0; i < 4; i++)
for (int i = 0; i < iCount; i++)
{
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
{
imageIndex = newIndex;
if (currentBitmap == null)
{
continue;
@ -108,44 +105,7 @@ namespace Jellyfin.Drawing.Skia
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
{
// draw image onto canvas
canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing);
if (subset == null)
{
continue;
}
// create reflection of image below the drawn image
using (var croppedBitmap = SKBitmap.FromImage(subset))
using (var reflectionBitmap = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType))
{
// resize to half height
currentBitmap.ScalePixels(reflectionBitmap, SKFilterQuality.High);
using (var flippedBitmap = new SKBitmap(reflectionBitmap.Width, reflectionBitmap.Height, reflectionBitmap.ColorType, reflectionBitmap.AlphaType))
using (var flippedCanvas = new SKCanvas(flippedBitmap))
{
// flip image vertically
var matrix = SKMatrix.MakeScale(1, -1);
matrix.SetScaleTranslate(1, -1, 0, flippedBitmap.Height);
flippedCanvas.SetMatrix(matrix);
flippedCanvas.DrawBitmap(reflectionBitmap, 0, 0);
flippedCanvas.ResetMatrix();
// create gradient to make image appear as a reflection
var remainingHeight = height - (iHeight + (2 * verticalSpacing));
flippedCanvas.ClipRect(SKRect.Create(reflectionBitmap.Width, remainingHeight));
using (var gradient = new SKPaint())
{
gradient.IsAntialias = true;
gradient.BlendMode = SKBlendMode.SrcOver;
gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, remainingHeight), new[] { new SKColor(0, 0, 0, 128), new SKColor(0, 0, 0, 208), new SKColor(0, 0, 0, 240), new SKColor(0, 0, 0, 255) }, null, SKShaderTileMode.Clamp);
flippedCanvas.DrawPaint(gradient);
}
// finally draw reflection onto canvas
canvas.DrawBitmap(flippedBitmap, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + (2 * verticalSpacing));
}
}
canvas.DrawImage(subset ?? image, iSlice * i, 0);
}
}
}

View File

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using Jellyfin.Server.SocketSharp;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Configuration;
@ -35,8 +34,6 @@ namespace Jellyfin.Server
public override bool CanSelfRestart => StartupOptions.RestartPath != null;
protected override bool SupportsDualModeSockets => true;
protected override void RestartInternal() => Program.Restart();
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
@ -45,17 +42,5 @@ namespace Jellyfin.Server
}
protected override void ShutdownInternal() => Program.Shutdown();
protected override IHttpListener CreateHttpListener()
=> new WebSocketSharpListener(
Logger,
Certificate,
StreamHelper,
NetworkManager,
SocketFactory,
CryptographyProvider,
SupportsDualModeSockets,
FileSystemManager,
EnvironmentInfo);
}
}

View File

@ -12,7 +12,8 @@
<!-- We need C# 7.1 for async main-->
<LangVersion>latest</LangVersion>
<!-- Disable documentation warnings (for now) -->
<NoWarn>SA1600;CS1591</NoWarn>
<NoWarn>SA1600;SA1601;CS1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
@ -23,10 +24,6 @@
<EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />

View File

@ -20,6 +20,7 @@ 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;
@ -124,7 +125,7 @@ namespace Jellyfin.Server
SQLitePCL.Batteries_V2.Init();
// Allow all https requests
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; } );
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths);
@ -144,8 +145,6 @@ namespace Jellyfin.Server
await appHost.RunStartupTasks().ConfigureAwait(false);
// TODO: read input for a stop command
try
{
// Block main thread until shutdown
@ -175,7 +174,7 @@ namespace Jellyfin.Server
{
// dataDir
// IF --datadir
// ELSE IF $JELLYFIN_DATA_PATH
// ELSE IF $JELLYFIN_DATA_DIR
// ELSE IF windows, use <%APPDATA%>/jellyfin
// ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
// ELSE use $HOME/.local/share/jellyfin
@ -183,7 +182,7 @@ namespace Jellyfin.Server
if (string.IsNullOrEmpty(dataDir))
{
dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR");
if (string.IsNullOrEmpty(dataDir))
{
@ -192,8 +191,6 @@ namespace Jellyfin.Server
}
}
Directory.CreateDirectory(dataDir);
// configDir
// IF --configdir
// ELSE IF $JELLYFIN_CONFIG_DIR
@ -286,6 +283,7 @@ namespace Jellyfin.Server
// Ensure the main folders exist before we continue
try
{
Directory.CreateDirectory(dataDir);
Directory.CreateDirectory(logDir);
Directory.CreateDirectory(configDir);
Directory.CreateDirectory(cacheDir);

View File

@ -1,245 +0,0 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Net;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using SocketHttpListener.Net;
namespace Jellyfin.Server.SocketSharp
{
public class WebSocketSharpListener : IHttpListener
{
private HttpListener _listener;
private readonly ILogger _logger;
private readonly X509Certificate _certificate;
private readonly IStreamHelper _streamHelper;
private readonly INetworkManager _networkManager;
private readonly ISocketFactory _socketFactory;
private readonly ICryptoProvider _cryptoProvider;
private readonly IFileSystem _fileSystem;
private readonly bool _enableDualMode;
private readonly IEnvironmentInfo _environment;
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private CancellationToken _disposeCancellationToken;
public WebSocketSharpListener(
ILogger logger,
X509Certificate certificate,
IStreamHelper streamHelper,
INetworkManager networkManager,
ISocketFactory socketFactory,
ICryptoProvider cryptoProvider,
bool enableDualMode,
IFileSystem fileSystem,
IEnvironmentInfo environment)
{
_logger = logger;
_certificate = certificate;
_streamHelper = streamHelper;
_networkManager = networkManager;
_socketFactory = socketFactory;
_cryptoProvider = cryptoProvider;
_enableDualMode = enableDualMode;
_fileSystem = fileSystem;
_environment = environment;
_disposeCancellationToken = _disposeCancellationTokenSource.Token;
}
public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
public Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; }
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
public void Start(IEnumerable<string> urlPrefixes)
{
if (_listener == null)
{
_listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment);
}
_listener.EnableDualMode = _enableDualMode;
if (_certificate != null)
{
_listener.LoadCert(_certificate);
}
_logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes);
_listener.Prefixes.AddRange(urlPrefixes);
_listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false);
_listener.Start();
}
private static void LogRequest(ILogger logger, HttpListenerRequest request)
{
var url = request.Url.ToString();
logger.LogInformation(
"{0} {1}. UserAgent: {2}",
request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod,
url,
request.UserAgent ?? string.Empty);
}
private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken)
{
IHttpRequest httpReq = null;
var request = context.Request;
try
{
if (request.IsWebSocketRequest)
{
LogRequest(_logger, request);
return ProcessWebSocketRequest(context);
}
httpReq = GetRequest(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing request");
httpReq = httpReq ?? GetRequest(context);
return ErrorHandler(ex, httpReq, true, true);
}
var uri = request.Url;
return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken);
}
private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
{
try
{
var endpoint = ctx.Request.RemoteEndPoint.ToString();
var url = ctx.Request.RawUrl;
var queryString = ctx.Request.QueryString;
var connectingArgs = new WebSocketConnectingEventArgs
{
Url = url,
QueryString = queryString,
Endpoint = endpoint
};
WebSocketConnecting?.Invoke(connectingArgs);
if (connectingArgs.AllowConnection)
{
_logger.LogDebug("Web socket connection allowed");
var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
if (WebSocketConnected != null)
{
var socket = new SharpWebSocket(webSocketContext.WebSocket, _logger);
await socket.ConnectAsServerAsync().ConfigureAwait(false);
WebSocketConnected(new WebSocketConnectEventArgs
{
Url = url,
QueryString = queryString,
WebSocket = socket,
Endpoint = endpoint
});
await socket.StartReceive().ConfigureAwait(false);
}
}
else
{
_logger.LogWarning("Web socket connection not allowed");
TryClose(ctx, 401);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "AcceptWebSocketAsync error");
TryClose(ctx, 500);
}
}
private void TryClose(HttpListenerContext ctx, int statusCode)
{
try
{
ctx.Response.StatusCode = statusCode;
ctx.Response.Close();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error closing web socket response");
}
}
private IHttpRequest GetRequest(HttpListenerContext httpContext)
{
var urlSegments = httpContext.Request.Url.Segments;
var operationName = urlSegments[urlSegments.Length - 1];
var req = new WebSocketSharpRequest(httpContext, operationName, _logger);
return req;
}
public Task Stop()
{
_disposeCancellationTokenSource.Cancel();
_listener?.Close();
return Task.CompletedTask;
}
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed;
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
/// <param name="disposing">Whether or not the managed resources should be disposed</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
Stop().GetAwaiter().GetResult();
}
_disposed = true;
}
}
}

View File

@ -1,181 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
using IRequest = MediaBrowser.Model.Services.IRequest;
namespace Jellyfin.Server.SocketSharp
{
public class WebSocketSharpResponse : IHttpResponse
{
private readonly ILogger _logger;
private readonly HttpListenerResponse _response;
public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
{
_logger = logger;
this._response = response;
Items = new Dictionary<string, object>();
Request = request;
}
public IRequest Request { get; private set; }
public Dictionary<string, object> Items { get; private set; }
public object OriginalResponse => _response;
public int StatusCode
{
get => this._response.StatusCode;
set => this._response.StatusCode = value;
}
public string StatusDescription
{
get => this._response.StatusDescription;
set => this._response.StatusDescription = value;
}
public string ContentType
{
get => _response.ContentType;
set => _response.ContentType = value;
}
public QueryParamCollection Headers => _response.Headers;
private static string AsHeaderValue(Cookie cookie)
{
DateTime defaultExpires = DateTime.MinValue;
var path = cookie.Expires == defaultExpires
? "/"
: cookie.Path ?? "/";
var sb = new StringBuilder();
sb.Append($"{cookie.Name}={cookie.Value};path={path}");
if (cookie.Expires != defaultExpires)
{
sb.Append($";expires={cookie.Expires:R}");
}
if (!string.IsNullOrEmpty(cookie.Domain))
{
sb.Append($";domain={cookie.Domain}");
}
if (cookie.Secure)
{
sb.Append(";Secure");
}
if (cookie.HttpOnly)
{
sb.Append(";HttpOnly");
}
return sb.ToString();
}
public void AddHeader(string name, string value)
{
if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
{
ContentType = value;
return;
}
_response.AddHeader(name, value);
}
public string GetHeader(string name)
{
return _response.Headers[name];
}
public void Redirect(string url)
{
_response.Redirect(url);
}
public Stream OutputStream => _response.OutputStream;
public void Close()
{
if (!this.IsClosed)
{
this.IsClosed = true;
try
{
var response = this._response;
var outputStream = response.OutputStream;
// This is needed with compression
outputStream.Flush();
outputStream.Dispose();
response.Close();
}
catch (SocketException)
{
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in HttpListenerResponseWrapper");
}
}
}
public bool IsClosed
{
get;
private set;
}
public void SetContentLength(long contentLength)
{
// you can happily set the Content-Length header in Asp.Net
// but HttpListener will complain if you do - you have to set ContentLength64 on the response.
// workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
_response.ContentLength64 = contentLength;
}
public void SetCookie(Cookie cookie)
{
var cookieStr = AsHeaderValue(cookie);
_response.Headers.Add("Set-Cookie", cookieStr);
}
public bool SendChunked
{
get => _response.SendChunked;
set => _response.SendChunked = value;
}
public bool KeepAlive { get; set; }
public void ClearCookies()
{
}
public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
{
return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken);
}
}
}

View File

@ -20,10 +20,10 @@ namespace Jellyfin.Server
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string LogDir { get; set; }
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")]
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
public string FFmpegPath { get; set; }
[Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")]
[Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")]
public string FFprobePath { get; set; }
[Option("service", Required = false, HelpText = "Run as headless service.")]

View File

@ -172,6 +172,11 @@ namespace MediaBrowser.Api
{
var path = _config.ApplicationPaths.GetTranscodingTempPath();
if (!Directory.Exists(path))
{
return;
}
foreach (var file in _fileSystem.GetFilePaths(path, true))
{
_fileSystem.DeleteFile(file);

View File

@ -18,6 +18,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace MediaBrowser.Api.Images
{
@ -311,35 +312,35 @@ namespace MediaBrowser.Api.Images
private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
{
int? width = null;
int? height = null;
long length = 0;
try
{
int? width = null;
int? height = null;
long length = 0;
try
if (info.IsLocalFile)
{
if (info.IsLocalFile)
var fileInfo = _fileSystem.GetFileInfo(info.Path);
length = fileInfo.Length;
ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true);
width = size.Width;
height = size.Height;
if (width <= 0 || height <= 0)
{
var fileInfo = _fileSystem.GetFileInfo(info.Path);
length = fileInfo.Length;
ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true);
width = size.Width;
height = size.Height;
if (width <= 0 || height <= 0)
{
width = null;
height = null;
}
width = null;
height = null;
}
}
catch
{
}
catch (Exception ex)
{
Logger.LogError(ex, "Error getting image information for {Item}", item.Name);
}
}
try
{
return new ImageInfo
{
Path = info.Path,
@ -353,7 +354,7 @@ namespace MediaBrowser.Api.Images
}
catch (Exception ex)
{
Logger.LogError(ex, "Error getting image information for {path}", info.Path);
Logger.LogError(ex, "Error getting image information for {Path}", info.Path);
return null;
}
@ -518,16 +519,16 @@ namespace MediaBrowser.Api.Images
request.AddPlayedIndicator = true;
}
}
if (request.PercentPlayed.HasValue)
{
request.UnplayedCount = null;
}
if (request.UnplayedCount.HasValue)
if (request.UnplayedCount.HasValue
&& request.UnplayedCount.Value <= 0)
{
if (request.UnplayedCount.Value <= 0)
{
request.UnplayedCount = null;
}
request.UnplayedCount = null;
}
if (item == null)
@ -541,7 +542,6 @@ namespace MediaBrowser.Api.Images
}
var imageInfo = GetImageInfo(request, item);
if (imageInfo == null)
{
var displayText = item == null ? itemId.ToString() : item.Name;
@ -549,7 +549,6 @@ namespace MediaBrowser.Api.Images
}
IImageEnhancer[] supportedImageEnhancers;
if (_imageProcessor.ImageEnhancers.Length > 0)
{
if (item == null)
@ -564,13 +563,15 @@ namespace MediaBrowser.Api.Images
supportedImageEnhancers = Array.Empty<IImageEnhancer>();
}
var cropwhitespace = request.Type == ImageType.Logo ||
request.Type == ImageType.Art;
bool cropwhitespace;
if (request.CropWhitespace.HasValue)
{
cropwhitespace = request.CropWhitespace.Value;
}
else
{
cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
}
var outputFormats = GetOutputFormats(request);
@ -634,7 +635,7 @@ namespace MediaBrowser.Api.Images
var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
headers["Vary"] = "Accept";
headers[HeaderNames.Vary] = HeaderNames.Accept;
return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
@ -652,12 +653,10 @@ namespace MediaBrowser.Api.Images
private ImageFormat[] GetOutputFormats(ImageRequest request)
{
if (!string.IsNullOrWhiteSpace(request.Format))
if (!string.IsNullOrWhiteSpace(request.Format)
&& Enum.TryParse(request.Format, true, out ImageFormat format))
{
if (Enum.TryParse(request.Format, true, out ImageFormat format))
{
return new ImageFormat[] { format };
}
return new ImageFormat[] { format };
}
return GetClientSupportedFormats();
@ -665,8 +664,19 @@ namespace MediaBrowser.Api.Images
private ImageFormat[] GetClientSupportedFormats()
{
//logger.LogDebug("Request types: {0}", string.Join(",", Request.AcceptTypes ?? Array.Empty<string>()));
var supportedFormats = (Request.AcceptTypes ?? Array.Empty<string>()).Select(i => i.Split(';')[0]).ToArray();
var supportedFormats = Request.AcceptTypes ?? Array.Empty<string>();
if (supportedFormats.Length > 0)
{
for (int i = 0; i < supportedFormats.Length; i++)
{
int index = supportedFormats[i].IndexOf(';');
if (index != -1)
{
supportedFormats[i] = supportedFormats[i].Substring(0, index);
}
}
}
var acceptParam = Request.QueryString["accept"];
var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
@ -699,7 +709,7 @@ namespace MediaBrowser.Api.Images
return formats.ToArray();
}
private bool SupportsFormat(string[] requestAcceptTypes, string acceptParam, string format, bool acceptAll)
private bool SupportsFormat(IEnumerable<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
{
var mimeType = "image/" + format;

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