mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
Merge remote-tracking branch 'remotes/upstream/master' into kestrel_poc
This commit is contained in:
commit
0abe57e930
183
.ci/azure-pipelines.yml
Normal file
183
.ci/azure-pipelines.yml
Normal 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: DownloadGitHubReleases@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
|
||||
|
||||
|
@ -21,6 +21,9 @@
|
||||
- [WillWill56](https://github.com/WillWill56)
|
||||
- [Liggy](https://github.com/Liggy)
|
||||
- [fruhnow](https://github.com/fruhnow)
|
||||
- [Lynxy](https://github.com/Lynxy)
|
||||
- [fasheng](https://github.com/fasheng)
|
||||
- [ploughpuff](https://github.com/ploughpuff)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
18
Dockerfile
18
Dockerfile
@ -4,10 +4,8 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
RUN dotnet publish \
|
||||
--configuration release \
|
||||
--output /jellyfin \
|
||||
Jellyfin.Server
|
||||
RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
|
||||
|
||||
FROM jellyfin/ffmpeg as ffmpeg
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
|
||||
@ -22,6 +20,16 @@ RUN apt-get update \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=ffmpeg / /
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config /media
|
||||
ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
|
||||
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
|
||||
--datadir /config \
|
||||
--cachedir /cache \
|
||||
--ffmpeg /usr/local/bin/ffmpeg \
|
||||
--ffprobe /usr/local/bin/ffprobe
|
||||
|
@ -17,11 +17,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish \
|
||||
-r linux-arm \
|
||||
--configuration release \
|
||||
--output /jellyfin \
|
||||
Jellyfin.Server
|
||||
RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
|
||||
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
|
||||
@ -31,6 +28,16 @@ RUN apt-get update \
|
||||
&& mkdir -p /cache /config /media \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config /media
|
||||
ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
|
||||
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
|
||||
--datadir /config \
|
||||
--cachedir /cache \
|
||||
--ffmpeg /usr/bin/ffmpeg \
|
||||
--ffprobe /usr/bin/ffprobe
|
||||
|
@ -18,11 +18,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish \
|
||||
-r linux-arm64 \
|
||||
--configuration release \
|
||||
--output /jellyfin \
|
||||
Jellyfin.Server
|
||||
RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
|
||||
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
|
||||
@ -32,6 +29,16 @@ RUN apt-get update \
|
||||
&& mkdir -p /cache /config /media \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config /media
|
||||
ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
|
||||
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
|
||||
--datadir /config \
|
||||
--cachedir /cache \
|
||||
--ffmpeg /usr/bin/ffmpeg \
|
||||
--ffprobe /usr/bin/ffprobe
|
||||
|
@ -26,17 +26,17 @@ namespace DvdLib.Ifo
|
||||
|
||||
if (vmgPath == null)
|
||||
{
|
||||
var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var ifo in allIfos)
|
||||
foreach (var ifo in allFiles)
|
||||
{
|
||||
var num = ifo.Name.Split('_').ElementAtOrDefault(1);
|
||||
var numbersRead = new List<ushort>();
|
||||
if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out var ifoNumber) && !numbersRead.Contains(ifoNumber))
|
||||
var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
|
||||
{
|
||||
ReadVTS(ifoNumber, ifo.FullName);
|
||||
numbersRead.Add(ifoNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,7 +76,7 @@ namespace DvdLib.Ifo
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles)
|
||||
private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
|
||||
{
|
||||
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
|
||||
|
||||
|
@ -7,6 +7,7 @@ namespace Emby.Dlna.Configuration
|
||||
public bool EnableServer { get; set; }
|
||||
public bool EnableDebugLog { get; set; }
|
||||
public bool BlastAliveMessages { get; set; }
|
||||
public bool SendOnlyMatchedHost { get; set; }
|
||||
public int ClientDiscoveryIntervalSeconds { get; set; }
|
||||
public int BlastAliveMessageIntervalSeconds { get; set; }
|
||||
public string DefaultUserId { get; set; }
|
||||
@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration
|
||||
EnablePlayTo = true;
|
||||
EnableServer = true;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
BlastAliveMessageIntervalSeconds = 1800;
|
||||
}
|
||||
|
@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
|
||||
if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
|
||||
{
|
||||
var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
|
||||
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
|
||||
|
||||
_didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
|
||||
}
|
||||
@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
}
|
||||
else
|
||||
{
|
||||
var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
|
||||
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
|
||||
totalCount = childrenResult.TotalRecordCount;
|
||||
|
||||
provided = childrenResult.Items.Length;
|
||||
|
@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
AddCommonFields(item, itemStubType, context, writer, filter);
|
||||
|
||||
var hasArtists = item as IHasArtist;
|
||||
var hasAlbumArtists = item as IHasAlbumArtist;
|
||||
|
||||
if (hasArtists != null)
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
foreach (var artist in hasArtists.Artists)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Reflection;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -31,7 +31,7 @@ namespace Emby.Dlna
|
||||
private readonly ILogger _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IAssemblyInfo _assemblyInfo;
|
||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||
|
||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||
|
||||
@ -41,8 +41,7 @@ namespace Emby.Dlna
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IServerApplicationHost appHost,
|
||||
IAssemblyInfo assemblyInfo)
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
@ -50,7 +49,6 @@ namespace Emby.Dlna
|
||||
_logger = loggerFactory.CreateLogger("Dlna");
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_appHost = appHost;
|
||||
_assemblyInfo = assemblyInfo;
|
||||
}
|
||||
|
||||
public async Task InitProfilesAsync()
|
||||
@ -367,15 +365,18 @@ namespace Emby.Dlna
|
||||
|
||||
var systemProfilesPath = SystemProfilesPath;
|
||||
|
||||
foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType())
|
||||
.Where(i => i.StartsWith(namespaceName))
|
||||
.ToList())
|
||||
foreach (var name in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!name.StartsWith(namespaceName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||
|
||||
var path = Path.Combine(systemProfilesPath, filename);
|
||||
|
||||
using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name))
|
||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||
{
|
||||
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||
|
||||
@ -513,7 +514,7 @@ namespace Emby.Dlna
|
||||
return new ImageStream
|
||||
{
|
||||
Format = format,
|
||||
Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource)
|
||||
Stream = _assembly.GetManifestResourceStream(resource)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -169,9 +169,10 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
if (_communicationsServer == null)
|
||||
{
|
||||
var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
|
||||
var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows ||
|
||||
_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux;
|
||||
|
||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
{
|
||||
IsShared = true
|
||||
};
|
||||
@ -229,7 +230,7 @@ namespace Emby.Dlna.Main
|
||||
|
||||
try
|
||||
{
|
||||
_Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion);
|
||||
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
|
||||
_Publisher.LogFunction = LogMessage;
|
||||
_Publisher.SupportPnpRootDevice = false;
|
||||
|
||||
@ -245,17 +246,17 @@ namespace Emby.Dlna.Main
|
||||
|
||||
private async Task RegisterServerEndpoints()
|
||||
{
|
||||
var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList();
|
||||
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var udn = CreateUuid(_appHost.SystemId);
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
// TODO: Remove this condition on platforms that support it
|
||||
//if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||
//{
|
||||
// continue;
|
||||
//}
|
||||
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||
{
|
||||
// Not support IPv6 right now
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
@ -268,6 +269,8 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
|
||||
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
Address = address,
|
||||
SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
|
||||
FriendlyName = "Jellyfin",
|
||||
Manufacturer = "Jellyfin",
|
||||
ModelName = "Jellyfin Server",
|
||||
|
@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (arg.Direction == "out")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.Name == "InstanceID")
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, null);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
|
||||
@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (arg.Direction == "out")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.Name == "InstanceID")
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (arg.Name == "InstanceID")
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else if (dictionary.ContainsKey(arg.Name))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
@ -22,7 +21,9 @@ namespace Emby.Naming.TV
|
||||
// There were no failed tests without this block, but to be safe, we can keep it until
|
||||
// the regex which require file extensions are modified so that they don't need them.
|
||||
if (IsDirectory)
|
||||
{
|
||||
path += ".mp4";
|
||||
}
|
||||
|
||||
EpisodePathParserResult result = null;
|
||||
|
||||
@ -35,6 +36,7 @@ namespace Emby.Naming.TV
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNamed.HasValue)
|
||||
{
|
||||
if (expression.IsNamed != isNamed.Value)
|
||||
@ -42,6 +44,7 @@ namespace Emby.Naming.TV
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOptimistic.HasValue)
|
||||
{
|
||||
if (expression.IsOptimistic != isOptimistic.Value)
|
||||
@ -191,13 +194,20 @@ namespace Emby.Naming.TV
|
||||
|
||||
private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
|
||||
{
|
||||
var results = expressions
|
||||
.Where(i => i.IsNamed)
|
||||
.Select(i => Parse(path, i))
|
||||
.Where(i => i.Success);
|
||||
|
||||
foreach (var result in results)
|
||||
foreach (var i in expressions)
|
||||
{
|
||||
if (!i.IsNamed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = Parse(path, i);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(info.SeriesName))
|
||||
{
|
||||
info.SeriesName = result.SeriesName;
|
||||
@ -208,12 +218,10 @@ namespace Emby.Naming.TV
|
||||
info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.SeriesName))
|
||||
if (!string.IsNullOrEmpty(info.SeriesName)
|
||||
&& (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
|
||||
{
|
||||
if (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +39,13 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
|
||||
|
||||
foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty)))
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
if (item.UserId == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(item.UserId);
|
||||
|
||||
if (user != null)
|
||||
|
@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data;
|
||||
using Emby.Server.Implementations.Devices;
|
||||
using Emby.Server.Implementations.Diagnostics;
|
||||
using Emby.Server.Implementations.Dto;
|
||||
using Emby.Server.Implementations.FFMpeg;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.Security;
|
||||
using Emby.Server.Implementations.IO;
|
||||
@ -541,7 +540,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||
|
||||
MediaEncoder.Init();
|
||||
MediaEncoder.SetFFmpegPath();
|
||||
|
||||
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
|
||||
//{
|
||||
@ -813,10 +812,8 @@ namespace Emby.Server.Implementations
|
||||
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
|
||||
serviceCollection.AddSingleton(TVSeriesManager);
|
||||
|
||||
var encryptionManager = new EncryptionManager();
|
||||
serviceCollection.AddSingleton<IEncryptionManager>(encryptionManager);
|
||||
|
||||
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
|
||||
|
||||
serviceCollection.AddSingleton(DeviceManager);
|
||||
|
||||
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
|
||||
@ -838,7 +835,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton(SessionManager);
|
||||
|
||||
serviceCollection.AddSingleton<IDlnaManager>(
|
||||
new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo));
|
||||
new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this));
|
||||
|
||||
CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
|
||||
serviceCollection.AddSingleton(CollectionManager);
|
||||
@ -861,7 +858,18 @@ namespace Emby.Server.Implementations
|
||||
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
|
||||
serviceCollection.AddSingleton(ChapterManager);
|
||||
|
||||
RegisterMediaEncoder(serviceCollection);
|
||||
MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
|
||||
LoggerFactory,
|
||||
JsonSerializer,
|
||||
StartupOptions.FFmpegPath,
|
||||
StartupOptions.FFprobePath,
|
||||
ServerConfigurationManager,
|
||||
FileSystemManager,
|
||||
() => SubtitleEncoder,
|
||||
() => MediaSourceManager,
|
||||
ProcessFactory,
|
||||
5000);
|
||||
serviceCollection.AddSingleton(MediaEncoder);
|
||||
|
||||
EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
|
||||
serviceCollection.AddSingleton(EncodingManager);
|
||||
@ -970,85 +978,6 @@ namespace Emby.Server.Implementations
|
||||
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
|
||||
}
|
||||
|
||||
protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()
|
||||
{
|
||||
var info = new FFMpegInstallInfo();
|
||||
|
||||
// Windows builds: http://ffmpeg.zeranoe.com/builds/
|
||||
// Linux builds: http://johnvansickle.com/ffmpeg/
|
||||
// OS X builds: http://ffmpegmac.net/
|
||||
// OS X x64: http://www.evermeet.cx/ffmpeg/
|
||||
|
||||
if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux)
|
||||
{
|
||||
info.FFMpegFilename = "ffmpeg";
|
||||
info.FFProbeFilename = "ffprobe";
|
||||
info.ArchiveType = "7z";
|
||||
info.Version = "20170308";
|
||||
}
|
||||
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
|
||||
{
|
||||
info.FFMpegFilename = "ffmpeg.exe";
|
||||
info.FFProbeFilename = "ffprobe.exe";
|
||||
info.Version = "20170308";
|
||||
info.ArchiveType = "7z";
|
||||
}
|
||||
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
|
||||
{
|
||||
info.FFMpegFilename = "ffmpeg";
|
||||
info.FFProbeFilename = "ffprobe";
|
||||
info.ArchiveType = "7z";
|
||||
info.Version = "20170308";
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
protected FFMpegInfo GetFFMpegInfo()
|
||||
{
|
||||
return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
|
||||
.GetFFMpegInfo(StartupOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the media encoder.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private void RegisterMediaEncoder(IServiceCollection serviceCollection)
|
||||
{
|
||||
string encoderPath = null;
|
||||
string probePath = null;
|
||||
|
||||
var info = GetFFMpegInfo();
|
||||
|
||||
encoderPath = info.EncoderPath;
|
||||
probePath = info.ProbePath;
|
||||
var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
|
||||
LoggerFactory,
|
||||
JsonSerializer,
|
||||
encoderPath,
|
||||
probePath,
|
||||
hasExternalEncoder,
|
||||
ServerConfigurationManager,
|
||||
FileSystemManager,
|
||||
LiveTvManager,
|
||||
IsoManager,
|
||||
LibraryManager,
|
||||
ChannelManager,
|
||||
SessionManager,
|
||||
() => SubtitleEncoder,
|
||||
() => MediaSourceManager,
|
||||
HttpClient,
|
||||
ZipClient,
|
||||
ProcessFactory,
|
||||
5000);
|
||||
|
||||
MediaEncoder = mediaEncoder;
|
||||
serviceCollection.AddSingleton(MediaEncoder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user repository.
|
||||
/// </summary>
|
||||
@ -1481,7 +1410,7 @@ namespace Emby.Server.Implementations
|
||||
ServerName = FriendlyName,
|
||||
LocalAddress = localAddress,
|
||||
SupportsLibraryMonitor = true,
|
||||
EncoderLocationType = MediaEncoder.EncoderLocationType,
|
||||
EncoderLocation = MediaEncoder.EncoderLocation,
|
||||
SystemArchitecture = EnvironmentInfo.SystemArchitecture,
|
||||
SystemUpdateLevel = SystemUpdateLevel,
|
||||
PackageName = StartupOptions.PackageName
|
||||
@ -1598,7 +1527,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
addresses.AddRange(NetworkManager.GetLocalIpAddresses());
|
||||
addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
|
||||
}
|
||||
|
||||
var resultList = new List<IpAddressInfo>();
|
||||
|
@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
foreach (var item in returnItems)
|
||||
{
|
||||
var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None);
|
||||
Task.WaitAll(task);
|
||||
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= allChannelsList.Count;
|
||||
|
||||
double percent = (double)numComplete / allChannelsList.Count;
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
|
||||
@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
var folder = item as Folder;
|
||||
|
||||
if (folder != null)
|
||||
if (item is Folder folder)
|
||||
{
|
||||
await GetChannelItemsInternal(new InternalItemsQuery
|
||||
{
|
||||
|
@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels
|
||||
public static string GetUserDistinctValue(User user)
|
||||
{
|
||||
var channels = user.Policy.EnabledChannels
|
||||
.OrderBy(i => i)
|
||||
.ToList();
|
||||
.OrderBy(i => i);
|
||||
|
||||
return string.Join("|", channels.ToArray());
|
||||
return string.Join("|", channels);
|
||||
}
|
||||
|
||||
private void CleanDatabase(CancellationToken cancellationToken)
|
||||
{
|
||||
var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
|
||||
|
||||
var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Channel).Name }
|
||||
IncludeItemTypes = new[] { typeof(Channel).Name },
|
||||
ExcludeItemIds = installedChannelIds.ToArray()
|
||||
});
|
||||
|
||||
var invalidIds = databaseIds
|
||||
.Except(installedChannelIds)
|
||||
.ToList();
|
||||
|
||||
foreach (var id in invalidIds)
|
||||
foreach (var channel in uninstalledChannels)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
CleanChannel(id, cancellationToken);
|
||||
CleanChannel((Channel)channel, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanChannel(Guid id, CancellationToken cancellationToken)
|
||||
private void CleanChannel(Channel channel, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Cleaning channel {0} from database", id);
|
||||
_logger.LogInformation("Cleaning channel {0} from database", channel.Id);
|
||||
|
||||
// Delete all channel items
|
||||
var allIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
ChannelIds = new[] { id }
|
||||
ChannelIds = new[] { channel.Id }
|
||||
});
|
||||
|
||||
foreach (var deleteId in allIds)
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
DeleteItem(deleteId);
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
|
||||
}, false);
|
||||
}
|
||||
|
||||
// Finally, delete the channel itself
|
||||
DeleteItem(id);
|
||||
}
|
||||
|
||||
private void DeleteItem(Guid id)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
_libraryManager.DeleteItem(channel, new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
|
||||
|
@ -1,13 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
public class CryptographyProvider : ICryptoProvider
|
||||
{
|
||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||
{
|
||||
"MD5",
|
||||
"System.Security.Cryptography.MD5",
|
||||
"SHA",
|
||||
"SHA1",
|
||||
"System.Security.Cryptography.SHA1",
|
||||
"SHA256",
|
||||
"SHA-256",
|
||||
"System.Security.Cryptography.SHA256",
|
||||
"SHA384",
|
||||
"SHA-384",
|
||||
"System.Security.Cryptography.SHA384",
|
||||
"SHA512",
|
||||
"SHA-512",
|
||||
"System.Security.Cryptography.SHA512"
|
||||
};
|
||||
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
|
||||
private RandomNumberGenerator _randomNumberGenerator;
|
||||
|
||||
private const int _defaultIterations = 1000;
|
||||
|
||||
public CryptographyProvider()
|
||||
{
|
||||
//FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
||||
//Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
||||
//there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
||||
//Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
||||
_randomNumberGenerator = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
public Guid GetMD5(string str)
|
||||
{
|
||||
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
||||
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
return provider.ComputeHash(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetSupportedHashMethods()
|
||||
{
|
||||
return _supportedHashMethods;
|
||||
}
|
||||
|
||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||
{
|
||||
//downgrading for now as we need this library to be dotnetstandard compliant
|
||||
//with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
||||
if (method == DefaultHashMethod)
|
||||
{
|
||||
using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
|
||||
{
|
||||
return r.GetBytes(32);
|
||||
}
|
||||
}
|
||||
|
||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes)
|
||||
{
|
||||
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
|
||||
}
|
||||
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
|
||||
{
|
||||
return ComputeHash(DefaultHashMethod, bytes);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||
{
|
||||
if (hashMethod == DefaultHashMethod)
|
||||
{
|
||||
return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
|
||||
}
|
||||
else if (_supportedHashMethods.Contains(hashMethod))
|
||||
{
|
||||
using (var h = HashAlgorithm.Create(hashMethod))
|
||||
{
|
||||
if (salt.Length == 0)
|
||||
{
|
||||
return h.ComputeHash(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
||||
Array.Copy(bytes, salted, bytes.Length);
|
||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
||||
return h.ComputeHash(salted);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||
{
|
||||
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(PasswordHash hash)
|
||||
{
|
||||
int iterations = _defaultIterations;
|
||||
if (!hash.Parameters.ContainsKey("iterations"))
|
||||
{
|
||||
hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
iterations = int.Parse(hash.Parameters["iterations"]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
|
||||
}
|
||||
}
|
||||
|
||||
return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
|
||||
}
|
||||
|
||||
public byte[] GenerateSalt()
|
||||
{
|
||||
byte[] salt = new byte[64];
|
||||
_randomNumberGenerator.GetBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2279,11 +2279,10 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"Audio",
|
||||
"MusicAlbum",
|
||||
"MusicVideo",
|
||||
"Book",
|
||||
"AudioBook",
|
||||
"AudioPodcast"
|
||||
"Episode",
|
||||
"Season"
|
||||
};
|
||||
|
||||
private bool HasSeriesFields(InternalItemsQuery query)
|
||||
|
@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
list.Add(row[0].ReadGuidFromBlob());
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Logger.LogError(ex, "Error while getting user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
TryMigrateToLocalUsersTable(connection);
|
||||
}
|
||||
|
||||
RemoveEmptyPasswordHashes();
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEmptyPasswordHashes()
|
||||
{
|
||||
foreach (var user in RetrieveAllUsers())
|
||||
{
|
||||
// If the user password is the sha1 hash of the empty string, remove it
|
||||
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|
||||
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
user.Password = null;
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = CreateConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
statement.TryBind("@InternalId", user.InternalId);
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a user in the repo
|
||||
/// </summary>
|
||||
|
@ -5,8 +5,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
return GetBaseItemDto(item, options, user, owner);
|
||||
}
|
||||
|
||||
public BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
return GetBaseItemDtos(items, items.Count, options, user, owner);
|
||||
}
|
||||
|
||||
public BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
return GetBaseItemDtos(items, items.Length, options, user, owner);
|
||||
}
|
||||
public BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
=> GetBaseItemDtos(items, items.Count, options, user, owner);
|
||||
|
||||
public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
|
||||
public string Name => "Default";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
|
||||
// This is dumb and an artifact of the backwards way auth providers were designed.
|
||||
// This version of authenticate was never meant to be called, but needs to be here for interface compat
|
||||
// Only the providers that don't provide local user support use this
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
// This is the verson that we need to use for local users. Because reasons.
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
||||
{
|
||||
bool success = false;
|
||||
if (resolvedUser == null)
|
||||
{
|
||||
throw new Exception("Invalid username or password");
|
||||
}
|
||||
|
||||
var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
||||
// As long as jellyfin supports passwordless users, we need this little block here to accomodate
|
||||
if (IsPasswordEmpty(resolvedUser, password))
|
||||
{
|
||||
return Task.FromResult(new ProviderAuthenticationResult
|
||||
{
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
ConvertPasswordFormat(resolvedUser);
|
||||
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
|
||||
byte[] calculatedHash;
|
||||
string calculatedHashString;
|
||||
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
|
||||
{
|
||||
if (string.IsNullOrEmpty(readyHash.Salt))
|
||||
{
|
||||
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
|
||||
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
|
||||
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
||||
}
|
||||
|
||||
if (calculatedHashString == readyHash.Hash)
|
||||
{
|
||||
success = true;
|
||||
// throw new Exception("Invalid username or password");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
|
||||
}
|
||||
|
||||
// var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
|
||||
});
|
||||
}
|
||||
|
||||
// This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
|
||||
// but at least they are in the new format.
|
||||
private void ConvertPasswordFormat(User user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.Password.Contains("$"))
|
||||
{
|
||||
string hash = user.Password;
|
||||
user.Password = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
|
||||
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
||||
{
|
||||
string hash = user.EasyPassword;
|
||||
user.EasyPassword = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> HasPassword(User user)
|
||||
{
|
||||
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
||||
return Task.FromResult(hasConfiguredPassword);
|
||||
}
|
||||
|
||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
||||
private bool IsPasswordEmpty(User user, string password)
|
||||
{
|
||||
return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
||||
return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
|
||||
}
|
||||
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
string newPasswordHash = null;
|
||||
|
||||
if (newPassword != null)
|
||||
ConvertPasswordFormat(user);
|
||||
// This is needed to support changing a no password user to a password user
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
newPasswordHash = GetHashedString(user, newPassword);
|
||||
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
|
||||
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
|
||||
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
|
||||
user.Password = newPasswordHash.ToString();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
||||
PasswordHash passwordHash = new PasswordHash(user.Password);
|
||||
if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
||||
passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
|
||||
passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
|
||||
}
|
||||
else if (newPassword != null)
|
||||
{
|
||||
passwordHash.Hash = GetHashedString(user, newPassword);
|
||||
}
|
||||
|
||||
user.Password = newPasswordHash;
|
||||
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(passwordHash.Hash));
|
||||
}
|
||||
|
||||
user.Password = passwordHash.ToString();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetPasswordHash(User user)
|
||||
{
|
||||
return string.IsNullOrEmpty(user.Password)
|
||||
? GetEmptyHashedString(user)
|
||||
: user.Password;
|
||||
return user.Password;
|
||||
}
|
||||
|
||||
public string GetEmptyHashedString(User user)
|
||||
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||
{
|
||||
return GetHashedString(user, string.Empty);
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
|
||||
/// </summary>
|
||||
public string GetHashedString(User user, string str)
|
||||
{
|
||||
var salt = user.Salt;
|
||||
if (salt != null)
|
||||
PasswordHash passwordHash;
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
// return BCrypt.HashPassword(str, salt);
|
||||
passwordHash = new PasswordHash(_cryptographyProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvertPasswordFormat(user);
|
||||
passwordHash = new PasswordHash(user.Password);
|
||||
}
|
||||
|
||||
// legacy
|
||||
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
|
||||
if (passwordHash.SaltBytes != null)
|
||||
{
|
||||
// the password is modern format with PBKDF and we should take advantage of that
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||
}
|
||||
else
|
||||
{
|
||||
// the password has no salt and should be called with the older method for safety
|
||||
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Events;
|
||||
@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValidUsername(string username)
|
||||
public static bool IsValidUsername(string username)
|
||||
{
|
||||
// Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
||||
foreach (var currentChar in username)
|
||||
{
|
||||
if (!IsValidUsernameCharacter(currentChar))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
//This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||
//In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
||||
return Regex.IsMatch(username, "^[\\w-'._@]*$");
|
||||
}
|
||||
|
||||
private static bool IsValidUsernameCharacter(char i)
|
||||
{
|
||||
return !char.Equals(i, '<') && !char.Equals(i, '>');
|
||||
return IsValidUsername(i.ToString());
|
||||
}
|
||||
|
||||
public string MakeValidUsername(string username)
|
||||
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
|
||||
private string GetLocalPasswordHash(User user)
|
||||
{
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? _defaultAuthenticationProvider.GetEmptyHashedString(user)
|
||||
? null
|
||||
: user.EasyPassword;
|
||||
}
|
||||
|
||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
||||
{
|
||||
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the users from the repository
|
||||
/// </summary>
|
||||
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
|
||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
||||
|
||||
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
hasConfiguredEasyPassword :
|
||||
hasConfiguredPassword;
|
||||
|
||||
var dto = new UserDto
|
||||
UserDto dto = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
|
||||
dto.EnableAutoLogin = true;
|
||||
}
|
||||
|
||||
var image = user.GetImageInfo(ImageType.Primary, 0);
|
||||
ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (!IsValidUsername(name))
|
||||
{
|
||||
throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||
}
|
||||
|
||||
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
|
@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization
|
||||
{
|
||||
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
|
||||
|
||||
Directory.CreateDirectory(LocalizationPath);
|
||||
|
||||
var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName);
|
||||
|
||||
// Extract from the assembly
|
||||
foreach (var resource in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization
|
||||
continue;
|
||||
}
|
||||
|
||||
string filename = "ratings-" + resource.Substring(ratingsResource.Length);
|
||||
string countryCode = resource.Substring(ratingsResource.Length, 2);
|
||||
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (existingFiles.Contains(filename))
|
||||
using (var str = _assembly.GetManifestResourceStream(resource))
|
||||
using (var reader = new StreamReader(str))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var stream = _assembly.GetManifestResourceStream(resource))
|
||||
{
|
||||
string target = Path.Combine(LocalizationPath, filename);
|
||||
_logger.LogInformation("Extracting ratings to {0}", target);
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
|
||||
string line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
await stream.CopyToAsync(fs);
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] parts = line.Split(',');
|
||||
if (parts.Length == 2
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
|
||||
{
|
||||
dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in GetRatingsFiles(LocalizationPath))
|
||||
{
|
||||
await LoadRatings(file);
|
||||
_allParentalRatings[countryCode] = dict;
|
||||
}
|
||||
|
||||
LoadAdditionalRatings();
|
||||
|
||||
await LoadCultures();
|
||||
}
|
||||
|
||||
private void LoadAdditionalRatings()
|
||||
{
|
||||
LoadRatings("au", new[]
|
||||
{
|
||||
new ParentalRating("AU-G", 1),
|
||||
new ParentalRating("AU-PG", 5),
|
||||
new ParentalRating("AU-M", 6),
|
||||
new ParentalRating("AU-MA15+", 7),
|
||||
new ParentalRating("AU-M15+", 8),
|
||||
new ParentalRating("AU-R18+", 9),
|
||||
new ParentalRating("AU-X18+", 10),
|
||||
new ParentalRating("AU-RC", 11)
|
||||
});
|
||||
|
||||
LoadRatings("be", new[]
|
||||
{
|
||||
new ParentalRating("BE-AL", 1),
|
||||
new ParentalRating("BE-MG6", 2),
|
||||
new ParentalRating("BE-6", 3),
|
||||
new ParentalRating("BE-9", 5),
|
||||
new ParentalRating("BE-12", 6),
|
||||
new ParentalRating("BE-16", 8)
|
||||
});
|
||||
|
||||
LoadRatings("de", new[]
|
||||
{
|
||||
new ParentalRating("DE-0", 1),
|
||||
new ParentalRating("FSK-0", 1),
|
||||
new ParentalRating("DE-6", 5),
|
||||
new ParentalRating("FSK-6", 5),
|
||||
new ParentalRating("DE-12", 7),
|
||||
new ParentalRating("FSK-12", 7),
|
||||
new ParentalRating("DE-16", 8),
|
||||
new ParentalRating("FSK-16", 8),
|
||||
new ParentalRating("DE-18", 9),
|
||||
new ParentalRating("FSK-18", 9)
|
||||
});
|
||||
|
||||
LoadRatings("ru", new[]
|
||||
{
|
||||
new ParentalRating("RU-0+", 1),
|
||||
new ParentalRating("RU-6+", 3),
|
||||
new ParentalRating("RU-12+", 7),
|
||||
new ParentalRating("RU-16+", 9),
|
||||
new ParentalRating("RU-18+", 10)
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadRatings(string country, ParentalRating[] ratings)
|
||||
{
|
||||
_allParentalRatings[country] = ratings.ToDictionary(i => i.Name);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetRatingsFiles(string directory)
|
||||
=> _fileSystem.GetFilePaths(directory, false)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localization path.
|
||||
/// </summary>
|
||||
/// <value>The localization path.</value>
|
||||
public string LocalizationPath
|
||||
=> Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization");
|
||||
|
||||
public string NormalizeFormKD(string text)
|
||||
=> text.Normalize(NormalizationForm.FormKD);
|
||||
|
||||
@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the ratings.
|
||||
/// </summary>
|
||||
/// <param name="file">The file.</param>
|
||||
/// <returns>Dictionary{System.StringParentalRating}.</returns>
|
||||
private async Task LoadRatings(string file)
|
||||
{
|
||||
Dictionary<string, ParentalRating> dict
|
||||
= new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
using (var str = File.OpenRead(file))
|
||||
using (var reader = new StreamReader(str))
|
||||
{
|
||||
string line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] parts = line.Split(',');
|
||||
if (parts.Length == 2
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
|
||||
{
|
||||
dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value }));
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Misformed line in {Path}", file);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1];
|
||||
|
||||
_allParentalRatings[countryCode] = dict;
|
||||
}
|
||||
|
||||
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
|
||||
|
||||
/// <summary>
|
||||
|
8
Emby.Server.Implementations/Localization/Ratings/au.csv
Normal file
8
Emby.Server.Implementations/Localization/Ratings/au.csv
Normal 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
|
|
6
Emby.Server.Implementations/Localization/Ratings/be.csv
Normal file
6
Emby.Server.Implementations/Localization/Ratings/be.csv
Normal file
@ -0,0 +1,6 @@
|
||||
BE-AL,1
|
||||
BE-MG6,2
|
||||
BE-6,3
|
||||
BE-9,5
|
||||
BE-12,6
|
||||
BE-16,8
|
|
10
Emby.Server.Implementations/Localization/Ratings/de.csv
Normal file
10
Emby.Server.Implementations/Localization/Ratings/de.csv
Normal 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
|
|
5
Emby.Server.Implementations/Localization/Ratings/ru.csv
Normal file
5
Emby.Server.Implementations/Localization/Ratings/ru.csv
Normal file
@ -0,0 +1,5 @@
|
||||
RU-0+,1
|
||||
RU-6+,3
|
||||
RU-12+,7
|
||||
RU-16+,9
|
||||
RU-18+,10
|
|
@ -79,13 +79,13 @@ namespace Emby.Server.Implementations.Networking
|
||||
private IpAddressInfo[] _localIpAddresses;
|
||||
private readonly object _localIpAddressSyncLock = new object();
|
||||
|
||||
public IpAddressInfo[] GetLocalIpAddresses()
|
||||
public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
|
||||
{
|
||||
lock (_localIpAddressSyncLock)
|
||||
{
|
||||
if (_localIpAddresses == null)
|
||||
{
|
||||
var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray();
|
||||
var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray();
|
||||
|
||||
_localIpAddresses = addresses;
|
||||
|
||||
@ -95,9 +95,9 @@ namespace Emby.Server.Implementations.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<IPAddress>> GetLocalIpAddressesInternal()
|
||||
private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool ignoreVirtualInterface)
|
||||
{
|
||||
var list = GetIPsDefault()
|
||||
var list = GetIPsDefault(ignoreVirtualInterface)
|
||||
.ToList();
|
||||
|
||||
if (list.Count == 0)
|
||||
@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
return Dns.GetHostAddressesAsync(hostName);
|
||||
}
|
||||
|
||||
private List<IPAddress> GetIPsDefault()
|
||||
private List<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
|
||||
{
|
||||
NetworkInterface[] interfaces;
|
||||
|
||||
@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
// Try to exclude virtual adapters
|
||||
// http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
|
||||
var addr = ipProperties.GatewayAddresses.FirstOrDefault();
|
||||
if (addr == null || string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
|
||||
if (addr == null || ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new List<IPAddress>();
|
||||
}
|
||||
@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask)
|
||||
{
|
||||
IPAddress network1 = GetNetworkAddress(ToIPAddress(address1), ToIPAddress(subnetMask));
|
||||
IPAddress network2 = GetNetworkAddress(ToIPAddress(address2), ToIPAddress(subnetMask));
|
||||
return network1.Equals(network2);
|
||||
}
|
||||
|
||||
private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
byte[] ipAdressBytes = address.GetAddressBytes();
|
||||
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
||||
|
||||
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
||||
{
|
||||
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
||||
}
|
||||
|
||||
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
||||
for (int i = 0; i < broadcastAddress.Length; i++)
|
||||
{
|
||||
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
|
||||
}
|
||||
return new IPAddress(broadcastAddress);
|
||||
}
|
||||
|
||||
public IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address)
|
||||
{
|
||||
NetworkInterface[] interfaces;
|
||||
IPAddress ipaddress = ToIPAddress(address);
|
||||
|
||||
try
|
||||
{
|
||||
var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
|
||||
|
||||
interfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(i => validStatuses.Contains(i.OperationalStatus))
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error in GetAllNetworkInterfaces");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (NetworkInterface ni in interfaces)
|
||||
{
|
||||
if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null)
|
||||
{
|
||||
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
if (ip.Address.Equals(ipaddress) && ip.IPv4Mask != null)
|
||||
{
|
||||
return ToIpAddressInfo(ip.IPv4Mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
|
||||
{
|
||||
if (endpoint == null)
|
||||
|
@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using MediaBrowser.Controller.Security;
|
||||
|
||||
namespace Emby.Server.Implementations.Security
|
||||
{
|
||||
public class EncryptionManager : IEncryptionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Encrypts the string.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="ArgumentNullException">value</exception>
|
||||
public string EncryptString(string value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
return EncryptStringUniversal(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts the string.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="ArgumentNullException">value</exception>
|
||||
public string DecryptString(string value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
return DecryptStringUniversal(value);
|
||||
}
|
||||
|
||||
private static string EncryptStringUniversal(string value)
|
||||
{
|
||||
// Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private static string DecryptStringUniversal(string value)
|
||||
{
|
||||
// Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now
|
||||
|
||||
var bytes = Convert.FromBase64String(value);
|
||||
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Services
|
||||
private const char ComponentSeperator = '.';
|
||||
private const string VariablePrefix = "{";
|
||||
|
||||
readonly bool[] componentsWithSeparators;
|
||||
private readonly bool[] componentsWithSeparators;
|
||||
|
||||
private readonly string restPath;
|
||||
public bool IsWildCardPath { get; private set; }
|
||||
@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services
|
||||
public string Description { get; private set; }
|
||||
public bool IsHidden { get; private set; }
|
||||
|
||||
public int Priority { get; set; } //passed back to RouteAttribute
|
||||
|
||||
public IEnumerable<string> PathVariables => this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e));
|
||||
|
||||
public static string[] GetPathPartsForMatching(string pathInfo)
|
||||
{
|
||||
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
|
||||
@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
list.Add(hashPrefix + part);
|
||||
|
||||
var subParts = part.Split(ComponentSeperator);
|
||||
if (subParts.Length == 1) continue;
|
||||
if (part.IndexOf(ComponentSeperator) == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var subParts = part.Split(ComponentSeperator);
|
||||
foreach (var subPart in subParts)
|
||||
{
|
||||
list.Add(hashPrefix + subPart);
|
||||
@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
if (string.IsNullOrEmpty(component)) continue;
|
||||
|
||||
if (StringContains(component, VariablePrefix)
|
||||
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& component.IndexOf(ComponentSeperator) != -1)
|
||||
{
|
||||
hasSeparators.Add(true);
|
||||
@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
for (var i = 0; i < components.Length - 1; i++)
|
||||
{
|
||||
if (!this.isWildcard[i]) continue;
|
||||
if (!this.isWildcard[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.literalsToMatch[i + 1] == null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
@ -173,7 +176,7 @@ namespace Emby.Server.Implementations.Services
|
||||
}
|
||||
}
|
||||
|
||||
this.wildcardCount = this.isWildcard.Count(x => x);
|
||||
this.wildcardCount = this.isWildcard.Length;
|
||||
this.IsWildCardPath = this.wildcardCount > 0;
|
||||
|
||||
this.FirstMatchHashKey = !this.IsWildCardPath
|
||||
@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services
|
||||
: WildCardChar + PathSeperator + firstLiteralMatch;
|
||||
|
||||
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
|
||||
RegisterCaseInsenstivePropertyNameMappings();
|
||||
|
||||
_propertyNamesMap = new HashSet<string>(
|
||||
GetSerializableProperties(RequestType).Select(x => x.Name),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void RegisterCaseInsenstivePropertyNameMappings()
|
||||
internal static string[] IgnoreAttributesNamed = new[]
|
||||
{
|
||||
foreach (var propertyInfo in GetSerializableProperties(RequestType))
|
||||
{
|
||||
var propertyName = propertyInfo.Name;
|
||||
propertyNamesMap.Add(propertyName.ToLowerInvariant(), propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string[] IgnoreAttributesNamed = new[] {
|
||||
"IgnoreDataMemberAttribute",
|
||||
"JsonIgnoreAttribute"
|
||||
};
|
||||
@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
private static Type excludeType = typeof(Stream);
|
||||
|
||||
internal static List<PropertyInfo> GetSerializableProperties(Type type)
|
||||
internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
|
||||
{
|
||||
var list = new List<PropertyInfo>();
|
||||
var props = GetPublicProperties(type);
|
||||
|
||||
foreach (var prop in props)
|
||||
foreach (var prop in GetPublicProperties(type))
|
||||
{
|
||||
if (prop.GetMethod == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (excludeType == prop.PropertyType)
|
||||
if (prop.GetMethod == null
|
||||
|| excludeType == prop.PropertyType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
if (!ignored)
|
||||
{
|
||||
list.Add(prop);
|
||||
yield return prop;
|
||||
}
|
||||
}
|
||||
|
||||
// else return those properties that are not decorated with IgnoreDataMember
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<PropertyInfo> GetPublicProperties(Type type)
|
||||
private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
|
||||
{
|
||||
if (type.GetTypeInfo().IsInterface)
|
||||
if (type.IsInterface)
|
||||
{
|
||||
var propertyInfos = new List<PropertyInfo>();
|
||||
|
||||
var considered = new List<Type>();
|
||||
var considered = new List<Type>()
|
||||
{
|
||||
type
|
||||
};
|
||||
var queue = new Queue<Type>();
|
||||
considered.Add(type);
|
||||
queue.Enqueue(type);
|
||||
|
||||
while (queue.Count > 0)
|
||||
@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services
|
||||
var subType = queue.Dequeue();
|
||||
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
|
||||
{
|
||||
if (considered.Contains(subInterface)) continue;
|
||||
if (considered.Contains(subInterface))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
considered.Add(subInterface);
|
||||
queue.Enqueue(subInterface);
|
||||
}
|
||||
|
||||
var typeProperties = GetTypesPublicProperties(subType);
|
||||
|
||||
var newPropertyInfos = typeProperties
|
||||
var newPropertyInfos = GetTypesPublicProperties(subType)
|
||||
.Where(x => !propertyInfos.Contains(x));
|
||||
|
||||
propertyInfos.InsertRange(0, newPropertyInfos);
|
||||
@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services
|
||||
return propertyInfos;
|
||||
}
|
||||
|
||||
var list = new List<PropertyInfo>();
|
||||
|
||||
foreach (var t in GetTypesPublicProperties(type))
|
||||
{
|
||||
if (t.GetIndexParameters().Length == 0)
|
||||
{
|
||||
list.Add(t);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
return GetTypesPublicProperties(type)
|
||||
.Where(x => x.GetIndexParameters().Length == 0);
|
||||
}
|
||||
|
||||
private static PropertyInfo[] GetTypesPublicProperties(Type subType)
|
||||
private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
|
||||
{
|
||||
var pis = new List<PropertyInfo>();
|
||||
foreach (var pi in subType.GetRuntimeProperties())
|
||||
{
|
||||
var mi = pi.GetMethod ?? pi.SetMethod;
|
||||
if (mi != null && mi.IsStatic) continue;
|
||||
pis.Add(pi);
|
||||
if (mi != null && mi.IsStatic)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return pi;
|
||||
}
|
||||
return pis.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
private readonly StringMapTypeDeserializer typeDeserializer;
|
||||
|
||||
private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
|
||||
private readonly HashSet<string> _propertyNamesMap;
|
||||
|
||||
public int MatchScore(string httpMethod, string[] withPathInfoParts)
|
||||
{
|
||||
@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services
|
||||
return -1;
|
||||
}
|
||||
|
||||
var score = 0;
|
||||
|
||||
//Routes with least wildcard matches get the highest score
|
||||
score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
|
||||
|
||||
//Routes with less variable (and more literal) matches
|
||||
score += Math.Max((10 - VariableArgsCount), 1) * 100;
|
||||
var score = Math.Max((100 - wildcardMatchCount), 1) * 1000
|
||||
//Routes with less variable (and more literal) matches
|
||||
+ Math.Max((10 - VariableArgsCount), 1) * 100;
|
||||
|
||||
//Exact verb match is better than ANY
|
||||
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
|
||||
@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services
|
||||
return score;
|
||||
}
|
||||
|
||||
private bool StringContains(string str1, string str2)
|
||||
{
|
||||
return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For performance withPathInfoParts should already be a lower case string
|
||||
/// to minimize redundant matching operations.
|
||||
@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services
|
||||
if (i < this.TotalComponentsCount - 1)
|
||||
{
|
||||
// Continue to consume up until a match with the next literal
|
||||
while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1]))
|
||||
while (pathIx < withPathInfoParts.Length
|
||||
&& !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
pathIx++;
|
||||
wildcardMatchCount++;
|
||||
@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services
|
||||
continue;
|
||||
}
|
||||
|
||||
if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
|
||||
if (withPathInfoParts.Length <= pathIx
|
||||
|| !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pathIx++;
|
||||
}
|
||||
}
|
||||
@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services
|
||||
return pathIx == withPathInfoParts.Length;
|
||||
}
|
||||
|
||||
private static bool LiteralsEqual(string str1, string str2)
|
||||
{
|
||||
// Most cases
|
||||
if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle turkish i
|
||||
str1 = str1.ToUpperInvariant();
|
||||
str2 = str2.ToUpperInvariant();
|
||||
|
||||
// Invariant IgnoreCase would probably be better but it's not available in PCL
|
||||
return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private bool ExplodeComponents(ref string[] withPathInfoParts)
|
||||
{
|
||||
var totalComponents = new List<string>();
|
||||
for (var i = 0; i < withPathInfoParts.Length; i++)
|
||||
{
|
||||
var component = withPathInfoParts[i];
|
||||
if (string.IsNullOrEmpty(component)) continue;
|
||||
if (string.IsNullOrEmpty(component))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.PathComponentsCount != this.TotalComponentsCount
|
||||
&& this.componentsWithSeparators[i])
|
||||
{
|
||||
var subComponents = component.Split(ComponentSeperator);
|
||||
if (subComponents.Length < 2) return false;
|
||||
if (subComponents.Length < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
totalComponents.AddRange(subComponents);
|
||||
}
|
||||
else
|
||||
@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest))
|
||||
if (!this._propertyNamesMap.Contains(variableName))
|
||||
{
|
||||
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
sb.Append(PathSeperatorChar + requestComponents[j]);
|
||||
}
|
||||
|
||||
value = sb.ToString();
|
||||
}
|
||||
else
|
||||
@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services
|
||||
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
|
||||
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(value);
|
||||
var sb = new StringBuilder(value);
|
||||
pathIx++;
|
||||
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
||||
}
|
||||
|
||||
value = sb.ToString();
|
||||
}
|
||||
else
|
||||
@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services
|
||||
pathIx++;
|
||||
}
|
||||
|
||||
requestKeyValuesMap[propertyNameOnRequest] = value;
|
||||
requestKeyValuesMap[variableName] = value;
|
||||
}
|
||||
|
||||
if (queryStringAndFormData != null)
|
||||
|
@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
internal class PropertySerializerEntry
|
||||
{
|
||||
public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn)
|
||||
public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
|
||||
{
|
||||
PropertySetFn = propertySetFn;
|
||||
PropertyParseStringFn = propertyParseStringFn;
|
||||
PropertyType = PropertyType;
|
||||
}
|
||||
|
||||
public Action<object, object> PropertySetFn;
|
||||
public Func<string, object> PropertyParseStringFn;
|
||||
public Type PropertyType;
|
||||
public Action<object, object> PropertySetFn { get; private set; }
|
||||
public Func<string, object> PropertyParseStringFn { get; private set; }
|
||||
public Type PropertyType { get; private set; }
|
||||
}
|
||||
|
||||
private readonly Type type;
|
||||
@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services
|
||||
public Func<string, object> GetParseFn(Type propertyType)
|
||||
{
|
||||
if (propertyType == typeof(string))
|
||||
{
|
||||
return s => s;
|
||||
}
|
||||
|
||||
return _GetParseFn(propertyType);
|
||||
}
|
||||
@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services
|
||||
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
|
||||
var propertyType = propertyInfo.PropertyType;
|
||||
var propertyParseStringFn = GetParseFn(propertyType);
|
||||
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
|
||||
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
|
||||
|
||||
propertySetterMap[propertyInfo.Name] = propertySerializer;
|
||||
}
|
||||
@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
|
||||
{
|
||||
string propertyName = null;
|
||||
string propertyTextValue = null;
|
||||
PropertySerializerEntry propertySerializerEntry = null;
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
instance = _CreateInstanceFn(type);
|
||||
}
|
||||
|
||||
foreach (var pair in keyValuePairs)
|
||||
{
|
||||
propertyName = pair.Key;
|
||||
propertyTextValue = pair.Value;
|
||||
string propertyName = pair.Key;
|
||||
string propertyTextValue = pair.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(propertyTextValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry))
|
||||
{
|
||||
if (propertyName == "v")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (propertySerializerEntry.PropertySetFn == null)
|
||||
if (string.IsNullOrEmpty(propertyTextValue)
|
||||
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|
||||
|| propertySerializerEntry.PropertySetFn == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
propertySerializerEntry.PropertySetFn(instance, value);
|
||||
}
|
||||
|
||||
@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
public static string LeftPart(string strVal, char needle)
|
||||
{
|
||||
if (strVal == null) return null;
|
||||
if (strVal == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pos = strVal.IndexOf(needle);
|
||||
return pos == -1
|
||||
? strVal
|
||||
@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
|
||||
{
|
||||
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) return null;
|
||||
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var setMethodInfo = propertyInfo.SetMethod;
|
||||
return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
|
||||
|
@ -1090,7 +1090,7 @@ namespace Emby.Server.Implementations.Session
|
||||
await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IList<BaseItem> TranslateItemForPlayback(Guid id, User user)
|
||||
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
|
||||
|
@ -13,9 +13,9 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
{
|
||||
public partial class WebSocketSharpRequest : IHttpRequest
|
||||
{
|
||||
internal static string GetParameter(string 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)
|
||||
{
|
||||
return null;
|
||||
@ -33,18 +33,19 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
ending = ' ';
|
||||
}
|
||||
|
||||
int end = header.IndexOf(ending, ap + 1);
|
||||
var slice = header.Slice(ap + 1);
|
||||
int end = slice.IndexOf(ending);
|
||||
if (end == -1)
|
||||
{
|
||||
return ending == '"' ? null : header.Substring(ap);
|
||||
return ending == '"' ? null : header.Slice(ap).ToString();
|
||||
}
|
||||
|
||||
return header.Substring(ap + 1, end - ap - 1);
|
||||
return slice.Slice(0, end - ap - 1).ToString();
|
||||
}
|
||||
|
||||
private async Task LoadMultiPart(WebROCollection form)
|
||||
{
|
||||
string boundary = GetParameter(ContentType, "; boundary=");
|
||||
string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
|
||||
if (boundary == null)
|
||||
{
|
||||
return;
|
||||
@ -377,17 +378,17 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
}
|
||||
|
||||
var elem = new Element();
|
||||
string header;
|
||||
while ((header = ReadHeaders()) != null)
|
||||
ReadOnlySpan<char> header;
|
||||
while ((header = ReadHeaders().AsSpan()) != null)
|
||||
{
|
||||
if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase))
|
||||
if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
elem.Name = GetContentDispositionAttribute(header, "name");
|
||||
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
|
||||
}
|
||||
else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
|
||||
else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
elem.ContentType = header.Substring("Content-Type:".Length).Trim();
|
||||
elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
|
||||
elem.Encoding = GetEncoding(elem.ContentType);
|
||||
}
|
||||
}
|
||||
@ -435,16 +436,16 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetContentDispositionAttribute(string 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)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int begin = idx + name.Length + "=\"".Length;
|
||||
int end = l.IndexOf('"', begin);
|
||||
int end = l.Slice(begin).IndexOf('"');
|
||||
if (end < 0)
|
||||
{
|
||||
return null;
|
||||
@ -455,19 +456,19 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return l.Substring(begin, end - begin);
|
||||
return l.Slice(begin, end - begin).ToString();
|
||||
}
|
||||
|
||||
private string GetContentDispositionAttributeWithEncoding(string 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)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int begin = idx + name.Length + "=\"".Length;
|
||||
int end = l.IndexOf('"', begin);
|
||||
int end = l.Slice(begin).IndexOf('"');
|
||||
if (end < 0)
|
||||
{
|
||||
return null;
|
||||
@ -478,7 +479,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string temp = l.Substring(begin, end - begin);
|
||||
ReadOnlySpan<char> temp = l.Slice(begin, end - begin);
|
||||
byte[] source = new byte[temp.Length];
|
||||
for (int i = temp.Length - 1; i >= 0; i--)
|
||||
{
|
||||
|
@ -56,19 +56,37 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
|
||||
|
||||
private string remoteIp;
|
||||
public string RemoteIp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (remoteIp != null)
|
||||
{
|
||||
return remoteIp;
|
||||
}
|
||||
|
||||
public string RemoteIp =>
|
||||
remoteIp ??
|
||||
(remoteIp = CheckBadChars(XForwardedFor) ??
|
||||
NormalizeIp(CheckBadChars(XRealIp) ??
|
||||
(string.IsNullOrEmpty(request.HttpContext.Connection.RemoteIpAddress.ToString()) ? null : NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString()))));
|
||||
var temp = CheckBadChars(XForwardedFor.AsSpan());
|
||||
if (temp.Length != 0)
|
||||
{
|
||||
return remoteIp = temp.ToString();
|
||||
}
|
||||
|
||||
temp = CheckBadChars(XRealIp.AsSpan());
|
||||
if (temp.Length != 0)
|
||||
{
|
||||
return remoteIp = NormalizeIp(temp).ToString();
|
||||
}
|
||||
|
||||
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
|
||||
|
||||
// CheckBadChars - throws on invalid chars to be not found in header name/value
|
||||
internal static string CheckBadChars(string name)
|
||||
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
|
||||
{
|
||||
if (name == null || name.Length == 0)
|
||||
if (name.Length == 0)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
@ -99,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
}
|
||||
else if (c == 127 || (c < ' ' && c != '\t'))
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidControlChars");
|
||||
throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
|
||||
}
|
||||
|
||||
break;
|
||||
@ -113,7 +131,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
break;
|
||||
}
|
||||
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||
}
|
||||
|
||||
case 2:
|
||||
@ -124,29 +142,29 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
break;
|
||||
}
|
||||
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crlf != 0)
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private string NormalizeIp(string ip)
|
||||
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ip))
|
||||
if (ip.Length != 0 && !ip.IsWhiteSpace())
|
||||
{
|
||||
// Handle ipv4 mapped to ipv6
|
||||
const string srch = "::ffff:";
|
||||
var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
||||
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
|
||||
if (index == 0)
|
||||
{
|
||||
ip = ip.Substring(srch.Length);
|
||||
ip = ip.Slice(srch.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,7 +342,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
}
|
||||
|
||||
this.pathInfo = WebUtility.UrlDecode(pathInfo);
|
||||
this.pathInfo = NormalizePathInfo(pathInfo, mode);
|
||||
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
|
||||
}
|
||||
|
||||
return this.pathInfo;
|
||||
@ -436,7 +454,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
|
||||
public static Encoding GetEncoding(string contentTypeHeader)
|
||||
{
|
||||
var param = GetParameter(contentTypeHeader, "charset=");
|
||||
var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
|
||||
if (param == null)
|
||||
{
|
||||
return null;
|
||||
@ -488,18 +506,18 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||
}
|
||||
}
|
||||
|
||||
public static string NormalizePathInfo(string pathInfo, string handlerPath)
|
||||
public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
|
||||
{
|
||||
if (handlerPath != null)
|
||||
{
|
||||
var trimmed = pathInfo.TrimStart('/');
|
||||
if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
|
||||
var trimmed = pathInfo.AsSpan().TrimStart('/');
|
||||
if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return trimmed.Substring(handlerPath.Length);
|
||||
return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
|
||||
}
|
||||
}
|
||||
|
||||
return pathInfo;
|
||||
return pathInfo.AsSpan();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -495,9 +495,7 @@ namespace Emby.XmlTv.Classes
|
||||
ParseMovieDbSystem(reader, result);
|
||||
break;
|
||||
case "SxxExx":
|
||||
// TODO
|
||||
// <episode-num system="SxxExx">S03E12</episode-num>
|
||||
reader.Skip();
|
||||
ParseSxxExxSystem(reader, result);
|
||||
break;
|
||||
default: // Handles empty string and nulls
|
||||
reader.Skip();
|
||||
@ -505,6 +503,29 @@ namespace Emby.XmlTv.Classes
|
||||
}
|
||||
}
|
||||
|
||||
public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result)
|
||||
{
|
||||
// <episode-num system="SxxExx">S012E32</episode-num>
|
||||
|
||||
var value = reader.ReadElementContentAsString();
|
||||
var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase);
|
||||
|
||||
if (res.Success)
|
||||
{
|
||||
int parsedInt;
|
||||
|
||||
if (int.TryParse(res.Groups[1].Value, out parsedInt))
|
||||
{
|
||||
result.Episode.Series = parsedInt;
|
||||
}
|
||||
|
||||
if (int.TryParse(res.Groups[2].Value, out parsedInt))
|
||||
{
|
||||
result.Episode.Episode = parsedInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result)
|
||||
{
|
||||
// <episode-num system="thetvdb.com">series/248841</episode-num>
|
||||
|
@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
foregroundWidth *= percent;
|
||||
foregroundWidth /= 100;
|
||||
|
||||
paint.Color = SKColor.Parse("#FF52B54B");
|
||||
paint.Color = SKColor.Parse("#FF00A4DC");
|
||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
|
||||
using (var paint = new SKPaint())
|
||||
{
|
||||
paint.Color = SKColor.Parse("#CC52B54B");
|
||||
paint.Color = SKColor.Parse("#CC00A4DC");
|
||||
paint.Style = SKPaintStyle.Fill;
|
||||
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
|
||||
using (var paint = new SKPaint())
|
||||
{
|
||||
paint.Color = SKColor.Parse("#CC52B54B");
|
||||
paint.Color = SKColor.Parse("#CC00A4DC");
|
||||
paint.Style = SKPaintStyle.Fill;
|
||||
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
|
||||
}
|
||||
|
@ -20,10 +20,10 @@ namespace Jellyfin.Server
|
||||
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
|
||||
public string LogDir { get; set; }
|
||||
|
||||
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")]
|
||||
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
|
||||
public string FFmpegPath { get; set; }
|
||||
|
||||
[Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")]
|
||||
[Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")]
|
||||
public string FFprobePath { get; set; }
|
||||
|
||||
[Option("service", Required = false, HelpText = "Run as headless service.")]
|
||||
|
@ -172,16 +172,9 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
|
||||
{
|
||||
if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes))
|
||||
{
|
||||
options.ImageTypes = Array.Empty<ImageType>();
|
||||
}
|
||||
else
|
||||
{
|
||||
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
|
||||
.ToArray();
|
||||
}
|
||||
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -180,7 +181,7 @@ namespace MediaBrowser.Api
|
||||
return ToOptimizedResult(filters);
|
||||
}
|
||||
|
||||
private QueryFiltersLegacy GetFilters(BaseItem[] items)
|
||||
private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
|
||||
{
|
||||
var result = new QueryFiltersLegacy();
|
||||
|
||||
|
@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
[Route("/Videos/{Id}/stream.mov", "GET")]
|
||||
[Route("/Videos/{Id}/stream.iso", "GET")]
|
||||
[Route("/Videos/{Id}/stream.flv", "GET")]
|
||||
[Route("/Videos/{Id}/stream.rm", "GET")]
|
||||
[Route("/Videos/{Id}/stream", "GET")]
|
||||
[Route("/Videos/{Id}/stream.ts", "HEAD")]
|
||||
[Route("/Videos/{Id}/stream.webm", "HEAD")]
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@ -197,29 +198,27 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
request.ParentId = null;
|
||||
}
|
||||
|
||||
var item = string.IsNullOrEmpty(request.ParentId) ?
|
||||
null :
|
||||
_libraryManager.GetItemById(request.ParentId);
|
||||
BaseItem item = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(request.ParentId))
|
||||
{
|
||||
item = _libraryManager.GetItemById(request.ParentId);
|
||||
}
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
item = string.IsNullOrEmpty(request.ParentId) ?
|
||||
user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
|
||||
_libraryManager.GetItemById(request.ParentId);
|
||||
item = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
// Default list type = children
|
||||
|
||||
var folder = item as Folder;
|
||||
Folder folder = item as Folder;
|
||||
if (folder == null)
|
||||
{
|
||||
folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
var hasCollectionType = folder as IHasCollectionType;
|
||||
var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isPlaylistQuery)
|
||||
if (hasCollectionType != null
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Recursive = true;
|
||||
request.IncludeItemTypes = "Playlist";
|
||||
@ -235,20 +234,12 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
};
|
||||
}
|
||||
|
||||
if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
|
||||
{
|
||||
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
|
||||
}
|
||||
|
||||
var userRoot = item as UserRootFolder;
|
||||
|
||||
if (userRoot == null)
|
||||
if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
|
||||
{
|
||||
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
|
||||
}
|
||||
|
||||
var itemsArray = folder.GetChildren(user, true).ToArray();
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = itemsArray,
|
||||
|
@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Net
|
||||
/// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
|
||||
bool IsInLocalNetwork(string endpoint);
|
||||
|
||||
IpAddressInfo[] GetLocalIpAddresses();
|
||||
IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface);
|
||||
|
||||
IpAddressInfo ParseIpAddress(string ipAddress);
|
||||
|
||||
@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net
|
||||
Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
|
||||
|
||||
bool IsAddressInSubnets(string addressString, string[] subnets);
|
||||
|
||||
bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask);
|
||||
IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address);
|
||||
}
|
||||
}
|
||||
|
@ -36,9 +36,7 @@ namespace MediaBrowser.Controller.Dto
|
||||
.ToArray();
|
||||
|
||||
public bool ContainsField(ItemFields field)
|
||||
{
|
||||
return AllItemFields.Contains(field);
|
||||
}
|
||||
=> Fields.Contains(field);
|
||||
|
||||
public DtoOptions(bool allFields)
|
||||
{
|
||||
@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto
|
||||
EnableUserData = true;
|
||||
AddCurrentProgram = true;
|
||||
|
||||
if (allFields)
|
||||
{
|
||||
Fields = AllItemFields;
|
||||
}
|
||||
else
|
||||
{
|
||||
Fields = new ItemFields[] { };
|
||||
}
|
||||
|
||||
Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
|
||||
ImageTypes = AllImageTypes;
|
||||
}
|
||||
|
||||
|
@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
|
||||
|
||||
BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
|
||||
BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item by name dto.
|
||||
|
@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
if (query.ItemIds.Length > 0)
|
||||
{
|
||||
var result = LibraryManager.GetItemsResult(query);
|
||||
|
||||
if (query.OrderBy.Length == 0)
|
||||
{
|
||||
var ids = query.ItemIds.ToList();
|
||||
|
||||
// Try to preserve order
|
||||
result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
|
||||
}
|
||||
return result;
|
||||
return LibraryManager.GetItemsResult(query);
|
||||
}
|
||||
|
||||
return GetItemsInternal(query);
|
||||
}
|
||||
|
||||
public BaseItem[] GetItemList(InternalItemsQuery query)
|
||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
|
||||
{
|
||||
query.EnableTotalRecordCount = false;
|
||||
|
||||
if (query.ItemIds.Length > 0)
|
||||
{
|
||||
var result = LibraryManager.GetItemList(query);
|
||||
|
||||
if (query.OrderBy.Length == 0)
|
||||
{
|
||||
var ids = query.ItemIds.ToList();
|
||||
|
||||
// Try to preserve order
|
||||
return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
|
||||
}
|
||||
return result.ToArray();
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
return GetItemsInternal(query).Items;
|
||||
|
@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
flags.Add("+ignidx");
|
||||
}
|
||||
if (state.GenPtsInput)
|
||||
if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
flags.Add("+genpts");
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// </summary>
|
||||
public interface IMediaEncoder : ITranscoderSupport
|
||||
{
|
||||
string EncoderLocationType { get; }
|
||||
FFmpegLocation EncoderLocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the encoder path.
|
||||
@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns>System.String.</returns>
|
||||
string EscapeSubtitleFilterPath(string path);
|
||||
|
||||
void Init();
|
||||
void SetFFmpegPath();
|
||||
|
||||
void UpdateEncoderPath(string path, string pathType);
|
||||
bool SupportsEncoder(string encoder);
|
||||
|
@ -32,16 +32,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
||||
|
||||
// If ffmpeg process is closed, the state is disposed, so don't write to target in that case
|
||||
if (!target.CanWrite)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await target.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
//TODO Investigate and properly fix.
|
||||
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading ffmpeg log");
|
||||
|
@ -1,19 +0,0 @@
|
||||
namespace MediaBrowser.Controller.Security
|
||||
{
|
||||
public interface IEncryptionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Encrypts the string.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string EncryptString(string value);
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts the string.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string DecryptString(string value);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images
|
||||
|
||||
var path = item.ContainingFolderPath;
|
||||
|
||||
// Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return Array.Empty<FileSystemMetadata>();
|
||||
}
|
||||
|
||||
if (includeDirectories)
|
||||
{
|
||||
return directoryService.GetFileSystemEntries(path)
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_processFactory = processFactory;
|
||||
}
|
||||
|
||||
public (IEnumerable<string> decoders, IEnumerable<string> encoders) Validate(string encoderPath)
|
||||
public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath)
|
||||
{
|
||||
_logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
|
||||
|
||||
@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
if (logOutput)
|
||||
{
|
||||
_logger.LogError("FFmpeg validation: The process returned no result");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -55,21 +59,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
if (logOutput)
|
||||
{
|
||||
_logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
output = " " + output + " ";
|
||||
// The min and max FFmpeg versions required to run jellyfin successfully
|
||||
var minRequired = new Version(4, 0);
|
||||
var maxRequired = new Version(4, 0);
|
||||
|
||||
for (var i = 2013; i <= 2015; i++)
|
||||
// Work out what the version under test is
|
||||
var underTest = GetFFmpegVersion(output);
|
||||
|
||||
if (logOutput)
|
||||
{
|
||||
var yearString = i.ToString(CultureInfo.InvariantCulture);
|
||||
if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
_logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown");
|
||||
|
||||
if (underTest == null) // Version is unknown
|
||||
{
|
||||
return false;
|
||||
if (minRequired.Equals(maxRequired))
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString());
|
||||
}
|
||||
}
|
||||
else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString());
|
||||
}
|
||||
else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString());
|
||||
}
|
||||
else // Version is ok
|
||||
{
|
||||
_logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
// underTest shall be null if versions is unknown
|
||||
return (underTest == null) ? false : (underTest.CompareTo(minRequired) >= 0 && underTest.CompareTo(maxRequired) <= 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
|
||||
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
|
||||
/// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
|
||||
/// If that fails then we use one of the main libraries to determine if it's new/older than the latest
|
||||
/// we have stored.
|
||||
/// </summary>
|
||||
/// <param name="output"></param>
|
||||
/// <returns></returns>
|
||||
static private Version GetFFmpegVersion(string output)
|
||||
{
|
||||
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
|
||||
var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)");
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return new Version(match.Groups[1].Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try and use the individual library versions to determine a FFmpeg version
|
||||
// This lookup table is to be maintained with the following command line:
|
||||
// $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
|
||||
var lut = new ReadOnlyDictionary<Version, string>
|
||||
(new Dictionary<Version, string>
|
||||
{
|
||||
{ new Version("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," },
|
||||
{ new Version("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," },
|
||||
{ new Version("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," },
|
||||
{ new Version("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," },
|
||||
{ new Version("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," },
|
||||
{ new Version("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," }
|
||||
});
|
||||
|
||||
// Create a reduced version string and lookup key from dictionary
|
||||
var reducedVersion = GetVersionString(output);
|
||||
|
||||
// Try to lookup the string and return Key, otherwise if not found returns null
|
||||
return lut.FirstOrDefault(x => x.Value == reducedVersion).Key;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
|
||||
/// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc."
|
||||
/// </summary>
|
||||
/// <param name="output"></param>
|
||||
/// <returns></returns>
|
||||
static private string GetVersionString(string output)
|
||||
{
|
||||
string pattern = @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))";
|
||||
RegexOptions options = RegexOptions.Multiline;
|
||||
|
||||
string rc = null;
|
||||
|
||||
foreach (Match m in Regex.Matches(output, pattern, options))
|
||||
{
|
||||
rc += string.Concat(m.Groups["name"], '=', m.Groups["major"], '.', m.Groups["minor"], ',');
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
private static readonly string[] requiredDecoders = new[]
|
||||
|
@ -3,17 +3,14 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.MediaEncoding.Probing;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public class MediaEncoder : IMediaEncoder, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The _logger
|
||||
/// Gets the encoder path.
|
||||
/// </summary>
|
||||
/// <value>The encoder path.</value>
|
||||
public string EncoderPath => FFmpegPath;
|
||||
|
||||
/// <summary>
|
||||
/// The location of the discovered FFmpeg tool.
|
||||
/// </summary>
|
||||
public FFmpegLocation EncoderLocation { get; private set; }
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the json serializer.
|
||||
/// </summary>
|
||||
/// <value>The json serializer.</value>
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// The _thumbnail resource pool
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
|
||||
|
||||
public string FFMpegPath { get; private set; }
|
||||
|
||||
public string FFProbePath { get; private set; }
|
||||
|
||||
private string FFmpegPath;
|
||||
private string FFprobePath;
|
||||
protected readonly IServerConfigurationManager ConfigurationManager;
|
||||
protected readonly IFileSystem FileSystem;
|
||||
protected readonly ILiveTvManager LiveTvManager;
|
||||
protected readonly IIsoManager IsoManager;
|
||||
protected readonly ILibraryManager LibraryManager;
|
||||
protected readonly IChannelManager ChannelManager;
|
||||
protected readonly ISessionManager SessionManager;
|
||||
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
|
||||
protected readonly Func<IMediaSourceManager> MediaSourceManager;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IZipClient _zipClient;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
|
||||
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||
private readonly bool _hasExternalEncoder;
|
||||
private readonly string _originalFFMpegPath;
|
||||
private readonly string _originalFFProbePath;
|
||||
private readonly int DefaultImageExtractionTimeoutMs;
|
||||
private readonly string StartupOptionFFmpegPath;
|
||||
private readonly string StartupOptionFFprobePath;
|
||||
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
|
||||
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||
|
||||
public MediaEncoder(
|
||||
ILoggerFactory loggerFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
string ffMpegPath,
|
||||
string ffProbePath,
|
||||
bool hasExternalEncoder,
|
||||
string startupOptionsFFmpegPath,
|
||||
string startupOptionsFFprobePath,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IFileSystem fileSystem,
|
||||
ILiveTvManager liveTvManager,
|
||||
IIsoManager isoManager,
|
||||
ILibraryManager libraryManager,
|
||||
IChannelManager channelManager,
|
||||
ISessionManager sessionManager,
|
||||
Func<ISubtitleEncoder> subtitleEncoder,
|
||||
Func<IMediaSourceManager> mediaSourceManager,
|
||||
IHttpClient httpClient,
|
||||
IZipClient zipClient,
|
||||
IProcessFactory processFactory,
|
||||
int defaultImageExtractionTimeoutMs)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
|
||||
_jsonSerializer = jsonSerializer;
|
||||
StartupOptionFFmpegPath = startupOptionsFFmpegPath;
|
||||
StartupOptionFFprobePath = startupOptionsFFprobePath;
|
||||
ConfigurationManager = configurationManager;
|
||||
FileSystem = fileSystem;
|
||||
LiveTvManager = liveTvManager;
|
||||
IsoManager = isoManager;
|
||||
LibraryManager = libraryManager;
|
||||
ChannelManager = channelManager;
|
||||
SessionManager = sessionManager;
|
||||
SubtitleEncoder = subtitleEncoder;
|
||||
MediaSourceManager = mediaSourceManager;
|
||||
_httpClient = httpClient;
|
||||
_zipClient = zipClient;
|
||||
_processFactory = processFactory;
|
||||
DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
|
||||
FFProbePath = ffProbePath;
|
||||
FFMpegPath = ffMpegPath;
|
||||
_originalFFProbePath = ffProbePath;
|
||||
_originalFFMpegPath = ffMpegPath;
|
||||
_hasExternalEncoder = hasExternalEncoder;
|
||||
}
|
||||
|
||||
public string EncoderLocationType
|
||||
/// <summary>
|
||||
/// Run at startup or if the user removes a Custom path from transcode page.
|
||||
/// Sets global variables FFmpegPath.
|
||||
/// Precedence is: Config > CLI > $PATH
|
||||
/// </summary>
|
||||
public void SetFFmpegPath()
|
||||
{
|
||||
get
|
||||
// ToDo - Finalise removal of the --ffprobe switch
|
||||
if (!string.IsNullOrEmpty(StartupOptionFFprobePath))
|
||||
{
|
||||
if (_hasExternalEncoder)
|
||||
{
|
||||
return "External";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(FFMpegPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSystemInstalledPath(FFMpegPath))
|
||||
{
|
||||
return "System";
|
||||
}
|
||||
|
||||
return "Custom";
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSystemInstalledPath(string path)
|
||||
{
|
||||
if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1)
|
||||
{
|
||||
return true;
|
||||
_logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
InitPaths();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FFMpegPath))
|
||||
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
|
||||
if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
|
||||
{
|
||||
var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath);
|
||||
// 2) Check if the --ffmpeg CLI switch has been given
|
||||
if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument))
|
||||
{
|
||||
// 3) Search system $PATH environment variable for valid FFmpeg
|
||||
if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
|
||||
{
|
||||
EncoderLocation = FFmpegLocation.NotFound;
|
||||
FFmpegPath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
|
||||
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty;
|
||||
ConfigurationManager.SaveConfiguration("encoding", config);
|
||||
|
||||
// Only if mpeg path is set, try and set path to probe
|
||||
if (FFmpegPath != null)
|
||||
{
|
||||
// Determine a probe path from the mpeg path
|
||||
FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
|
||||
|
||||
// Interrogate to understand what coders are supported
|
||||
var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath);
|
||||
|
||||
SetAvailableDecoders(result.decoders);
|
||||
SetAvailableEncoders(result.encoders);
|
||||
}
|
||||
|
||||
_logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty);
|
||||
}
|
||||
|
||||
private void InitPaths()
|
||||
{
|
||||
ConfigureEncoderPaths();
|
||||
|
||||
if (_hasExternalEncoder)
|
||||
{
|
||||
LogPaths();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the path was passed in, save it into config now.
|
||||
var encodingOptions = GetEncodingOptions();
|
||||
var appPath = encodingOptions.EncoderAppPath;
|
||||
|
||||
var valueToSave = FFMpegPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(valueToSave))
|
||||
{
|
||||
// if using system variable, don't save this.
|
||||
if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder)
|
||||
{
|
||||
valueToSave = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal))
|
||||
{
|
||||
encodingOptions.EncoderAppPath = valueToSave;
|
||||
ConfigurationManager.SaveConfiguration("encoding", encodingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
|
||||
/// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="pathType"></param>
|
||||
public void UpdateEncoderPath(string path, string pathType)
|
||||
{
|
||||
if (_hasExternalEncoder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string newPath;
|
||||
|
||||
_logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty);
|
||||
|
||||
Tuple<string, string> newPaths;
|
||||
|
||||
if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = "ffmpeg";
|
||||
|
||||
newPaths = TestForInstalledVersions();
|
||||
}
|
||||
else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
newPaths = GetEncoderPaths(path);
|
||||
}
|
||||
else
|
||||
if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Unexpected pathType value");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPaths.Item1))
|
||||
else if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ResourceNotFoundException("ffmpeg not found");
|
||||
// User had cleared the custom path in UI
|
||||
newPath = string.Empty;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(newPaths.Item2))
|
||||
else if (File.Exists(path))
|
||||
{
|
||||
throw new ResourceNotFoundException("ffprobe not found");
|
||||
newPath = path;
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
// Given path is directory, so resolve down to filename
|
||||
newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
path = newPaths.Item1;
|
||||
|
||||
if (!ValidateVersion(path, true))
|
||||
{
|
||||
throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required.");
|
||||
}
|
||||
|
||||
var config = GetEncodingOptions();
|
||||
config.EncoderAppPath = path;
|
||||
// Write the new ffmpeg path to the xml as <EncoderAppPath>
|
||||
// This ensures its not lost on next startup
|
||||
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
config.EncoderAppPath = newPath;
|
||||
ConfigurationManager.SaveConfiguration("encoding", config);
|
||||
|
||||
Init();
|
||||
// Trigger SetFFmpegPath so we validate the new path and setup probe path
|
||||
SetFFmpegPath();
|
||||
}
|
||||
|
||||
private bool ValidateVersion(string path, bool logOutput)
|
||||
/// <summary>
|
||||
/// Validates the supplied FQPN to ensure it is a ffmpeg utility.
|
||||
/// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
|
||||
/// </summary>
|
||||
/// <param name="path">FQPN to test</param>
|
||||
/// <param name="location">Location (External, Custom, System) of tool</param>
|
||||
/// <returns></returns>
|
||||
private bool ValidatePath(string path, FFmpegLocation location)
|
||||
{
|
||||
return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput);
|
||||
}
|
||||
bool rc = false;
|
||||
|
||||
private void ConfigureEncoderPaths()
|
||||
{
|
||||
if (_hasExternalEncoder)
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var appPath = GetEncodingOptions().EncoderAppPath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(appPath))
|
||||
{
|
||||
appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg");
|
||||
}
|
||||
|
||||
var newPaths = GetEncoderPaths(appPath);
|
||||
if (string.IsNullOrWhiteSpace(newPaths.Item1) || string.IsNullOrWhiteSpace(newPaths.Item2) || IsSystemInstalledPath(appPath))
|
||||
{
|
||||
newPaths = TestForInstalledVersions();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2))
|
||||
{
|
||||
FFMpegPath = newPaths.Item1;
|
||||
FFProbePath = newPaths.Item2;
|
||||
}
|
||||
|
||||
LogPaths();
|
||||
}
|
||||
|
||||
private Tuple<string, string> GetEncoderPaths(string configuredPath)
|
||||
{
|
||||
var appPath = configuredPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(appPath))
|
||||
{
|
||||
if (Directory.Exists(appPath))
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return GetPathsFromDirectory(appPath);
|
||||
rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true);
|
||||
|
||||
if (!rc)
|
||||
{
|
||||
_logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path);
|
||||
}
|
||||
|
||||
// ToDo - Enable the ffmpeg validator. At the moment any version can be used.
|
||||
rc = true;
|
||||
|
||||
FFmpegPath = path;
|
||||
EncoderLocation = location;
|
||||
}
|
||||
|
||||
if (File.Exists(appPath))
|
||||
else
|
||||
{
|
||||
return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath));
|
||||
_logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path);
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<string, string>(null, null);
|
||||
return rc;
|
||||
}
|
||||
|
||||
private Tuple<string, string> TestForInstalledVersions()
|
||||
private string GetEncoderPathFromDirectory(string path, string filename)
|
||||
{
|
||||
string encoderPath = null;
|
||||
string probePath = null;
|
||||
|
||||
if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true))
|
||||
try
|
||||
{
|
||||
encoderPath = _originalFFMpegPath;
|
||||
probePath = _originalFFProbePath;
|
||||
var files = FileSystem.GetFilePaths(path);
|
||||
|
||||
var excludeExtensions = new[] { ".c" };
|
||||
|
||||
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
|
||||
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(encoderPath))
|
||||
catch (Exception)
|
||||
{
|
||||
if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false))
|
||||
// Trap all exceptions, like DirNotExists, and return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search the system $PATH environment variable looking for given filename.
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
/// <returns></returns>
|
||||
private string ExistsOnSystemPath(string filename)
|
||||
{
|
||||
var values = Environment.GetEnvironmentVariable("PATH");
|
||||
|
||||
foreach (var path in values.Split(Path.PathSeparator))
|
||||
{
|
||||
var candidatePath = GetEncoderPathFromDirectory(path, filename);
|
||||
|
||||
if (!string.IsNullOrEmpty(candidatePath))
|
||||
{
|
||||
encoderPath = "ffmpeg";
|
||||
probePath = "ffprobe";
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<string, string>(encoderPath, probePath);
|
||||
}
|
||||
|
||||
private Tuple<string, string> GetPathsFromDirectory(string path)
|
||||
{
|
||||
// Since we can't predict the file extension, first try directly within the folder
|
||||
// If that doesn't pan out, then do a recursive search
|
||||
var files = FileSystem.GetFilePaths(path);
|
||||
|
||||
var excludeExtensions = new[] { ".c" };
|
||||
|
||||
var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||
var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath))
|
||||
{
|
||||
files = FileSystem.GetFilePaths(path, true);
|
||||
|
||||
ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ffmpegPath))
|
||||
{
|
||||
ffprobePath = GetProbePathFromEncoderPath(ffmpegPath);
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<string, string>(ffmpegPath, ffprobePath);
|
||||
}
|
||||
|
||||
private string GetProbePathFromEncoderPath(string appPath)
|
||||
{
|
||||
return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath))
|
||||
.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private void LogPaths()
|
||||
{
|
||||
_logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found");
|
||||
_logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found");
|
||||
}
|
||||
|
||||
private EncodingOptions GetEncodingOptions()
|
||||
{
|
||||
return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<string> _encoders = new List<string>();
|
||||
@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the encoder path.
|
||||
/// </summary>
|
||||
/// <value>The encoder path.</value>
|
||||
public string EncoderPath => FFMpegPath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media info.
|
||||
/// </summary>
|
||||
@ -489,7 +364,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardOutput = true,
|
||||
FileName = FFProbePath,
|
||||
FileName = FFprobePath,
|
||||
Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
|
||||
|
||||
IsHidden = true,
|
||||
@ -691,10 +566,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = FFMpegPath,
|
||||
FileName = FFmpegPath,
|
||||
Arguments = args,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false
|
||||
ErrorDialog = false,
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
|
||||
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
@ -813,10 +689,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = FFMpegPath,
|
||||
FileName = FFmpegPath,
|
||||
Arguments = args,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false
|
||||
ErrorDialog = false,
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
|
||||
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@ -29,17 +30,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IEncryptionManager _encryption;
|
||||
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem)
|
||||
public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger(GetType().Name);
|
||||
_httpClient = httpClient;
|
||||
_config = config;
|
||||
_encryption = encryption;
|
||||
_json = json;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
@ -63,16 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
!string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
|
||||
!options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash);
|
||||
options.OpenSubtitlesPasswordHash = EncodePassword(options.OpenSubtitlesPasswordHash);
|
||||
}
|
||||
}
|
||||
|
||||
private string EncryptPassword(string password)
|
||||
private static string EncodePassword(string password)
|
||||
{
|
||||
return PasswordHashPrefix + _encryption.EncryptString(password);
|
||||
var bytes = Encoding.UTF8.GetBytes(password);
|
||||
return PasswordHashPrefix + Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string DecryptPassword(string password)
|
||||
private static string DecodePassword(string password)
|
||||
{
|
||||
if (password == null ||
|
||||
!password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
@ -80,7 +80,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _encryption.DecryptString(password.Substring(2));
|
||||
var bytes = Convert.FromBase64String(password.Substring(2));
|
||||
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public string Name => "Open Subtitles";
|
||||
@ -186,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var options = GetOptions();
|
||||
|
||||
var user = options.OpenSubtitlesUsername ?? string.Empty;
|
||||
var password = DecryptPassword(options.OpenSubtitlesPasswordHash);
|
||||
var password = DecodePassword(options.OpenSubtitlesPasswordHash);
|
||||
|
||||
var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration
|
||||
public bool EnableThrottling { get; set; }
|
||||
public int ThrottleDelaySeconds { get; set; }
|
||||
public string HardwareAccelerationType { get; set; }
|
||||
/// <summary>
|
||||
/// FFmpeg path as set by the user via the UI
|
||||
/// </summary>
|
||||
public string EncoderAppPath { get; set; }
|
||||
/// <summary>
|
||||
/// The current FFmpeg path being used by the system and displayed on the transcode page
|
||||
/// </summary>
|
||||
public string EncoderAppPathDisplay { get; set; }
|
||||
public string VaapiDevice { get; set; }
|
||||
public int H264Crf { get; set; }
|
||||
public string H264Preset { get; set; }
|
||||
|
@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
public string[] LocalNetworkSubnets { get; set; }
|
||||
public string[] LocalNetworkAddresses { get; set; }
|
||||
public string[] CodecsUsed { get; set; }
|
||||
public bool IgnoreVirtualInterfaces { get; set; }
|
||||
public bool EnableExternalContentInSuggestions { get; set; }
|
||||
public bool RequireHttps { get; set; }
|
||||
public bool IsBehindProxy { get; set; }
|
||||
@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
CodecsUsed = Array.Empty<string>();
|
||||
ImageExtractionTimeoutMs = 0;
|
||||
PathSubstitutions = Array.Empty<PathSubstitution>();
|
||||
IgnoreVirtualInterfaces = false;
|
||||
EnableSimpleArtistDetection = true;
|
||||
|
||||
DisplaySpecialsWithinSeasons = true;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
|
||||
byte[] ComputeMD5(Stream str);
|
||||
byte[] ComputeMD5(byte[] bytes);
|
||||
byte[] ComputeSHA1(byte[] bytes);
|
||||
IEnumerable<string> GetSupportedHashMethods();
|
||||
byte[] ComputeHash(string HashMethod, byte[] bytes);
|
||||
byte[] ComputeHashWithDefaultMethod(byte[] bytes);
|
||||
byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt);
|
||||
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
|
||||
byte[] ComputeHash(PasswordHash hash);
|
||||
byte[] GenerateSalt();
|
||||
string DefaultHashMethod { get; }
|
||||
}
|
||||
}
|
||||
|
153
MediaBrowser.Model/Cryptography/PasswordHash.cs
Normal file
153
MediaBrowser.Model/Cryptography/PasswordHash.cs
Normal file
@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
public class PasswordHash
|
||||
{
|
||||
// Defined from this hash storage spec
|
||||
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
// $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
|
||||
// with one slight amendment to ease the transition, we're writing out the bytes in hex
|
||||
// rather than making them a BASE64 string with stripped padding
|
||||
|
||||
private string _id;
|
||||
|
||||
private Dictionary<string, string> _parameters = new Dictionary<string, string>();
|
||||
|
||||
private string _salt;
|
||||
|
||||
private byte[] _saltBytes;
|
||||
|
||||
private string _hash;
|
||||
|
||||
private byte[] _hashBytes;
|
||||
|
||||
public string Id { get => _id; set => _id = value; }
|
||||
|
||||
public Dictionary<string, string> Parameters { get => _parameters; set => _parameters = value; }
|
||||
|
||||
public string Salt { get => _salt; set => _salt = value; }
|
||||
|
||||
public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; }
|
||||
|
||||
public string Hash { get => _hash; set => _hash = value; }
|
||||
|
||||
public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; }
|
||||
|
||||
public PasswordHash(string storageString)
|
||||
{
|
||||
string[] splitted = storageString.Split('$');
|
||||
_id = splitted[1];
|
||||
if (splitted[2].Contains("="))
|
||||
{
|
||||
foreach (string paramset in (splitted[2].Split(',')))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(paramset))
|
||||
{
|
||||
string[] fields = paramset.Split('=');
|
||||
if (fields.Length == 2)
|
||||
{
|
||||
_parameters.Add(fields[0], fields[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Malformed parameter in password hash string {paramset}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (splitted.Length == 5)
|
||||
{
|
||||
_salt = splitted[3];
|
||||
_saltBytes = ConvertFromByteString(_salt);
|
||||
_hash = splitted[4];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_salt = string.Empty;
|
||||
_hash = splitted[3];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (splitted.Length == 4)
|
||||
{
|
||||
_salt = splitted[2];
|
||||
_saltBytes = ConvertFromByteString(_salt);
|
||||
_hash = splitted[3];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_salt = string.Empty;
|
||||
_hash = splitted[2];
|
||||
_hashBytes = ConvertFromByteString(_hash);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public PasswordHash(ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_id = cryptoProvider.DefaultHashMethod;
|
||||
_saltBytes = cryptoProvider.GenerateSalt();
|
||||
_salt = ConvertToByteString(SaltBytes);
|
||||
}
|
||||
|
||||
public static byte[] ConvertFromByteString(string byteString)
|
||||
{
|
||||
byte[] bytes = new byte[byteString.Length / 2];
|
||||
for (int i = 0; i < byteString.Length; i += 2)
|
||||
{
|
||||
// TODO: NetStandard2.1 switch this to use a span instead of a substring.
|
||||
bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static string ConvertToByteString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes).Replace("-", "");
|
||||
}
|
||||
|
||||
private string SerializeParameters()
|
||||
{
|
||||
string returnString = string.Empty;
|
||||
foreach (var KVP in _parameters)
|
||||
{
|
||||
returnString += $",{KVP.Key}={KVP.Value}";
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',')
|
||||
{
|
||||
returnString = returnString.Remove(0, 1);
|
||||
}
|
||||
|
||||
return returnString;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string outString = "$" + _id;
|
||||
string paramstring = SerializeParameters();
|
||||
if (!string.IsNullOrEmpty(paramstring))
|
||||
{
|
||||
outString += $"${paramstring}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_salt))
|
||||
{
|
||||
outString += $"${_salt}";
|
||||
}
|
||||
|
||||
outString += $"${_hash}";
|
||||
return outString;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Net
|
||||
public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
|
||||
|
||||
public string Address { get; set; }
|
||||
public IpAddressInfo SubnetMask { get; set; }
|
||||
public IpAddressFamily AddressFamily { get; set; }
|
||||
|
||||
public IpAddressInfo(string address, IpAddressFamily addressFamily)
|
||||
|
@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates;
|
||||
|
||||
namespace MediaBrowser.Model.System
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum describing the location of the FFmpeg tool.
|
||||
/// </summary>
|
||||
public enum FFmpegLocation
|
||||
{
|
||||
/// <summary>No path to FFmpeg found.</summary>
|
||||
NotFound,
|
||||
/// <summary>Path supplied via command line using switch --ffmpeg.</summary>
|
||||
SetByArgument,
|
||||
/// <summary>User has supplied path via Transcoding UI page.</summary>
|
||||
Custom,
|
||||
/// <summary>FFmpeg tool found on system $PATH.</summary>
|
||||
System
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Class SystemInfo
|
||||
/// </summary>
|
||||
@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
|
||||
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
|
||||
public bool HasUpdateAvailable { get; set; }
|
||||
|
||||
public string EncoderLocationType { get; set; }
|
||||
public FFmpegLocation EncoderLocation { get; set; }
|
||||
|
||||
public Architecture SystemArchitecture { get; set; }
|
||||
|
||||
|
@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users
|
||||
|
||||
public UserPolicy()
|
||||
{
|
||||
EnableContentDeletion = true;
|
||||
EnableContentDeletion = false;
|
||||
EnableContentDeletionFromFolders = Array.Empty<string>();
|
||||
|
||||
EnableSyncTranscoding = true;
|
||||
|
@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
catch (Exception ex)
|
||||
{
|
||||
localImagesFailed = true;
|
||||
if (!(item is IItemByName))
|
||||
{
|
||||
Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
|
||||
}
|
||||
Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
|
||||
}
|
||||
|
||||
var metadataResult = new MetadataResult<TItemType>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<a href="https://github.com/jellyfin/jellyfin"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin.svg"/></a>
|
||||
<a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a>
|
||||
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
|
||||
<a href="https://cloud.drone.io/jellyfin/jellyfin"><img alt="Build Status" src="https://cloud.drone.io/api/badges/jellyfin/jellyfin/status.svg"/></a>
|
||||
<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"><img alt="Azure DevOps builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"></a>
|
||||
<a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a>
|
||||
</br>
|
||||
<a href="https://opencollective.com/jellyfin"><img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/></a>
|
||||
|
@ -45,8 +45,8 @@ namespace Rssdp.Infrastructure
|
||||
/// <summary>
|
||||
/// Sends a message to the SSDP multicast address and port.
|
||||
/// </summary>
|
||||
Task SendMulticastMessage(string message, CancellationToken cancellationToken);
|
||||
Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken);
|
||||
Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
|
||||
Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
||||
namespace Rssdp.Infrastructure
|
||||
{
|
||||
@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure
|
||||
private readonly ILogger _logger;
|
||||
private ISocketFactory _SocketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
private int _LocalPort;
|
||||
private int _MulticastTtl;
|
||||
@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure
|
||||
/// Minimum constructor.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
|
||||
public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
|
||||
public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory,
|
||||
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
|
||||
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -236,15 +240,15 @@ namespace Rssdp.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendMulticastMessage(string message, CancellationToken cancellationToken)
|
||||
public Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken);
|
||||
return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the SSDP multicast address and port.
|
||||
/// </summary>
|
||||
public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken)
|
||||
public async Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null) throw new ArgumentNullException(nameof(message));
|
||||
|
||||
@ -264,7 +268,7 @@ namespace Rssdp.Infrastructure
|
||||
IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
|
||||
Port = SsdpConstants.MulticastPort
|
||||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}, fromLocalIpAddress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@ -332,14 +336,15 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken)
|
||||
private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
|
||||
{
|
||||
var sockets = _sendSockets;
|
||||
if (sockets != null)
|
||||
{
|
||||
sockets = sockets.ToList();
|
||||
|
||||
var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
|
||||
var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress)))
|
||||
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
@ -363,11 +368,11 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
if (_enableMultiSocketBinding)
|
||||
{
|
||||
foreach (var address in _networkManager.GetLocalIpAddresses())
|
||||
foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces))
|
||||
{
|
||||
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||
{
|
||||
// Not supported ?
|
||||
// Not support IPv6 right now
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
var message = BuildMessage(header, values);
|
||||
|
||||
return _CommunicationsServer.SendMulticastMessage(message, cancellationToken);
|
||||
return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
|
||||
}
|
||||
|
||||
private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)
|
||||
|
@ -7,6 +7,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Rssdp;
|
||||
|
||||
namespace Rssdp.Infrastructure
|
||||
@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure
|
||||
/// </summary>
|
||||
public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
|
||||
{
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
private ISsdpCommunicationsServer _CommsServer;
|
||||
private string _OSName;
|
||||
private string _OSVersion;
|
||||
private bool _sendOnlyMatchedHost;
|
||||
|
||||
private bool _SupportPnpRootDevice;
|
||||
|
||||
@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure
|
||||
/// <summary>
|
||||
/// Default constructor.
|
||||
/// </summary>
|
||||
public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion)
|
||||
public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager,
|
||||
string osName, string osVersion, bool sendOnlyMatchedHost)
|
||||
{
|
||||
if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer));
|
||||
if (networkManager == null) throw new ArgumentNullException(nameof(networkManager));
|
||||
if (osName == null) throw new ArgumentNullException(nameof(osName));
|
||||
if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName));
|
||||
if (osVersion == null) throw new ArgumentNullException(nameof(osVersion));
|
||||
@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure
|
||||
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
|
||||
_Random = new Random();
|
||||
|
||||
_networkManager = networkManager;
|
||||
_CommsServer = communicationsServer;
|
||||
_CommsServer.RequestReceived += CommsServer_RequestReceived;
|
||||
_OSName = osName;
|
||||
_OSVersion = osVersion;
|
||||
_sendOnlyMatchedHost = sendOnlyMatchedHost;
|
||||
|
||||
_CommsServer.BeginListeningForBroadcasts();
|
||||
}
|
||||
@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
foreach (var device in deviceList)
|
||||
{
|
||||
SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
|
||||
if (!_sendOnlyMatchedHost ||
|
||||
_networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.IpAddress, device.ToRootDevice().SubnetMask))
|
||||
{
|
||||
SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -427,7 +438,7 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
var message = BuildMessage(header, values);
|
||||
|
||||
_CommsServer.SendMulticastMessage(message, cancellationToken);
|
||||
_CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken);
|
||||
|
||||
//WriteTrace(String.Format("Sent alive notification"), device);
|
||||
}
|
||||
@ -472,7 +483,7 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
var sendCount = IsDisposed ? 1 : 3;
|
||||
WriteTrace(String.Format("Sent byebye notification"), device);
|
||||
return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken);
|
||||
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
|
||||
}
|
||||
|
||||
private void DisposeRebroadcastTimer()
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Rssdp.Infrastructure;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
||||
namespace Rssdp
|
||||
{
|
||||
@ -52,6 +53,15 @@ namespace Rssdp
|
||||
/// </summary>
|
||||
public Uri Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required.
|
||||
/// </summary>
|
||||
public IpAddressInfo Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required.
|
||||
/// </summary>
|
||||
public IpAddressInfo SubnetMask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.2.1")]
|
||||
[assembly: AssemblyFileVersion("10.2.1")]
|
||||
[assembly: AssemblyVersion("10.2.2")]
|
||||
[assembly: AssemblyFileVersion("10.2.2")]
|
||||
|
60
build
60
build
@ -26,7 +26,7 @@ usage() {
|
||||
echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
|
||||
echo -e ""
|
||||
echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
|
||||
echo -e "The web_branch defaults to the same branch name as the current main branch."
|
||||
echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
|
||||
echo -e "To build all platforms, use 'all'."
|
||||
echo -e "To perform all build actions, use 'all'."
|
||||
echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
|
||||
@ -164,38 +164,40 @@ for target_platform in ${platform[@]}; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Initialize submodules
|
||||
git submodule update --init --recursive
|
||||
if [[ ${web_branch} != 'local' ]]; then
|
||||
# Initialize submodules
|
||||
git submodule update --init --recursive
|
||||
|
||||
# configure branch
|
||||
pushd MediaBrowser.WebDashboard/jellyfin-web
|
||||
# configure branch
|
||||
pushd MediaBrowser.WebDashboard/jellyfin-web
|
||||
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
popd
|
||||
echo
|
||||
echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!"
|
||||
echo "This script will overwrite your unstaged and unpushed changes."
|
||||
echo "Please do development on 'jellyfin-web' outside of the submodule."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --all
|
||||
# If this is an official branch name, fetch it from origin
|
||||
official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$"
|
||||
if [[ ${web_branch} =~ ${official_branches_regex} ]]; then
|
||||
git checkout origin/${web_branch} || {
|
||||
echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid."
|
||||
exit 1
|
||||
}
|
||||
# Otherwise, just check out the local branch (for testing, etc.)
|
||||
else
|
||||
git checkout ${web_branch} || {
|
||||
echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid."
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
popd
|
||||
echo
|
||||
echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!"
|
||||
echo "This script will overwrite your unstaged and unpushed changes."
|
||||
echo "Please do development on 'jellyfin-web' outside of the submodule."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --all
|
||||
# If this is an official branch name, fetch it from origin
|
||||
official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$"
|
||||
if [[ ${web_branch} =~ ${official_branches_regex} ]]; then
|
||||
git checkout origin/${web_branch} || {
|
||||
echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid."
|
||||
exit 1
|
||||
}
|
||||
# Otherwise, just check out the local branch (for testing, etc.)
|
||||
else
|
||||
git checkout ${web_branch} || {
|
||||
echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid."
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
popd
|
||||
|
||||
# Execute each platform and action in order, if said action is enabled
|
||||
pushd deployment/
|
||||
for target_platform in ${platform[@]}; do
|
||||
@ -217,7 +219,7 @@ for target_platform in ${platform[@]}; do
|
||||
done
|
||||
if [[ -d pkg-dist/ ]]; then
|
||||
echo -e ">> Collecting build artifacts"
|
||||
target_dir="../../../jellyfin-build/${target_platform}"
|
||||
target_dir="../../../bin/${target_platform}"
|
||||
mkdir -p ${target_dir}
|
||||
mv pkg-dist/* ${target_dir}/
|
||||
fi
|
||||
|
15
build.yaml
Normal file
15
build.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.2.2"
|
||||
packages:
|
||||
- debian-package-x64
|
||||
- debian-package-armhf
|
||||
- ubuntu-package-x64
|
||||
- fedora-package-x64
|
||||
- centos-package-x64
|
||||
- linux-x64
|
||||
- macos
|
||||
- portable
|
||||
- win-x64
|
||||
- win-x86
|
@ -15,7 +15,6 @@ DEFAULT_CONFIG="Release"
|
||||
DEFAULT_OUTPUT_DIR="dist/jellyfin-git"
|
||||
DEFAULT_PKG_DIR="pkg-dist"
|
||||
DEFAULT_DOCKERFILE="Dockerfile"
|
||||
DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD`
|
||||
DEFAULT_ARCHIVE_CMD="tar -xvzf"
|
||||
|
||||
# Parse the version from the AssemblyVersion
|
||||
@ -36,9 +35,9 @@ build_jellyfin()
|
||||
|
||||
echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
|
||||
if [[ $DOTNETRUNTIME == 'framework' ]]; then
|
||||
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}"
|
||||
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
|
||||
else
|
||||
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME}
|
||||
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
|
||||
fi
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
@ -53,7 +52,7 @@ build_jellyfin_docker()
|
||||
(
|
||||
BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
|
||||
DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
|
||||
IMAGE_TAG=${3-$DEFAULT_IMAGE_TAG}
|
||||
IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"}
|
||||
|
||||
echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}"
|
||||
docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT}
|
||||
|
42
deployment/debian-package-armhf/Dockerfile.amd64
Normal file
42
deployment/debian-package-armhf/Dockerfile.amd64
Normal file
@ -0,0 +1,42 @@
|
||||
FROM debian:9
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG SDK_VERSION=2.2
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=amd64
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Prepare the cross-toolchain
|
||||
RUN dpkg --add-architecture armhf \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y cross-gcc-dev \
|
||||
&& TARGET_LIST="armhf" cross-gcc-gensource 6 \
|
||||
&& cd cross-gcc-packages-amd64/cross-gcc-6-armhf \
|
||||
&& apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf
|
||||
|
||||
# Link to docker-build script
|
||||
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Link to Debian source dir; mkdir needed or it fails, can't force dest
|
||||
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
34
deployment/debian-package-armhf/Dockerfile.armhf
Normal file
34
deployment/debian-package-armhf/Dockerfile.armhf
Normal file
@ -0,0 +1,34 @@
|
||||
FROM debian:9
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG SDK_VERSION=2.2
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=armhf
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Link to docker-build script
|
||||
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Link to Debian source dir; mkdir needed or it fails, can't force dest
|
||||
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
29
deployment/debian-package-armhf/clean.sh
Executable file
29
deployment/debian-package-armhf/clean.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ../common.build.sh
|
||||
|
||||
keep_artifacts="${1}"
|
||||
|
||||
WORKDIR="$( pwd )"
|
||||
|
||||
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
|
||||
output_dir="${WORKDIR}/pkg-dist"
|
||||
current_user="$( whoami )"
|
||||
image_name="jellyfin-debian_armhf-build"
|
||||
|
||||
rm -rf "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo rm -rf "${package_temporary_dir}" &>/dev/null
|
||||
|
||||
rm -rf "${output_dir}" &>/dev/null \
|
||||
|| sudo rm -rf "${output_dir}" &>/dev/null
|
||||
|
||||
if [[ ${keep_artifacts} == 'n' ]]; then
|
||||
docker_sudo=""
|
||||
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
|
||||
&& [[ ! ${EUID:-1000} -eq 0 ]] \
|
||||
&& [[ ! ${USER} == "root" ]] \
|
||||
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
|
||||
docker_sudo=sudo
|
||||
fi
|
||||
${docker_sudo} docker image rm ${image_name} --force
|
||||
fi
|
1
deployment/debian-package-armhf/dependencies.txt
Normal file
1
deployment/debian-package-armhf/dependencies.txt
Normal file
@ -0,0 +1 @@
|
||||
docker
|
20
deployment/debian-package-armhf/docker-build.sh
Executable file
20
deployment/debian-package-armhf/docker-build.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Builds the DEB inside the Docker container
|
||||
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
# Move to source directory
|
||||
pushd ${SOURCE_DIR}
|
||||
|
||||
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
|
||||
sed -i '/dotnet-sdk-2.2,/d' debian/control
|
||||
|
||||
# Build DEB
|
||||
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
|
||||
dpkg-buildpackage -us -uc -aarmhf
|
||||
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
42
deployment/debian-package-armhf/package.sh
Executable file
42
deployment/debian-package-armhf/package.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ../common.build.sh
|
||||
|
||||
ARCH="$( arch )"
|
||||
WORKDIR="$( pwd )"
|
||||
|
||||
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
|
||||
output_dir="${WORKDIR}/pkg-dist"
|
||||
current_user="$( whoami )"
|
||||
image_name="jellyfin-debian_armhf-build"
|
||||
|
||||
# Determine if sudo should be used for Docker
|
||||
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
|
||||
&& [[ ! ${EUID:-1000} -eq 0 ]] \
|
||||
&& [[ ! ${USER} == "root" ]] \
|
||||
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
|
||||
docker_sudo="sudo"
|
||||
else
|
||||
docker_sudo=""
|
||||
fi
|
||||
|
||||
# Determine which Dockerfile to use
|
||||
case $ARCH in
|
||||
'x86_64')
|
||||
DOCKERFILE="Dockerfile.amd64"
|
||||
;;
|
||||
'armv7l')
|
||||
DOCKERFILE="Dockerfile.armhf"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
1
deployment/debian-package-armhf/pkg-src
Symbolic link
1
deployment/debian-package-armhf/pkg-src
Symbolic link
@ -0,0 +1 @@
|
||||
../debian-package-x64/pkg-src
|
@ -1,3 +1,20 @@
|
||||
jellyfin (10.2.2-1) unstable; urgency=medium
|
||||
|
||||
* jellyfin:
|
||||
* PR968 Release 10.2.z copr autobuild
|
||||
* PR964 Install the dotnet runtime package in Fedora build
|
||||
* PR979 Build Package releases without debug turned on
|
||||
* PR990 Fix slow local image validation
|
||||
* PR991 Fix the ffmpeg compatibility
|
||||
* PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
|
||||
* PR998 Set EnableRaisingEvents to true for processes that require it
|
||||
* PR1017 Set ffmpeg+ffprobe paths in Docker container
|
||||
* jellyfin-web:
|
||||
* PR152 Go back on Media stop
|
||||
* PR156 Fix volume slider not working on nowplayingbar
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 28 Feb 2019 15:32:16 -0500
|
||||
|
||||
jellyfin (10.2.1-1) unstable; urgency=medium
|
||||
|
||||
* jellyfin:
|
||||
|
@ -21,9 +21,9 @@ JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin"
|
||||
# Restart script for in-app server control
|
||||
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
||||
|
||||
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
|
||||
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
|
||||
#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe"
|
||||
# ffmpeg binary paths, overriding the system values
|
||||
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg"
|
||||
JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe"
|
||||
|
||||
# [OPTIONAL] run Jellyfin as a headless service
|
||||
#JELLYFIN_SERVICE_OPT="--service"
|
||||
|
@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
|
||||
Architecture: any
|
||||
Depends: at,
|
||||
libsqlite3-0,
|
||||
ffmpeg (<7:4.1) | jellyfin-ffmpeg,
|
||||
jellyfin-ffmpeg,
|
||||
libfontconfig1,
|
||||
libfreetype6,
|
||||
libssl1.0.0 | libssl1.0.2
|
||||
|
@ -2,7 +2,23 @@
|
||||
CONFIG := Release
|
||||
TERM := xterm
|
||||
SHELL := /bin/bash
|
||||
DOTNETRUNTIME := debian-x64
|
||||
|
||||
HOST_ARCH := $(shell arch)
|
||||
BUILD_ARCH := ${DEB_HOST_MULTIARCH}
|
||||
ifeq ($(HOST_ARCH),x86_64)
|
||||
ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
|
||||
# Cross-building ARM on AMD64
|
||||
DOTNETRUNTIME := debian-arm
|
||||
else
|
||||
# Building AMD64
|
||||
DOTNETRUNTIME := debian-x64
|
||||
endif
|
||||
endif
|
||||
ifeq ($(HOST_ARCH),armv7l)
|
||||
# Building ARM
|
||||
DOTNETRUNTIME := debian-arm
|
||||
endif
|
||||
|
||||
export DH_VERBOSE=1
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
@ -16,7 +32,8 @@ override_dh_auto_test:
|
||||
override_dh_clistrip:
|
||||
|
||||
override_dh_auto_build:
|
||||
dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server
|
||||
dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
|
||||
"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
|
||||
|
||||
override_dh_auto_clean:
|
||||
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
|
||||
|
@ -13,7 +13,7 @@ RUN dnf update -y \
|
||||
&& dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
|
||||
&& dnf copr enable -y @dotnet-sig/dotnet \
|
||||
&& rpmdev-setuptree \
|
||||
&& dnf install -y dotnet-sdk-${SDK_VERSION} \
|
||||
&& dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \
|
||||
&& ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
|
||||
&& mkdir -p ${SOURCE_DIR}/SPECS \
|
||||
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
|
||||
|
@ -7,7 +7,7 @@
|
||||
%endif
|
||||
|
||||
Name: jellyfin
|
||||
Version: 10.2.1
|
||||
Version: 10.2.2
|
||||
Release: 1%{?dist}
|
||||
Summary: The Free Software Media Browser
|
||||
License: GPLv2
|
||||
@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
|
||||
Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
|
||||
# Requirements not packaged in main repos
|
||||
# COPR @dotnet-sig/dotnet
|
||||
BuildRequires: dotnet-sdk-2.2
|
||||
BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2
|
||||
# RPMfusion free
|
||||
Requires: ffmpeg
|
||||
|
||||
@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an
|
||||
%install
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
|
||||
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} Jellyfin.Server
|
||||
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
|
||||
"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
|
||||
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
||||
%{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
|
||||
%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
|
||||
@ -73,7 +74,6 @@ EOF
|
||||
%{_libdir}/%{name}/jellyfin-web/*
|
||||
%attr(755,root,root) %{_bindir}/%{name}
|
||||
%{_libdir}/%{name}/*.json
|
||||
%{_libdir}/%{name}/*.pdb
|
||||
%{_libdir}/%{name}/*.dll
|
||||
%{_libdir}/%{name}/*.so
|
||||
%{_libdir}/%{name}/*.a
|
||||
@ -140,6 +140,19 @@ fi
|
||||
%systemd_postun_with_restart jellyfin.service
|
||||
|
||||
%changelog
|
||||
* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- jellyfin:
|
||||
- PR968 Release 10.2.z copr autobuild
|
||||
- PR964 Install the dotnet runtime package in Fedora build
|
||||
- PR979 Build Package releases without debug turned on
|
||||
- PR990 Fix slow local image validation
|
||||
- PR991 Fix the ffmpeg compatibility
|
||||
- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
|
||||
- PR998 Set EnableRaisingEvents to true for processes that require it
|
||||
- PR1017 Set ffmpeg+ffprobe paths in Docker container
|
||||
- jellyfin-web:
|
||||
- PR152 Go back on Media stop
|
||||
- PR156 Fix volume slider not working on nowplayingbar
|
||||
* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- jellyfin:
|
||||
- PR920 Fix cachedir missing from Docker container
|
||||
|
@ -21,8 +21,8 @@ package_win64() (
|
||||
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
|
||||
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
|
||||
rm -r ${TEMP_DIR}
|
||||
cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
||||
cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
|
||||
cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
||||
cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
|
||||
mkdir -p ${PKG_DIR}
|
||||
pushd ${OUTPUT_DIR}
|
||||
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
|
||||
|
@ -20,8 +20,8 @@ package_win32() (
|
||||
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
|
||||
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
|
||||
rm -r ${TEMP_DIR}
|
||||
cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
||||
cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
|
||||
cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
||||
cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
|
||||
mkdir -p ${PKG_DIR}
|
||||
pushd ${OUTPUT_DIR}
|
||||
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
|
||||
|
Loading…
Reference in New Issue
Block a user