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) - [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy) - [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng) - [fasheng](https://github.com/fasheng)
- [ploughpuff](https://github.com/ploughpuff)
# Emby Contributors # 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 public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
private ushort _nextProgramNumber; private ushort _nextProgramNumber;
public readonly ProgramChain Next;
private ushort _prevProgramNumber; private ushort _prevProgramNumber;
public readonly ProgramChain Previous;
private ushort _goupProgramNumber; private ushort _goupProgramNumber;
public readonly ProgramChain Goup; // ?? maybe Group
public ProgramPlaybackMode PlaybackMode { get; private set; } public ProgramPlaybackMode PlaybackMode { get; private set; }
public uint ProgramCount { 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 public byte[] Palette { get; private set; } // 16*4 entries
private ushort _commandTableOffset; private ushort _commandTableOffset;
public readonly ProgramChainCommandTable CommandTable;
private ushort _programMapOffset; private ushort _programMapOffset;
private ushort _cellPlaybackOffset; 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 url = Request.AbsoluteUri;
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); 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 cacheLength = TimeSpan.FromDays(1);
var cacheKey = Request.RawUrl.GetMD5(); var cacheKey = Request.RawUrl.GetMD5();
@ -147,21 +147,21 @@ namespace Emby.Dlna.Api
public object Get(GetContentDirectory request) public object Get(GetContentDirectory request)
{ {
var xml = ContentDirectory.GetServiceXml(Request.Headers.ToDictionary()); var xml = ContentDirectory.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
public object Get(GetMediaReceiverRegistrar request) public object Get(GetMediaReceiverRegistrar request)
{ {
var xml = MediaReceiverRegistrar.GetServiceXml(Request.Headers.ToDictionary()); var xml = MediaReceiverRegistrar.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
public object Get(GetConnnectionManager request) public object Get(GetConnnectionManager request)
{ {
var xml = ConnectionManager.GetServiceXml(Request.Headers.ToDictionary()); var xml = ConnectionManager.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
@ -193,7 +193,7 @@ namespace Emby.Dlna.Api
return service.ProcessControlRequest(new ControlRequest return service.ProcessControlRequest(new ControlRequest
{ {
Headers = Request.Headers.ToDictionary(), Headers = Request.Headers,
InputXml = requestStream, InputXml = requestStream,
TargetServerUuId = id, TargetServerUuId = id,
RequestedUrl = Request.AbsoluteUri RequestedUrl = Request.AbsoluteUri

View File

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

View File

@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna namespace Emby.Dlna
{ {
public class ControlRequest public class ControlRequest
{ {
public IDictionary<string, string> Headers { get; set; } public IHeaderDictionary Headers { get; set; }
public Stream InputXml { get; set; } public Stream InputXml { get; set; }
@ -15,7 +15,7 @@ namespace Emby.Dlna
public ControlRequest() 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.Drawing;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Emby.Dlna 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) if (headers == null)
{ {
throw new ArgumentNullException(nameof(headers)); 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)); var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
if (profile != null) if (profile != null)
@ -228,12 +227,12 @@ namespace Emby.Dlna
return profile; 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)); 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 // Handle invalid user setup
if (string.IsNullOrEmpty(header.Name)) if (string.IsNullOrEmpty(header.Name))
@ -241,14 +240,14 @@ namespace Emby.Dlna
return false; return false;
} }
if (headers.TryGetValue(header.Name, out string value)) if (headers.TryGetValue(header.Name, out StringValues value))
{ {
switch (header.Match) switch (header.Match)
{ {
case HeaderMatchType.Equals: case HeaderMatchType.Equals:
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase); return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
case HeaderMatchType.Substring: 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); //_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
return isMatch; return isMatch;
case HeaderMatchType.Regex: case HeaderMatchType.Regex:
@ -494,7 +493,7 @@ namespace Emby.Dlna
internal string Path { get; set; } 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) ?? var profile = GetProfile(headers) ??
GetDefaultProfile(); GetDefaultProfile();

View File

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

View File

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

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -16,7 +15,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
_config = config; _config = config;
} }
public string GetServiceXml(IDictionary<string, string> headers) public string GetServiceXml()
{ {
return new MediaReceiverRegistrarXmlBuilder().GetXml(); 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) private void OnPlaybackStart(uBaseObject mediaInfo)
{ {
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
return;
}
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
{ {
MediaInfo = mediaInfo MediaInfo = mediaInfo
@ -1134,8 +1139,7 @@ namespace Emby.Dlna.PlayTo
private void OnPlaybackProgress(uBaseObject mediaInfo) private void OnPlaybackProgress(uBaseObject mediaInfo)
{ {
var mediaUrl = mediaInfo.Url; if (string.IsNullOrWhiteSpace(mediaInfo.Url))
if (string.IsNullOrWhiteSpace(mediaUrl))
{ {
return; return;
} }
@ -1148,7 +1152,6 @@ namespace Emby.Dlna.PlayTo
private void OnPlaybackStop(uBaseObject mediaInfo) private void OnPlaybackStop(uBaseObject mediaInfo)
{ {
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{ {
MediaInfo = mediaInfo MediaInfo = mediaInfo

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -17,8 +18,8 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
@ -847,13 +848,13 @@ namespace Emby.Dlna.PlayTo
if (index == -1) return request; if (index == -1) return request;
var query = url.Substring(index + 1); 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.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
request.DeviceId = values.Get("DeviceId"); request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.Get("MediaSourceId"); request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.Get("LiveStreamId"); request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.Get("Static"), StringComparison.OrdinalIgnoreCase); request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); 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)) if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{ {
@ -879,9 +880,9 @@ namespace Emby.Dlna.PlayTo
return null; 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)) if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{ {

View File

@ -9,8 +9,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaylistItemFactory public class PlaylistItemFactory
{ {
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public PlaylistItem Create(Photo item, DeviceProfile profile) public PlaylistItem Create(Photo item, DeviceProfile profile)
{ {
var playlistItem = new PlaylistItem 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 public class ActivityLogEntryPoint : IServerEntryPoint
{ {
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
//private readonly ILogger _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IActivityManager _activityManager; private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subManager; private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -61,41 +58,37 @@ namespace Emby.Server.Implementations.Activity
public Task RunAsync() public Task RunAsync()
{ {
_taskManager.TaskCompleted += _taskManager_TaskCompleted; _taskManager.TaskCompleted += OnTaskCompleted;
_installationManager.PluginInstalled += _installationManager_PluginInstalled; _installationManager.PluginInstalled += OnPluginInstalled;
_installationManager.PluginUninstalled += _installationManager_PluginUninstalled; _installationManager.PluginUninstalled += OnPluginUninstalled;
_installationManager.PluginUpdated += _installationManager_PluginUpdated; _installationManager.PluginUpdated += OnPluginUpdated;
_installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed; _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
_sessionManager.SessionStarted += _sessionManager_SessionStarted; _sessionManager.SessionStarted += OnSessionStarted;
_sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed; _sessionManager.AuthenticationFailed += OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded += _sessionManager_AuthenticationSucceeded; _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
_sessionManager.SessionEnded += _sessionManager_SessionEnded; _sessionManager.SessionEnded += OnSessionEnded;
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart; _sessionManager.PlaybackStart += OnPlaybackStart;
_sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; _sessionManager.PlaybackStopped += OnPlaybackStopped;
//_subManager.SubtitlesDownloaded += _subManager_SubtitlesDownloaded; _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
_subManager.SubtitleDownloadFailure += _subManager_SubtitleDownloadFailure;
_userManager.UserCreated += _userManager_UserCreated; _userManager.UserCreated += OnUserCreated;
_userManager.UserPasswordChanged += _userManager_UserPasswordChanged; _userManager.UserPasswordChanged += OnUserPasswordChanged;
_userManager.UserDeleted += _userManager_UserDeleted; _userManager.UserDeleted += OnUserDeleted;
_userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserLockedOut += _userManager_UserLockedOut; _userManager.UserLockedOut += OnUserLockedOut;
//_config.ConfigurationUpdated += _config_ConfigurationUpdated; _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
//_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
_deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded; _appHost.ApplicationUpdated += OnApplicationUpdated;
_appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
return Task.CompletedTask; return Task.CompletedTask;
} }
void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
{ {
CreateLogEntry(new ActivityLogEntry 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 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 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; var item = e.MediaInfo;
@ -146,7 +139,7 @@ namespace Emby.Server.Implementations.Activity
return; return;
} }
var user = e.Users.First(); var user = e.Users[0];
CreateLogEntry(new ActivityLogEntry 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; var item = e.MediaInfo;
@ -232,7 +225,7 @@ namespace Emby.Server.Implementations.Activity
return null; return null;
} }
void _sessionManager_SessionEnded(object sender, SessionEventArgs e) private void OnSessionEnded(object sender, SessionEventArgs e)
{ {
string name; string name;
var session = e.SessionInfo; 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; 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 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 CreateLogEntry(new ActivityLogEntry
{ {
@ -292,25 +285,7 @@ namespace Emby.Server.Implementations.Activity
}); });
} }
void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> 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)
{ {
CreateLogEntry(new ActivityLogEntry 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 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 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 CreateLogEntry(new ActivityLogEntry
{ {
@ -349,18 +324,7 @@ namespace Emby.Server.Implementations.Activity
}); });
} }
void _subManager_SubtitlesDownloaded(object sender, SubtitleDownloadEventArgs e) private void OnSessionStarted(object sender, SessionEventArgs 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)
{ {
string name; string name;
var session = e.SessionInfo; 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 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 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 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; 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 result = e.Result;
var task = e.Task; var task = e.Task;
@ -468,48 +432,36 @@ namespace Emby.Server.Implementations.Activity
} }
private void CreateLogEntry(ActivityLogEntry entry) private void CreateLogEntry(ActivityLogEntry entry)
{ => _activityManager.Create(entry);
try
{
_activityManager.Create(entry);
}
catch
{
// Logged at lower levels
}
}
public void Dispose() public void Dispose()
{ {
_taskManager.TaskCompleted -= _taskManager_TaskCompleted; _taskManager.TaskCompleted -= OnTaskCompleted;
_installationManager.PluginInstalled -= _installationManager_PluginInstalled; _installationManager.PluginInstalled -= OnPluginInstalled;
_installationManager.PluginUninstalled -= _installationManager_PluginUninstalled; _installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PluginUpdated -= _installationManager_PluginUpdated; _installationManager.PluginUpdated -= OnPluginUpdated;
_installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed; _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
_sessionManager.SessionStarted -= _sessionManager_SessionStarted; _sessionManager.SessionStarted -= OnSessionStarted;
_sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed; _sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded -= _sessionManager_AuthenticationSucceeded; _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
_sessionManager.SessionEnded -= _sessionManager_SessionEnded; _sessionManager.SessionEnded -= OnSessionEnded;
_sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; _sessionManager.PlaybackStart -= OnPlaybackStart;
_sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; _sessionManager.PlaybackStopped -= OnPlaybackStopped;
_subManager.SubtitleDownloadFailure -= _subManager_SubtitleDownloadFailure; _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
_userManager.UserCreated -= _userManager_UserCreated; _userManager.UserCreated -= OnUserCreated;
_userManager.UserPasswordChanged -= _userManager_UserPasswordChanged; _userManager.UserPasswordChanged -= OnUserPasswordChanged;
_userManager.UserDeleted -= _userManager_UserDeleted; _userManager.UserDeleted -= OnUserDeleted;
_userManager.UserPolicyUpdated -= _userManager_UserPolicyUpdated; _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserLockedOut -= _userManager_UserLockedOut; _userManager.UserLockedOut -= OnUserLockedOut;
_config.ConfigurationUpdated -= _config_ConfigurationUpdated; _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
_config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated;
_deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded; _appHost.ApplicationUpdated -= OnApplicationUpdated;
_appHost.ApplicationUpdated -= _appHost_ApplicationUpdated;
} }
/// <summary> /// <summary>
@ -531,6 +483,7 @@ namespace Emby.Server.Implementations.Activity
values.Add(CreateValueString(years, "year")); values.Add(CreateValueString(years, "year"));
days = days % DaysInYear; days = days % DaysInYear;
} }
// Number of months // Number of months
if (days >= DaysInMonth) if (days >= DaysInMonth)
{ {
@ -538,25 +491,39 @@ namespace Emby.Server.Implementations.Activity
values.Add(CreateValueString(months, "month")); values.Add(CreateValueString(months, "month"));
days = days % DaysInMonth; days = days % DaysInMonth;
} }
// Number of days // Number of days
if (days >= 1) if (days >= 1)
{
values.Add(CreateValueString(days, "day")); values.Add(CreateValueString(days, "day"));
}
// Number of hours // Number of hours
if (span.Hours >= 1) if (span.Hours >= 1)
{
values.Add(CreateValueString(span.Hours, "hour")); values.Add(CreateValueString(span.Hours, "hour"));
}
// Number of minutes // Number of minutes
if (span.Minutes >= 1) if (span.Minutes >= 1)
{
values.Add(CreateValueString(span.Minutes, "minute")); values.Add(CreateValueString(span.Minutes, "minute"));
}
// Number of seconds (include when 0 if no other components included) // Number of seconds (include when 0 if no other components included)
if (span.Seconds >= 1 || values.Count == 0) if (span.Seconds >= 1 || values.Count == 0)
{
values.Add(CreateValueString(span.Seconds, "second")); values.Add(CreateValueString(span.Seconds, "second"));
}
// Combine values into string // Combine values into string
var builder = new StringBuilder(); var builder = new StringBuilder();
for (int i = 0; i < values.Count; i++) for (int i = 0; i < values.Count; i++)
{ {
if (builder.Length > 0) if (builder.Length > 0)
{
builder.Append(i == values.Count - 1 ? " and " : ", "); builder.Append(i == values.Count - 1 ? " and " : ", ");
}
builder.Append(values[i]); builder.Append(values[i]);
} }
// Return result // Return result

View File

@ -28,20 +28,20 @@ using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Diagnostics;
using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.FFMpeg;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO; using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Middleware;
using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Reflection;
using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using MediaBrowser.Api; using MediaBrowser.Api;
@ -91,7 +91,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
@ -103,11 +102,15 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.Providers.TV.TheTVDB;
using MediaBrowser.WebDashboard.Api; using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers; 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.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using ServiceStack; using ServiceStack;
using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations
{ {
@ -533,7 +536,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
MediaEncoder.Init(); MediaEncoder.SetFFmpegPath();
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
//{ //{
@ -610,6 +613,68 @@ namespace Emby.Server.Implementations
await RegisterResources(serviceCollection); await RegisterResources(serviceCollection);
FindParts(); 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() protected virtual IHttpClient CreateHttpClient()
@ -631,6 +696,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IConfiguration>(_configuration);
serviceCollection.AddSingleton(JsonSerializer); serviceCollection.AddSingleton(JsonSerializer);
serviceCollection.AddSingleton(LoggerFactory); serviceCollection.AddSingleton(LoggerFactory);
@ -672,7 +739,7 @@ namespace Emby.Server.Implementations
ZipClient = new ZipClient(); ZipClient = new ZipClient();
serviceCollection.AddSingleton(ZipClient); serviceCollection.AddSingleton(ZipClient);
HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, CreateBrotliCompressor()); HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper);
serviceCollection.AddSingleton(HttpResultFactory); serviceCollection.AddSingleton(HttpResultFactory);
serviceCollection.AddSingleton<IServerApplicationHost>(this); serviceCollection.AddSingleton<IServerApplicationHost>(this);
@ -680,9 +747,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(ServerConfigurationManager); serviceCollection.AddSingleton(ServerConfigurationManager);
var assemblyInfo = new AssemblyInfo();
serviceCollection.AddSingleton<IAssemblyInfo>(assemblyInfo);
LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LoggerFactory); LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LoggerFactory);
await LocalizationManager.LoadAll(); await LocalizationManager.LoadAll();
serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager); serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager);
@ -699,7 +763,7 @@ namespace Emby.Server.Implementations
var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager); var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager);
serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo); serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo);
ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, assemblyInfo); ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory);
serviceCollection.AddSingleton<IItemRepository>(ItemRepository); serviceCollection.AddSingleton<IItemRepository>(ItemRepository);
AuthenticationRepository = GetAuthenticationRepository(); AuthenticationRepository = GetAuthenticationRepository();
@ -729,7 +793,8 @@ namespace Emby.Server.Implementations
_configuration, _configuration,
NetworkManager, NetworkManager,
JsonSerializer, JsonSerializer,
XmlSerializer); XmlSerializer,
CreateHttpListener());
HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
serviceCollection.AddSingleton(HttpServer); serviceCollection.AddSingleton(HttpServer);
@ -786,7 +851,18 @@ namespace Emby.Server.Implementations
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
serviceCollection.AddSingleton(ChapterManager); 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); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
serviceCollection.AddSingleton(EncodingManager); serviceCollection.AddSingleton(EncodingManager);
@ -822,11 +898,6 @@ namespace Emby.Server.Implementations
_serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider = serviceCollection.BuildServiceProvider();
} }
protected virtual IBrotliCompressor CreateBrotliCompressor()
{
return null;
}
public virtual string PackageRuntime => "netcore"; public virtual string PackageRuntime => "netcore";
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, EnvironmentInfo.EnvironmentInfo environmentInfo) 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 X509Certificate2 GetCertificate(CertificateInfo info)
private X509Certificate GetCertificate(CertificateInfo info)
{ {
var certificateLocation = info == null ? null : info.Path; var certificateLocation = info?.Path;
if (string.IsNullOrWhiteSpace(certificateLocation)) if (string.IsNullOrWhiteSpace(certificateLocation))
{ {
@ -902,85 +971,6 @@ namespace Emby.Server.Implementations
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); 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> /// <summary>
/// Gets the user repository. /// Gets the user repository.
/// </summary> /// </summary>
@ -1017,7 +1007,7 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
private void SetStaticProperties() private void SetStaticProperties()
{ {
((SqliteItemRepository)ItemRepository).ImageProcessor = ImageProcessor; ItemRepository.ImageProcessor = ImageProcessor;
// For now there's no real way to inject these properly // For now there's no real way to inject these properly
BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem"); BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem");
@ -1059,9 +1049,7 @@ namespace Emby.Server.Implementations
.Where(i => i != null) .Where(i => i != null)
.ToArray(); .ToArray();
HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>()); HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>(), GetUrlPrefixes());
StartServer();
LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(), GetExports<IItemResolver>(),
@ -1145,15 +1133,12 @@ namespace Emby.Server.Implementations
AllConcreteTypes = GetComposablePartAssemblies() AllConcreteTypes = GetComposablePartAssemblies()
.SelectMany(x => x.ExportedTypes) .SelectMany(x => x.ExportedTypes)
.Where(type => .Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
{
return type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType;
})
.ToArray(); .ToArray();
} }
private CertificateInfo CertificateInfo { get; set; } private CertificateInfo CertificateInfo { get; set; }
protected X509Certificate Certificate { get; private set; } protected X509Certificate2 Certificate { get; private set; }
private IEnumerable<string> GetUrlPrefixes() private IEnumerable<string> GetUrlPrefixes()
{ {
@ -1175,45 +1160,7 @@ namespace Emby.Server.Implementations
}); });
} }
protected abstract IHttpListener CreateHttpListener(); protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger);
/// <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;
}
}
private CertificateInfo GetCertificateInfo(bool generateCertificate) private CertificateInfo GetCertificateInfo(bool generateCertificate)
{ {
@ -1456,7 +1403,7 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName, ServerName = FriendlyName,
LocalAddress = localAddress, LocalAddress = localAddress,
SupportsLibraryMonitor = true, SupportsLibraryMonitor = true,
EncoderLocationType = MediaEncoder.EncoderLocationType, EncoderLocation = MediaEncoder.EncoderLocation,
SystemArchitecture = EnvironmentInfo.SystemArchitecture, SystemArchitecture = EnvironmentInfo.SystemArchitecture,
SystemUpdateLevel = SystemUpdateLevel, SystemUpdateLevel = SystemUpdateLevel,
PackageName = StartupOptions.PackageName PackageName = StartupOptions.PackageName
@ -1650,7 +1597,7 @@ namespace Emby.Server.Implementations
LogErrorResponseBody = false, LogErrorResponseBody = false,
LogErrors = logPing, LogErrors = logPing,
LogRequest = logPing, LogRequest = logPing,
TimeoutMs = 30000, TimeoutMs = 5000,
BufferContent = false, BufferContent = false,
CancellationToken = cancellationToken CancellationToken = cancellationToken

View File

@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Collections
return base.Supports(item); return base.Supports(item);
} }
protected override List<BaseItem> GetItemsWithImages(BaseItem item) protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{ {
var playlist = (BoxSet)item; var playlist = (BoxSet)item;
@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Collections
.ToList(); .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); 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> 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;
using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Linq;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Cryptography namespace Emby.Server.Implementations.Cryptography
{ {
public class CryptographyProvider : ICryptoProvider 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) public Guid GetMD5(string str)
{ {
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
return provider.ComputeHash(bytes); 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.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty; using SQLitePCL.pretty;
@ -65,8 +64,7 @@ namespace Emby.Server.Implementations.Data
IServerConfigurationManager config, IServerConfigurationManager config,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory)
IAssemblyInfo assemblyInfo)
: base(loggerFactory.CreateLogger(nameof(SqliteItemRepository))) : base(loggerFactory.CreateLogger(nameof(SqliteItemRepository)))
{ {
if (config == null) if (config == null)
@ -82,7 +80,7 @@ namespace Emby.Server.Implementations.Data
_appHost = appHost; _appHost = appHost;
_config = config; _config = config;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_typeMapper = new TypeMapper(assemblyInfo); _typeMapper = new TypeMapper();
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
} }

View File

@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
{ {
TryMigrateToLocalUsersTable(connection); 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> /// <summary>
/// Save a user in the repo /// Save a user in the repo
/// </summary> /// </summary>

View File

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

View File

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

View File

@ -9,7 +9,6 @@
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="..\SocketHttpListener\SocketHttpListener.csproj" />
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj" /> <ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj" />
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
@ -22,6 +21,14 @@
</ItemGroup> </ItemGroup>
<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.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
@ -40,6 +47,21 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup> </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> <ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" /> <EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" /> <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.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpClientManager namespace Emby.Server.Implementations.HttpClientManager
{ {
@ -179,11 +180,11 @@ namespace Emby.Server.Implementations.HttpClientManager
foreach (var header in options.RequestHeaders) 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; 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); SetUserAgent(request, header.Value);
hasUserAgent = true; hasUserAgent = true;
@ -327,7 +328,6 @@ namespace Emby.Server.Implementations.HttpClientManager
} }
httpWebRequest.ContentType = contentType; httpWebRequest.ContentType = contentType;
httpWebRequest.ContentLength = bytes.Length;
(await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
} }
catch (Exception ex) catch (Exception ex)

View File

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

View File

@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -20,6 +21,9 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ServiceStack.Text.Jsv; using ServiceStack.Text.Jsv;
@ -29,12 +33,8 @@ namespace Emby.Server.Implementations.HttpServer
public class HttpListenerHost : IHttpServer, IDisposable public class HttpListenerHost : IHttpServer, IDisposable
{ {
private string DefaultRedirectPath { get; set; } private string DefaultRedirectPath { get; set; }
private readonly ILogger _logger;
public string[] UrlPrefixes { get; private set; } public string[] UrlPrefixes { get; private set; }
private IHttpListener _listener;
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
@ -42,6 +42,7 @@ namespace Emby.Server.Implementations.HttpServer
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IHttpListener _socketListener;
private readonly Func<Type, Func<string, object>> _funcParseFn; private readonly Func<Type, Func<string, object>> _funcParseFn;
public Action<IRequest, IResponse, object>[] ResponseFilters { get; set; } public Action<IRequest, IResponse, object>[] ResponseFilters { get; set; }
@ -59,15 +60,18 @@ namespace Emby.Server.Implementations.HttpServer
IConfiguration configuration, IConfiguration configuration,
INetworkManager networkManager, INetworkManager networkManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer) IXmlSerializer xmlSerializer,
IHttpListener socketListener)
{ {
_appHost = applicationHost; _appHost = applicationHost;
_logger = loggerFactory.CreateLogger("HttpServer"); Logger = loggerFactory.CreateLogger("HttpServer");
_config = config; _config = config;
DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"];
_networkManager = networkManager; _networkManager = networkManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer; _xmlSerializer = xmlSerializer;
_socketListener = socketListener;
_socketListener.WebSocketConnected = OnWebSocketConnected;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s); _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
@ -77,7 +81,7 @@ namespace Emby.Server.Implementations.HttpServer
public string GlobalResponse { get; set; } public string GlobalResponse { get; set; }
protected ILogger Logger => _logger; protected ILogger Logger { get; }
public object CreateInstance(Type type) public object CreateInstance(Type type)
{ {
@ -143,11 +147,11 @@ namespace Emby.Server.Implementations.HttpServer
return; return;
} }
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, Logger)
{ {
OnReceive = ProcessWebSocketMessageReceived, OnReceive = ProcessWebSocketMessageReceived,
Url = e.Url, Url = e.Url,
QueryString = e.QueryString ?? new QueryParamCollection() QueryString = e.QueryString ?? new QueryCollection()
}; };
connection.Closed += Connection_Closed; connection.Closed += Connection_Closed;
@ -212,16 +216,16 @@ namespace Emby.Server.Implementations.HttpServer
if (logExceptionStackTrace) if (logExceptionStackTrace)
{ {
_logger.LogError(ex, "Error processing request"); Logger.LogError(ex, "Error processing request");
} }
else if (logExceptionMessage) else if (logExceptionMessage)
{ {
_logger.LogError(ex.Message); Logger.LogError(ex.Message);
} }
var httpRes = httpReq.Response; var httpRes = httpReq.Response;
if (httpRes.IsClosed) if (httpRes.OriginalResponse.HasStarted)
{ {
return; return;
} }
@ -234,7 +238,7 @@ namespace Emby.Server.Implementations.HttpServer
} }
catch (Exception errorEx) 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) public static string RemoveQueryStringByKey(string url, string key)
@ -292,7 +288,7 @@ namespace Emby.Server.Implementations.HttpServer
var uri = new Uri(url); var uri = new Uri(url);
// this gets all the query string key value pairs as a collection // 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; var originalCount = newQueryString.Count;
@ -313,7 +309,7 @@ namespace Emby.Server.Implementations.HttpServer
string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
return newQueryString.Count > 0 return newQueryString.Count > 0
? string.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString) ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
: pagePathWithoutQueryString; : pagePathWithoutQueryString;
} }
@ -422,7 +418,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary> /// <summary>
/// Overridable method that can be used to implement a custom hnandler /// Overridable method that can be used to implement a custom hnandler
/// </summary> /// </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(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
@ -599,17 +595,15 @@ namespace Emby.Server.Implementations.HttpServer
} }
finally finally
{ {
httpRes.Close();
stopWatch.Stop(); stopWatch.Stop();
var elapsed = stopWatch.Elapsed; var elapsed = stopWatch.Elapsed;
if (elapsed.TotalMilliseconds > 500) 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 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('/'); var pathParts = pathInfo.TrimStart('/').Split('/');
if (pathParts.Length == 0) 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; 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; return null;
} }
private static Task Write(IResponse response, string text) private static Task Write(IResponse response, string text)
{ {
var bOutput = Encoding.UTF8.GetBytes(text); var bOutput = Encoding.UTF8.GetBytes(text);
response.SetContentLength(bOutput.Length);
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
} }
@ -663,6 +655,7 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
// TODO what is this?
var httpsUrl = url var httpsUrl = url
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), 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. /// Adds the rest handlers.
/// </summary> /// </summary>
/// <param name="services">The services.</param> /// <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(); _webSocketListeners = listeners.ToArray();
UrlPrefixes = urlPrefixes.ToArray();
ServiceController = new ServiceController(); ServiceController = new ServiceController();
_logger.LogInformation("Calling ServiceStack AppHost.Init"); Logger.LogInformation("Calling ServiceStack AppHost.Init");
var types = services.Select(r => r.GetType()).ToArray(); var types = services.Select(r => r.GetType()).ToArray();
@ -697,7 +692,7 @@ namespace Emby.Server.Implementations.HttpServer
ResponseFilters = new Action<IRequest, IResponse, object>[] 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); 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) private static string NormalizeEmbyRoutePath(string path)
{ {
if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
@ -793,6 +792,7 @@ namespace Emby.Server.Implementations.HttpServer
private bool _disposed; private bool _disposed;
private readonly object _disposeLock = new object(); private readonly object _disposeLock = new object();
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (_disposed) return; if (_disposed) return;
@ -821,7 +821,7 @@ namespace Emby.Server.Implementations.HttpServer
return Task.CompletedTask; 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 () => var tasks = _webSocketListeners.Select(i => Task.Run(async () =>
{ {
@ -831,7 +831,7 @@ namespace Emby.Server.Implementations.HttpServer
} }
catch (Exception ex) 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); 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.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IRequest = MediaBrowser.Model.Services.IRequest; using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes; using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
@ -32,17 +34,16 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IStreamHelper _streamHelper;
private IBrotliCompressor _brotliCompressor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HttpResultFactory" /> class. /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
/// </summary> /// </summary>
public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor) public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_brotliCompressor = brotliCompressor; _streamHelper = streamHelper;
_logger = loggerfactory.CreateLogger("HttpResultFactory"); _logger = loggerfactory.CreateLogger("HttpResultFactory");
} }
@ -76,7 +77,7 @@ namespace Emby.Server.Implementations.HttpServer
public object GetRedirectResult(string url) public object GetRedirectResult(string url)
{ {
var responseHeaders = new Dictionary<string, string>(); var responseHeaders = new Dictionary<string, string>();
responseHeaders["Location"] = url; responseHeaders[HeaderNames.Location] = url;
var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect); 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>(); 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); AddResponseHeaders(result, responseHeaders);
@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
content = Array.Empty<byte>(); content = Array.Empty<byte>();
} }
result = new StreamWriter(content, contentType, contentLength); result = new StreamWriter(content, contentType);
} }
else else
{ {
@ -143,9 +144,9 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>(); 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); AddResponseHeaders(result, responseHeaders);
@ -175,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
bytes = Array.Empty<byte>(); bytes = Array.Empty<byte>();
} }
result = new StreamWriter(bytes, contentType, contentLength); result = new StreamWriter(bytes, contentType);
} }
else else
{ {
@ -187,9 +188,9 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>(); 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); AddResponseHeaders(result, responseHeaders);
@ -214,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} }
responseHeaders["Expires"] = "-1"; responseHeaders[HeaderNames.Expires] = "-1";
return ToOptimizedResultInternal(requestContext, result, responseHeaders); return ToOptimizedResultInternal(requestContext, result, responseHeaders);
} }
@ -246,9 +247,9 @@ namespace Emby.Server.Implementations.HttpServer
private static string GetCompressionType(IRequest request) 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) //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
// return "br"; // return "br";
@ -326,21 +327,21 @@ namespace Emby.Server.Implementations.HttpServer
} }
content = Compress(content, requestedCompressionType); 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; var contentLength = content.Length;
if (isHeadRequest) if (isHeadRequest)
{ {
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength); var result = new StreamWriter(Array.Empty<byte>(), contentType);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
else else
{ {
var result = new StreamWriter(content, contentType, contentLength); var result = new StreamWriter(content, contentType);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
@ -348,11 +349,6 @@ namespace Emby.Server.Implementations.HttpServer
private byte[] Compress(byte[] bytes, string compressionType) private byte[] Compress(byte[] bytes, string compressionType)
{ {
if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
{
return CompressBrotli(bytes);
}
if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
{ {
return Deflate(bytes); return Deflate(bytes);
@ -366,11 +362,6 @@ namespace Emby.Server.Implementations.HttpServer
throw new NotSupportedException(compressionType); throw new NotSupportedException(compressionType);
} }
private byte[] CompressBrotli(byte[] bytes)
{
return _brotliCompressor.Compress(bytes);
}
private static byte[] Deflate(byte[] bytes) private static byte[] Deflate(byte[] bytes)
{ {
// In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
@ -424,12 +415,12 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options) 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); AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
if (!noCache) 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)) 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); options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var contentType = options.ContentType; 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 // See if the result is already cached in the browser
var result = GetCachedResult(requestContext, options.ResponseHeaders, options); var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
@ -548,11 +539,11 @@ namespace Emby.Server.Implementations.HttpServer
AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
AddAgeHeader(responseHeaders, options.DateLastModified); AddAgeHeader(responseHeaders, options.DateLastModified);
var rangeHeader = requestContext.Headers.Get("Range"); var rangeHeader = requestContext.Headers[HeaderNames.Range];
if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) 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, OnComplete = options.OnComplete,
OnError = options.OnError, OnError = options.OnError,
@ -590,11 +581,6 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture);
}
if (isHeadRequest) if (isHeadRequest)
{ {
using (stream) 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> /// <summary>
/// Adds the caching responseHeaders. /// Adds the caching responseHeaders.
/// </summary> /// </summary>
@ -627,23 +608,23 @@ namespace Emby.Server.Implementations.HttpServer
{ {
if (noCache) if (noCache)
{ {
responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate"; responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
responseHeaders["pragma"] = "no-cache, no-store, must-revalidate"; responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
return; return;
} }
if (cacheDuration.HasValue) if (cacheDuration.HasValue)
{ {
responseHeaders["Cache-Control"] = "public, max-age=" + cacheDuration.Value.TotalSeconds; responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
} }
else else
{ {
responseHeaders["Cache-Control"] = "public"; responseHeaders[HeaderNames.CacheControl] = "public";
} }
if (lastModifiedDate.HasValue) 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) 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;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
{ {
@ -28,21 +27,11 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The web socket handler.</value> /// <value>The web socket handler.</value>
Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } 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> /// <summary>
/// Stops this instance. /// Stops this instance.
/// </summary> /// </summary>
Task Stop(); Task Stop();
Task ProcessWebSocketRequest(HttpContext ctx);
} }
} }

