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)
|
- [WillWill56](https://github.com/WillWill56)
|
||||||
- [Liggy](https://github.com/Liggy)
|
- [Liggy](https://github.com/Liggy)
|
||||||
- [fruhnow](https://github.com/fruhnow)
|
- [fruhnow](https://github.com/fruhnow)
|
||||||
|
- [Lynxy](https://github.com/Lynxy)
|
||||||
|
- [fasheng](https://github.com/fasheng)
|
||||||
|
- [ploughpuff](https://github.com/ploughpuff)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
18
Dockerfile
18
Dockerfile
@ -4,10 +4,8 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
|
|||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
RUN dotnet publish \
|
RUN bash -c "source deployment/common.build.sh && \
|
||||||
--configuration release \
|
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
|
||||||
--output /jellyfin \
|
|
||||||
Jellyfin.Server
|
|
||||||
|
|
||||||
FROM jellyfin/ffmpeg as ffmpeg
|
FROM jellyfin/ffmpeg as ffmpeg
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
|
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
|
||||||
@ -22,6 +20,16 @@ RUN apt-get update \
|
|||||||
&& chmod 777 /cache /config /media
|
&& chmod 777 /cache /config /media
|
||||||
COPY --from=ffmpeg / /
|
COPY --from=ffmpeg / /
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
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
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
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
|
# Discard objs - may cause failures if exists
|
||||||
RUN find . -type d -name obj | xargs -r rm -r
|
RUN find . -type d -name obj | xargs -r rm -r
|
||||||
# Build
|
# Build
|
||||||
RUN dotnet publish \
|
RUN bash -c "source deployment/common.build.sh && \
|
||||||
-r linux-arm \
|
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
|
||||||
--configuration release \
|
|
||||||
--output /jellyfin \
|
|
||||||
Jellyfin.Server
|
|
||||||
|
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
|
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
|
||||||
@ -31,6 +28,16 @@ RUN apt-get update \
|
|||||||
&& mkdir -p /cache /config /media \
|
&& mkdir -p /cache /config /media \
|
||||||
&& chmod 777 /cache /config /media
|
&& chmod 777 /cache /config /media
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
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
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
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
|
# Discard objs - may cause failures if exists
|
||||||
RUN find . -type d -name obj | xargs -r rm -r
|
RUN find . -type d -name obj | xargs -r rm -r
|
||||||
# Build
|
# Build
|
||||||
RUN dotnet publish \
|
RUN bash -c "source deployment/common.build.sh && \
|
||||||
-r linux-arm64 \
|
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
|
||||||
--configuration release \
|
|
||||||
--output /jellyfin \
|
|
||||||
Jellyfin.Server
|
|
||||||
|
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
|
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
|
||||||
@ -32,6 +29,16 @@ RUN apt-get update \
|
|||||||
&& mkdir -p /cache /config /media \
|
&& mkdir -p /cache /config /media \
|
||||||
&& chmod 777 /cache /config /media
|
&& chmod 777 /cache /config /media
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
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
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
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)
|
if (vmgPath == null)
|
||||||
{
|
{
|
||||||
var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase));
|
foreach (var ifo in allFiles)
|
||||||
|
|
||||||
foreach (var ifo in allIfos)
|
|
||||||
{
|
{
|
||||||
var num = ifo.Name.Split('_').ElementAtOrDefault(1);
|
if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
|
||||||
var numbersRead = new List<ushort>();
|
{
|
||||||
|
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);
|
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);
|
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 EnableServer { get; set; }
|
||||||
public bool EnableDebugLog { get; set; }
|
public bool EnableDebugLog { get; set; }
|
||||||
public bool BlastAliveMessages { get; set; }
|
public bool BlastAliveMessages { get; set; }
|
||||||
|
public bool SendOnlyMatchedHost { get; set; }
|
||||||
public int ClientDiscoveryIntervalSeconds { get; set; }
|
public int ClientDiscoveryIntervalSeconds { get; set; }
|
||||||
public int BlastAliveMessageIntervalSeconds { get; set; }
|
public int BlastAliveMessageIntervalSeconds { get; set; }
|
||||||
public string DefaultUserId { get; set; }
|
public string DefaultUserId { get; set; }
|
||||||
@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration
|
|||||||
EnablePlayTo = true;
|
EnablePlayTo = true;
|
||||||
EnableServer = true;
|
EnableServer = true;
|
||||||
BlastAliveMessages = true;
|
BlastAliveMessages = true;
|
||||||
|
SendOnlyMatchedHost = true;
|
||||||
ClientDiscoveryIntervalSeconds = 60;
|
ClientDiscoveryIntervalSeconds = 60;
|
||||||
BlastAliveMessageIntervalSeconds = 1800;
|
BlastAliveMessageIntervalSeconds = 1800;
|
||||||
}
|
}
|
||||||
|
@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
|
|
||||||
if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
|
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);
|
_didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
|
||||||
}
|
}
|
||||||
@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
|
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
|
||||||
totalCount = childrenResult.TotalRecordCount;
|
totalCount = childrenResult.TotalRecordCount;
|
||||||
|
|
||||||
provided = childrenResult.Items.Length;
|
provided = childrenResult.Items.Length;
|
||||||
|
@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
AddCommonFields(item, itemStubType, context, writer, filter);
|
AddCommonFields(item, itemStubType, context, writer, filter);
|
||||||
|
|
||||||
var hasArtists = item as IHasArtist;
|
|
||||||
var hasAlbumArtists = item as IHasAlbumArtist;
|
var hasAlbumArtists = item as IHasAlbumArtist;
|
||||||
|
|
||||||
if (hasArtists != null)
|
if (item is IHasArtist hasArtists)
|
||||||
{
|
{
|
||||||
foreach (var artist in hasArtists.Artists)
|
foreach (var artist in hasArtists.Artists)
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing;
|
|||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Reflection;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -31,7 +31,7 @@ namespace Emby.Dlna
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IServerApplicationHost _appHost;
|
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);
|
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,
|
IApplicationPaths appPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost)
|
||||||
IAssemblyInfo assemblyInfo)
|
|
||||||
{
|
{
|
||||||
_xmlSerializer = xmlSerializer;
|
_xmlSerializer = xmlSerializer;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
@ -50,7 +49,6 @@ namespace Emby.Dlna
|
|||||||
_logger = loggerFactory.CreateLogger("Dlna");
|
_logger = loggerFactory.CreateLogger("Dlna");
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_assemblyInfo = assemblyInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitProfilesAsync()
|
public async Task InitProfilesAsync()
|
||||||
@ -367,15 +365,18 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
var systemProfilesPath = SystemProfilesPath;
|
var systemProfilesPath = SystemProfilesPath;
|
||||||
|
|
||||||
foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType())
|
foreach (var name in _assembly.GetManifestResourceNames())
|
||||||
.Where(i => i.StartsWith(namespaceName))
|
|
||||||
.ToList())
|
|
||||||
{
|
{
|
||||||
|
if (!name.StartsWith(namespaceName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||||
|
|
||||||
var path = Path.Combine(systemProfilesPath, filename);
|
var path = Path.Combine(systemProfilesPath, filename);
|
||||||
|
|
||||||
using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name))
|
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||||
{
|
{
|
||||||
var fileInfo = _fileSystem.GetFileInfo(path);
|
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||||
|
|
||||||
@ -513,7 +514,7 @@ namespace Emby.Dlna
|
|||||||
return new ImageStream
|
return new ImageStream
|
||||||
{
|
{
|
||||||
Format = format,
|
Format = format,
|
||||||
Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource)
|
Stream = _assembly.GetManifestResourceStream(resource)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,9 +169,10 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
if (_communicationsServer == null)
|
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
|
IsShared = true
|
||||||
};
|
};
|
||||||
@ -229,7 +230,7 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
try
|
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.LogFunction = LogMessage;
|
||||||
_Publisher.SupportPnpRootDevice = false;
|
_Publisher.SupportPnpRootDevice = false;
|
||||||
|
|
||||||
@ -245,17 +246,17 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
private async Task RegisterServerEndpoints()
|
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);
|
var udn = CreateUuid(_appHost.SystemId);
|
||||||
|
|
||||||
foreach (var address in addresses)
|
foreach (var address in addresses)
|
||||||
{
|
{
|
||||||
// TODO: Remove this condition on platforms that support it
|
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||||
//if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
{
|
||||||
//{
|
// Not support IPv6 right now
|
||||||
// continue;
|
continue;
|
||||||
//}
|
}
|
||||||
|
|
||||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
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.
|
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.
|
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||||
|
Address = address,
|
||||||
|
SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
|
||||||
FriendlyName = "Jellyfin",
|
FriendlyName = "Jellyfin",
|
||||||
Manufacturer = "Jellyfin",
|
Manufacturer = "Jellyfin",
|
||||||
ModelName = "Jellyfin Server",
|
ModelName = "Jellyfin Server",
|
||||||
|
@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo
|
|||||||
foreach (var arg in action.ArgumentList)
|
foreach (var arg in action.ArgumentList)
|
||||||
{
|
{
|
||||||
if (arg.Direction == "out")
|
if (arg.Direction == "out")
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg.Name == "InstanceID")
|
if (arg.Name == "InstanceID")
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, "0");
|
stateString += BuildArgumentXml(arg, "0");
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, null);
|
stateString += BuildArgumentXml(arg, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
|
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
|
||||||
@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo
|
|||||||
foreach (var arg in action.ArgumentList)
|
foreach (var arg in action.ArgumentList)
|
||||||
{
|
{
|
||||||
if (arg.Direction == "out")
|
if (arg.Direction == "out")
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg.Name == "InstanceID")
|
if (arg.Name == "InstanceID")
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, "0");
|
stateString += BuildArgumentXml(arg, "0");
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
|
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||||
@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
foreach (var arg in action.ArgumentList)
|
foreach (var arg in action.ArgumentList)
|
||||||
{
|
{
|
||||||
if (arg.Name == "InstanceID")
|
if (arg.Name == "InstanceID")
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, "0");
|
stateString += BuildArgumentXml(arg, "0");
|
||||||
|
}
|
||||||
else if (dictionary.ContainsKey(arg.Name))
|
else if (dictionary.ContainsKey(arg.Name))
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
|
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
stateString += BuildArgumentXml(arg, value.ToString());
|
stateString += BuildArgumentXml(arg, value.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||||
|
@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
namespace Emby.Naming.TV
|
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
|
// 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.
|
// the regex which require file extensions are modified so that they don't need them.
|
||||||
if (IsDirectory)
|
if (IsDirectory)
|
||||||
|
{
|
||||||
path += ".mp4";
|
path += ".mp4";
|
||||||
|
}
|
||||||
|
|
||||||
EpisodePathParserResult result = null;
|
EpisodePathParserResult result = null;
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ namespace Emby.Naming.TV
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNamed.HasValue)
|
if (isNamed.HasValue)
|
||||||
{
|
{
|
||||||
if (expression.IsNamed != isNamed.Value)
|
if (expression.IsNamed != isNamed.Value)
|
||||||
@ -42,6 +44,7 @@ namespace Emby.Naming.TV
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOptimistic.HasValue)
|
if (isOptimistic.HasValue)
|
||||||
{
|
{
|
||||||
if (expression.IsOptimistic != isOptimistic.Value)
|
if (expression.IsOptimistic != isOptimistic.Value)
|
||||||
@ -191,13 +194,20 @@ namespace Emby.Naming.TV
|
|||||||
|
|
||||||
private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
|
private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
|
||||||
{
|
{
|
||||||
var results = expressions
|
foreach (var i in expressions)
|
||||||
.Where(i => i.IsNamed)
|
|
||||||
.Select(i => Parse(path, i))
|
|
||||||
.Where(i => i.Success);
|
|
||||||
|
|
||||||
foreach (var result in results)
|
|
||||||
{
|
{
|
||||||
|
if (!i.IsNamed)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = Parse(path, i);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(info.SeriesName))
|
if (string.IsNullOrEmpty(info.SeriesName))
|
||||||
{
|
{
|
||||||
info.SeriesName = result.SeriesName;
|
info.SeriesName = result.SeriesName;
|
||||||
@ -208,12 +218,10 @@ namespace Emby.Naming.TV
|
|||||||
info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
|
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);
|
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);
|
var user = _userManager.GetUserById(item.UserId);
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
|
@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data;
|
|||||||
using Emby.Server.Implementations.Devices;
|
using Emby.Server.Implementations.Devices;
|
||||||
using Emby.Server.Implementations.Diagnostics;
|
using Emby.Server.Implementations.Diagnostics;
|
||||||
using Emby.Server.Implementations.Dto;
|
using Emby.Server.Implementations.Dto;
|
||||||
using Emby.Server.Implementations.FFMpeg;
|
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using Emby.Server.Implementations.HttpServer.Security;
|
using Emby.Server.Implementations.HttpServer.Security;
|
||||||
using Emby.Server.Implementations.IO;
|
using Emby.Server.Implementations.IO;
|
||||||
@ -541,7 +540,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
|
||||||
MediaEncoder.Init();
|
MediaEncoder.SetFFmpegPath();
|
||||||
|
|
||||||
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
|
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
|
||||||
//{
|
//{
|
||||||
@ -813,10 +812,8 @@ namespace Emby.Server.Implementations
|
|||||||
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
|
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
|
||||||
serviceCollection.AddSingleton(TVSeriesManager);
|
serviceCollection.AddSingleton(TVSeriesManager);
|
||||||
|
|
||||||
var encryptionManager = new EncryptionManager();
|
|
||||||
serviceCollection.AddSingleton<IEncryptionManager>(encryptionManager);
|
|
||||||
|
|
||||||
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
|
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
|
||||||
|
|
||||||
serviceCollection.AddSingleton(DeviceManager);
|
serviceCollection.AddSingleton(DeviceManager);
|
||||||
|
|
||||||
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
|
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(SessionManager);
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IDlnaManager>(
|
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);
|
CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
|
||||||
serviceCollection.AddSingleton(CollectionManager);
|
serviceCollection.AddSingleton(CollectionManager);
|
||||||
@ -861,7 +858,18 @@ namespace Emby.Server.Implementations
|
|||||||
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
|
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
|
||||||
serviceCollection.AddSingleton(ChapterManager);
|
serviceCollection.AddSingleton(ChapterManager);
|
||||||
|
|
||||||
RegisterMediaEncoder(serviceCollection);
|
MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
|
||||||
|
LoggerFactory,
|
||||||
|
JsonSerializer,
|
||||||
|
StartupOptions.FFmpegPath,
|
||||||
|
StartupOptions.FFprobePath,
|
||||||
|
ServerConfigurationManager,
|
||||||
|
FileSystemManager,
|
||||||
|
() => SubtitleEncoder,
|
||||||
|
() => MediaSourceManager,
|
||||||
|
ProcessFactory,
|
||||||
|
5000);
|
||||||
|
serviceCollection.AddSingleton(MediaEncoder);
|
||||||
|
|
||||||
EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
|
EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
|
||||||
serviceCollection.AddSingleton(EncodingManager);
|
serviceCollection.AddSingleton(EncodingManager);
|
||||||
@ -970,85 +978,6 @@ namespace Emby.Server.Implementations
|
|||||||
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
|
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()
|
|
||||||
{
|
|
||||||
var info = new FFMpegInstallInfo();
|
|
||||||
|
|
||||||
// Windows builds: http://ffmpeg.zeranoe.com/builds/
|
|
||||||
// Linux builds: http://johnvansickle.com/ffmpeg/
|
|
||||||
// OS X builds: http://ffmpegmac.net/
|
|
||||||
// OS X x64: http://www.evermeet.cx/ffmpeg/
|
|
||||||
|
|
||||||
if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux)
|
|
||||||
{
|
|
||||||
info.FFMpegFilename = "ffmpeg";
|
|
||||||
info.FFProbeFilename = "ffprobe";
|
|
||||||
info.ArchiveType = "7z";
|
|
||||||
info.Version = "20170308";
|
|
||||||
}
|
|
||||||
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
|
|
||||||
{
|
|
||||||
info.FFMpegFilename = "ffmpeg.exe";
|
|
||||||
info.FFProbeFilename = "ffprobe.exe";
|
|
||||||
info.Version = "20170308";
|
|
||||||
info.ArchiveType = "7z";
|
|
||||||
}
|
|
||||||
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
|
|
||||||
{
|
|
||||||
info.FFMpegFilename = "ffmpeg";
|
|
||||||
info.FFProbeFilename = "ffprobe";
|
|
||||||
info.ArchiveType = "7z";
|
|
||||||
info.Version = "20170308";
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected FFMpegInfo GetFFMpegInfo()
|
|
||||||
{
|
|
||||||
return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
|
|
||||||
.GetFFMpegInfo(StartupOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers the media encoder.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private void RegisterMediaEncoder(IServiceCollection serviceCollection)
|
|
||||||
{
|
|
||||||
string encoderPath = null;
|
|
||||||
string probePath = null;
|
|
||||||
|
|
||||||
var info = GetFFMpegInfo();
|
|
||||||
|
|
||||||
encoderPath = info.EncoderPath;
|
|
||||||
probePath = info.ProbePath;
|
|
||||||
var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
|
|
||||||
LoggerFactory,
|
|
||||||
JsonSerializer,
|
|
||||||
encoderPath,
|
|
||||||
probePath,
|
|
||||||
hasExternalEncoder,
|
|
||||||
ServerConfigurationManager,
|
|
||||||
FileSystemManager,
|
|
||||||
LiveTvManager,
|
|
||||||
IsoManager,
|
|
||||||
LibraryManager,
|
|
||||||
ChannelManager,
|
|
||||||
SessionManager,
|
|
||||||
() => SubtitleEncoder,
|
|
||||||
() => MediaSourceManager,
|
|
||||||
HttpClient,
|
|
||||||
ZipClient,
|
|
||||||
ProcessFactory,
|
|
||||||
5000);
|
|
||||||
|
|
||||||
MediaEncoder = mediaEncoder;
|
|
||||||
serviceCollection.AddSingleton(MediaEncoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user repository.
|
/// Gets the user repository.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1481,7 +1410,7 @@ namespace Emby.Server.Implementations
|
|||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = localAddress,
|
LocalAddress = localAddress,
|
||||||
SupportsLibraryMonitor = true,
|
SupportsLibraryMonitor = true,
|
||||||
EncoderLocationType = MediaEncoder.EncoderLocationType,
|
EncoderLocation = MediaEncoder.EncoderLocation,
|
||||||
SystemArchitecture = EnvironmentInfo.SystemArchitecture,
|
SystemArchitecture = EnvironmentInfo.SystemArchitecture,
|
||||||
SystemUpdateLevel = SystemUpdateLevel,
|
SystemUpdateLevel = SystemUpdateLevel,
|
||||||
PackageName = StartupOptions.PackageName
|
PackageName = StartupOptions.PackageName
|
||||||
@ -1598,7 +1527,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
if (addresses.Count == 0)
|
if (addresses.Count == 0)
|
||||||
{
|
{
|
||||||
addresses.AddRange(NetworkManager.GetLocalIpAddresses());
|
addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultList = new List<IpAddressInfo>();
|
var resultList = new List<IpAddressInfo>();
|
||||||
|
@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
{
|
{
|
||||||
foreach (var item in returnItems)
|
foreach (var item in returnItems)
|
||||||
{
|
{
|
||||||
var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None);
|
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
}
|
}
|
||||||
|
|
||||||
numComplete++;
|
numComplete++;
|
||||||
double percent = numComplete;
|
double percent = (double)numComplete / allChannelsList.Count;
|
||||||
percent /= allChannelsList.Count;
|
|
||||||
|
|
||||||
progress.Report(100 * percent);
|
progress.Report(100 * percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
|
|
||||||
foreach (var item in result.Items)
|
foreach (var item in result.Items)
|
||||||
{
|
{
|
||||||
var folder = item as Folder;
|
if (item is Folder folder)
|
||||||
|
|
||||||
if (folder != null)
|
|
||||||
{
|
{
|
||||||
await GetChannelItemsInternal(new InternalItemsQuery
|
await GetChannelItemsInternal(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
|
@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
public static string GetUserDistinctValue(User user)
|
public static string GetUserDistinctValue(User user)
|
||||||
{
|
{
|
||||||
var channels = user.Policy.EnabledChannels
|
var channels = user.Policy.EnabledChannels
|
||||||
.OrderBy(i => i)
|
.OrderBy(i => i);
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return string.Join("|", channels.ToArray());
|
return string.Join("|", channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CleanDatabase(CancellationToken cancellationToken)
|
private void CleanDatabase(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
|
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
|
foreach (var channel in uninstalledChannels)
|
||||||
.Except(installedChannelIds)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var id in invalidIds)
|
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
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
|
// 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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
DeleteItem(deleteId);
|
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||||
|
{
|
||||||
|
DeleteFileLocation = false
|
||||||
|
|
||||||
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, delete the channel itself
|
// Finally, delete the channel itself
|
||||||
DeleteItem(id);
|
_libraryManager.DeleteItem(channel, new DeleteOptions
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteItem(Guid id)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
|
|
||||||
if (item == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
|
||||||
{
|
{
|
||||||
DeleteFileLocation = false
|
DeleteFileLocation = false
|
||||||
|
|
||||||
|
@ -1,13 +1,49 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Cryptography
|
namespace Emby.Server.Implementations.Cryptography
|
||||||
{
|
{
|
||||||
public class CryptographyProvider : ICryptoProvider
|
public class CryptographyProvider : ICryptoProvider
|
||||||
{
|
{
|
||||||
|
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||||
|
{
|
||||||
|
"MD5",
|
||||||
|
"System.Security.Cryptography.MD5",
|
||||||
|
"SHA",
|
||||||
|
"SHA1",
|
||||||
|
"System.Security.Cryptography.SHA1",
|
||||||
|
"SHA256",
|
||||||
|
"SHA-256",
|
||||||
|
"System.Security.Cryptography.SHA256",
|
||||||
|
"SHA384",
|
||||||
|
"SHA-384",
|
||||||
|
"System.Security.Cryptography.SHA384",
|
||||||
|
"SHA512",
|
||||||
|
"SHA-512",
|
||||||
|
"System.Security.Cryptography.SHA512"
|
||||||
|
};
|
||||||
|
|
||||||
|
public string DefaultHashMethod => "PBKDF2";
|
||||||
|
|
||||||
|
private RandomNumberGenerator _randomNumberGenerator;
|
||||||
|
|
||||||
|
private const int _defaultIterations = 1000;
|
||||||
|
|
||||||
|
public CryptographyProvider()
|
||||||
|
{
|
||||||
|
//FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
||||||
|
//Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
||||||
|
//there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
||||||
|
//Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
||||||
|
_randomNumberGenerator = RandomNumberGenerator.Create();
|
||||||
|
}
|
||||||
|
|
||||||
public Guid GetMD5(string str)
|
public Guid GetMD5(string str)
|
||||||
{
|
{
|
||||||
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
|
||||||
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
return provider.ComputeHash(bytes);
|
return provider.ComputeHash(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetSupportedHashMethods()
|
||||||
|
{
|
||||||
|
return _supportedHashMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||||
|
{
|
||||||
|
//downgrading for now as we need this library to be dotnetstandard compliant
|
||||||
|
//with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
||||||
|
if (method == DefaultHashMethod)
|
||||||
|
{
|
||||||
|
using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
|
||||||
|
{
|
||||||
|
return r.GetBytes(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ComputeHash(string hashMethod, byte[] bytes)
|
||||||
|
{
|
||||||
|
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
|
||||||
|
{
|
||||||
|
return ComputeHash(DefaultHashMethod, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||||
|
{
|
||||||
|
if (hashMethod == DefaultHashMethod)
|
||||||
|
{
|
||||||
|
return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
|
||||||
|
}
|
||||||
|
else if (_supportedHashMethods.Contains(hashMethod))
|
||||||
|
{
|
||||||
|
using (var h = HashAlgorithm.Create(hashMethod))
|
||||||
|
{
|
||||||
|
if (salt.Length == 0)
|
||||||
|
{
|
||||||
|
return h.ComputeHash(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte[] salted = new byte[bytes.Length + salt.Length];
|
||||||
|
Array.Copy(bytes, salted, bytes.Length);
|
||||||
|
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
||||||
|
return h.ComputeHash(salted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||||
|
{
|
||||||
|
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ComputeHash(PasswordHash hash)
|
||||||
|
{
|
||||||
|
int iterations = _defaultIterations;
|
||||||
|
if (!hash.Parameters.ContainsKey("iterations"))
|
||||||
|
{
|
||||||
|
hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
iterations = int.Parse(hash.Parameters["iterations"]);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] GenerateSalt()
|
||||||
|
{
|
||||||
|
byte[] salt = new byte[64];
|
||||||
|
_randomNumberGenerator.GetBytes(salt);
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2279,11 +2279,10 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"Audio",
|
"Book",
|
||||||
"MusicAlbum",
|
|
||||||
"MusicVideo",
|
|
||||||
"AudioBook",
|
"AudioBook",
|
||||||
"AudioPodcast"
|
"Episode",
|
||||||
|
"Season"
|
||||||
};
|
};
|
||||||
|
|
||||||
private bool HasSeriesFields(InternalItemsQuery query)
|
private bool HasSeriesFields(InternalItemsQuery query)
|
||||||
|
@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
list.Add(row[0].ReadGuidFromBlob());
|
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);
|
TryMigrateToLocalUsersTable(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RemoveEmptyPasswordHashes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RemoveEmptyPasswordHashes()
|
||||||
|
{
|
||||||
|
foreach (var user in RetrieveAllUsers())
|
||||||
|
{
|
||||||
|
// If the user password is the sha1 hash of the empty string, remove it
|
||||||
|
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|
||||||
|
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = null;
|
||||||
|
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||||
|
|
||||||
|
using (WriteLock.Write())
|
||||||
|
using (var connection = CreateConnection())
|
||||||
|
{
|
||||||
|
connection.RunInTransaction(db =>
|
||||||
|
{
|
||||||
|
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||||
|
{
|
||||||
|
statement.TryBind("@InternalId", user.InternalId);
|
||||||
|
statement.TryBind("@data", serialized);
|
||||||
|
statement.MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
}, TransactionMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a user in the repo
|
/// Save a user in the repo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -5,8 +5,6 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Devices;
|
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers;
|
|||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return GetBaseItemDto(item, options, user, owner);
|
return GetBaseItemDto(item, options, user, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
public BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||||
{
|
=> GetBaseItemDtos(items, items.Count, options, user, owner);
|
||||||
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(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null)
|
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;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public string Name => "Default";
|
public string Name => "Default";
|
||||||
|
|
||||||
public bool IsEnabled => true;
|
public bool IsEnabled => true;
|
||||||
|
|
||||||
|
// This is dumb and an artifact of the backwards way auth providers were designed.
|
||||||
|
// This version of authenticate was never meant to be called, but needs to be here for interface compat
|
||||||
|
// Only the providers that don't provide local user support use this
|
||||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the verson that we need to use for local users. Because reasons.
|
||||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
||||||
{
|
{
|
||||||
|
bool success = false;
|
||||||
if (resolvedUser == null)
|
if (resolvedUser == null)
|
||||||
{
|
{
|
||||||
throw new Exception("Invalid username or password");
|
throw new Exception("Invalid username or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
// As long as jellyfin supports passwordless users, we need this little block here to accomodate
|
||||||
|
if (IsPasswordEmpty(resolvedUser, password))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ProviderAuthenticationResult
|
||||||
|
{
|
||||||
|
Username = username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ConvertPasswordFormat(resolvedUser);
|
||||||
|
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
|
||||||
|
|
||||||
|
PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
|
||||||
|
byte[] calculatedHash;
|
||||||
|
string calculatedHashString;
|
||||||
|
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(readyHash.Salt))
|
||||||
|
{
|
||||||
|
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
|
||||||
|
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
|
||||||
|
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calculatedHashString == readyHash.Hash)
|
||||||
|
{
|
||||||
|
success = true;
|
||||||
|
// throw new Exception("Invalid username or password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
|
||||||
|
// but at least they are in the new format.
|
||||||
|
private void ConvertPasswordFormat(User user)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(user.Password))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.Password.Contains("$"))
|
||||||
|
{
|
||||||
|
string hash = user.Password;
|
||||||
|
user.Password = string.Format("$SHA1${0}", hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
||||||
|
{
|
||||||
|
string hash = user.EasyPassword;
|
||||||
|
user.EasyPassword = string.Format("$SHA1${0}", hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Task<bool> HasPassword(User user)
|
public Task<bool> HasPassword(User user)
|
||||||
{
|
{
|
||||||
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
|
||||||
return Task.FromResult(hasConfiguredPassword);
|
return Task.FromResult(hasConfiguredPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
private bool IsPasswordEmpty(User user, string password)
|
||||||
{
|
{
|
||||||
return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ChangePassword(User user, string newPassword)
|
public Task ChangePassword(User user, string newPassword)
|
||||||
{
|
{
|
||||||
string newPasswordHash = null;
|
ConvertPasswordFormat(user);
|
||||||
|
// This is needed to support changing a no password user to a password user
|
||||||
if (newPassword != null)
|
if (string.IsNullOrEmpty(user.Password))
|
||||||
{
|
{
|
||||||
newPasswordHash = GetHashedString(user, newPassword);
|
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
|
||||||
|
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||||
|
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
|
||||||
|
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||||
|
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
|
||||||
|
user.Password = newPasswordHash.ToString();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
PasswordHash passwordHash = new PasswordHash(user.Password);
|
||||||
|
if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
|
||||||
|
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
|
||||||
|
passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
|
||||||
|
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
|
||||||
|
}
|
||||||
|
else if (newPassword != null)
|
||||||
|
{
|
||||||
|
passwordHash.Hash = GetHashedString(user, newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Password = newPasswordHash;
|
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(passwordHash.Hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = passwordHash.ToString();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPasswordHash(User user)
|
public string GetPasswordHash(User user)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(user.Password)
|
return user.Password;
|
||||||
? GetEmptyHashedString(user)
|
|
||||||
: user.Password;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetEmptyHashedString(User user)
|
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||||
{
|
{
|
||||||
return GetHashedString(user, string.Empty);
|
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
||||||
|
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetHashedString(User user, string str)
|
public string GetHashedString(User user, string str)
|
||||||
{
|
{
|
||||||
var salt = user.Salt;
|
PasswordHash passwordHash;
|
||||||
if (salt != null)
|
if (string.IsNullOrEmpty(user.Password))
|
||||||
{
|
{
|
||||||
// return BCrypt.HashPassword(str, salt);
|
passwordHash = new PasswordHash(_cryptographyProvider);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConvertPasswordFormat(user);
|
||||||
|
passwordHash = new PasswordHash(user.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// legacy
|
if (passwordHash.SaltBytes != null)
|
||||||
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
|
{
|
||||||
|
// the password is modern format with PBKDF and we should take advantage of that
|
||||||
|
passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
|
||||||
|
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the password has no salt and should be called with the older method for safety
|
||||||
|
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsValidUsername(string username)
|
public static bool IsValidUsername(string username)
|
||||||
{
|
{
|
||||||
// Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
//This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||||
foreach (var currentChar in username)
|
//In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||||
{
|
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
||||||
if (!IsValidUsernameCharacter(currentChar))
|
return Regex.IsMatch(username, "^[\\w-'._@]*$");
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidUsernameCharacter(char i)
|
private static bool IsValidUsernameCharacter(char i)
|
||||||
{
|
{
|
||||||
return !char.Equals(i, '<') && !char.Equals(i, '>');
|
return IsValidUsername(i.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MakeValidUsername(string username)
|
public string MakeValidUsername(string username)
|
||||||
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private string GetLocalPasswordHash(User user)
|
private string GetLocalPasswordHash(User user)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(user.EasyPassword)
|
return string.IsNullOrEmpty(user.EasyPassword)
|
||||||
? _defaultAuthenticationProvider.GetEmptyHashedString(user)
|
? null
|
||||||
: user.EasyPassword;
|
: user.EasyPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPasswordEmpty(User user, string passwordHash)
|
|
||||||
{
|
|
||||||
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads the users from the repository
|
/// Loads the users from the repository
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
|
|||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||||
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
|
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
||||||
|
|
||||||
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||||
hasConfiguredEasyPassword :
|
hasConfiguredEasyPassword :
|
||||||
hasConfiguredPassword;
|
hasConfiguredPassword;
|
||||||
|
|
||||||
var dto = new UserDto
|
UserDto dto = new UserDto
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
dto.EnableAutoLogin = true;
|
dto.EnableAutoLogin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var image = user.GetImageInfo(ImageType.Primary, 0);
|
ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
|
||||||
|
|
||||||
if (image != null)
|
if (image != null)
|
||||||
{
|
{
|
||||||
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (!IsValidUsername(name))
|
if (!IsValidUsername(name))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
{
|
{
|
||||||
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
|
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
|
||||||
|
|
||||||
Directory.CreateDirectory(LocalizationPath);
|
|
||||||
|
|
||||||
var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName);
|
|
||||||
|
|
||||||
// Extract from the assembly
|
// Extract from the assembly
|
||||||
foreach (var resource in _assembly.GetManifestResourceNames())
|
foreach (var resource in _assembly.GetManifestResourceNames())
|
||||||
{
|
{
|
||||||
@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string filename = "ratings-" + resource.Substring(ratingsResource.Length);
|
string countryCode = resource.Substring(ratingsResource.Length, 2);
|
||||||
|
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (existingFiles.Contains(filename))
|
using (var str = _assembly.GetManifestResourceStream(resource))
|
||||||
|
using (var reader = new StreamReader(str))
|
||||||
{
|
{
|
||||||
continue;
|
string line;
|
||||||
}
|
while ((line = await reader.ReadLineAsync()) != null)
|
||||||
|
|
||||||
using (var stream = _assembly.GetManifestResourceStream(resource))
|
|
||||||
{
|
|
||||||
string target = Path.Combine(LocalizationPath, filename);
|
|
||||||
_logger.LogInformation("Extracting ratings to {0}", target);
|
|
||||||
|
|
||||||
using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
|
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fs);
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] parts = line.Split(',');
|
||||||
|
if (parts.Length == 2
|
||||||
|
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
|
||||||
|
{
|
||||||
|
dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in GetRatingsFiles(LocalizationPath))
|
_allParentalRatings[countryCode] = dict;
|
||||||
{
|
|
||||||
await LoadRatings(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadAdditionalRatings();
|
|
||||||
|
|
||||||
await LoadCultures();
|
await LoadCultures();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadAdditionalRatings()
|
|
||||||
{
|
|
||||||
LoadRatings("au", new[]
|
|
||||||
{
|
|
||||||
new ParentalRating("AU-G", 1),
|
|
||||||
new ParentalRating("AU-PG", 5),
|
|
||||||
new ParentalRating("AU-M", 6),
|
|
||||||
new ParentalRating("AU-MA15+", 7),
|
|
||||||
new ParentalRating("AU-M15+", 8),
|
|
||||||
new ParentalRating("AU-R18+", 9),
|
|
||||||
new ParentalRating("AU-X18+", 10),
|
|
||||||
new ParentalRating("AU-RC", 11)
|
|
||||||
});
|
|
||||||
|
|
||||||
LoadRatings("be", new[]
|
|
||||||
{
|
|
||||||
new ParentalRating("BE-AL", 1),
|
|
||||||
new ParentalRating("BE-MG6", 2),
|
|
||||||
new ParentalRating("BE-6", 3),
|
|
||||||
new ParentalRating("BE-9", 5),
|
|
||||||
new ParentalRating("BE-12", 6),
|
|
||||||
new ParentalRating("BE-16", 8)
|
|
||||||
});
|
|
||||||
|
|
||||||
LoadRatings("de", new[]
|
|
||||||
{
|
|
||||||
new ParentalRating("DE-0", 1),
|
|
||||||
new ParentalRating("FSK-0", 1),
|
|
||||||
new ParentalRating("DE-6", 5),
|
|
||||||
new ParentalRating("FSK-6", 5),
|
|
||||||
new ParentalRating("DE-12", 7),
|
|
||||||
new ParentalRating("FSK-12", 7),
|
|
||||||
new ParentalRating("DE-16", 8),
|
|
||||||
new ParentalRating("FSK-16", 8),
|
|
||||||
new ParentalRating("DE-18", 9),
|
|
||||||
new ParentalRating("FSK-18", 9)
|
|
||||||
});
|
|
||||||
|
|
||||||
LoadRatings("ru", new[]
|
|
||||||
{
|
|
||||||
new ParentalRating("RU-0+", 1),
|
|
||||||
new ParentalRating("RU-6+", 3),
|
|
||||||
new ParentalRating("RU-12+", 7),
|
|
||||||
new ParentalRating("RU-16+", 9),
|
|
||||||
new ParentalRating("RU-18+", 10)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadRatings(string country, ParentalRating[] ratings)
|
|
||||||
{
|
|
||||||
_allParentalRatings[country] = ratings.ToDictionary(i => i.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<string> GetRatingsFiles(string directory)
|
|
||||||
=> _fileSystem.GetFilePaths(directory, false)
|
|
||||||
.Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the localization path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The localization path.</value>
|
|
||||||
public string LocalizationPath
|
|
||||||
=> Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization");
|
|
||||||
|
|
||||||
public string NormalizeFormKD(string text)
|
public string NormalizeFormKD(string text)
|
||||||
=> text.Normalize(NormalizationForm.FormKD);
|
=> text.Normalize(NormalizationForm.FormKD);
|
||||||
|
|
||||||
@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads the ratings.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="file">The file.</param>
|
|
||||||
/// <returns>Dictionary{System.StringParentalRating}.</returns>
|
|
||||||
private async Task LoadRatings(string file)
|
|
||||||
{
|
|
||||||
Dictionary<string, ParentalRating> dict
|
|
||||||
= new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
using (var str = File.OpenRead(file))
|
|
||||||
using (var reader = new StreamReader(str))
|
|
||||||
{
|
|
||||||
string line;
|
|
||||||
while ((line = await reader.ReadLineAsync()) != null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] parts = line.Split(',');
|
|
||||||
if (parts.Length == 2
|
|
||||||
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
|
|
||||||
{
|
|
||||||
dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value }));
|
|
||||||
}
|
|
||||||
#if DEBUG
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Misformed line in {Path}", file);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1];
|
|
||||||
|
|
||||||
_allParentalRatings[countryCode] = dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
|
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
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 IpAddressInfo[] _localIpAddresses;
|
||||||
private readonly object _localIpAddressSyncLock = new object();
|
private readonly object _localIpAddressSyncLock = new object();
|
||||||
|
|
||||||
public IpAddressInfo[] GetLocalIpAddresses()
|
public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
|
||||||
{
|
{
|
||||||
lock (_localIpAddressSyncLock)
|
lock (_localIpAddressSyncLock)
|
||||||
{
|
{
|
||||||
if (_localIpAddresses == null)
|
if (_localIpAddresses == null)
|
||||||
{
|
{
|
||||||
var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray();
|
var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray();
|
||||||
|
|
||||||
_localIpAddresses = addresses;
|
_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();
|
.ToList();
|
||||||
|
|
||||||
if (list.Count == 0)
|
if (list.Count == 0)
|
||||||
@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking
|
|||||||
return Dns.GetHostAddressesAsync(hostName);
|
return Dns.GetHostAddressesAsync(hostName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IPAddress> GetIPsDefault()
|
private List<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
|
||||||
{
|
{
|
||||||
NetworkInterface[] interfaces;
|
NetworkInterface[] interfaces;
|
||||||
|
|
||||||
@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking
|
|||||||
// Try to exclude virtual adapters
|
// Try to exclude virtual adapters
|
||||||
// http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
|
// http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
|
||||||
var addr = ipProperties.GatewayAddresses.FirstOrDefault();
|
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>();
|
return new List<IPAddress>();
|
||||||
}
|
}
|
||||||
@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking
|
|||||||
return false;
|
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)
|
public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
|
||||||
{
|
{
|
||||||
if (endpoint == null)
|
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 char ComponentSeperator = '.';
|
||||||
private const string VariablePrefix = "{";
|
private const string VariablePrefix = "{";
|
||||||
|
|
||||||
readonly bool[] componentsWithSeparators;
|
private readonly bool[] componentsWithSeparators;
|
||||||
|
|
||||||
private readonly string restPath;
|
private readonly string restPath;
|
||||||
public bool IsWildCardPath { get; private set; }
|
public bool IsWildCardPath { get; private set; }
|
||||||
@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services
|
|||||||
public string Description { get; private set; }
|
public string Description { get; private set; }
|
||||||
public bool IsHidden { 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)
|
public static string[] GetPathPartsForMatching(string pathInfo)
|
||||||
{
|
{
|
||||||
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
|
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
list.Add(hashPrefix + part);
|
list.Add(hashPrefix + part);
|
||||||
|
|
||||||
var subParts = part.Split(ComponentSeperator);
|
if (part.IndexOf(ComponentSeperator) == -1)
|
||||||
if (subParts.Length == 1) continue;
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subParts = part.Split(ComponentSeperator);
|
||||||
foreach (var subPart in subParts)
|
foreach (var subPart in subParts)
|
||||||
{
|
{
|
||||||
list.Add(hashPrefix + subPart);
|
list.Add(hashPrefix + subPart);
|
||||||
@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(component)) continue;
|
if (string.IsNullOrEmpty(component)) continue;
|
||||||
|
|
||||||
if (StringContains(component, VariablePrefix)
|
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
|
||||||
&& component.IndexOf(ComponentSeperator) != -1)
|
&& component.IndexOf(ComponentSeperator) != -1)
|
||||||
{
|
{
|
||||||
hasSeparators.Add(true);
|
hasSeparators.Add(true);
|
||||||
@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
for (var i = 0; i < components.Length - 1; i++)
|
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)
|
if (this.literalsToMatch[i + 1] == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
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.IsWildCardPath = this.wildcardCount > 0;
|
||||||
|
|
||||||
this.FirstMatchHashKey = !this.IsWildCardPath
|
this.FirstMatchHashKey = !this.IsWildCardPath
|
||||||
@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services
|
|||||||
: WildCardChar + PathSeperator + firstLiteralMatch;
|
: WildCardChar + PathSeperator + firstLiteralMatch;
|
||||||
|
|
||||||
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
|
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",
|
"IgnoreDataMemberAttribute",
|
||||||
"JsonIgnoreAttribute"
|
"JsonIgnoreAttribute"
|
||||||
};
|
};
|
||||||
@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
private static Type excludeType = typeof(Stream);
|
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>();
|
foreach (var prop in GetPublicProperties(type))
|
||||||
var props = GetPublicProperties(type);
|
|
||||||
|
|
||||||
foreach (var prop in props)
|
|
||||||
{
|
{
|
||||||
if (prop.GetMethod == null)
|
if (prop.GetMethod == null
|
||||||
{
|
|| excludeType == prop.PropertyType)
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeType == prop.PropertyType)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
if (!ignored)
|
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 propertyInfos = new List<PropertyInfo>();
|
||||||
|
var considered = new List<Type>()
|
||||||
var considered = new List<Type>();
|
{
|
||||||
|
type
|
||||||
|
};
|
||||||
var queue = new Queue<Type>();
|
var queue = new Queue<Type>();
|
||||||
considered.Add(type);
|
|
||||||
queue.Enqueue(type);
|
queue.Enqueue(type);
|
||||||
|
|
||||||
while (queue.Count > 0)
|
while (queue.Count > 0)
|
||||||
@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services
|
|||||||
var subType = queue.Dequeue();
|
var subType = queue.Dequeue();
|
||||||
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
|
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
|
||||||
{
|
{
|
||||||
if (considered.Contains(subInterface)) continue;
|
if (considered.Contains(subInterface))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
considered.Add(subInterface);
|
considered.Add(subInterface);
|
||||||
queue.Enqueue(subInterface);
|
queue.Enqueue(subInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeProperties = GetTypesPublicProperties(subType);
|
var newPropertyInfos = GetTypesPublicProperties(subType)
|
||||||
|
|
||||||
var newPropertyInfos = typeProperties
|
|
||||||
.Where(x => !propertyInfos.Contains(x));
|
.Where(x => !propertyInfos.Contains(x));
|
||||||
|
|
||||||
propertyInfos.InsertRange(0, newPropertyInfos);
|
propertyInfos.InsertRange(0, newPropertyInfos);
|
||||||
@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services
|
|||||||
return propertyInfos;
|
return propertyInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<PropertyInfo>();
|
return GetTypesPublicProperties(type)
|
||||||
|
.Where(x => x.GetIndexParameters().Length == 0);
|
||||||
foreach (var t in GetTypesPublicProperties(type))
|
|
||||||
{
|
|
||||||
if (t.GetIndexParameters().Length == 0)
|
|
||||||
{
|
|
||||||
list.Add(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PropertyInfo[] GetTypesPublicProperties(Type subType)
|
private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
|
||||||
{
|
{
|
||||||
var pis = new List<PropertyInfo>();
|
|
||||||
foreach (var pi in subType.GetRuntimeProperties())
|
foreach (var pi in subType.GetRuntimeProperties())
|
||||||
{
|
{
|
||||||
var mi = pi.GetMethod ?? pi.SetMethod;
|
var mi = pi.GetMethod ?? pi.SetMethod;
|
||||||
if (mi != null && mi.IsStatic) continue;
|
if (mi != null && mi.IsStatic)
|
||||||
pis.Add(pi);
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return pi;
|
||||||
}
|
}
|
||||||
return pis.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
private readonly StringMapTypeDeserializer typeDeserializer;
|
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)
|
public int MatchScore(string httpMethod, string[] withPathInfoParts)
|
||||||
{
|
{
|
||||||
@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var score = 0;
|
|
||||||
|
|
||||||
//Routes with least wildcard matches get the highest score
|
//Routes with least wildcard matches get the highest score
|
||||||
score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
|
var score = Math.Max((100 - wildcardMatchCount), 1) * 1000
|
||||||
|
//Routes with less variable (and more literal) matches
|
||||||
//Routes with less variable (and more literal) matches
|
+ Math.Max((10 - VariableArgsCount), 1) * 100;
|
||||||
score += Math.Max((10 - VariableArgsCount), 1) * 100;
|
|
||||||
|
|
||||||
//Exact verb match is better than ANY
|
//Exact verb match is better than ANY
|
||||||
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
|
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
|
||||||
@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services
|
|||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool StringContains(string str1, string str2)
|
|
||||||
{
|
|
||||||
return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For performance withPathInfoParts should already be a lower case string
|
/// For performance withPathInfoParts should already be a lower case string
|
||||||
/// to minimize redundant matching operations.
|
/// to minimize redundant matching operations.
|
||||||
@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services
|
|||||||
if (i < this.TotalComponentsCount - 1)
|
if (i < this.TotalComponentsCount - 1)
|
||||||
{
|
{
|
||||||
// Continue to consume up until a match with the next literal
|
// 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++;
|
pathIx++;
|
||||||
wildcardMatchCount++;
|
wildcardMatchCount++;
|
||||||
@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
|
if (withPathInfoParts.Length <= pathIx
|
||||||
|
|| !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pathIx++;
|
pathIx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services
|
|||||||
return pathIx == withPathInfoParts.Length;
|
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)
|
private bool ExplodeComponents(ref string[] withPathInfoParts)
|
||||||
{
|
{
|
||||||
var totalComponents = new List<string>();
|
var totalComponents = new List<string>();
|
||||||
for (var i = 0; i < withPathInfoParts.Length; i++)
|
for (var i = 0; i < withPathInfoParts.Length; i++)
|
||||||
{
|
{
|
||||||
var component = withPathInfoParts[i];
|
var component = withPathInfoParts[i];
|
||||||
if (string.IsNullOrEmpty(component)) continue;
|
if (string.IsNullOrEmpty(component))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.PathComponentsCount != this.TotalComponentsCount
|
if (this.PathComponentsCount != this.TotalComponentsCount
|
||||||
&& this.componentsWithSeparators[i])
|
&& this.componentsWithSeparators[i])
|
||||||
{
|
{
|
||||||
var subComponents = component.Split(ComponentSeperator);
|
var subComponents = component.Split(ComponentSeperator);
|
||||||
if (subComponents.Length < 2) return false;
|
if (subComponents.Length < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
totalComponents.AddRange(subComponents);
|
totalComponents.AddRange(subComponents);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest))
|
if (!this._propertyNamesMap.Contains(variableName))
|
||||||
{
|
{
|
||||||
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
sb.Append(PathSeperatorChar + requestComponents[j]);
|
sb.Append(PathSeperatorChar + requestComponents[j]);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = sb.ToString();
|
value = sb.ToString();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services
|
|||||||
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
|
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
|
||||||
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder(value);
|
||||||
sb.Append(value);
|
|
||||||
pathIx++;
|
pathIx++;
|
||||||
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = sb.ToString();
|
value = sb.ToString();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
pathIx++;
|
pathIx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
requestKeyValuesMap[propertyNameOnRequest] = value;
|
requestKeyValuesMap[variableName] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryStringAndFormData != null)
|
if (queryStringAndFormData != null)
|
||||||
|
@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
internal class PropertySerializerEntry
|
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;
|
PropertySetFn = propertySetFn;
|
||||||
PropertyParseStringFn = propertyParseStringFn;
|
PropertyParseStringFn = propertyParseStringFn;
|
||||||
|
PropertyType = PropertyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Action<object, object> PropertySetFn;
|
public Action<object, object> PropertySetFn { get; private set; }
|
||||||
public Func<string, object> PropertyParseStringFn;
|
public Func<string, object> PropertyParseStringFn { get; private set; }
|
||||||
public Type PropertyType;
|
public Type PropertyType { get; private set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Type type;
|
private readonly Type type;
|
||||||
@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services
|
|||||||
public Func<string, object> GetParseFn(Type propertyType)
|
public Func<string, object> GetParseFn(Type propertyType)
|
||||||
{
|
{
|
||||||
if (propertyType == typeof(string))
|
if (propertyType == typeof(string))
|
||||||
|
{
|
||||||
return s => s;
|
return s => s;
|
||||||
|
}
|
||||||
|
|
||||||
return _GetParseFn(propertyType);
|
return _GetParseFn(propertyType);
|
||||||
}
|
}
|
||||||
@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
|
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
|
||||||
var propertyType = propertyInfo.PropertyType;
|
var propertyType = propertyInfo.PropertyType;
|
||||||
var propertyParseStringFn = GetParseFn(propertyType);
|
var propertyParseStringFn = GetParseFn(propertyType);
|
||||||
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
|
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
|
||||||
|
|
||||||
propertySetterMap[propertyInfo.Name] = propertySerializer;
|
propertySetterMap[propertyInfo.Name] = propertySerializer;
|
||||||
}
|
}
|
||||||
@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
|
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
|
||||||
{
|
{
|
||||||
string propertyName = null;
|
|
||||||
string propertyTextValue = null;
|
|
||||||
PropertySerializerEntry propertySerializerEntry = null;
|
PropertySerializerEntry propertySerializerEntry = null;
|
||||||
|
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
|
{
|
||||||
instance = _CreateInstanceFn(type);
|
instance = _CreateInstanceFn(type);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var pair in keyValuePairs)
|
foreach (var pair in keyValuePairs)
|
||||||
{
|
{
|
||||||
propertyName = pair.Key;
|
string propertyName = pair.Key;
|
||||||
propertyTextValue = pair.Value;
|
string propertyTextValue = pair.Value;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(propertyTextValue))
|
if (string.IsNullOrEmpty(propertyTextValue)
|
||||||
{
|
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|
||||||
continue;
|
|| propertySerializerEntry.PropertySetFn == null)
|
||||||
}
|
|
||||||
|
|
||||||
if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry))
|
|
||||||
{
|
|
||||||
if (propertyName == "v")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (propertySerializerEntry.PropertySetFn == null)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
propertySerializerEntry.PropertySetFn(instance, value);
|
propertySerializerEntry.PropertySetFn(instance, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
public static string LeftPart(string strVal, char needle)
|
public static string LeftPart(string strVal, char needle)
|
||||||
{
|
{
|
||||||
if (strVal == null) return null;
|
if (strVal == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var pos = strVal.IndexOf(needle);
|
var pos = strVal.IndexOf(needle);
|
||||||
return pos == -1
|
return pos == -1
|
||||||
? strVal
|
? strVal
|
||||||
@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
|
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;
|
var setMethodInfo = propertyInfo.SetMethod;
|
||||||
return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
|
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);
|
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);
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
{
|
{
|
||||||
public partial class WebSocketSharpRequest : IHttpRequest
|
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)
|
if (ap == -1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -33,18 +33,19 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
ending = ' ';
|
ending = ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
int end = header.IndexOf(ending, ap + 1);
|
var slice = header.Slice(ap + 1);
|
||||||
|
int end = slice.IndexOf(ending);
|
||||||
if (end == -1)
|
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)
|
private async Task LoadMultiPart(WebROCollection form)
|
||||||
{
|
{
|
||||||
string boundary = GetParameter(ContentType, "; boundary=");
|
string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
|
||||||
if (boundary == null)
|
if (boundary == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -377,17 +378,17 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
}
|
}
|
||||||
|
|
||||||
var elem = new Element();
|
var elem = new Element();
|
||||||
string header;
|
ReadOnlySpan<char> header;
|
||||||
while ((header = ReadHeaders()) != null)
|
while ((header = ReadHeaders().AsSpan()) != null)
|
||||||
{
|
{
|
||||||
if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase))
|
if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
elem.Name = GetContentDispositionAttribute(header, "name");
|
elem.Name = GetContentDispositionAttribute(header, "name");
|
||||||
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
|
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
|
||||||
}
|
}
|
||||||
else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
|
else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
elem.ContentType = header.Substring("Content-Type:".Length).Trim();
|
elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
|
||||||
elem.Encoding = GetEncoding(elem.ContentType);
|
elem.Encoding = GetEncoding(elem.ContentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -435,16 +436,16 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
return sb.ToString();
|
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)
|
if (idx < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int begin = idx + name.Length + "=\"".Length;
|
int begin = idx + name.Length + "=\"".Length;
|
||||||
int end = l.IndexOf('"', begin);
|
int end = l.Slice(begin).IndexOf('"');
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -455,19 +456,19 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
return string.Empty;
|
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)
|
if (idx < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int begin = idx + name.Length + "=\"".Length;
|
int begin = idx + name.Length + "=\"".Length;
|
||||||
int end = l.IndexOf('"', begin);
|
int end = l.Slice(begin).IndexOf('"');
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -478,7 +479,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
string temp = l.Substring(begin, end - begin);
|
ReadOnlySpan<char> temp = l.Slice(begin, end - begin);
|
||||||
byte[] source = new byte[temp.Length];
|
byte[] source = new byte[temp.Length];
|
||||||
for (int i = temp.Length - 1; i >= 0; i--)
|
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();
|
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
|
||||||
|
|
||||||
private string remoteIp;
|
private string remoteIp;
|
||||||
|
public string RemoteIp
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (remoteIp != null)
|
||||||
|
{
|
||||||
|
return remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
public string RemoteIp =>
|
var temp = CheckBadChars(XForwardedFor.AsSpan());
|
||||||
remoteIp ??
|
if (temp.Length != 0)
|
||||||
(remoteIp = CheckBadChars(XForwardedFor) ??
|
{
|
||||||
NormalizeIp(CheckBadChars(XRealIp) ??
|
return remoteIp = temp.ToString();
|
||||||
(string.IsNullOrEmpty(request.HttpContext.Connection.RemoteIpAddress.ToString()) ? null : NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.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 };
|
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
|
// 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;
|
return name;
|
||||||
}
|
}
|
||||||
@ -99,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
}
|
}
|
||||||
else if (c == 127 || (c < ' ' && c != '\t'))
|
else if (c == 127 || (c < ' ' && c != '\t'))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("net_WebHeaderInvalidControlChars");
|
throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -113,7 +131,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
@ -124,29 +142,29 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (crlf != 0)
|
if (crlf != 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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
|
// Handle ipv4 mapped to ipv6
|
||||||
const string srch = "::ffff:";
|
const string srch = "::ffff:";
|
||||||
var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
|
||||||
if (index == 0)
|
if (index == 0)
|
||||||
{
|
{
|
||||||
ip = ip.Substring(srch.Length);
|
ip = ip.Slice(srch.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +342,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.pathInfo = WebUtility.UrlDecode(pathInfo);
|
this.pathInfo = WebUtility.UrlDecode(pathInfo);
|
||||||
this.pathInfo = NormalizePathInfo(pathInfo, mode);
|
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.pathInfo;
|
return this.pathInfo;
|
||||||
@ -436,7 +454,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
|||||||
|
|
||||||
public static Encoding GetEncoding(string contentTypeHeader)
|
public static Encoding GetEncoding(string contentTypeHeader)
|
||||||
{
|
{
|
||||||
var param = GetParameter(contentTypeHeader, "charset=");
|
var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
|
||||||
if (param == null)
|
if (param == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -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)
|
if (handlerPath != null)
|
||||||
{
|
{
|
||||||
var trimmed = pathInfo.TrimStart('/');
|
var trimmed = pathInfo.AsSpan().TrimStart('/');
|
||||||
if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
|
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);
|
ParseMovieDbSystem(reader, result);
|
||||||
break;
|
break;
|
||||||
case "SxxExx":
|
case "SxxExx":
|
||||||
// TODO
|
ParseSxxExxSystem(reader, result);
|
||||||
// <episode-num system="SxxExx">S03E12</episode-num>
|
|
||||||
reader.Skip();
|
|
||||||
break;
|
break;
|
||||||
default: // Handles empty string and nulls
|
default: // Handles empty string and nulls
|
||||||
reader.Skip();
|
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)
|
public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result)
|
||||||
{
|
{
|
||||||
// <episode-num system="thetvdb.com">series/248841</episode-num>
|
// <episode-num system="thetvdb.com">series/248841</episode-num>
|
||||||
|
@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
foregroundWidth *= percent;
|
foregroundWidth *= percent;
|
||||||
foregroundWidth /= 100;
|
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);
|
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())
|
using (var paint = new SKPaint())
|
||||||
{
|
{
|
||||||
paint.Color = SKColor.Parse("#CC52B54B");
|
paint.Color = SKColor.Parse("#CC00A4DC");
|
||||||
paint.Style = SKPaintStyle.Fill;
|
paint.Style = SKPaintStyle.Fill;
|
||||||
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
|
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
|
|
||||||
using (var paint = new SKPaint())
|
using (var paint = new SKPaint())
|
||||||
{
|
{
|
||||||
paint.Color = SKColor.Parse("#CC52B54B");
|
paint.Color = SKColor.Parse("#CC00A4DC");
|
||||||
paint.Style = SKPaintStyle.Fill;
|
paint.Style = SKPaintStyle.Fill;
|
||||||
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
|
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.")]
|
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
|
||||||
public string LogDir { get; set; }
|
public string LogDir { get; set; }
|
||||||
|
|
||||||
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")]
|
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
|
||||||
public string FFmpegPath { get; set; }
|
public string FFmpegPath { get; set; }
|
||||||
|
|
||||||
[Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")]
|
[Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")]
|
||||||
public string FFprobePath { get; set; }
|
public string FFprobePath { get; set; }
|
||||||
|
|
||||||
[Option("service", Required = false, HelpText = "Run as headless service.")]
|
[Option("service", Required = false, HelpText = "Run as headless service.")]
|
||||||
|
@ -172,16 +172,9 @@ namespace MediaBrowser.Api
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
|
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes))
|
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
{
|
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
|
||||||
options.ImageTypes = Array.Empty<ImageType>();
|
.ToArray();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@ -180,7 +181,7 @@ namespace MediaBrowser.Api
|
|||||||
return ToOptimizedResult(filters);
|
return ToOptimizedResult(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryFiltersLegacy GetFilters(BaseItem[] items)
|
private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
|
||||||
{
|
{
|
||||||
var result = new QueryFiltersLegacy();
|
var result = new QueryFiltersLegacy();
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||||||
[Route("/Videos/{Id}/stream.mov", "GET")]
|
[Route("/Videos/{Id}/stream.mov", "GET")]
|
||||||
[Route("/Videos/{Id}/stream.iso", "GET")]
|
[Route("/Videos/{Id}/stream.iso", "GET")]
|
||||||
[Route("/Videos/{Id}/stream.flv", "GET")]
|
[Route("/Videos/{Id}/stream.flv", "GET")]
|
||||||
|
[Route("/Videos/{Id}/stream.rm", "GET")]
|
||||||
[Route("/Videos/{Id}/stream", "GET")]
|
[Route("/Videos/{Id}/stream", "GET")]
|
||||||
[Route("/Videos/{Id}/stream.ts", "HEAD")]
|
[Route("/Videos/{Id}/stream.ts", "HEAD")]
|
||||||
[Route("/Videos/{Id}/stream.webm", "HEAD")]
|
[Route("/Videos/{Id}/stream.webm", "HEAD")]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
@ -197,29 +198,27 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
request.ParentId = null;
|
request.ParentId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = string.IsNullOrEmpty(request.ParentId) ?
|
BaseItem item = null;
|
||||||
null :
|
|
||||||
_libraryManager.GetItemById(request.ParentId);
|
if (!string.IsNullOrEmpty(request.ParentId))
|
||||||
|
{
|
||||||
|
item = _libraryManager.GetItemById(request.ParentId);
|
||||||
|
}
|
||||||
|
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
item = string.IsNullOrEmpty(request.ParentId) ?
|
item = _libraryManager.GetUserRootFolder();
|
||||||
user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
|
|
||||||
_libraryManager.GetItemById(request.ParentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default list type = children
|
Folder folder = item as Folder;
|
||||||
|
|
||||||
var folder = item as Folder;
|
|
||||||
if (folder == null)
|
if (folder == null)
|
||||||
{
|
{
|
||||||
folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
|
folder = _libraryManager.GetUserRootFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasCollectionType = folder as IHasCollectionType;
|
var hasCollectionType = folder as IHasCollectionType;
|
||||||
var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase));
|
if (hasCollectionType != null
|
||||||
|
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||||
if (isPlaylistQuery)
|
|
||||||
{
|
{
|
||||||
request.Recursive = true;
|
request.Recursive = true;
|
||||||
request.IncludeItemTypes = "Playlist";
|
request.IncludeItemTypes = "Playlist";
|
||||||
@ -235,20 +234,12 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
|
if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
|
||||||
{
|
|
||||||
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
|
|
||||||
}
|
|
||||||
|
|
||||||
var userRoot = item as UserRootFolder;
|
|
||||||
|
|
||||||
if (userRoot == null)
|
|
||||||
{
|
{
|
||||||
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
|
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemsArray = folder.GetChildren(user, true).ToArray();
|
var itemsArray = folder.GetChildren(user, true).ToArray();
|
||||||
|
|
||||||
return new QueryResult<BaseItem>
|
return new QueryResult<BaseItem>
|
||||||
{
|
{
|
||||||
Items = itemsArray,
|
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>
|
/// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
|
||||||
bool IsInLocalNetwork(string endpoint);
|
bool IsInLocalNetwork(string endpoint);
|
||||||
|
|
||||||
IpAddressInfo[] GetLocalIpAddresses();
|
IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface);
|
||||||
|
|
||||||
IpAddressInfo ParseIpAddress(string ipAddress);
|
IpAddressInfo ParseIpAddress(string ipAddress);
|
||||||
|
|
||||||
@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net
|
|||||||
Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
|
Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
|
||||||
|
|
||||||
bool IsAddressInSubnets(string addressString, string[] subnets);
|
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();
|
.ToArray();
|
||||||
|
|
||||||
public bool ContainsField(ItemFields field)
|
public bool ContainsField(ItemFields field)
|
||||||
{
|
=> Fields.Contains(field);
|
||||||
return AllItemFields.Contains(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DtoOptions(bool allFields)
|
public DtoOptions(bool allFields)
|
||||||
{
|
{
|
||||||
@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto
|
|||||||
EnableUserData = true;
|
EnableUserData = true;
|
||||||
AddCurrentProgram = true;
|
AddCurrentProgram = true;
|
||||||
|
|
||||||
if (allFields)
|
Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
|
||||||
{
|
|
||||||
Fields = AllItemFields;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Fields = new ItemFields[] { };
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageTypes = AllImageTypes;
|
ImageTypes = AllImageTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto
|
|||||||
/// <param name="options">The options.</param>
|
/// <param name="options">The options.</param>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <param name="owner">The owner.</param>
|
/// <param name="owner">The owner.</param>
|
||||||
BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
|
BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
|
||||||
|
|
||||||
BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the item by name dto.
|
/// Gets the item by name dto.
|
||||||
|
@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
if (query.ItemIds.Length > 0)
|
if (query.ItemIds.Length > 0)
|
||||||
{
|
{
|
||||||
var result = LibraryManager.GetItemsResult(query);
|
return 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 GetItemsInternal(query);
|
return GetItemsInternal(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseItem[] GetItemList(InternalItemsQuery query)
|
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
query.EnableTotalRecordCount = false;
|
query.EnableTotalRecordCount = false;
|
||||||
|
|
||||||
if (query.ItemIds.Length > 0)
|
if (query.ItemIds.Length > 0)
|
||||||
{
|
{
|
||||||
var result = LibraryManager.GetItemList(query);
|
return 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 GetItemsInternal(query).Items;
|
return GetItemsInternal(query).Items;
|
||||||
|
@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
flags.Add("+ignidx");
|
flags.Add("+ignidx");
|
||||||
}
|
}
|
||||||
if (state.GenPtsInput)
|
if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
flags.Add("+genpts");
|
flags.Add("+genpts");
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using MediaBrowser.Model.System;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.MediaEncoding
|
namespace MediaBrowser.Controller.MediaEncoding
|
||||||
{
|
{
|
||||||
@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMediaEncoder : ITranscoderSupport
|
public interface IMediaEncoder : ITranscoderSupport
|
||||||
{
|
{
|
||||||
string EncoderLocationType { get; }
|
FFmpegLocation EncoderLocation { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the encoder path.
|
/// Gets the encoder path.
|
||||||
@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
string EscapeSubtitleFilterPath(string path);
|
string EscapeSubtitleFilterPath(string path);
|
||||||
|
|
||||||
void Init();
|
void SetFFmpegPath();
|
||||||
|
|
||||||
void UpdateEncoderPath(string path, string pathType);
|
void UpdateEncoderPath(string path, string pathType);
|
||||||
bool SupportsEncoder(string encoder);
|
bool SupportsEncoder(string encoder);
|
||||||
|
@ -32,16 +32,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
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.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||||
await target.FlushAsync().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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error reading ffmpeg log");
|
_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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||||||
|
|
||||||
var path = item.ContainingFolderPath;
|
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)
|
if (includeDirectories)
|
||||||
{
|
{
|
||||||
return directoryService.GetFileSystemEntries(path)
|
return directoryService.GetFileSystemEntries(path)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using MediaBrowser.Model.Diagnostics;
|
using MediaBrowser.Model.Diagnostics;
|
||||||
@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
_processFactory = processFactory;
|
_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);
|
_logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
|
||||||
|
|
||||||
@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(output))
|
if (string.IsNullOrWhiteSpace(output))
|
||||||
{
|
{
|
||||||
|
if (logOutput)
|
||||||
|
{
|
||||||
|
_logger.LogError("FFmpeg validation: The process returned no result");
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,21 +59,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
|
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
|
if (logOutput)
|
||||||
|
{
|
||||||
|
_logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
|
||||||
|
}
|
||||||
return false;
|
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);
|
_logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown");
|
||||||
if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
|
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[]
|
private static readonly string[] requiredDecoders = new[]
|
||||||
|
@ -3,17 +3,14 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Channels;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.MediaEncoding.Probing;
|
using MediaBrowser.MediaEncoding.Probing;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Diagnostics;
|
using MediaBrowser.Model.Diagnostics;
|
||||||
@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Model.System;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.MediaEncoding.Encoder
|
namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
public class MediaEncoder : IMediaEncoder, IDisposable
|
public class MediaEncoder : IMediaEncoder, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _logger
|
/// Gets the encoder path.
|
||||||
/// </summary>
|
/// </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;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the json serializer.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The json serializer.</value>
|
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
|
private string FFmpegPath;
|
||||||
/// <summary>
|
private string FFprobePath;
|
||||||
/// 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; }
|
|
||||||
|
|
||||||
protected readonly IServerConfigurationManager ConfigurationManager;
|
protected readonly IServerConfigurationManager ConfigurationManager;
|
||||||
protected readonly IFileSystem FileSystem;
|
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<ISubtitleEncoder> SubtitleEncoder;
|
||||||
protected readonly Func<IMediaSourceManager> MediaSourceManager;
|
protected readonly Func<IMediaSourceManager> MediaSourceManager;
|
||||||
private readonly IHttpClient _httpClient;
|
|
||||||
private readonly IZipClient _zipClient;
|
|
||||||
private readonly IProcessFactory _processFactory;
|
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 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(
|
public MediaEncoder(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
string ffMpegPath,
|
string startupOptionsFFmpegPath,
|
||||||
string ffProbePath,
|
string startupOptionsFFprobePath,
|
||||||
bool hasExternalEncoder,
|
|
||||||
IServerConfigurationManager configurationManager,
|
IServerConfigurationManager configurationManager,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
ILiveTvManager liveTvManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IChannelManager channelManager,
|
|
||||||
ISessionManager sessionManager,
|
|
||||||
Func<ISubtitleEncoder> subtitleEncoder,
|
Func<ISubtitleEncoder> subtitleEncoder,
|
||||||
Func<IMediaSourceManager> mediaSourceManager,
|
Func<IMediaSourceManager> mediaSourceManager,
|
||||||
IHttpClient httpClient,
|
|
||||||
IZipClient zipClient,
|
|
||||||
IProcessFactory processFactory,
|
IProcessFactory processFactory,
|
||||||
int defaultImageExtractionTimeoutMs)
|
int defaultImageExtractionTimeoutMs)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
|
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
|
StartupOptionFFmpegPath = startupOptionsFFmpegPath;
|
||||||
|
StartupOptionFFprobePath = startupOptionsFFprobePath;
|
||||||
ConfigurationManager = configurationManager;
|
ConfigurationManager = configurationManager;
|
||||||
FileSystem = fileSystem;
|
FileSystem = fileSystem;
|
||||||
LiveTvManager = liveTvManager;
|
|
||||||
IsoManager = isoManager;
|
|
||||||
LibraryManager = libraryManager;
|
|
||||||
ChannelManager = channelManager;
|
|
||||||
SessionManager = sessionManager;
|
|
||||||
SubtitleEncoder = subtitleEncoder;
|
SubtitleEncoder = subtitleEncoder;
|
||||||
MediaSourceManager = mediaSourceManager;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_zipClient = zipClient;
|
|
||||||
_processFactory = processFactory;
|
_processFactory = processFactory;
|
||||||
DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
|
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)
|
_logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release");
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
|
||||||
}
|
if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
|
||||||
|
|
||||||
public void Init()
|
|
||||||
{
|
|
||||||
InitPaths();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(FFMpegPath))
|
|
||||||
{
|
{
|
||||||
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);
|
SetAvailableDecoders(result.decoders);
|
||||||
SetAvailableEncoders(result.encoders);
|
SetAvailableEncoders(result.encoders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitPaths()
|
/// <summary>
|
||||||
{
|
/// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
|
||||||
ConfigureEncoderPaths();
|
/// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
|
||||||
|
/// </summary>
|
||||||
if (_hasExternalEncoder)
|
/// <param name="path"></param>
|
||||||
{
|
/// <param name="pathType"></param>
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateEncoderPath(string path, string pathType)
|
public void UpdateEncoderPath(string path, string pathType)
|
||||||
{
|
{
|
||||||
if (_hasExternalEncoder)
|
string newPath;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty);
|
_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, "custom", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Unexpected pathType value");
|
throw new ArgumentException("Unexpected pathType value");
|
||||||
}
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(path))
|
||||||
if (string.IsNullOrWhiteSpace(newPaths.Item1))
|
|
||||||
{
|
{
|
||||||
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;
|
// Write the new ffmpeg path to the xml as <EncoderAppPath>
|
||||||
|
// This ensures its not lost on next startup
|
||||||
if (!ValidateVersion(path, true))
|
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||||
{
|
config.EncoderAppPath = newPath;
|
||||||
throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = GetEncodingOptions();
|
|
||||||
config.EncoderAppPath = path;
|
|
||||||
ConfigurationManager.SaveConfiguration("encoding", config);
|
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 (!string.IsNullOrEmpty(path))
|
||||||
{
|
|
||||||
if (_hasExternalEncoder)
|
|
||||||
{
|
{
|
||||||
return;
|
if (File.Exists(path))
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (File.Exists(appPath))
|
|
||||||
{
|
{
|
||||||
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;
|
try
|
||||||
string probePath = null;
|
|
||||||
|
|
||||||
if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true))
|
|
||||||
{
|
{
|
||||||
encoderPath = _originalFFMpegPath;
|
var files = FileSystem.GetFilePaths(path);
|
||||||
probePath = _originalFFProbePath;
|
|
||||||
|
var excludeExtensions = new[] { ".c" };
|
||||||
|
|
||||||
|
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
if (string.IsNullOrWhiteSpace(encoderPath))
|
|
||||||
{
|
{
|
||||||
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";
|
return candidatePath;
|
||||||
probePath = "ffprobe";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> _encoders = new List<string>();
|
private List<string> _encoders = new List<string>();
|
||||||
@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the encoder path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The encoder path.</value>
|
|
||||||
public string EncoderPath => FFMpegPath;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the media info.
|
/// Gets the media info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -489,7 +364,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
FileName = FFProbePath,
|
FileName = FFprobePath,
|
||||||
Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
|
Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
|
||||||
|
|
||||||
IsHidden = true,
|
IsHidden = true,
|
||||||
@ -691,10 +566,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
{
|
{
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
FileName = FFMpegPath,
|
FileName = FFmpegPath,
|
||||||
Arguments = args,
|
Arguments = args,
|
||||||
IsHidden = true,
|
IsHidden = true,
|
||||||
ErrorDialog = false
|
ErrorDialog = false,
|
||||||
|
EnableRaisingEvents = true
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||||
@ -813,10 +689,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
{
|
{
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
FileName = FFMpegPath,
|
FileName = FFmpegPath,
|
||||||
Arguments = args,
|
Arguments = args,
|
||||||
IsHidden = true,
|
IsHidden = true,
|
||||||
ErrorDialog = false
|
ErrorDialog = false,
|
||||||
|
EnableRaisingEvents = true
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
@ -29,17 +30,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IEncryptionManager _encryption;
|
|
||||||
|
|
||||||
private readonly IJsonSerializer _json;
|
private readonly IJsonSerializer _json;
|
||||||
private readonly IFileSystem _fileSystem;
|
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);
|
_logger = loggerFactory.CreateLogger(GetType().Name);
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_config = config;
|
_config = config;
|
||||||
_encryption = encryption;
|
|
||||||
_json = json;
|
_json = json;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
|
||||||
@ -63,16 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
!string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
|
!string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
|
||||||
!options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
|
!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 ||
|
if (password == null ||
|
||||||
!password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
|
!password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
@ -80,7 +80,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
return string.Empty;
|
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";
|
public string Name => "Open Subtitles";
|
||||||
@ -186,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
var options = GetOptions();
|
var options = GetOptions();
|
||||||
|
|
||||||
var user = options.OpenSubtitlesUsername ?? string.Empty;
|
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);
|
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 bool EnableThrottling { get; set; }
|
||||||
public int ThrottleDelaySeconds { get; set; }
|
public int ThrottleDelaySeconds { get; set; }
|
||||||
public string HardwareAccelerationType { get; set; }
|
public string HardwareAccelerationType { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// FFmpeg path as set by the user via the UI
|
||||||
|
/// </summary>
|
||||||
public string EncoderAppPath { get; set; }
|
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 string VaapiDevice { get; set; }
|
||||||
public int H264Crf { get; set; }
|
public int H264Crf { get; set; }
|
||||||
public string H264Preset { get; set; }
|
public string H264Preset { get; set; }
|
||||||
|
@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
public string[] LocalNetworkSubnets { get; set; }
|
public string[] LocalNetworkSubnets { get; set; }
|
||||||
public string[] LocalNetworkAddresses { get; set; }
|
public string[] LocalNetworkAddresses { get; set; }
|
||||||
public string[] CodecsUsed { get; set; }
|
public string[] CodecsUsed { get; set; }
|
||||||
|
public bool IgnoreVirtualInterfaces { get; set; }
|
||||||
public bool EnableExternalContentInSuggestions { get; set; }
|
public bool EnableExternalContentInSuggestions { get; set; }
|
||||||
public bool RequireHttps { get; set; }
|
public bool RequireHttps { get; set; }
|
||||||
public bool IsBehindProxy { get; set; }
|
public bool IsBehindProxy { get; set; }
|
||||||
@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
CodecsUsed = Array.Empty<string>();
|
CodecsUsed = Array.Empty<string>();
|
||||||
ImageExtractionTimeoutMs = 0;
|
ImageExtractionTimeoutMs = 0;
|
||||||
PathSubstitutions = Array.Empty<PathSubstitution>();
|
PathSubstitutions = Array.Empty<PathSubstitution>();
|
||||||
|
IgnoreVirtualInterfaces = false;
|
||||||
EnableSimpleArtistDetection = true;
|
EnableSimpleArtistDetection = true;
|
||||||
|
|
||||||
DisplaySpecialsWithinSeasons = true;
|
DisplaySpecialsWithinSeasons = true;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Cryptography
|
namespace MediaBrowser.Model.Cryptography
|
||||||
{
|
{
|
||||||
@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
|
|||||||
byte[] ComputeMD5(Stream str);
|
byte[] ComputeMD5(Stream str);
|
||||||
byte[] ComputeMD5(byte[] bytes);
|
byte[] ComputeMD5(byte[] bytes);
|
||||||
byte[] ComputeSHA1(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 static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
|
||||||
|
|
||||||
public string Address { get; set; }
|
public string Address { get; set; }
|
||||||
|
public IpAddressInfo SubnetMask { get; set; }
|
||||||
public IpAddressFamily AddressFamily { get; set; }
|
public IpAddressFamily AddressFamily { get; set; }
|
||||||
|
|
||||||
public IpAddressInfo(string address, IpAddressFamily addressFamily)
|
public IpAddressInfo(string address, IpAddressFamily addressFamily)
|
||||||
|
@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates;
|
|||||||
|
|
||||||
namespace MediaBrowser.Model.System
|
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>
|
/// <summary>
|
||||||
/// Class SystemInfo
|
/// Class SystemInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
|
|||||||
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
|
||||||
public bool HasUpdateAvailable { get; set; }
|
public bool HasUpdateAvailable { get; set; }
|
||||||
|
|
||||||
public string EncoderLocationType { get; set; }
|
public FFmpegLocation EncoderLocation { get; set; }
|
||||||
|
|
||||||
public Architecture SystemArchitecture { get; set; }
|
public Architecture SystemArchitecture { get; set; }
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users
|
|||||||
|
|
||||||
public UserPolicy()
|
public UserPolicy()
|
||||||
{
|
{
|
||||||
EnableContentDeletion = true;
|
EnableContentDeletion = false;
|
||||||
EnableContentDeletionFromFolders = Array.Empty<string>();
|
EnableContentDeletionFromFolders = Array.Empty<string>();
|
||||||
|
|
||||||
EnableSyncTranscoding = true;
|
EnableSyncTranscoding = true;
|
||||||
|
@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
localImagesFailed = true;
|
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>
|
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"><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://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://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>
|
<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>
|
</br>
|
||||||
<a href="https://opencollective.com/jellyfin"><img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/></a>
|
<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>
|
/// <summary>
|
||||||
/// Sends a message to the SSDP multicast address and port.
|
/// Sends a message to the SSDP multicast address and port.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task SendMulticastMessage(string message, CancellationToken cancellationToken);
|
Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
|
||||||
Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken);
|
Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
|
||||||
namespace Rssdp.Infrastructure
|
namespace Rssdp.Infrastructure
|
||||||
{
|
{
|
||||||
@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private ISocketFactory _SocketFactory;
|
private ISocketFactory _SocketFactory;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly IServerConfigurationManager _config;
|
||||||
|
|
||||||
private int _LocalPort;
|
private int _LocalPort;
|
||||||
private int _MulticastTtl;
|
private int _MulticastTtl;
|
||||||
@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure
|
|||||||
/// Minimum constructor.
|
/// Minimum constructor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
|
/// <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)
|
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
|
||||||
{
|
{
|
||||||
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// <summary>
|
||||||
/// Sends a message to the SSDP multicast address and port.
|
/// Sends a message to the SSDP multicast address and port.
|
||||||
/// </summary>
|
/// </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));
|
if (message == null) throw new ArgumentNullException(nameof(message));
|
||||||
|
|
||||||
@ -264,7 +268,7 @@ namespace Rssdp.Infrastructure
|
|||||||
IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
|
IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
|
||||||
Port = SsdpConstants.MulticastPort
|
Port = SsdpConstants.MulticastPort
|
||||||
|
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
}, fromLocalIpAddress, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@ -332,14 +336,15 @@ namespace Rssdp.Infrastructure
|
|||||||
|
|
||||||
#region Private Methods
|
#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;
|
var sockets = _sendSockets;
|
||||||
if (sockets != null)
|
if (sockets != null)
|
||||||
{
|
{
|
||||||
sockets = sockets.ToList();
|
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);
|
return Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,11 +368,11 @@ namespace Rssdp.Infrastructure
|
|||||||
|
|
||||||
if (_enableMultiSocketBinding)
|
if (_enableMultiSocketBinding)
|
||||||
{
|
{
|
||||||
foreach (var address in _networkManager.GetLocalIpAddresses())
|
foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces))
|
||||||
{
|
{
|
||||||
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||||
{
|
{
|
||||||
// Not supported ?
|
// Not support IPv6 right now
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure
|
|||||||
|
|
||||||
var message = BuildMessage(header, values);
|
var message = BuildMessage(header, values);
|
||||||
|
|
||||||
return _CommunicationsServer.SendMulticastMessage(message, cancellationToken);
|
return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)
|
private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)
|
||||||
|
@ -7,6 +7,7 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
using Rssdp;
|
using Rssdp;
|
||||||
|
|
||||||
namespace Rssdp.Infrastructure
|
namespace Rssdp.Infrastructure
|
||||||
@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
|
public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
|
||||||
{
|
{
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
|
||||||
private ISsdpCommunicationsServer _CommsServer;
|
private ISsdpCommunicationsServer _CommsServer;
|
||||||
private string _OSName;
|
private string _OSName;
|
||||||
private string _OSVersion;
|
private string _OSVersion;
|
||||||
|
private bool _sendOnlyMatchedHost;
|
||||||
|
|
||||||
private bool _SupportPnpRootDevice;
|
private bool _SupportPnpRootDevice;
|
||||||
|
|
||||||
@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default constructor.
|
/// Default constructor.
|
||||||
/// </summary>
|
/// </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 (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 == null) throw new ArgumentNullException(nameof(osName));
|
||||||
if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", 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));
|
if (osVersion == null) throw new ArgumentNullException(nameof(osVersion));
|
||||||
@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure
|
|||||||
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
|
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
|
||||||
_Random = new Random();
|
_Random = new Random();
|
||||||
|
|
||||||
|
_networkManager = networkManager;
|
||||||
_CommsServer = communicationsServer;
|
_CommsServer = communicationsServer;
|
||||||
_CommsServer.RequestReceived += CommsServer_RequestReceived;
|
_CommsServer.RequestReceived += CommsServer_RequestReceived;
|
||||||
_OSName = osName;
|
_OSName = osName;
|
||||||
_OSVersion = osVersion;
|
_OSVersion = osVersion;
|
||||||
|
_sendOnlyMatchedHost = sendOnlyMatchedHost;
|
||||||
|
|
||||||
_CommsServer.BeginListeningForBroadcasts();
|
_CommsServer.BeginListeningForBroadcasts();
|
||||||
}
|
}
|
||||||
@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure
|
|||||||
|
|
||||||
foreach (var device in deviceList)
|
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
|
else
|
||||||
@ -427,7 +438,7 @@ namespace Rssdp.Infrastructure
|
|||||||
|
|
||||||
var message = BuildMessage(header, values);
|
var message = BuildMessage(header, values);
|
||||||
|
|
||||||
_CommsServer.SendMulticastMessage(message, cancellationToken);
|
_CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken);
|
||||||
|
|
||||||
//WriteTrace(String.Format("Sent alive notification"), device);
|
//WriteTrace(String.Format("Sent alive notification"), device);
|
||||||
}
|
}
|
||||||
@ -472,7 +483,7 @@ namespace Rssdp.Infrastructure
|
|||||||
|
|
||||||
var sendCount = IsDisposed ? 1 : 3;
|
var sendCount = IsDisposed ? 1 : 3;
|
||||||
WriteTrace(String.Format("Sent byebye notification"), device);
|
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()
|
private void DisposeRebroadcastTimer()
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Rssdp.Infrastructure;
|
using Rssdp.Infrastructure;
|
||||||
|
using MediaBrowser.Model.Net;
|
||||||
|
|
||||||
namespace Rssdp
|
namespace Rssdp
|
||||||
{
|
{
|
||||||
@ -52,6 +53,15 @@ namespace Rssdp
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Uri Location { get; set; }
|
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>
|
/// <summary>
|
||||||
/// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.
|
/// 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;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.2.1")]
|
[assembly: AssemblyVersion("10.2.2")]
|
||||||
[assembly: AssemblyFileVersion("10.2.1")]
|
[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 " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
|
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 build all platforms, use 'all'."
|
||||||
echo -e "To perform all build actions, use 'all'."
|
echo -e "To perform all build actions, use 'all'."
|
||||||
echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
|
echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
|
||||||
@ -164,38 +164,40 @@ for target_platform in ${platform[@]}; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Initialize submodules
|
if [[ ${web_branch} != 'local' ]]; then
|
||||||
git submodule update --init --recursive
|
# Initialize submodules
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
# configure branch
|
# configure branch
|
||||||
pushd MediaBrowser.WebDashboard/jellyfin-web
|
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
|
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
|
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
|
# Execute each platform and action in order, if said action is enabled
|
||||||
pushd deployment/
|
pushd deployment/
|
||||||
for target_platform in ${platform[@]}; do
|
for target_platform in ${platform[@]}; do
|
||||||
@ -217,7 +219,7 @@ for target_platform in ${platform[@]}; do
|
|||||||
done
|
done
|
||||||
if [[ -d pkg-dist/ ]]; then
|
if [[ -d pkg-dist/ ]]; then
|
||||||
echo -e ">> Collecting build artifacts"
|
echo -e ">> Collecting build artifacts"
|
||||||
target_dir="../../../jellyfin-build/${target_platform}"
|
target_dir="../../../bin/${target_platform}"
|
||||||
mkdir -p ${target_dir}
|
mkdir -p ${target_dir}
|
||||||
mv pkg-dist/* ${target_dir}/
|
mv pkg-dist/* ${target_dir}/
|
||||||
fi
|
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_OUTPUT_DIR="dist/jellyfin-git"
|
||||||
DEFAULT_PKG_DIR="pkg-dist"
|
DEFAULT_PKG_DIR="pkg-dist"
|
||||||
DEFAULT_DOCKERFILE="Dockerfile"
|
DEFAULT_DOCKERFILE="Dockerfile"
|
||||||
DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD`
|
|
||||||
DEFAULT_ARCHIVE_CMD="tar -xvzf"
|
DEFAULT_ARCHIVE_CMD="tar -xvzf"
|
||||||
|
|
||||||
# Parse the version from the AssemblyVersion
|
# 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}"
|
echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
|
||||||
if [[ $DOTNETRUNTIME == 'framework' ]]; then
|
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
|
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
|
fi
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
if [ $EXIT_CODE -eq 0 ]; then
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
@ -53,7 +52,7 @@ build_jellyfin_docker()
|
|||||||
(
|
(
|
||||||
BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
|
BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
|
||||||
DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
|
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}"
|
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}
|
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 (10.2.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
* jellyfin:
|
* jellyfin:
|
||||||
|
@ -21,9 +21,9 @@ JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin"
|
|||||||
# Restart script for in-app server control
|
# Restart script for in-app server control
|
||||||
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
||||||
|
|
||||||
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
|
# ffmpeg binary paths, overriding the system values
|
||||||
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
|
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg"
|
||||||
#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe"
|
JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe"
|
||||||
|
|
||||||
# [OPTIONAL] run Jellyfin as a headless service
|
# [OPTIONAL] run Jellyfin as a headless service
|
||||||
#JELLYFIN_SERVICE_OPT="--service"
|
#JELLYFIN_SERVICE_OPT="--service"
|
||||||
|
@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
|
|||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: at,
|
Depends: at,
|
||||||
libsqlite3-0,
|
libsqlite3-0,
|
||||||
ffmpeg (<7:4.1) | jellyfin-ffmpeg,
|
jellyfin-ffmpeg,
|
||||||
libfontconfig1,
|
libfontconfig1,
|
||||||
libfreetype6,
|
libfreetype6,
|
||||||
libssl1.0.0 | libssl1.0.2
|
libssl1.0.0 | libssl1.0.2
|
||||||
|
@ -2,7 +2,23 @@
|
|||||||
CONFIG := Release
|
CONFIG := Release
|
||||||
TERM := xterm
|
TERM := xterm
|
||||||
SHELL := /bin/bash
|
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 DH_VERBOSE=1
|
||||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
|
||||||
@ -16,7 +32,8 @@ override_dh_auto_test:
|
|||||||
override_dh_clistrip:
|
override_dh_clistrip:
|
||||||
|
|
||||||
override_dh_auto_build:
|
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:
|
override_dh_auto_clean:
|
||||||
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
|
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 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 \
|
&& dnf copr enable -y @dotnet-sig/dotnet \
|
||||||
&& rpmdev-setuptree \
|
&& 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 \
|
&& ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
|
||||||
&& mkdir -p ${SOURCE_DIR}/SPECS \
|
&& mkdir -p ${SOURCE_DIR}/SPECS \
|
||||||
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
|
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
%endif
|
%endif
|
||||||
|
|
||||||
Name: jellyfin
|
Name: jellyfin
|
||||||
Version: 10.2.1
|
Version: 10.2.2
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: The Free Software Media Browser
|
Summary: The Free Software Media Browser
|
||||||
License: GPLv2
|
License: GPLv2
|
||||||
@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
|
|||||||
Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
|
Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
|
||||||
# Requirements not packaged in main repos
|
# Requirements not packaged in main repos
|
||||||
# COPR @dotnet-sig/dotnet
|
# COPR @dotnet-sig/dotnet
|
||||||
BuildRequires: dotnet-sdk-2.2
|
BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2
|
||||||
# RPMfusion free
|
# RPMfusion free
|
||||||
Requires: ffmpeg
|
Requires: ffmpeg
|
||||||
|
|
||||||
@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an
|
|||||||
%install
|
%install
|
||||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=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 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
||||||
%{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
|
%{__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
|
%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
|
||||||
@ -73,7 +74,6 @@ EOF
|
|||||||
%{_libdir}/%{name}/jellyfin-web/*
|
%{_libdir}/%{name}/jellyfin-web/*
|
||||||
%attr(755,root,root) %{_bindir}/%{name}
|
%attr(755,root,root) %{_bindir}/%{name}
|
||||||
%{_libdir}/%{name}/*.json
|
%{_libdir}/%{name}/*.json
|
||||||
%{_libdir}/%{name}/*.pdb
|
|
||||||
%{_libdir}/%{name}/*.dll
|
%{_libdir}/%{name}/*.dll
|
||||||
%{_libdir}/%{name}/*.so
|
%{_libdir}/%{name}/*.so
|
||||||
%{_libdir}/%{name}/*.a
|
%{_libdir}/%{name}/*.a
|
||||||
@ -140,6 +140,19 @@ fi
|
|||||||
%systemd_postun_with_restart jellyfin.service
|
%systemd_postun_with_restart jellyfin.service
|
||||||
|
|
||||||
%changelog
|
%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>
|
* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||||
- jellyfin:
|
- jellyfin:
|
||||||
- PR920 Fix cachedir missing from Docker container
|
- 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/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
|
||||||
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
|
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
|
||||||
rm -r ${TEMP_DIR}
|
rm -r ${TEMP_DIR}
|
||||||
cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
||||||
cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
|
cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
|
||||||
mkdir -p ${PKG_DIR}
|
mkdir -p ${PKG_DIR}
|
||||||
pushd ${OUTPUT_DIR}
|
pushd ${OUTPUT_DIR}
|
||||||
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
|
${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/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
|
||||||
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
|
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
|
||||||
rm -r ${TEMP_DIR}
|
rm -r ${TEMP_DIR}
|
||||||
cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
|
||||||
cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
|
cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
|
||||||
mkdir -p ${PKG_DIR}
|
mkdir -p ${PKG_DIR}
|
||||||
pushd ${OUTPUT_DIR}
|
pushd ${OUTPUT_DIR}
|
||||||
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
|
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
|
||||||
|
Loading…
Reference in New Issue
Block a user