View File

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

View File

@ -3,6 +3,7 @@ using System.Globalization;
using System.Text; using System.Text;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
{ {
@ -25,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
public void FilterResponse(IRequest req, IResponse res, object dto) public void FilterResponse(IRequest req, IResponse res, object dto)
{ {
// Try to prevent compatibility view // Try to prevent compatibility view
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-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-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.AddHeader("Access-Control-Allow-Origin", "*"); res.AddHeader("Access-Control-Allow-Origin", "*");
@ -44,20 +45,19 @@ namespace Emby.Server.Implementations.HttpServer
if (dto is IHasHeaders hasHeaders) 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 // 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)) && !string.IsNullOrEmpty(contentLength))
{ {
var length = long.Parse(contentLength, UsCulture); var length = long.Parse(contentLength, UsCulture);
if (length > 0) if (length > 0)
{ {
res.SetContentLength(length);
res.SendChunked = false; res.SendChunked = false;
} }
} }

View File

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

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
{ {
@ -52,12 +53,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceStream = source; SourceStream = source;
Headers["Content-Type"] = contentType; Headers[HeaderNames.ContentType] = contentType;
if (source.CanSeek)
{
Headers["Content-Length"] = source.Length.ToString(UsCulture);
}
} }
/// <summary> /// <summary>
@ -65,8 +61,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
/// <param name="source">The source.</param> /// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param> /// <param name="contentType">Type of the content.</param>
/// <param name="logger">The logger.</param> public StreamWriter(byte[] source, string contentType)
public StreamWriter(byte[] source, string contentType, int contentLength)
{ {
if (string.IsNullOrEmpty(contentType)) if (string.IsNullOrEmpty(contentType))
{ {
@ -75,9 +70,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceBytes = source; SourceBytes = source;
Headers["Content-Type"] = contentType; Headers[HeaderNames.ContentType] = contentType;
Headers["Content-Length"] = contentLength.ToString(UsCulture);
} }
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) 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.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using UtfUnknown; using UtfUnknown;
@ -67,7 +68,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Gets or sets the query string. /// Gets or sets the query string.
/// </summary> /// </summary>
/// <value>The query string.</value> /// <value>The query string.</value>
public QueryParamCollection QueryString { get; set; } public IQueryCollection QueryString { get; set; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
@ -101,12 +102,6 @@ namespace Emby.Server.Implementations.HttpServer
_socket = socket; _socket = socket;
_socket.OnReceiveBytes = OnReceiveInternal; _socket.OnReceiveBytes = OnReceiveInternal;
var memorySocket = socket as IMemoryWebSocket;
if (memorySocket != null)
{
memorySocket.OnReceiveMemoryBytes = OnReceiveInternal;
}
RemoteEndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint;
_logger = logger; _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) private void OnReceiveInternal(string message)
{ {
LastActivityDate = DateTime.UtcNow; LastActivityDate = DateTime.UtcNow;
@ -193,7 +160,7 @@ namespace Emby.Server.Implementations.HttpServer
var info = new WebSocketMessageInfo var info = new WebSocketMessageInfo
{ {
MessageType = stub.MessageType, MessageType = stub.MessageType,
Data = stub.Data == null ? null : stub.Data.ToString(), Data = stub.Data?.ToString(),
Connection = this Connection = this
}; };

View File

@ -7,7 +7,6 @@ using System.Text;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
@ -711,20 +710,20 @@ namespace Emby.Server.Implementations.IO
return GetFiles(path, null, false, recursive); 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; var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
// On linux and osx the search pattern is case sensitive // 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 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)); return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
} }
var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption); var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
if (extensions != null && extensions.Length > 0) if (extensions != null && extensions.Count > 0)
{ {
files = files.Where(i => 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 public abstract class BaseDynamicImageProvider<T> : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder
where T : BaseItem where T : BaseItem
{ {
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
= new ImageType[] { ImageType.Primary };
protected IFileSystem FileSystem { get; private set; } protected IFileSystem FileSystem { get; private set; }
protected IProviderManager ProviderManager { get; private set; } protected IProviderManager ProviderManager { get; private set; }
protected IApplicationPaths ApplicationPaths { get; private set; } protected IApplicationPaths ApplicationPaths { get; private set; }
@ -33,18 +36,7 @@ namespace Emby.Server.Implementations.Images
ImageProcessor = imageProcessor; ImageProcessor = imageProcessor;
} }
protected virtual bool Supports(BaseItem item) protected virtual bool Supports(BaseItem _) => true;
{
return true;
}
public virtual ImageType[] GetSupportedImages(BaseItem item)
{
return new ImageType[]
{
ImageType.Primary
};
}
public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) 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 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); var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | primaryResult; updateType = updateType | primaryResult;
} }
if (supportedImages.Contains(ImageType.Thumb)) if (SupportedImages.Contains(ImageType.Thumb))
{ {
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false); var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | thumbResult; updateType = updateType | thumbResult;
@ -94,7 +85,7 @@ namespace Emby.Server.Implementations.Images
} }
protected async Task<ItemUpdateType> FetchToFileInternal(BaseItem item, protected async Task<ItemUpdateType> FetchToFileInternal(BaseItem item,
List<BaseItem> itemsWithImages, IReadOnlyList<BaseItem> itemsWithImages,
ImageType imageType, ImageType imageType,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -119,9 +110,9 @@ namespace Emby.Server.Implementations.Images
return ItemUpdateType.ImageUpdate; 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); return CreateCollage(primaryItem, items, outputPath, 640, 360);
} }
@ -132,38 +123,38 @@ namespace Emby.Server.Implementations.Images
.Select(i => .Select(i =>
{ {
var image = i.GetImageInfo(ImageType.Primary, 0); var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile) if (image != null && image.IsLocalFile)
{ {
return image.Path; return image.Path;
} }
image = i.GetImageInfo(ImageType.Thumb, 0); image = i.GetImageInfo(ImageType.Thumb, 0);
if (image != null && image.IsLocalFile) if (image != null && image.IsLocalFile)
{ {
return image.Path; return image.Path;
} }
return null; return null;
}) })
.Where(i => !string.IsNullOrEmpty(i)); .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); 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); 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); 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)); Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
@ -192,7 +183,7 @@ namespace Emby.Server.Implementations.Images
public string Name => "Dynamic Image Provider"; public string Name => "Dynamic Image Provider";
protected virtual string CreateImage(BaseItem item, protected virtual string CreateImage(BaseItem item,
List<BaseItem> itemsWithImages, IReadOnlyCollection<BaseItem> itemsWithImages,
string outputPathWithoutExtension, string outputPathWithoutExtension,
ImageType imageType, ImageType imageType,
int imageIndex) int imageIndex)
@ -211,18 +202,15 @@ namespace Emby.Server.Implementations.Images
if (imageType == ImageType.Primary) if (imageType == ImageType.Primary)
{ {
if (item is UserView) if (item is UserView || item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum)
{
return CreateSquareCollage(item, itemsWithImages, outputPath);
}
if (item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum)
{ {
return CreateSquareCollage(item, itemsWithImages, outputPath); return CreateSquareCollage(item, itemsWithImages, outputPath);
} }
return CreatePosterCollage(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; protected virtual int MaxImageAgeDays => 7;
@ -234,13 +222,11 @@ namespace Emby.Server.Implementations.Images
return false; 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; return true;
} }
if (supportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb)) if (SupportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb))
{ {
return true; return true;
} }
@ -285,7 +271,7 @@ namespace Emby.Server.Implementations.Images
public int Order => 0; 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 var image = itemsWithImages
.Where(i => i.HasImage(imageType) && i.GetImageInfo(imageType, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(imageType))) .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;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
public string Name => "Default"; public string Name => "Default";
public bool IsEnabled => true; 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) public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
{ {
throw new NotImplementedException(); 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) public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{ {
bool success = false;
if (resolvedUser == null) if (resolvedUser == null)
{ {
throw new Exception("Invalid username or password"); 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) 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) public Task<bool> HasPassword(User user)
{ {
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
return Task.FromResult(hasConfiguredPassword); 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) public Task ChangePassword(User user, string newPassword)
{ {
string newPasswordHash = null; ConvertPasswordFormat(user);
// This is needed to support changing a no password user to a password user
if (newPassword != null) 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; return Task.CompletedTask;
} }
public string GetPasswordHash(User user) public string GetPasswordHash(User user)
{ {
return string.IsNullOrEmpty(user.Password) return user.Password;
? GetEmptyHashedString(user)
: 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> /// <summary>
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public string GetHashedString(User user, string str) public string GetHashedString(User user, string str)
{ {
var salt = user.Salt; PasswordHash passwordHash;
if (salt != null) if (string.IsNullOrEmpty(user.Password))
{ {
// return BCrypt.HashPassword(str, salt); passwordHash = new PasswordHash(_cryptographyProvider);
}
else
{
ConvertPasswordFormat(user);
passwordHash = new PasswordHash(user.Password);
} }
// legacy if (passwordHash.SaltBytes != null)
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); {
// 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)); throw new ArgumentNullException(nameof(item));
} }
if (item is IItemByName) if (item is IItemByName)
{ {
if (!(item is MusicArtist)) if (!(item is MusicArtist))
@ -285,18 +286,7 @@ namespace Emby.Server.Implementations.Library
return; return;
} }
} }
else if (!item.IsFolder)
else if (item.IsFolder)
{
//if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder))
//{
// if (item.SourceType != SourceType.Library)
// {
// return;
// }
//}
}
else
{ {
if (!(item is Video) && !(item is LiveTvChannel)) if (!(item is Video) && !(item is LiveTvChannel))
{ {
@ -371,19 +361,20 @@ namespace Emby.Server.Implementations.Library
foreach (var metadataPath in GetMetadataPaths(item, children)) 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 try
{ {
Directory.Delete(metadataPath, true); Directory.Delete(metadataPath, true);
}
catch (IOException)
{
} }
catch (Exception ex) 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.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Events; 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 (.) //This is some regex that matches only on unicode "word" characters, as well as -, _ and @
foreach (var currentChar in username) //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 (.)
if (!IsValidUsernameCharacter(currentChar)) return Regex.IsMatch(username, "^[\\w-'._@]*$");
{
return false;
}
}
return true;
} }
private static bool IsValidUsernameCharacter(char i) private static bool IsValidUsernameCharacter(char i)
{ {
return !char.Equals(i, '<') && !char.Equals(i, '>'); return IsValidUsername(i.ToString());
} }
public string MakeValidUsername(string username) public string MakeValidUsername(string username)
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
private string GetLocalPasswordHash(User user) private string GetLocalPasswordHash(User user)
{ {
return string.IsNullOrEmpty(user.EasyPassword) return string.IsNullOrEmpty(user.EasyPassword)
? _defaultAuthenticationProvider.GetEmptyHashedString(user) ? null
: user.EasyPassword; : user.EasyPassword;
} }
private bool IsPasswordEmpty(User user, string passwordHash)
{
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Loads the users from the repository /// Loads the users from the repository
/// </summary> /// </summary>
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); 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 : hasConfiguredEasyPassword :
hasConfiguredPassword; hasConfiguredPassword;
var dto = new UserDto UserDto dto = new UserDto
{ {
Id = user.Id, Id = user.Id,
Name = user.Name, Name = user.Name,
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
dto.EnableAutoLogin = true; dto.EnableAutoLogin = true;
} }
var image = user.GetImageInfo(ImageType.Primary, 0); ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
if (image != null) if (image != null)
{ {
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
if (!IsValidUsername(name)) 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))) 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.MediaInfo;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IProcessFactory _processFactory; private readonly IProcessFactory _processFactory;
private readonly IAssemblyInfo _assemblyInfo;
private IMediaSourceManager _mediaSourceManager; private IMediaSourceManager _mediaSourceManager;
public static EmbyTV Current; public static EmbyTV Current;
@ -74,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public EmbyTV(IServerApplicationHost appHost, public EmbyTV(IServerApplicationHost appHost,
IStreamHelper streamHelper, IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IAssemblyInfo assemblyInfo,
ILogger logger, ILogger logger,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IHttpClient httpClient, IHttpClient httpClient,
@ -101,12 +98,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_processFactory = processFactory; _processFactory = processFactory;
_liveTvManager = (LiveTvManager)liveTvManager; _liveTvManager = (LiveTvManager)liveTvManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_assemblyInfo = assemblyInfo;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper; _streamHelper = streamHelper;
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger); _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"), _logger);
_timerProvider.TimerFired += _timerProvider_TimerFired; _timerProvider.TimerFired += _timerProvider_TimerFired;
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;

View File

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

View File

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

View File

@ -19,6 +19,7 @@ using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (protocol == MediaProtocol.Http) if (protocol == MediaProtocol.Http)
{ {
// Use user-defined user-agent. If there isn't one, make it look like a browser. // 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" : "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; info.UserAgent;
} }

View File

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

View File

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

View File

@ -44,7 +44,7 @@
"NameInstallFailed": "{0} échec d'installation", "NameInstallFailed": "{0} échec d'installation",
"NameSeasonNumber": "Saison {0}", "NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue", "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", "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
"NotificationOptionAudioPlayback": "Lecture audio démarrée", "NotificationOptionAudioPlayback": "Lecture audio démarrée",
@ -89,7 +89,7 @@
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}", "UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié", "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", "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}", "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie",
"ValueSpecialEpisodeName": "Spécial - {0}", "ValueSpecialEpisodeName": "Spécial - {0}",

View File

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

View File

@ -34,17 +34,17 @@
"LabelRunningTimeValue": "Durata: {0}", "LabelRunningTimeValue": "Durata: {0}",
"Latest": "Più recenti", "Latest": "Più recenti",
"MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato", "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", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata", "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto", "MixedContent": "Contenuto misto",
"Movies": "Film", "Movies": "Film",
"Music": "Musica", "Music": "Musica",
"MusicVideos": "Video musicali", "MusicVideos": "Video musicali",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}", "NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuto", "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", "NotificationOptionApplicationUpdateAvailable": "Aggiornamento dell'applicazione disponibile",
"NotificationOptionApplicationUpdateInstalled": "Aggiornamento dell'applicazione installato", "NotificationOptionApplicationUpdateInstalled": "Aggiornamento dell'applicazione installato",
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata", "NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
@ -70,12 +70,12 @@
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} fallito", "ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviati", "ScheduledTaskStartedWithName": "{0} avviati",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Programmi", "Shows": "Programmi",
"Songs": "Canzoni", "Songs": "Canzoni",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}", "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}", "SubtitlesDownloadedForItem": "Sottotitoli scaricati per {0}",
"Sync": "Sincronizza", "Sync": "Sincronizza",
"System": "Sistema", "System": "Sistema",
@ -91,7 +91,7 @@
"UserPolicyUpdatedWithName": "La politica dell'utente è stata aggiornata per {0}", "UserPolicyUpdatedWithName": "La politica dell'utente è stata aggiornata per {0}",
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1}", "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto 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}", "ValueSpecialEpisodeName": "Speciale - {0}",
"VersionNumber": "Versione {0}" "VersionNumber": "Versione {0}"
} }

View File

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

View File

@ -5,7 +5,7 @@
"Artists": "Artistas", "Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros", "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", "Channels": "Canais",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
"Collections": "Coletâneas", "Collections": "Coletâneas",
@ -30,21 +30,21 @@
"Inherit": "Herdar", "Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca",
"LabelIpAddressValue": "Endereço ip: {0}", "LabelIpAddressValue": "Endereço IP: {0}",
"LabelRunningTimeValue": "Tempo de execução: {0}", "LabelRunningTimeValue": "Tempo de execução: {0}",
"Latest": "Recente", "Latest": "Recente",
"MessageApplicationUpdated": "O servidor Jellyfin foi atualizado", "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", "MessageNamedServerConfigurationUpdatedWithValue": "A seção {0} da configuração do servidor foi atualizada",
"MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada", "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
"MixedContent": "Conteúdo misto", "MixedContent": "Conteúdo misto",
"Movies": "Filmes", "Movies": "Filmes",
"Music": "Música", "Music": "Música",
"MusicVideos": "Vídeos musicais", "MusicVideos": "Vídeos musicais",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "A instalação de {0} falhou",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconhecida", "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", "NotificationOptionApplicationUpdateAvailable": "Atualização de aplicativo disponível",
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicativo instalada", "NotificationOptionApplicationUpdateInstalled": "Atualização de aplicativo instalada",
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada", "NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
@ -70,12 +70,12 @@
"ProviderValue": "Provedor: {0}", "ProviderValue": "Provedor: {0}",
"ScheduledTaskFailedWithName": "{0} falhou", "ScheduledTaskFailedWithName": "{0} falhou",
"ScheduledTaskStartedWithName": "{0} iniciada", "ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "O servidor {0} precisa ser reiniciado",
"Shows": "Séries", "Shows": "Séries",
"Songs": "Músicas", "Songs": "Músicas",
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor tente novamente em breve.", "StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor tente novamente em breve.",
"SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}", "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}", "SubtitlesDownloadedForItem": "Legendas baixadas para {0}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
@ -91,7 +91,7 @@
"UserPolicyUpdatedWithName": "A política de usuário foi atualizada para {0}", "UserPolicyUpdatedWithName": "A política de usuário foi atualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} iniciou a reprodução de {1}", "UserStartedPlayingItemWithValues": "{0} iniciou a reprodução de {1}",
"UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {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}", "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versão {0}" "VersionNumber": "Versão {0}"
} }

View File

@ -1,62 +1,62 @@
{ {
"Albums": "Albums", "Albums": "Albumi",
"AppDeviceValues": "App: {0}, Device: {1}", "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
"Application": "Application", "Application": "Aplikacija",
"Artists": "Artists", "Artists": "Izvajalci",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated", "AuthenticationSucceededWithUserName": "{0} preverjanje uspešno",
"Books": "Books", "Books": "Knjige",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}",
"Channels": "Channels", "Channels": "Kanali",
"ChapterNameValue": "Chapter {0}", "ChapterNameValue": "Poglavje {0}",
"Collections": "Collections", "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Favorites", "Favorites": "Priljubljeni",
"Folders": "Folders", "Folders": "Mape",
"Genres": "Genres", "Genres": "Zvrsti",
"HeaderAlbumArtists": "Album Artists", "HeaderAlbumArtists": "Izvajalci albuma",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Posnetki kamere",
"HeaderContinueWatching": "Continue Watching", "HeaderContinueWatching": "Nadaljuj gledanje",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteEpisodes": "Priljubljene epizode",
"HeaderFavoriteShows": "Favorite Shows", "HeaderFavoriteShows": "Priljubljene serije",
"HeaderFavoriteSongs": "Favorite Songs", "HeaderFavoriteSongs": "Priljubljene pesmi",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "TV v živo",
"HeaderNextUp": "Next Up", "HeaderNextUp": "Sledi",
"HeaderRecordingGroups": "Recording Groups", "HeaderRecordingGroups": "Zbirke posnetkov",
"HomeVideos": "Home videos", "HomeVideos": "Domači posnetki",
"Inherit": "Inherit", "Inherit": "Podeduj",
"ItemAddedWithName": "{0} was added to the library", "ItemAddedWithName": "{0} je dodan v knjižnico",
"ItemRemovedWithName": "{0} was removed from the library", "ItemRemovedWithName": "{0} je bil odstranjen iz knjižnice",
"LabelIpAddressValue": "Ip address: {0}", "LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Running time: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Latest", "Latest": "Najnovejše",
"MessageApplicationUpdated": "Jellyfin Server has been updated", "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
"MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
"MixedContent": "Mixed content", "MixedContent": "Razne vsebine",
"Movies": "Movies", "Movies": "Filmi",
"Music": "Music", "Music": "Glasba",
"MusicVideos": "Music videos", "MusicVideos": "Glasbeni posnetki",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "{0} namestitev neuspešna",
"NameSeasonNumber": "Season {0}", "NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Season Unknown", "NameSeasonUnknown": "Season neznana",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "Nova razničica Jellyfin strežnika je na voljo za prenos.",
"NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
"NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
"NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno",
"NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen",
"NotificationOptionInstallationFailed": "Installation failure", "NotificationOptionInstallationFailed": "Napaka pri nameščanju",
"NotificationOptionNewLibraryContent": "New content added", "NotificationOptionNewLibraryContent": "Nove vsebine dodane",
"NotificationOptionPluginError": "Plugin failure", "NotificationOptionPluginError": "Napaka dodatka",
"NotificationOptionPluginInstalled": "Plugin installed", "NotificationOptionPluginInstalled": "Dodatek nameščen",
"NotificationOptionPluginUninstalled": "Plugin uninstalled", "NotificationOptionPluginUninstalled": "Dodatek odstranjen",
"NotificationOptionPluginUpdateInstalled": "Plugin update installed", "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
"NotificationOptionServerRestartRequired": "Server restart required", "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
"NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionTaskFailed": "Scheduled task failure",
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "User locked out",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "Video playback started",

View File

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

View File

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

View File

@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization
{ {
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings."; const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
Directory.CreateDirectory(LocalizationPath);
var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName);
// Extract from the assembly // Extract from the assembly
foreach (var resource in _assembly.GetManifestResourceNames()) foreach (var resource in _assembly.GetManifestResourceNames())
{ {
@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization
continue; 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; string line;
} while ((line = await reader.ReadLineAsync()) != null)
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))
{ {
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)) _allParentalRatings[countryCode] = dict;
{
await LoadRatings(file);
} }
LoadAdditionalRatings();
await LoadCultures(); 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) public string NormalizeFormKD(string text)
=> text.Normalize(NormalizationForm.FormKD); => text.Normalize(NormalizationForm.FormKD);
@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization
return value; 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" }; private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
/// <summary> /// <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) private static List<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
{ {
var path = GetChapterImagesPath(video); var path = GetChapterImagesPath(video);
if (!Directory.Exists(path))
{
return new List<string>();
}
try 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> /// <returns>Task.</returns>
Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken); 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;
using System.Net.WebSockets;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.Net namespace Emby.Server.Implementations.Net
{ {
@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Net
/// Gets or sets the query string. /// Gets or sets the query string.
/// </summary> /// </summary>
/// <value>The query string.</value> /// <value>The query string.</value>
public QueryParamCollection QueryString { get; set; } public IQueryCollection QueryString { get; set; }
/// <summary> /// <summary>
/// Gets or sets the web socket. /// Gets or sets the web socket.
/// </summary> /// </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; var playlist = (Playlist)item;
@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Playlists
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
protected override List<BaseItem> GetItemsWithImages(BaseItem item) protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{ {
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
@ -89,7 +89,6 @@ namespace Emby.Server.Implementations.Playlists
Recursive = true, Recursive = true,
ImageTypes = new[] { ImageType.Primary }, ImageTypes = new[] { ImageType.Primary },
DtoOptions = new DtoOptions(false) DtoOptions = new DtoOptions(false)
}); });
} }
} }
@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.Playlists
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
protected override List<BaseItem> GetItemsWithImages(BaseItem item) protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{ {
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
@ -116,11 +115,5 @@ namespace Emby.Server.Implementations.Playlists
DtoOptions = new DtoOptions(false) 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; var contentLength = bytesResponse.Length;
if (response != null)
{
response.SetContentLength(contentLength);
}
if (contentLength > 0) if (contentLength > 0)
{ {
await responseStream.WriteAsync(bytesResponse, 0, contentLength).ConfigureAwait(false); await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
} }
return; return;
} }

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session 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) if (queryString == null)
{ {

View File

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

View File

@ -63,6 +63,28 @@ public sealed class HttpPostedFile : IDisposable
_position = offset; _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() public override void Flush()
{ {
} }
@ -178,27 +200,5 @@ public sealed class HttpPostedFile : IDisposable
{ {
throw new NotSupportedException(); 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.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.Services; 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 public partial class WebSocketSharpRequest : IHttpRequest
{ {
internal static string GetParameter(ReadOnlySpan<char> header, string attr) 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) if (ap == -1)
{ {
return null; return null;
@ -43,7 +45,7 @@ namespace Jellyfin.Server.SocketSharp
private async Task LoadMultiPart(WebROCollection form) private async Task LoadMultiPart(WebROCollection form)
{ {
string boundary = GetParameter(ContentType, "; boundary="); string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
if (boundary == null) if (boundary == null)
{ {
return; return;
@ -77,7 +79,7 @@ namespace Jellyfin.Server.SocketSharp
byte[] copy = new byte[e.Length]; byte[] copy = new byte[e.Length];
input.Position = e.Start; 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)); form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length));
} }
@ -96,23 +98,15 @@ namespace Jellyfin.Server.SocketSharp
var form = new WebROCollection(); var form = new WebROCollection();
files = new Dictionary<string, HttpPostedFile>(); files = new Dictionary<string, HttpPostedFile>();
if (IsContentType("multipart/form-data", true)) if (IsContentType("multipart/form-data"))
{ {
await LoadMultiPart(form).ConfigureAwait(false); 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); 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) if (validate_form && !checked_form)
{ {
checked_form = true; checked_form = true;
@ -122,15 +116,11 @@ namespace Jellyfin.Server.SocketSharp
return form; 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 validate_form { get; set; }
protected bool checked_cookies { get; set; }
protected bool checked_query_string { get; set; }
protected bool checked_form { get; set; } protected bool checked_form { get; set; }
private static void ThrowValidationException(string name, string key, string value) private static void ThrowValidationException(string name, string key, string value)
@ -210,26 +200,14 @@ namespace Jellyfin.Server.SocketSharp
return false; return false;
} }
public void ValidateInput() private bool IsContentType(string ct)
{ {
validate_cookies = true; if (ContentType == null)
validate_query_string = true;
validate_form = true;
}
private bool IsContentType(string ct, bool starts_with)
{
if (ct == null || ContentType == null)
{ {
return false; return false;
} }
if (starts_with) return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase);
{
return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase);
}
return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase);
} }
private async Task LoadWwwForm(WebROCollection form) private async Task LoadWwwForm(WebROCollection form)
@ -396,14 +374,14 @@ namespace Jellyfin.Server.SocketSharp
var elem = new Element(); var elem = new Element();
ReadOnlySpan<char> header; 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.Name = GetContentDispositionAttribute(header, "name");
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); 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.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
elem.Encoding = GetEncoding(elem.ContentType); elem.Encoding = GetEncoding(elem.ContentType);
@ -455,7 +433,7 @@ namespace Jellyfin.Server.SocketSharp
private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name) 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) if (idx < 0)
{ {
return null; return null;
@ -478,7 +456,7 @@ namespace Jellyfin.Server.SocketSharp
private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name) 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) if (idx < 0)
{ {
return null; return null;

View File

@ -1,11 +1,12 @@
using System; using System;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.SocketSharp namespace Emby.Server.Implementations.SocketSharp
{ {
public class SharpWebSocket : IWebSocket public class SharpWebSocket : IWebSocket
{ {
@ -20,67 +21,22 @@ namespace Jellyfin.Server.SocketSharp
/// Gets or sets the web socket. /// Gets or sets the web socket.
/// </summary> /// </summary>
/// <value>The web socket.</value> /// <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 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) _logger = logger ?? throw new ArgumentNullException(nameof(logger));
{ _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
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);
}
} }
/// <summary> /// <summary>
/// Gets or sets the state. /// Gets or sets the state.
/// </summary> /// </summary>
/// <value>The state.</value> /// <value>The state.</value>
public WebSocketState State => WebSocket.ReadyState; public WebSocketState State => _webSocket.State;
/// <summary> /// <summary>
/// Sends the async. /// Sends the async.
@ -91,7 +47,7 @@ namespace Jellyfin.Server.SocketSharp
/// <returns>Task.</returns> /// <returns>Task.</returns>
public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) 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> /// <summary>
@ -103,7 +59,7 @@ namespace Jellyfin.Server.SocketSharp
/// <returns>Task.</returns> /// <returns>Task.</returns>
public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) 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> /// <summary>
@ -128,13 +84,13 @@ namespace Jellyfin.Server.SocketSharp
if (dispose) if (dispose)
{ {
WebSocket.OnMessage -= OnSocketMessage;
WebSocket.OnClose -= OnSocketClose;
WebSocket.OnError -= OnSocketError;
_cancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
if (_webSocket.State == WebSocketState.Open)
WebSocket.CloseAsync().GetAwaiter().GetResult(); {
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
CancellationToken.None);
}
Closed?.Invoke(this, EventArgs.Empty);
} }
_disposed = true; _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.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net;
using System.Text; using System.Text;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SocketHttpListener.Net; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpFile = MediaBrowser.Model.Services.IHttpFile;
using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
using IResponse = MediaBrowser.Model.Services.IResponse; using IResponse = MediaBrowser.Model.Services.IResponse;
namespace Jellyfin.Server.SocketSharp namespace Emby.Server.Implementations.SocketSharp
{ {
public partial class WebSocketSharpRequest : IHttpRequest public partial class WebSocketSharpRequest : IHttpRequest
{ {
private readonly HttpListenerRequest request; private readonly HttpRequest request;
private readonly IHttpResponse response;
public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger) public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger)
{ {
this.OperationName = operationName; this.OperationName = operationName;
this.request = httpContext.Request; this.request = httpContext;
this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); this.Response = new WebSocketSharpResponse(logger, response);
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
} }
public HttpListenerRequest HttpRequest => request; public HttpRequest HttpRequest => request;
public object OriginalRequest => request; public IResponse Response { get; }
public IResponse Response => response;
public IHttpResponse HttpResponse => response;
public string OperationName { get; set; } public string OperationName { get; set; }
public object Dto { 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 AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
public string UserHostAddress => request.UserHostAddress;
public string XForwardedFor 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 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; private string remoteIp;
public string RemoteIp public string RemoteIp
@ -66,19 +61,19 @@ namespace Jellyfin.Server.SocketSharp
return remoteIp; return remoteIp;
} }
var temp = CheckBadChars(XForwardedFor); var temp = CheckBadChars(XForwardedFor.AsSpan());
if (temp.Length != 0) if (temp.Length != 0)
{ {
return remoteIp = temp.ToString(); return remoteIp = temp.ToString();
} }
temp = CheckBadChars(XRealIp); temp = CheckBadChars(XRealIp.AsSpan());
if (temp.Length != 0) if (temp.Length != 0)
{ {
return remoteIp = NormalizeIp(temp).ToString(); 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; 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) private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
{ {
if (ip.Length != 0 && !ip.IsWhiteSpace()) if (ip.Length != 0 && !ip.IsWhiteSpace())
{ {
// Handle ipv4 mapped to ipv6 // Handle ipv4 mapped to ipv6
const string srch = "::ffff:"; const string srch = "::ffff:";
var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0) if (index == 0)
{ {
ip = ip.Slice(srch.Length); ip = ip.Slice(srch.Length);
@ -185,9 +167,7 @@ namespace Jellyfin.Server.SocketSharp
return ip; return ip;
} }
public bool IsSecureConnection => request.IsSecureConnection || XForwardedProtocol == "https"; public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
public string[] AcceptTypes => request.AcceptTypes;
private Dictionary<string, object> items; private Dictionary<string, object> items;
public Dictionary<string, object> Items => items ?? (items = new Dictionary<string, object>()); public Dictionary<string, object> Items => items ?? (items = new Dictionary<string, object>());
@ -197,13 +177,13 @@ namespace Jellyfin.Server.SocketSharp
{ {
get => get =>
responseContentType responseContentType
?? (responseContentType = GetResponseContentType(this)); ?? (responseContentType = GetResponseContentType(HttpRequest));
set => this.responseContentType = value; set => this.responseContentType = value;
} }
public const string FormUrlEncoded = "application/x-www-form-urlencoded"; public const string FormUrlEncoded = "application/x-www-form-urlencoded";
public const string MultiPartFormData = "multipart/form-data"; public const string MultiPartFormData = "multipart/form-data";
public static string GetResponseContentType(IRequest httpReq) public static string GetResponseContentType(HttpRequest httpReq)
{ {
var specifiedContentType = GetQueryStringContentType(httpReq); var specifiedContentType = GetQueryStringContentType(httpReq);
if (!string.IsNullOrEmpty(specifiedContentType)) if (!string.IsNullOrEmpty(specifiedContentType))
@ -213,7 +193,7 @@ namespace Jellyfin.Server.SocketSharp
const string serverDefaultContentType = "application/json"; const string serverDefaultContentType = "application/json";
var acceptContentTypes = httpReq.AcceptTypes; var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
string defaultContentType = null; string defaultContentType = null;
if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
{ {
@ -261,7 +241,7 @@ namespace Jellyfin.Server.SocketSharp
public const string Soap11 = "text/xml; charset=utf-8"; 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) if (contentTypes == null || request.ContentType == null)
{ {
@ -279,18 +259,18 @@ namespace Jellyfin.Server.SocketSharp
return false; 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); 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) if (format == null)
{ {
const int formatMaxLength = 4; const int formatMaxLength = 4;
ReadOnlySpan<char> pi = httpReq.PathInfo; ReadOnlySpan<char> pi = httpReq.Path.ToString().AsSpan();
if (pi == null || pi.Length <= formatMaxLength) if (pi == null || pi.Length <= formatMaxLength)
{ {
return null; return null;
@ -309,11 +289,11 @@ namespace Jellyfin.Server.SocketSharp
} }
format = LeftPart(format, '.'); format = LeftPart(format, '.');
if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) if (format.Contains("json".AsSpan(), StringComparison.OrdinalIgnoreCase))
{ {
return "application/json"; return "application/json";
} }
else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) else if (format.Contains("xml".AsSpan(), StringComparison.OrdinalIgnoreCase))
{ {
return "application/xml"; return "application/xml";
} }
@ -343,10 +323,10 @@ namespace Jellyfin.Server.SocketSharp
{ {
var mode = HandlerFactoryPath; var mode = HandlerFactoryPath;
var pos = request.RawUrl.IndexOf('?', StringComparison.Ordinal); var pos = RawUrl.IndexOf("?", StringComparison.Ordinal);
if (pos != -1) if (pos != -1)
{ {
var path = request.RawUrl.Substring(0, pos); var path = RawUrl.Substring(0, pos);
this.pathInfo = GetPathInfo( this.pathInfo = GetPathInfo(
path, path,
mode, mode,
@ -354,10 +334,10 @@ namespace Jellyfin.Server.SocketSharp
} }
else 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(); this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
} }
@ -421,59 +401,55 @@ namespace Jellyfin.Server.SocketSharp
return null; return null;
} }
var path = sbPathInfo.ToString(); return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/";
return path.Length > 1 ? path.TrimEnd('/') : "/";
} }
private Dictionary<string, System.Net.Cookie> cookies; public string UserAgent => request.Headers[HeaderNames.UserAgent];
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);
}
}
return cookies; public IHeaderDictionary Headers => request.Headers;
}
}
public string UserAgent => request.UserAgent; public IQueryCollection QueryString => request.Query;
public QueryParamCollection Headers => request.Headers; public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString());
private QueryParamCollection queryString;
public QueryParamCollection QueryString => queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query));
public bool IsLocal => request.IsLocal;
private string httpMethod; private string httpMethod;
public string HttpMethod => public string HttpMethod =>
httpMethod httpMethod
?? (httpMethod = request.HttpMethod); ?? (httpMethod = request.Method);
public string Verb => HttpMethod; public string Verb => HttpMethod;
public string ContentType => request.ContentType; public string ContentType => request.ContentType;
private Encoding contentEncoding; private Encoding ContentEncoding
public Encoding ContentEncoding
{ {
get => contentEncoding ?? request.ContentEncoding; get
set => contentEncoding = value; {
// 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) public static Encoding GetEncoding(string contentTypeHeader)
{ {
var param = GetParameter(contentTypeHeader, "charset="); var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
if (param == null) if (param == null)
{ {
return 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; private IHttpFile[] httpFiles;
public IHttpFile[] Files public IHttpFile[] Files
@ -530,13 +506,13 @@ namespace Jellyfin.Server.SocketSharp
if (handlerPath != null) if (handlerPath != null)
{ {
var trimmed = pathInfo.AsSpan().TrimStart('/'); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Emby.Server.Implementations.Images; using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; 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 view = (CollectionFolder)item;
var viewType = view.CollectionType; var viewType = view.CollectionType;
@ -56,7 +55,7 @@ namespace Emby.Server.Implementations.UserViews
includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" }; 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 return view.GetItemList(new InternalItemsQuery
{ {
@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.UserViews
}, },
IncludeItemTypes = includeItemTypes IncludeItemTypes = includeItemTypes
}).ToList(); });
} }
protected override bool Supports(BaseItem item) protected override bool Supports(BaseItem item)
@ -79,7 +78,7 @@ namespace Emby.Server.Implementations.UserViews
return item is CollectionFolder; 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"); var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png");

View File

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

View File

@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.UserViews
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
protected override List<BaseItem> GetItemsWithImages(BaseItem item) protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{ {
return _libraryManager.GetItemList(new InternalItemsQuery 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); 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); 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 // determine sizes for each image that will composited into the final image
var iSlice = Convert.ToInt32(width * 0.23475); var iSlice = Convert.ToInt32(width / iCount);
int iTrans = Convert.ToInt32(height * .25); int iHeight = Convert.ToInt32(height * 1.00);
int iHeight = Convert.ToInt32(height * .70);
var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
int imageIndex = 0; int imageIndex = 0;
for (int i = 0; i < iCount; i++)
for (int i = 0; i < 4; i++)
{ {
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
{ {
imageIndex = newIndex; imageIndex = newIndex;
if (currentBitmap == null) if (currentBitmap == null)
{ {
continue; continue;
@ -108,44 +105,7 @@ namespace Jellyfin.Drawing.Skia
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
{ {
// draw image onto canvas // draw image onto canvas
canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing); canvas.DrawImage(subset ?? image, iSlice * i, 0);
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));
}
}
} }
} }
} }

View File

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using Jellyfin.Server.SocketSharp;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -35,8 +34,6 @@ namespace Jellyfin.Server
public override bool CanSelfRestart => StartupOptions.RestartPath != null; public override bool CanSelfRestart => StartupOptions.RestartPath != null;
protected override bool SupportsDualModeSockets => true;
protected override void RestartInternal() => Program.Restart(); protected override void RestartInternal() => Program.Restart();
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal() protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
@ -45,17 +42,5 @@ namespace Jellyfin.Server
} }
protected override void ShutdownInternal() => Program.Shutdown(); 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--> <!-- We need C# 7.1 for async main-->
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<!-- Disable documentation warnings (for now) --> <!-- Disable documentation warnings (for now) -->
<NoWarn>SA1600;CS1591</NoWarn> <NoWarn>SA1600;SA1601;CS1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -23,10 +24,6 @@
<EmbeddedResource Include="Resources/Configuration/*" /> <EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code analysers--> <!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" /> <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.Controller.Drawing;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -124,7 +125,7 @@ namespace Jellyfin.Server
SQLitePCL.Batteries_V2.Init(); SQLitePCL.Batteries_V2.Init();
// Allow all https requests // 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); var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths);
@ -144,8 +145,6 @@ namespace Jellyfin.Server
await appHost.RunStartupTasks().ConfigureAwait(false); await appHost.RunStartupTasks().ConfigureAwait(false);
// TODO: read input for a stop command
try try
{ {
// Block main thread until shutdown // Block main thread until shutdown
@ -175,7 +174,7 @@ namespace Jellyfin.Server
{ {
// dataDir // dataDir
// IF --datadir // IF --datadir
// ELSE IF $JELLYFIN_DATA_PATH // ELSE IF $JELLYFIN_DATA_DIR
// ELSE IF windows, use <%APPDATA%>/jellyfin // ELSE IF windows, use <%APPDATA%>/jellyfin
// ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
// ELSE use $HOME/.local/share/jellyfin // ELSE use $HOME/.local/share/jellyfin
@ -183,7 +182,7 @@ namespace Jellyfin.Server
if (string.IsNullOrEmpty(dataDir)) if (string.IsNullOrEmpty(dataDir))
{ {
dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH"); dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR");
if (string.IsNullOrEmpty(dataDir)) if (string.IsNullOrEmpty(dataDir))
{ {
@ -192,8 +191,6 @@ namespace Jellyfin.Server
} }
} }
Directory.CreateDirectory(dataDir);
// configDir // configDir
// IF --configdir // IF --configdir
// ELSE IF $JELLYFIN_CONFIG_DIR // ELSE IF $JELLYFIN_CONFIG_DIR
@ -286,6 +283,7 @@ namespace Jellyfin.Server
// Ensure the main folders exist before we continue // Ensure the main folders exist before we continue
try try
{ {
Directory.CreateDirectory(dataDir);
Directory.CreateDirectory(logDir); Directory.CreateDirectory(logDir);
Directory.CreateDirectory(configDir); Directory.CreateDirectory(configDir);
Directory.CreateDirectory(cacheDir); 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.")] [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string LogDir { get; set; } 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; } 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; } public string FFprobePath { get; set; }
[Option("service", Required = false, HelpText = "Run as headless service.")] [Option("service", Required = false, HelpText = "Run as headless service.")]

View File

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

View File

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

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