aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines.yml183
-rw-r--r--CONTRIBUTORS.md3
-rw-r--r--Dockerfile18
-rw-r--r--Dockerfile.arm19
-rw-r--r--Dockerfile.arm6419
-rw-r--r--DvdLib/Ifo/Dvd.cs16
-rw-r--r--Emby.Dlna/Configuration/DlnaOptions.cs2
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs4
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs3
-rw-r--r--Emby.Dlna/DlnaManager.cs21
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs21
-rw-r--r--Emby.Dlna/PlayTo/TransportCommands.cs19
-rw-r--r--Emby.Naming/TV/EpisodePathParser.cs32
-rw-r--r--Emby.Server.Implementations/Activity/ActivityManager.cs7
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs105
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs11
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs48
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs129
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs7
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserRepository.cs34
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs15
-rw-r--r--Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs24
-rw-r--r--Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs17
-rw-r--r--Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs132
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs141
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs37
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs152
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv8
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.csv5
-rw-r--r--Emby.Server.Implementations/Networking/NetworkManager.cs72
-rw-r--r--Emby.Server.Implementations/Security/EncryptionManager.cs57
-rw-r--r--Emby.Server.Implementations/Services/ServicePath.cs161
-rw-r--r--Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs52
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Emby.Server.Implementations/SocketSharp/RequestMono.cs39
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs62
-rw-r--r--Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs27
-rw-r--r--Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs2
-rw-r--r--Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs2
-rw-r--r--Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs2
-rw-r--r--Jellyfin.Server/StartupOptions.cs4
-rw-r--r--MediaBrowser.Api/BaseApiService.cs13
-rw-r--r--MediaBrowser.Api/FilterService.cs3
-rw-r--r--MediaBrowser.Api/Playback/Progressive/VideoService.cs1
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs35
-rw-r--r--MediaBrowser.Common/Net/INetworkManager.cs5
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs14
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs24
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs11
-rw-r--r--MediaBrowser.Controller/Security/IEncryptionManager.cs19
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs113
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs385
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs19
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs7
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs2
-rw-r--r--MediaBrowser.Model/Cryptography/ICryptoProvider.cs9
-rw-r--r--MediaBrowser.Model/Cryptography/PasswordHash.cs153
-rw-r--r--MediaBrowser.Model/Net/IpAddressInfo.cs1
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs17
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs5
-rw-r--r--README.md2
-rw-r--r--RSSDP/ISsdpCommunicationsServer.cs6
-rw-r--r--RSSDP/RSSDP.csproj1
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs23
-rw-r--r--RSSDP/SsdpDeviceLocator.cs2
-rw-r--r--RSSDP/SsdpDevicePublisher.cs19
-rw-r--r--RSSDP/SsdpRootDevice.cs10
-rw-r--r--SharedVersion.cs4
-rwxr-xr-xbuild60
-rw-r--r--build.yaml15
-rwxr-xr-xdeployment/common.build.sh7
-rw-r--r--deployment/debian-package-armhf/Dockerfile.amd6442
-rw-r--r--deployment/debian-package-armhf/Dockerfile.armhf34
-rwxr-xr-xdeployment/debian-package-armhf/clean.sh29
-rw-r--r--deployment/debian-package-armhf/dependencies.txt1
-rwxr-xr-xdeployment/debian-package-armhf/docker-build.sh20
-rwxr-xr-xdeployment/debian-package-armhf/package.sh42
l---------deployment/debian-package-armhf/pkg-src1
-rw-r--r--deployment/debian-package-x64/pkg-src/changelog17
-rw-r--r--deployment/debian-package-x64/pkg-src/conf/jellyfin6
-rw-r--r--deployment/debian-package-x64/pkg-src/control2
-rw-r--r--deployment/debian-package-x64/pkg-src/rules21
-rw-r--r--deployment/fedora-package-x64/Dockerfile2
-rw-r--r--deployment/fedora-package-x64/pkg-src/jellyfin.spec21
-rwxr-xr-xdeployment/win-x64/package.sh4
-rwxr-xr-xdeployment/win-x86/package.sh4
94 files changed, 1757 insertions, 1228 deletions
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
new file mode 100644
index 000000000..e5845c0ef
--- /dev/null
+++ b/.ci/azure-pipelines.yml
@@ -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
+
+
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 758202af6..81857e57c 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -21,6 +21,9 @@
- [WillWill56](https://github.com/WillWill56)
- [Liggy](https://github.com/Liggy)
- [fruhnow](https://github.com/fruhnow)
+ - [Lynxy](https://github.com/Lynxy)
+ - [fasheng](https://github.com/fasheng)
+ - [ploughpuff](https://github.com/ploughpuff)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index 978b0d540..91a4f5a2d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,10 +4,8 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-RUN dotnet publish \
- --configuration release \
- --output /jellyfin \
- Jellyfin.Server
+RUN bash -c "source deployment/common.build.sh && \
+ build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
FROM jellyfin/ffmpeg as ffmpeg
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
@@ -22,6 +20,16 @@ RUN apt-get update \
&& chmod 777 /cache /config /media
COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin
+
+ARG JELLYFIN_WEB_VERSION=10.2.2
+RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && rm -rf /jellyfin/jellyfin-web \
+ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+ --datadir /config \
+ --cachedir /cache \
+ --ffmpeg /usr/local/bin/ffmpeg \
+ --ffprobe /usr/local/bin/ffprobe
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 9d1c30619..42f0354a3 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -17,11 +17,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
-RUN dotnet publish \
- -r linux-arm \
- --configuration release \
- --output /jellyfin \
- Jellyfin.Server
+RUN bash -c "source deployment/common.build.sh && \
+ build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
@@ -31,6 +28,16 @@ RUN apt-get update \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin
+
+ARG JELLYFIN_WEB_VERSION=10.2.2
+RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && rm -rf /jellyfin/jellyfin-web \
+ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+ --datadir /config \
+ --cachedir /cache \
+ --ffmpeg /usr/bin/ffmpeg \
+ --ffprobe /usr/bin/ffprobe
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index e61aaa167..d3103d389 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -18,11 +18,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
-RUN dotnet publish \
- -r linux-arm64 \
- --configuration release \
- --output /jellyfin \
- Jellyfin.Server
+RUN bash -c "source deployment/common.build.sh && \
+ build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
@@ -32,6 +29,16 @@ RUN apt-get update \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin
+
+ARG JELLYFIN_WEB_VERSION=10.2.2
+RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && rm -rf /jellyfin/jellyfin-web \
+ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+ --datadir /config \
+ --cachedir /cache \
+ --ffmpeg /usr/bin/ffmpeg \
+ --ffprobe /usr/bin/ffprobe
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index f784be83e..90125fa3e 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -26,17 +26,17 @@ namespace DvdLib.Ifo
if (vmgPath == null)
{
- var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase));
-
- foreach (var ifo in allIfos)
+ foreach (var ifo in allFiles)
{
- var num = ifo.Name.Split('_').ElementAtOrDefault(1);
- var numbersRead = new List<ushort>();
+ if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out var ifoNumber) && !numbersRead.Contains(ifoNumber))
+ var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries);
+ if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{
ReadVTS(ifoNumber, ifo.FullName);
- numbersRead.Add(ifoNumber);
}
}
}
@@ -76,7 +76,7 @@ namespace DvdLib.Ifo
}
}
- private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles)
+ private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
{
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index 0ebb490a1..c7cb364a8 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -7,6 +7,7 @@ namespace Emby.Dlna.Configuration
public bool EnableServer { get; set; }
public bool EnableDebugLog { get; set; }
public bool BlastAliveMessages { get; set; }
+ public bool SendOnlyMatchedHost { get; set; }
public int ClientDiscoveryIntervalSeconds { get; set; }
public int BlastAliveMessageIntervalSeconds { get; set; }
public string DefaultUserId { get; set; }
@@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration
EnablePlayTo = true;
EnableServer = true;
BlastAliveMessages = true;
+ SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60;
BlastAliveMessageIntervalSeconds = 1800;
}
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 1150afdba..84f38ff76 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory
if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
{
- var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
+ var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
_didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
}
@@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory
}
else
{
- var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
+ var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
totalCount = childrenResult.TotalRecordCount;
provided = childrenResult.Items.Length;
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 605f4f37b..1268f3d5c 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl
{
AddCommonFields(item, itemStubType, context, writer, filter);
- var hasArtists = item as IHasArtist;
var hasAlbumArtists = item as IHasAlbumArtist;
- if (hasArtists != null)
+ if (item is IHasArtist hasArtists)
{
foreach (var artist in hasArtists.Artists)
{
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 770a90152..2b76d2702 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -31,7 +31,7 @@ namespace Emby.Dlna
private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
- private readonly IAssemblyInfo _assemblyInfo;
+ private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@@ -41,8 +41,7 @@ namespace Emby.Dlna
IApplicationPaths appPaths,
ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer,
- IServerApplicationHost appHost,
- IAssemblyInfo assemblyInfo)
+ IServerApplicationHost appHost)
{
_xmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
@@ -50,7 +49,6 @@ namespace Emby.Dlna
_logger = loggerFactory.CreateLogger("Dlna");
_jsonSerializer = jsonSerializer;
_appHost = appHost;
- _assemblyInfo = assemblyInfo;
}
public async Task InitProfilesAsync()
@@ -367,15 +365,18 @@ namespace Emby.Dlna
var systemProfilesPath = SystemProfilesPath;
- foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType())
- .Where(i => i.StartsWith(namespaceName))
- .ToList())
+ foreach (var name in _assembly.GetManifestResourceNames())
{
+ if (!name.StartsWith(namespaceName))
+ {
+ continue;
+ }
+
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
var path = Path.Combine(systemProfilesPath, filename);
- using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name))
+ using (var stream = _assembly.GetManifestResourceStream(name))
{
var fileInfo = _fileSystem.GetFileInfo(path);
@@ -513,7 +514,7 @@ namespace Emby.Dlna
return new ImageStream
{
Format = format,
- Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource)
+ Stream = _assembly.GetManifestResourceStream(resource)
};
}
}
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index a20006578..57ed0097a 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -169,9 +169,10 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer == null)
{
- var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
+ var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows ||
+ _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux;
- _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
+ _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
IsShared = true
};
@@ -229,7 +230,7 @@ namespace Emby.Dlna.Main
try
{
- _Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion);
+ _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
_Publisher.LogFunction = LogMessage;
_Publisher.SupportPnpRootDevice = false;
@@ -245,17 +246,17 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints()
{
- var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList();
+ var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId);
foreach (var address in addresses)
{
- // TODO: Remove this condition on platforms that support it
- //if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
- //{
- // continue;
- //}
+ if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
+ {
+ // Not support IPv6 right now
+ continue;
+ }
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
@@ -268,6 +269,8 @@ namespace Emby.Dlna.Main
{
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document.
+ Address = address,
+ SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index b96fa43e5..4f9e398e9 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
if (arg.Direction == "out")
+ {
continue;
+ }
if (arg.Name == "InstanceID")
+ {
stateString += BuildArgumentXml(arg, "0");
+ }
else
+ {
stateString += BuildArgumentXml(arg, null);
+ }
}
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
@@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
if (arg.Direction == "out")
+ {
continue;
+ }
+
if (arg.Name == "InstanceID")
+ {
stateString += BuildArgumentXml(arg, "0");
+ }
else
+ {
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
+ }
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
@@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
if (arg.Name == "InstanceID")
+ {
stateString += BuildArgumentXml(arg, "0");
+ }
else if (dictionary.ContainsKey(arg.Name))
+ {
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
+ }
else
+ {
stateString += BuildArgumentXml(arg, value.ToString());
+ }
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index 9485d697b..a8f81a3b8 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text.RegularExpressions;
using Emby.Naming.Common;
namespace Emby.Naming.TV
@@ -22,7 +21,9 @@ namespace Emby.Naming.TV
// There were no failed tests without this block, but to be safe, we can keep it until
// the regex which require file extensions are modified so that they don't need them.
if (IsDirectory)
+ {
path += ".mp4";
+ }
EpisodePathParserResult result = null;
@@ -35,6 +36,7 @@ namespace Emby.Naming.TV
continue;
}
}
+
if (isNamed.HasValue)
{
if (expression.IsNamed != isNamed.Value)
@@ -42,6 +44,7 @@ namespace Emby.Naming.TV
continue;
}
}
+
if (isOptimistic.HasValue)
{
if (expression.IsOptimistic != isOptimistic.Value)
@@ -191,13 +194,20 @@ namespace Emby.Naming.TV
private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
{
- var results = expressions
- .Where(i => i.IsNamed)
- .Select(i => Parse(path, i))
- .Where(i => i.Success);
-
- foreach (var result in results)
+ foreach (var i in expressions)
{
+ if (!i.IsNamed)
+ {
+ continue;
+ }
+
+ var result = Parse(path, i);
+
+ if (!result.Success)
+ {
+ continue;
+ }
+
if (string.IsNullOrEmpty(info.SeriesName))
{
info.SeriesName = result.SeriesName;
@@ -208,12 +218,10 @@ namespace Emby.Naming.TV
info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
}
- if (!string.IsNullOrEmpty(info.SeriesName))
+ if (!string.IsNullOrEmpty(info.SeriesName)
+ && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
{
- if (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)
- {
- break;
- }
+ break;
}
}
}
diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs
index 6febcc2f7..0c513ea12 100644
--- a/Emby.Server.Implementations/Activity/ActivityManager.cs
+++ b/Emby.Server.Implementations/Activity/ActivityManager.cs
@@ -39,8 +39,13 @@ namespace Emby.Server.Implementations.Activity
{
var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
- foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty)))
+ foreach (var item in result.Items)
{
+ if (item.UserId == Guid.Empty)
+ {
+ continue;
+ }
+
var user = _userManager.GetUserById(item.UserId);
if (user != null)
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 71a112dac..a581214c7 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Diagnostics;
using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.FFMpeg;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
@@ -541,7 +540,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
- MediaEncoder.Init();
+ MediaEncoder.SetFFmpegPath();
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
//{
@@ -813,10 +812,8 @@ namespace Emby.Server.Implementations
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
serviceCollection.AddSingleton(TVSeriesManager);
- var encryptionManager = new EncryptionManager();
- serviceCollection.AddSingleton<IEncryptionManager>(encryptionManager);
-
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
+
serviceCollection.AddSingleton(DeviceManager);
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
@@ -838,7 +835,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(SessionManager);
serviceCollection.AddSingleton<IDlnaManager>(
- new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo));
+ new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this));
CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
serviceCollection.AddSingleton(CollectionManager);
@@ -861,7 +858,18 @@ namespace Emby.Server.Implementations
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
serviceCollection.AddSingleton(ChapterManager);
- RegisterMediaEncoder(serviceCollection);
+ MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
+ LoggerFactory,
+ JsonSerializer,
+ StartupOptions.FFmpegPath,
+ StartupOptions.FFprobePath,
+ ServerConfigurationManager,
+ FileSystemManager,
+ () => SubtitleEncoder,
+ () => MediaSourceManager,
+ ProcessFactory,
+ 5000);
+ serviceCollection.AddSingleton(MediaEncoder);
EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
serviceCollection.AddSingleton(EncodingManager);
@@ -970,85 +978,6 @@ namespace Emby.Server.Implementations
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
}
- protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()
- {
- var info = new FFMpegInstallInfo();
-
- // Windows builds: http://ffmpeg.zeranoe.com/builds/
- // Linux builds: http://johnvansickle.com/ffmpeg/
- // OS X builds: http://ffmpegmac.net/
- // OS X x64: http://www.evermeet.cx/ffmpeg/
-
- if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux)
- {
- info.FFMpegFilename = "ffmpeg";
- info.FFProbeFilename = "ffprobe";
- info.ArchiveType = "7z";
- info.Version = "20170308";
- }
- else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
- {
- info.FFMpegFilename = "ffmpeg.exe";
- info.FFProbeFilename = "ffprobe.exe";
- info.Version = "20170308";
- info.ArchiveType = "7z";
- }
- else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
- {
- info.FFMpegFilename = "ffmpeg";
- info.FFProbeFilename = "ffprobe";
- info.ArchiveType = "7z";
- info.Version = "20170308";
- }
-
- return info;
- }
-
- protected FFMpegInfo GetFFMpegInfo()
- {
- return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
- .GetFFMpegInfo(StartupOptions);
- }
-
- /// <summary>
- /// Registers the media encoder.
- /// </summary>
- /// <returns>Task.</returns>
- private void RegisterMediaEncoder(IServiceCollection serviceCollection)
- {
- string encoderPath = null;
- string probePath = null;
-
- var info = GetFFMpegInfo();
-
- encoderPath = info.EncoderPath;
- probePath = info.ProbePath;
- var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
-
- var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
- LoggerFactory,
- JsonSerializer,
- encoderPath,
- probePath,
- hasExternalEncoder,
- ServerConfigurationManager,
- FileSystemManager,
- LiveTvManager,
- IsoManager,
- LibraryManager,
- ChannelManager,
- SessionManager,
- () => SubtitleEncoder,
- () => MediaSourceManager,
- HttpClient,
- ZipClient,
- ProcessFactory,
- 5000);
-
- MediaEncoder = mediaEncoder;
- serviceCollection.AddSingleton(MediaEncoder);
- }
-
/// <summary>
/// Gets the user repository.
/// </summary>
@@ -1481,7 +1410,7 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName,
LocalAddress = localAddress,
SupportsLibraryMonitor = true,
- EncoderLocationType = MediaEncoder.EncoderLocationType,
+ EncoderLocation = MediaEncoder.EncoderLocation,
SystemArchitecture = EnvironmentInfo.SystemArchitecture,
SystemUpdateLevel = SystemUpdateLevel,
PackageName = StartupOptions.PackageName
@@ -1598,7 +1527,7 @@ namespace Emby.Server.Implementations
if (addresses.Count == 0)
{
- addresses.AddRange(NetworkManager.GetLocalIpAddresses());
+ addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
}
var resultList = new List<IpAddressInfo>();
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 949b89226..7e50650d7 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in returnItems)
{
- var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None);
- Task.WaitAll(task);
+ RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
}
}
@@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels
}
numComplete++;
- double percent = numComplete;
- percent /= allChannelsList.Count;
-
+ double percent = (double)numComplete / allChannelsList.Count;
progress.Report(100 * percent);
}
@@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels
foreach (var item in result.Items)
{
- var folder = item as Folder;
-
- if (folder != null)
+ if (item is Folder folder)
{
await GetChannelItemsInternal(new InternalItemsQuery
{
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index ad6c537ef..3c7cbb115 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels
public static string GetUserDistinctValue(User user)
{
var channels = user.Policy.EnabledChannels
- .OrderBy(i => i)
- .ToList();
+ .OrderBy(i => i);
- return string.Join("|", channels.ToArray());
+ return string.Join("|", channels);
}
private void CleanDatabase(CancellationToken cancellationToken)
{
var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
- var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Channel).Name }
+ IncludeItemTypes = new[] { typeof(Channel).Name },
+ ExcludeItemIds = installedChannelIds.ToArray()
});
- var invalidIds = databaseIds
- .Except(installedChannelIds)
- .ToList();
-
- foreach (var id in invalidIds)
+ foreach (var channel in uninstalledChannels)
{
cancellationToken.ThrowIfCancellationRequested();
- CleanChannel(id, cancellationToken);
+ CleanChannel((Channel)channel, cancellationToken);
}
}
- private void CleanChannel(Guid id, CancellationToken cancellationToken)
+ private void CleanChannel(Channel channel, CancellationToken cancellationToken)
{
- _logger.LogInformation("Cleaning channel {0} from database", id);
+ _logger.LogInformation("Cleaning channel {0} from database", channel.Id);
// Delete all channel items
- var allIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ var items = _libraryManager.GetItemList(new InternalItemsQuery
{
- ChannelIds = new[] { id }
+ ChannelIds = new[] { channel.Id }
});
- foreach (var deleteId in allIds)
+ foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
- DeleteItem(deleteId);
- }
-
- // Finally, delete the channel itself
- DeleteItem(id);
- }
+ _libraryManager.DeleteItem(item, new DeleteOptions
+ {
+ DeleteFileLocation = false
- private void DeleteItem(Guid id)
- {
- var item = _libraryManager.GetItemById(id);
-
- if (item == null)
- {
- return;
+ }, false);
}
- _libraryManager.DeleteItem(item, new DeleteOptions
+ // Finally, delete the channel itself
+ _libraryManager.DeleteItem(channel, new DeleteOptions
{
DeleteFileLocation = false
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 09fdbc856..982bba625 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,13 +1,49 @@
using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
+using System.Linq;
using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Cryptography
{
public class CryptographyProvider : ICryptoProvider
{
+ private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
+ {
+ "MD5",
+ "System.Security.Cryptography.MD5",
+ "SHA",
+ "SHA1",
+ "System.Security.Cryptography.SHA1",
+ "SHA256",
+ "SHA-256",
+ "System.Security.Cryptography.SHA256",
+ "SHA384",
+ "SHA-384",
+ "System.Security.Cryptography.SHA384",
+ "SHA512",
+ "SHA-512",
+ "System.Security.Cryptography.SHA512"
+ };
+
+ public string DefaultHashMethod => "PBKDF2";
+
+ private RandomNumberGenerator _randomNumberGenerator;
+
+ private const int _defaultIterations = 1000;
+
+ public CryptographyProvider()
+ {
+ //FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
+ //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
+ //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
+ //Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
+ _randomNumberGenerator = RandomNumberGenerator.Create();
+ }
+
public Guid GetMD5(string str)
{
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
@@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
return provider.ComputeHash(bytes);
}
}
+
+ public IEnumerable<string> GetSupportedHashMethods()
+ {
+ return _supportedHashMethods;
+ }
+
+ private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
+ {
+ //downgrading for now as we need this library to be dotnetstandard compliant
+ //with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
+ if (method == DefaultHashMethod)
+ {
+ using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
+ {
+ return r.GetBytes(32);
+ }
+ }
+
+ throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
+ }
+
+ public byte[] ComputeHash(string hashMethod, byte[] bytes)
+ {
+ return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
+ }
+
+ public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
+ {
+ return ComputeHash(DefaultHashMethod, bytes);
+ }
+
+ public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
+ {
+ if (hashMethod == DefaultHashMethod)
+ {
+ return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
+ }
+ else if (_supportedHashMethods.Contains(hashMethod))
+ {
+ using (var h = HashAlgorithm.Create(hashMethod))
+ {
+ if (salt.Length == 0)
+ {
+ return h.ComputeHash(bytes);
+ }
+ else
+ {
+ byte[] salted = new byte[bytes.Length + salt.Length];
+ Array.Copy(bytes, salted, bytes.Length);
+ Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
+ return h.ComputeHash(salted);
+ }
+ }
+ }
+ else
+ {
+ throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+ }
+ }
+
+ public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
+ {
+ return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
+ }
+
+ public byte[] ComputeHash(PasswordHash hash)
+ {
+ int iterations = _defaultIterations;
+ if (!hash.Parameters.ContainsKey("iterations"))
+ {
+ hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ try
+ {
+ iterations = int.Parse(hash.Parameters["iterations"]);
+ }
+ catch (Exception e)
+ {
+ throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
+ }
+ }
+
+ return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
+ }
+
+ public byte[] GenerateSalt()
+ {
+ byte[] salt = new byte[64];
+ _randomNumberGenerator.GetBytes(salt);
+ return salt;
+ }
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 70e5fa640..06f6563a3 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -2279,11 +2279,10 @@ namespace Emby.Server.Implementations.Data
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
- "Audio",
- "MusicAlbum",
- "MusicVideo",
+ "Book",
"AudioBook",
- "AudioPodcast"
+ "Episode",
+ "Season"
};
private bool HasSeriesFields(InternalItemsQuery query)
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 7a9b72244..4109b7ad1 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data
{
list.Add(row[0].ReadGuidFromBlob());
}
- catch
+ catch (Exception ex)
{
-
+ Logger.LogError(ex, "Error while getting user");
}
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
index db359d7dd..182df0edc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
@@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
{
TryMigrateToLocalUsersTable(connection);
}
+
+ RemoveEmptyPasswordHashes();
}
}
@@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
}
}
+ private void RemoveEmptyPasswordHashes()
+ {
+ foreach (var user in RetrieveAllUsers())
+ {
+ // If the user password is the sha1 hash of the empty string, remove it
+ if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
+ || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ user.Password = null;
+ var serialized = _jsonSerializer.SerializeToBytes(user);
+
+ using (WriteLock.Write())
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
+ {
+ statement.TryBind("@InternalId", user.InternalId);
+ statement.TryBind("@data", serialized);
+ statement.MoveNext();
+ }
+
+ }, TransactionMode);
+ }
+ }
+
+ }
+
/// <summary>
/// Save a user in the repo
/// </summary>
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 2233d3d40..7b28a22a8 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -5,8 +5,6 @@ using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
@@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto
return GetBaseItemDto(item, options, user, owner);
}
- public BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
- {
- return GetBaseItemDtos(items, items.Count, options, user, owner);
- }
-
- public BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null)
- {
- return GetBaseItemDtos(items, items.Length, options, user, owner);
- }
+ public BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
+ => GetBaseItemDtos(items, items.Count, options, user, owner);
public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null)
{
diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs
deleted file mode 100644
index 60cd7b3d7..000000000
--- a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs
+++ /dev/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; }
- }
-}
diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs
deleted file mode 100644
index fa9cb5e01..000000000
--- a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs
+++ /dev/null
@@ -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";
- }
- }
-}
diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs
deleted file mode 100644
index bbf51dd24..000000000
--- a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
index 4013ac0c8..3ec1f81d3 100644
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
@@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
public string Name => "Default";
public bool IsEnabled => true;
-
+
+ // This is dumb and an artifact of the backwards way auth providers were designed.
+ // This version of authenticate was never meant to be called, but needs to be here for interface compat
+ // Only the providers that don't provide local user support use this
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
{
throw new NotImplementedException();
}
-
+
+ // This is the verson that we need to use for local users. Because reasons.
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{
+ bool success = false;
if (resolvedUser == null)
{
throw new Exception("Invalid username or password");
}
- var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
+ // As long as jellyfin supports passwordless users, we need this little block here to accomodate
+ if (IsPasswordEmpty(resolvedUser, password))
+ {
+ return Task.FromResult(new ProviderAuthenticationResult
+ {
+ Username = username
+ });
+ }
+
+ ConvertPasswordFormat(resolvedUser);
+ byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
+
+ PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
+ byte[] calculatedHash;
+ string calculatedHashString;
+ if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
+ {
+ if (string.IsNullOrEmpty(readyHash.Salt))
+ {
+ calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
+ calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
+ }
+ else
+ {
+ calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
+ calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
+ }
+
+ if (calculatedHashString == readyHash.Hash)
+ {
+ success = true;
+ // throw new Exception("Invalid username or password");
+ }
+ }
+ else
+ {
+ throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
+ }
+
+ // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
if (!success)
{
@@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
});
}
+ // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
+ // but at least they are in the new format.
+ private void ConvertPasswordFormat(User user)
+ {
+ if (string.IsNullOrEmpty(user.Password))
+ {
+ return;
+ }
+
+ if (!user.Password.Contains("$"))
+ {
+ string hash = user.Password;
+ user.Password = string.Format("$SHA1${0}", hash);
+ }
+
+ if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
+ {
+ string hash = user.EasyPassword;
+ user.EasyPassword = string.Format("$SHA1${0}", hash);
+ }
+ }
+
public Task<bool> HasPassword(User user)
{
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
return Task.FromResult(hasConfiguredPassword);
}
- private bool IsPasswordEmpty(User user, string passwordHash)
+ private bool IsPasswordEmpty(User user, string password)
{
- return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
+ return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
}
public Task ChangePassword(User user, string newPassword)
{
- string newPasswordHash = null;
+ ConvertPasswordFormat(user);
+ // This is needed to support changing a no password user to a password user
+ if (string.IsNullOrEmpty(user.Password))
+ {
+ 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 (newPassword != null)
+ PasswordHash passwordHash = new PasswordHash(user.Password);
+ if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
{
- newPasswordHash = GetHashedString(user, newPassword);
+ 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);
}
- if (string.IsNullOrWhiteSpace(newPasswordHash))
+ if (string.IsNullOrWhiteSpace(passwordHash.Hash))
{
- throw new ArgumentNullException(nameof(newPasswordHash));
+ throw new ArgumentNullException(nameof(passwordHash.Hash));
}
- user.Password = newPasswordHash;
+ user.Password = passwordHash.ToString();
return Task.CompletedTask;
}
public string GetPasswordHash(User user)
{
- return string.IsNullOrEmpty(user.Password)
- ? GetEmptyHashedString(user)
- : user.Password;
+ return user.Password;
}
- public string GetEmptyHashedString(User user)
+ public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
{
- return GetHashedString(user, string.Empty);
+ passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
}
/// <summary>
@@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public string GetHashedString(User user, string str)
{
- var salt = user.Salt;
- if (salt != null)
+ PasswordHash passwordHash;
+ if (string.IsNullOrEmpty(user.Password))
+ {
+ passwordHash = new PasswordHash(_cryptographyProvider);
+ }
+ else
{
- // return BCrypt.HashPassword(str, salt);
+ ConvertPasswordFormat(user);
+ passwordHash = new PasswordHash(user.Password);
}
- // legacy
- return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
+ if (passwordHash.SaltBytes != null)
+ {
+ // the password is modern format with PBKDF and we should take advantage of that
+ passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
+ }
+ else
+ {
+ // the password has no salt and should be called with the older method for safety
+ return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index dfef8e997..efb1ef4a5 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Events;
@@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
}
}
- public bool IsValidUsername(string username)
+ public static bool IsValidUsername(string username)
{
- // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
- foreach (var currentChar in username)
- {
- if (!IsValidUsernameCharacter(currentChar))
- {
- return false;
- }
- }
- return true;
+ //This is some regex that matches only on unicode "word" characters, as well as -, _ and @
+ //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
+ // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
+ return Regex.IsMatch(username, "^[\\w-'._@]*$");
}
private static bool IsValidUsernameCharacter(char i)
{
- return !char.Equals(i, '<') && !char.Equals(i, '>');
+ return IsValidUsername(i.ToString());
}
public string MakeValidUsername(string username)
@@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
private string GetLocalPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
- ? _defaultAuthenticationProvider.GetEmptyHashedString(user)
+ ? null
: user.EasyPassword;
}
- private bool IsPasswordEmpty(User user, string passwordHash)
- {
- return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
- }
-
/// <summary>
/// Loads the users from the repository
/// </summary>
@@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user));
}
- var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
- var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
+ bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
+ bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
- var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
+ bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
hasConfiguredEasyPassword :
hasConfiguredPassword;
- var dto = new UserDto
+ UserDto dto = new UserDto
{
Id = user.Id,
Name = user.Name,
@@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
dto.EnableAutoLogin = true;
}
- var image = user.GetImageInfo(ImageType.Primary, 0);
+ ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
if (image != null)
{
@@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
if (!IsValidUsername(name))
{
- throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+ throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 31217730b..762649b71 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization
{
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
- Directory.CreateDirectory(LocalizationPath);
-
- var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName);
-
// Extract from the assembly
foreach (var resource in _assembly.GetManifestResourceNames())
{
@@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization
continue;
}
- string filename = "ratings-" + resource.Substring(ratingsResource.Length);
-
- if (existingFiles.Contains(filename))
- {
- continue;
- }
+ string countryCode = resource.Substring(ratingsResource.Length, 2);
+ var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
- using (var stream = _assembly.GetManifestResourceStream(resource))
+ using (var str = _assembly.GetManifestResourceStream(resource))
+ using (var reader = new StreamReader(str))
{
- string target = Path.Combine(LocalizationPath, filename);
- _logger.LogInformation("Extracting ratings to {0}", target);
-
- using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ string line;
+ while ((line = await reader.ReadLineAsync()) != null)
{
- await stream.CopyToAsync(fs);
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ string[] parts = line.Split(',');
+ if (parts.Length == 2
+ && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
+ {
+ dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
+ }
+#if DEBUG
+ else
+ {
+ _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+ }
+#endif
}
}
- }
- foreach (var file in GetRatingsFiles(LocalizationPath))
- {
- await LoadRatings(file);
+ _allParentalRatings[countryCode] = dict;
}
- LoadAdditionalRatings();
-
await LoadCultures();
}
- private void LoadAdditionalRatings()
- {
- LoadRatings("au", new[]
- {
- new ParentalRating("AU-G", 1),
- new ParentalRating("AU-PG", 5),
- new ParentalRating("AU-M", 6),
- new ParentalRating("AU-MA15+", 7),
- new ParentalRating("AU-M15+", 8),
- new ParentalRating("AU-R18+", 9),
- new ParentalRating("AU-X18+", 10),
- new ParentalRating("AU-RC", 11)
- });
-
- LoadRatings("be", new[]
- {
- new ParentalRating("BE-AL", 1),
- new ParentalRating("BE-MG6", 2),
- new ParentalRating("BE-6", 3),
- new ParentalRating("BE-9", 5),
- new ParentalRating("BE-12", 6),
- new ParentalRating("BE-16", 8)
- });
-
- LoadRatings("de", new[]
- {
- new ParentalRating("DE-0", 1),
- new ParentalRating("FSK-0", 1),
- new ParentalRating("DE-6", 5),
- new ParentalRating("FSK-6", 5),
- new ParentalRating("DE-12", 7),
- new ParentalRating("FSK-12", 7),
- new ParentalRating("DE-16", 8),
- new ParentalRating("FSK-16", 8),
- new ParentalRating("DE-18", 9),
- new ParentalRating("FSK-18", 9)
- });
-
- LoadRatings("ru", new[]
- {
- new ParentalRating("RU-0+", 1),
- new ParentalRating("RU-6+", 3),
- new ParentalRating("RU-12+", 7),
- new ParentalRating("RU-16+", 9),
- new ParentalRating("RU-18+", 10)
- });
- }
-
- private void LoadRatings(string country, ParentalRating[] ratings)
- {
- _allParentalRatings[country] = ratings.ToDictionary(i => i.Name);
- }
-
- private IEnumerable<string> GetRatingsFiles(string directory)
- => _fileSystem.GetFilePaths(directory, false)
- .Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase))
- .Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase));
-
- /// <summary>
- /// Gets the localization path.
- /// </summary>
- /// <value>The localization path.</value>
- public string LocalizationPath
- => Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization");
-
public string NormalizeFormKD(string text)
=> text.Normalize(NormalizationForm.FormKD);
@@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization
return value;
}
- /// <summary>
- /// Loads the ratings.
- /// </summary>
- /// <param name="file">The file.</param>
- /// <returns>Dictionary{System.StringParentalRating}.</returns>
- private async Task LoadRatings(string file)
- {
- Dictionary<string, ParentalRating> dict
- = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
-
- using (var str = File.OpenRead(file))
- using (var reader = new StreamReader(str))
- {
- string line;
- while ((line = await reader.ReadLineAsync()) != null)
- {
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
- {
- dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value }));
- }
-#if DEBUG
- else
- {
- _logger.LogWarning("Misformed line in {Path}", file);
- }
-#endif
- }
- }
-
- var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1];
-
- _allParentalRatings[countryCode] = dict;
- }
-
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
/// <summary>
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
new file mode 100644
index 000000000..940375e26
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/au.csv
@@ -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
diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv
new file mode 100644
index 000000000..d3937caf7
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/be.csv
@@ -0,0 +1,6 @@
+BE-AL,1
+BE-MG6,2
+BE-6,3
+BE-9,5
+BE-12,6
+BE-16,8
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv
new file mode 100644
index 000000000..f944a140d
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/de.csv
@@ -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
diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv
new file mode 100644
index 000000000..1bc94affd
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ru.csv
@@ -0,0 +1,5 @@
+RU-0+,1
+RU-6+,3
+RU-12+,7
+RU-16+,9
+RU-18+,10
diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs
index 60cc9b88e..ace93ebde 100644
--- a/Emby.Server.Implementations/Networking/NetworkManager.cs
+++ b/Emby.Server.Implementations/Networking/NetworkManager.cs
@@ -79,13 +79,13 @@ namespace Emby.Server.Implementations.Networking
private IpAddressInfo[] _localIpAddresses;
private readonly object _localIpAddressSyncLock = new object();
- public IpAddressInfo[] GetLocalIpAddresses()
+ public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
{
lock (_localIpAddressSyncLock)
{
if (_localIpAddresses == null)
{
- var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray();
+ var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray();
_localIpAddresses = addresses;
@@ -95,9 +95,9 @@ namespace Emby.Server.Implementations.Networking
}
}
- private async Task<List<IPAddress>> GetLocalIpAddressesInternal()
+ private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool ignoreVirtualInterface)
{
- var list = GetIPsDefault()
+ var list = GetIPsDefault(ignoreVirtualInterface)
.ToList();
if (list.Count == 0)
@@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking
return Dns.GetHostAddressesAsync(hostName);
}
- private List<IPAddress> GetIPsDefault()
+ private List<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
{
NetworkInterface[] interfaces;
@@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking
// Try to exclude virtual adapters
// http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
var addr = ipProperties.GatewayAddresses.FirstOrDefault();
- if (addr == null || string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
+ if (addr == null || ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
{
return new List<IPAddress>();
}
@@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking
return false;
}
+ public bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask)
+ {
+ IPAddress network1 = GetNetworkAddress(ToIPAddress(address1), ToIPAddress(subnetMask));
+ IPAddress network2 = GetNetworkAddress(ToIPAddress(address2), ToIPAddress(subnetMask));
+ return network1.Equals(network2);
+ }
+
+ private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
+ {
+ byte[] ipAdressBytes = address.GetAddressBytes();
+ byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
+
+ if (ipAdressBytes.Length != subnetMaskBytes.Length)
+ {
+ throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
+ }
+
+ byte[] broadcastAddress = new byte[ipAdressBytes.Length];
+ for (int i = 0; i < broadcastAddress.Length; i++)
+ {
+ broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
+ }
+ return new IPAddress(broadcastAddress);
+ }
+
+ public IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address)
+ {
+ NetworkInterface[] interfaces;
+ IPAddress ipaddress = ToIPAddress(address);
+
+ try
+ {
+ var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
+
+ interfaces = NetworkInterface.GetAllNetworkInterfaces()
+ .Where(i => validStatuses.Contains(i.OperationalStatus))
+ .ToArray();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error in GetAllNetworkInterfaces");
+ return null;
+ }
+
+ foreach (NetworkInterface ni in interfaces)
+ {
+ if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null)
+ {
+ foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
+ {
+ if (ip.Address.Equals(ipaddress) && ip.IPv4Mask != null)
+ {
+ return ToIpAddressInfo(ip.IPv4Mask);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
{
if (endpoint == null)
diff --git a/Emby.Server.Implementations/Security/EncryptionManager.cs b/Emby.Server.Implementations/Security/EncryptionManager.cs
deleted file mode 100644
index fa8872ccc..000000000
--- a/Emby.Server.Implementations/Security/EncryptionManager.cs
+++ /dev/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);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
index f575baca3..ccb28e8df 100644
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ b/Emby.Server.Implementations/Services/ServicePath.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Services
private const char ComponentSeperator = '.';
private const string VariablePrefix = "{";
- readonly bool[] componentsWithSeparators;
+ private readonly bool[] componentsWithSeparators;
private readonly string restPath;
public bool IsWildCardPath { get; private set; }
@@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services
public string Description { get; private set; }
public bool IsHidden { get; private set; }
- public int Priority { get; set; } //passed back to RouteAttribute
-
- public IEnumerable<string> PathVariables => this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e));
-
public static string[] GetPathPartsForMatching(string pathInfo)
{
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
@@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services
{
list.Add(hashPrefix + part);
- var subParts = part.Split(ComponentSeperator);
- if (subParts.Length == 1) continue;
+ if (part.IndexOf(ComponentSeperator) == -1)
+ {
+ continue;
+ }
+ var subParts = part.Split(ComponentSeperator);
foreach (var subPart in subParts)
{
list.Add(hashPrefix + subPart);
@@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services
{
if (string.IsNullOrEmpty(component)) continue;
- if (StringContains(component, VariablePrefix)
+ if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
&& component.IndexOf(ComponentSeperator) != -1)
{
hasSeparators.Add(true);
@@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services
for (var i = 0; i < components.Length - 1; i++)
{
- if (!this.isWildcard[i]) continue;
+ if (!this.isWildcard[i])
+ {
+ continue;
+ }
+
if (this.literalsToMatch[i + 1] == null)
{
throw new ArgumentException(
@@ -173,7 +176,7 @@ namespace Emby.Server.Implementations.Services
}
}
- this.wildcardCount = this.isWildcard.Count(x => x);
+ this.wildcardCount = this.isWildcard.Length;
this.IsWildCardPath = this.wildcardCount > 0;
this.FirstMatchHashKey = !this.IsWildCardPath
@@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services
: WildCardChar + PathSeperator + firstLiteralMatch;
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
- RegisterCaseInsenstivePropertyNameMappings();
- }
- private void RegisterCaseInsenstivePropertyNameMappings()
- {
- foreach (var propertyInfo in GetSerializableProperties(RequestType))
- {
- var propertyName = propertyInfo.Name;
- propertyNamesMap.Add(propertyName.ToLowerInvariant(), propertyName);
- }
+ _propertyNamesMap = new HashSet<string>(
+ GetSerializableProperties(RequestType).Select(x => x.Name),
+ StringComparer.OrdinalIgnoreCase);
}
- internal static string[] IgnoreAttributesNamed = new[] {
+ internal static string[] IgnoreAttributesNamed = new[]
+ {
"IgnoreDataMemberAttribute",
"JsonIgnoreAttribute"
};
@@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services
private static Type excludeType = typeof(Stream);
- internal static List<PropertyInfo> GetSerializableProperties(Type type)
+ internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
{
- var list = new List<PropertyInfo>();
- var props = GetPublicProperties(type);
-
- foreach (var prop in props)
+ foreach (var prop in GetPublicProperties(type))
{
- if (prop.GetMethod == null)
- {
- continue;
- }
-
- if (excludeType == prop.PropertyType)
+ if (prop.GetMethod == null
+ || excludeType == prop.PropertyType)
{
continue;
}
@@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services
if (!ignored)
{
- list.Add(prop);
+ yield return prop;
}
}
-
- // else return those properties that are not decorated with IgnoreDataMember
- return list;
}
- private static List<PropertyInfo> GetPublicProperties(Type type)
+ private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
{
- if (type.GetTypeInfo().IsInterface)
+ if (type.IsInterface)
{
var propertyInfos = new List<PropertyInfo>();
-
- var considered = new List<Type>();
+ var considered = new List<Type>()
+ {
+ type
+ };
var queue = new Queue<Type>();
- considered.Add(type);
queue.Enqueue(type);
while (queue.Count > 0)
@@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services
var subType = queue.Dequeue();
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
{
- if (considered.Contains(subInterface)) continue;
+ if (considered.Contains(subInterface))
+ {
+ continue;
+ }
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
- var typeProperties = GetTypesPublicProperties(subType);
-
- var newPropertyInfos = typeProperties
+ var newPropertyInfos = GetTypesPublicProperties(subType)
.Where(x => !propertyInfos.Contains(x));
propertyInfos.InsertRange(0, newPropertyInfos);
@@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services
return propertyInfos;
}
- var list = new List<PropertyInfo>();
-
- foreach (var t in GetTypesPublicProperties(type))
- {
- if (t.GetIndexParameters().Length == 0)
- {
- list.Add(t);
- }
- }
- return list;
+ return GetTypesPublicProperties(type)
+ .Where(x => x.GetIndexParameters().Length == 0);
}
- private static PropertyInfo[] GetTypesPublicProperties(Type subType)
+ private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
{
- var pis = new List<PropertyInfo>();
foreach (var pi in subType.GetRuntimeProperties())
{
var mi = pi.GetMethod ?? pi.SetMethod;
- if (mi != null && mi.IsStatic) continue;
- pis.Add(pi);
+ if (mi != null && mi.IsStatic)
+ {
+ continue;
+ }
+
+ yield return pi;
}
- return pis.ToArray();
}
/// <summary>
@@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services
private readonly StringMapTypeDeserializer typeDeserializer;
- private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
+ private readonly HashSet<string> _propertyNamesMap;
public int MatchScore(string httpMethod, string[] withPathInfoParts)
{
@@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services
return -1;
}
- var score = 0;
-
//Routes with least wildcard matches get the highest score
- score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
-
- //Routes with less variable (and more literal) matches
- score += Math.Max((10 - VariableArgsCount), 1) * 100;
+ var score = Math.Max((100 - wildcardMatchCount), 1) * 1000
+ //Routes with less variable (and more literal) matches
+ + Math.Max((10 - VariableArgsCount), 1) * 100;
//Exact verb match is better than ANY
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
@@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services
return score;
}
- private bool StringContains(string str1, string str2)
- {
- return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1;
- }
-
/// <summary>
/// For performance withPathInfoParts should already be a lower case string
/// to minimize redundant matching operations.
@@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services
if (i < this.TotalComponentsCount - 1)
{
// Continue to consume up until a match with the next literal
- while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1]))
+ while (pathIx < withPathInfoParts.Length
+ && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
{
pathIx++;
wildcardMatchCount++;
@@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services
continue;
}
- if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
+ if (withPathInfoParts.Length <= pathIx
+ || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
+
pathIx++;
}
}
@@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services
return pathIx == withPathInfoParts.Length;
}
- private static bool LiteralsEqual(string str1, string str2)
- {
- // Most cases
- if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
-
- // Handle turkish i
- str1 = str1.ToUpperInvariant();
- str2 = str2.ToUpperInvariant();
-
- // Invariant IgnoreCase would probably be better but it's not available in PCL
- return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase);
- }
-
private bool ExplodeComponents(ref string[] withPathInfoParts)
{
var totalComponents = new List<string>();
for (var i = 0; i < withPathInfoParts.Length; i++)
{
var component = withPathInfoParts[i];
- if (string.IsNullOrEmpty(component)) continue;
+ if (string.IsNullOrEmpty(component))
+ {
+ continue;
+ }
if (this.PathComponentsCount != this.TotalComponentsCount
&& this.componentsWithSeparators[i])
{
var subComponents = component.Split(ComponentSeperator);
- if (subComponents.Length < 2) return false;
+ if (subComponents.Length < 2)
+ {
+ return false;
+ }
+
totalComponents.AddRange(subComponents);
}
else
@@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services
continue;
}
- if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest))
+ if (!this._propertyNamesMap.Contains(variableName))
{
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
{
@@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services
{
sb.Append(PathSeperatorChar + requestComponents[j]);
}
+
value = sb.ToString();
}
else
@@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
- var sb = new StringBuilder();
- sb.Append(value);
+ var sb = new StringBuilder(value);
pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
}
+
value = sb.ToString();
}
else
@@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services
pathIx++;
}
- requestKeyValuesMap[propertyNameOnRequest] = value;
+ requestKeyValuesMap[variableName] = value;
}
if (queryStringAndFormData != null)
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
index d13935fba..f835aa1b5 100644
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
@@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services
{
internal class PropertySerializerEntry
{
- public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn)
+ public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
{
PropertySetFn = propertySetFn;
PropertyParseStringFn = propertyParseStringFn;
+ PropertyType = PropertyType;
}
- public Action<object, object> PropertySetFn;
- public Func<string, object> PropertyParseStringFn;
- public Type PropertyType;
+ public Action<object, object> PropertySetFn { get; private set; }
+ public Func<string, object> PropertyParseStringFn { get; private set; }
+ public Type PropertyType { get; private set; }
}
private readonly Type type;
@@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services
public Func<string, object> GetParseFn(Type propertyType)
{
if (propertyType == typeof(string))
+ {
return s => s;
+ }
return _GetParseFn(propertyType);
}
@@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
var propertyType = propertyInfo.PropertyType;
var propertyParseStringFn = GetParseFn(propertyType);
- var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
+ var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
propertySetterMap[propertyInfo.Name] = propertySerializer;
}
@@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
{
- string propertyName = null;
- string propertyTextValue = null;
PropertySerializerEntry propertySerializerEntry = null;
if (instance == null)
+ {
instance = _CreateInstanceFn(type);
+ }
foreach (var pair in keyValuePairs)
{
- propertyName = pair.Key;
- propertyTextValue = pair.Value;
-
- if (string.IsNullOrEmpty(propertyTextValue))
- {
- continue;
- }
+ string propertyName = pair.Key;
+ string propertyTextValue = pair.Value;
- if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry))
- {
- if (propertyName == "v")
- {
- continue;
- }
-
- continue;
- }
-
- if (propertySerializerEntry.PropertySetFn == null)
+ if (string.IsNullOrEmpty(propertyTextValue)
+ || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
+ || propertySerializerEntry.PropertySetFn == null)
{
continue;
}
@@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services
{
continue;
}
+
propertySerializerEntry.PropertySetFn(instance, value);
}
@@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services
public static string LeftPart(string strVal, char needle)
{
- if (strVal == null) return null;
+ if (strVal == null)
+ {
+ return null;
+ }
+
var pos = strVal.IndexOf(needle);
return pos == -1
? strVal
@@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services
{
public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
{
- if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) return null;
+ if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
+ {
+ return null;
+ }
var setMethodInfo = propertyInfo.SetMethod;
return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index fa0ab62d3..03e7b2654 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1090,7 +1090,7 @@ namespace Emby.Server.Implementations.Session
await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
}
- private IList<BaseItem> TranslateItemForPlayback(Guid id, User user)
+ private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs
index 82bcf2f04..5e29e4058 100644
--- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs
+++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs
@@ -13,9 +13,9 @@ namespace Emby.Server.Implementations.SocketSharp
{
public partial class WebSocketSharpRequest : IHttpRequest
{
- internal static string GetParameter(string header, string attr)
+ internal static string GetParameter(ReadOnlySpan<char> header, string attr)
{
- int ap = header.IndexOf(attr, StringComparison.Ordinal);
+ int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal);
if (ap == -1)
{
return null;
@@ -33,18 +33,19 @@ namespace Emby.Server.Implementations.SocketSharp
ending = ' ';
}
- int end = header.IndexOf(ending, ap + 1);
+ var slice = header.Slice(ap + 1);
+ int end = slice.IndexOf(ending);
if (end == -1)
{
- return ending == '"' ? null : header.Substring(ap);
+ return ending == '"' ? null : header.Slice(ap).ToString();
}
- return header.Substring(ap + 1, end - ap - 1);
+ return slice.Slice(0, end - ap - 1).ToString();
}
private async Task LoadMultiPart(WebROCollection form)
{
- string boundary = GetParameter(ContentType, "; boundary=");
+ string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
if (boundary == null)
{
return;
@@ -377,17 +378,17 @@ namespace Emby.Server.Implementations.SocketSharp
}
var elem = new Element();
- string header;
- while ((header = ReadHeaders()) != null)
+ ReadOnlySpan<char> header;
+ while ((header = ReadHeaders().AsSpan()) != null)
{
- if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase))
+ if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
elem.Name = GetContentDispositionAttribute(header, "name");
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
}
- else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
+ else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
- elem.ContentType = header.Substring("Content-Type:".Length).Trim();
+ elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
elem.Encoding = GetEncoding(elem.ContentType);
}
}
@@ -435,16 +436,16 @@ namespace Emby.Server.Implementations.SocketSharp
return sb.ToString();
}
- private static string GetContentDispositionAttribute(string l, string name)
+ private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name)
{
- int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
+ int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
if (idx < 0)
{
return null;
}
int begin = idx + name.Length + "=\"".Length;
- int end = l.IndexOf('"', begin);
+ int end = l.Slice(begin).IndexOf('"');
if (end < 0)
{
return null;
@@ -455,19 +456,19 @@ namespace Emby.Server.Implementations.SocketSharp
return string.Empty;
}
- return l.Substring(begin, end - begin);
+ return l.Slice(begin, end - begin).ToString();
}
- private string GetContentDispositionAttributeWithEncoding(string l, string name)
+ private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name)
{
- int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
+ int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
if (idx < 0)
{
return null;
}
int begin = idx + name.Length + "=\"".Length;
- int end = l.IndexOf('"', begin);
+ int end = l.Slice(begin).IndexOf('"');
if (end < 0)
{
return null;
@@ -478,7 +479,7 @@ namespace Emby.Server.Implementations.SocketSharp
return string.Empty;
}
- string temp = l.Substring(begin, end - begin);
+ ReadOnlySpan<char> temp = l.Slice(begin, end - begin);
byte[] source = new byte[temp.Length];
for (int i = temp.Length - 1; i >= 0; i--)
{
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
index 976f2ec06..bc002dc4c 100644
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
@@ -56,19 +56,37 @@ namespace Emby.Server.Implementations.SocketSharp
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
private string remoteIp;
+ public string RemoteIp
+ {
+ get
+ {
+ if (remoteIp != null)
+ {
+ return remoteIp;
+ }
- public string RemoteIp =>
- remoteIp ??
- (remoteIp = CheckBadChars(XForwardedFor) ??
- NormalizeIp(CheckBadChars(XRealIp) ??
- (string.IsNullOrEmpty(request.HttpContext.Connection.RemoteIpAddress.ToString()) ? null : NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString()))));
+ var temp = CheckBadChars(XForwardedFor.AsSpan());
+ if (temp.Length != 0)
+ {
+ return remoteIp = temp.ToString();
+ }
+
+ temp = CheckBadChars(XRealIp.AsSpan());
+ if (temp.Length != 0)
+ {
+ return remoteIp = NormalizeIp(temp).ToString();
+ }
+
+ return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
+ }
+ }
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
// CheckBadChars - throws on invalid chars to be not found in header name/value
- internal static string CheckBadChars(string name)
+ internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
{
- if (name == null || name.Length == 0)
+ if (name.Length == 0)
{
return name;
}
@@ -99,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp
}
else if (c == 127 || (c < ' ' && c != '\t'))
{
- throw new ArgumentException("net_WebHeaderInvalidControlChars");
+ throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
}
break;
@@ -113,7 +131,7 @@ namespace Emby.Server.Implementations.SocketSharp
break;
}
- throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
case 2:
@@ -124,29 +142,29 @@ namespace Emby.Server.Implementations.SocketSharp
break;
}
- throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
}
}
if (crlf != 0)
{
- throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
return name;
}
- private string NormalizeIp(string ip)
+ private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
{
- if (!string.IsNullOrWhiteSpace(ip))
+ if (ip.Length != 0 && !ip.IsWhiteSpace())
{
// Handle ipv4 mapped to ipv6
const string srch = "::ffff:";
- var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0)
{
- ip = ip.Substring(srch.Length);
+ ip = ip.Slice(srch.Length);
}
}
@@ -324,7 +342,7 @@ namespace Emby.Server.Implementations.SocketSharp
}
this.pathInfo = WebUtility.UrlDecode(pathInfo);
- this.pathInfo = NormalizePathInfo(pathInfo, mode);
+ this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
}
return this.pathInfo;
@@ -436,7 +454,7 @@ namespace Emby.Server.Implementations.SocketSharp
public static Encoding GetEncoding(string contentTypeHeader)
{
- var param = GetParameter(contentTypeHeader, "charset=");
+ var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
if (param == null)
{
return null;
@@ -488,18 +506,18 @@ namespace Emby.Server.Implementations.SocketSharp
}
}
- public static string NormalizePathInfo(string pathInfo, string handlerPath)
+ public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
{
if (handlerPath != null)
{
- var trimmed = pathInfo.TrimStart('/');
- if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
+ var trimmed = pathInfo.AsSpan().TrimStart('/');
+ if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
- return trimmed.Substring(handlerPath.Length);
+ return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
}
}
- return pathInfo;
+ return pathInfo.AsSpan();
}
}
}
diff --git a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs
index 52ec7a135..46bf6cc21 100644
--- a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs
+++ b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs
@@ -495,9 +495,7 @@ namespace Emby.XmlTv.Classes
ParseMovieDbSystem(reader, result);
break;
case "SxxExx":
- // TODO
- // <episode-num system="SxxExx">S03E12</episode-num>
- reader.Skip();
+ ParseSxxExxSystem(reader, result);
break;
default: // Handles empty string and nulls
reader.Skip();
@@ -505,6 +503,29 @@ namespace Emby.XmlTv.Classes
}
}
+ public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result)
+ {
+ // <episode-num system="SxxExx">S012E32</episode-num>
+
+ var value = reader.ReadElementContentAsString();
+ var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase);
+
+ if (res.Success)
+ {
+ int parsedInt;
+
+ if (int.TryParse(res.Groups[1].Value, out parsedInt))
+ {
+ result.Episode.Series = parsedInt;
+ }
+
+ if (int.TryParse(res.Groups[2].Value, out parsedInt))
+ {
+ result.Episode.Episode = parsedInt;
+ }
+ }
+ }
+
public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result)
{
// <episode-num system="thetvdb.com">series/248841</episode-num>
diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
index 0d5a1d3c0..c72f295fd 100644
--- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
+++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia
foregroundWidth *= percent;
foregroundWidth /= 100;
- paint.Color = SKColor.Parse("#FF52B54B");
+ paint.Color = SKColor.Parse("#FF00A4DC");
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
}
}
diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
index 62497da27..7f3c18bb2 100644
--- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
+++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
using (var paint = new SKPaint())
{
- paint.Color = SKColor.Parse("#CC52B54B");
+ paint.Color = SKColor.Parse("#CC00A4DC");
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
}
diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
index ba712bff7..dbf935f4e 100644
--- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
+++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia
using (var paint = new SKPaint())
{
- paint.Color = SKColor.Parse("#CC52B54B");
+ paint.Color = SKColor.Parse("#CC00A4DC");
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
}
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 5d3f7b171..c8cdb984d 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -20,10 +20,10 @@ namespace Jellyfin.Server
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string LogDir { get; set; }
- [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")]
+ [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
public string FFmpegPath { get; set; }
- [Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")]
+ [Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")]
public string FFprobePath { get; set; }
[Option("service", Required = false, HelpText = "Run as headless service.")]
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs
index a037357ed..69673a49c 100644
--- a/MediaBrowser.Api/BaseApiService.cs
+++ b/MediaBrowser.Api/BaseApiService.cs
@@ -172,16 +172,9 @@ namespace MediaBrowser.Api
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
{
- if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes))
- {
- options.ImageTypes = Array.Empty<ImageType>();
- }
- else
- {
- options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
- .ToArray();
- }
+ options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
+ .ToArray();
}
}
diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs
index 9caf07cea..201efe737 100644
--- a/MediaBrowser.Api/FilterService.cs
+++ b/MediaBrowser.Api/FilterService.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -180,7 +181,7 @@ namespace MediaBrowser.Api
return ToOptimizedResult(filters);
}
- private QueryFiltersLegacy GetFilters(BaseItem[] items)
+ private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
{
var result = new QueryFiltersLegacy();
diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
index 7aeb0e9e8..bf15cc756 100644
--- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
@@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive
[Route("/Videos/{Id}/stream.mov", "GET")]
[Route("/Videos/{Id}/stream.iso", "GET")]
[Route("/Videos/{Id}/stream.flv", "GET")]
+ [Route("/Videos/{Id}/stream.rm", "GET")]
[Route("/Videos/{Id}/stream", "GET")]
[Route("/Videos/{Id}/stream.ts", "HEAD")]
[Route("/Videos/{Id}/stream.webm", "HEAD")]
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index 84475467f..3c7ad1d0a 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.Linq;
using MediaBrowser.Controller.Dto;
@@ -197,29 +198,27 @@ namespace MediaBrowser.Api.UserLibrary
request.ParentId = null;
}
- var item = string.IsNullOrEmpty(request.ParentId) ?
- null :
- _libraryManager.GetItemById(request.ParentId);
+ BaseItem item = null;
- if (item == null)
+ if (!string.IsNullOrEmpty(request.ParentId))
{
- item = string.IsNullOrEmpty(request.ParentId) ?
- user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
- _libraryManager.GetItemById(request.ParentId);
+ item = _libraryManager.GetItemById(request.ParentId);
}
- // Default list type = children
+ if (item == null)
+ {
+ item = _libraryManager.GetUserRootFolder();
+ }
- var folder = item as Folder;
+ Folder folder = item as Folder;
if (folder == null)
{
- folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
+ folder = _libraryManager.GetUserRootFolder();
}
var hasCollectionType = folder as IHasCollectionType;
- var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase));
-
- if (isPlaylistQuery)
+ if (hasCollectionType != null
+ && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
request.Recursive = true;
request.IncludeItemTypes = "Playlist";
@@ -235,20 +234,12 @@ namespace MediaBrowser.Api.UserLibrary
};
}
- if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
- {
- return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
- }
-
- var userRoot = item as UserRootFolder;
-
- if (userRoot == null)
+ if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
{
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
}
var itemsArray = folder.GetChildren(user, true).ToArray();
-
return new QueryResult<BaseItem>
{
Items = itemsArray,
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
index 72fb6e2b8..34c6f5866 100644
--- a/MediaBrowser.Common/Net/INetworkManager.cs
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Net
/// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
bool IsInLocalNetwork(string endpoint);
- IpAddressInfo[] GetLocalIpAddresses();
+ IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface);
IpAddressInfo ParseIpAddress(string ipAddress);
@@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net
Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
bool IsAddressInSubnets(string addressString, string[] subnets);
+
+ bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask);
+ IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address);
}
}
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index aa99f6b58..cdaf95f5c 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -36,9 +36,7 @@ namespace MediaBrowser.Controller.Dto
.ToArray();
public bool ContainsField(ItemFields field)
- {
- return AllItemFields.Contains(field);
- }
+ => Fields.Contains(field);
public DtoOptions(bool allFields)
{
@@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto
EnableUserData = true;
AddCurrentProgram = true;
- if (allFields)
- {
- Fields = AllItemFields;
- }
- else
- {
- Fields = new ItemFields[] { };
- }
-
+ Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
ImageTypes = AllImageTypes;
}
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index df5ec5dd0..4b6fd58fe 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
- BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
-
- BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
+ BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
/// <summary>
/// Gets the item by name dto.
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 8bfadbee6..e49ff20ba 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities
{
if (query.ItemIds.Length > 0)
{
- var result = LibraryManager.GetItemsResult(query);
-
- if (query.OrderBy.Length == 0)
- {
- var ids = query.ItemIds.ToList();
-
- // Try to preserve order
- result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
- }
- return result;
+ return LibraryManager.GetItemsResult(query);
}
return GetItemsInternal(query);
}
- public BaseItem[] GetItemList(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
{
query.EnableTotalRecordCount = false;
if (query.ItemIds.Length > 0)
{
- var result = LibraryManager.GetItemList(query);
-
- if (query.OrderBy.Length == 0)
- {
- var ids = query.ItemIds.ToList();
-
- // Try to preserve order
- return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
- }
- return result.ToArray();
+ return LibraryManager.GetItemList(query);
}
return GetItemsInternal(query).Items;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index f5f147db1..e378c2b89 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
flags.Add("+ignidx");
}
- if (state.GenPtsInput)
+ if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
flags.Add("+genpts");
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 057e43910..d4ac3b7c3 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.System;
namespace MediaBrowser.Controller.MediaEncoding
{
@@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
public interface IMediaEncoder : ITranscoderSupport
{
- string EncoderLocationType { get; }
+ FFmpegLocation EncoderLocation { get; }
/// <summary>
/// Gets the encoder path.
@@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns>
string EscapeSubtitleFilterPath(string path);
- void Init();
+ void SetFFmpegPath();
void UpdateEncoderPath(string path, string pathType);
bool SupportsEncoder(string encoder);
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index b812a8ddc..46593fb2f 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -32,16 +32,17 @@ namespace MediaBrowser.Controller.MediaEncoding
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
+ // If ffmpeg process is closed, the state is disposed, so don't write to target in that case
+ if (!target.CanWrite)
+ {
+ break;
+ }
+
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await target.FlushAsync().ConfigureAwait(false);
}
}
}
- catch (ObjectDisposedException)
- {
- //TODO Investigate and properly fix.
- // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
- }
catch (Exception ex)
{
_logger.LogError(ex, "Error reading ffmpeg log");
diff --git a/MediaBrowser.Controller/Security/IEncryptionManager.cs b/MediaBrowser.Controller/Security/IEncryptionManager.cs
deleted file mode 100644
index 68680fdf3..000000000
--- a/MediaBrowser.Controller/Security/IEncryptionManager.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index 1a7654bfd..7c330ad86 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images
var path = item.ContainingFolderPath;
+ // Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
+ if (!Directory.Exists(path))
+ {
+ return Array.Empty<FileSystemMetadata>();
+ }
+
if (includeDirectories)
{
return directoryService.GetFileSystemEntries(path)
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 262772959..3eed891cb 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
+using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using MediaBrowser.Model.Diagnostics;
@@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_processFactory = processFactory;
}
- public (IEnumerable<string> decoders, IEnumerable<string> encoders) Validate(string encoderPath)
+ public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath)
{
_logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
@@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (string.IsNullOrWhiteSpace(output))
{
+ if (logOutput)
+ {
+ _logger.LogError("FFmpeg validation: The process returned no result");
+ }
return false;
}
@@ -55,21 +59,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
{
+ if (logOutput)
+ {
+ _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
+ }
return false;
}
- output = " " + output + " ";
+ // The min and max FFmpeg versions required to run jellyfin successfully
+ var minRequired = new Version(4, 0);
+ var maxRequired = new Version(4, 0);
+
+ // Work out what the version under test is
+ var underTest = GetFFmpegVersion(output);
- for (var i = 2013; i <= 2015; i++)
+ if (logOutput)
{
- var yearString = i.ToString(CultureInfo.InvariantCulture);
- if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1)
+ _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown");
+
+ if (underTest == null) // Version is unknown
{
- return false;
+ if (minRequired.Equals(maxRequired))
+ {
+ _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString());
+ }
+ else
+ {
+ _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString());
+ }
}
+ else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend
+ {
+ _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString());
+ }
+ else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend
+ {
+ _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString());
+ }
+ else // Version is ok
+ {
+ _logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version");
+ }
+ }
+
+ // 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 true;
+ return rc;
}
private static readonly string[] requiredDecoders = new[]
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index d922f1068..292457788 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -3,17 +3,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Session;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Diagnostics;
@@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
@@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder
public class MediaEncoder : IMediaEncoder, IDisposable
{
/// <summary>
- /// The _logger
- /// </summary>
- private readonly ILogger _logger;
-
- /// <summary>
- /// Gets the json serializer.
+ /// Gets the encoder path.
/// </summary>
- /// <value>The json serializer.</value>
- private readonly IJsonSerializer _jsonSerializer;
+ /// <value>The encoder path.</value>
+ public string EncoderPath => FFmpegPath;
/// <summary>
- /// The _thumbnail resource pool
+ /// The location of the discovered FFmpeg tool.
/// </summary>
- private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
-
- public string FFMpegPath { get; private set; }
-
- public string FFProbePath { get; private set; }
+ public FFmpegLocation EncoderLocation { get; private set; }
+ private readonly ILogger _logger;
+ private readonly IJsonSerializer _jsonSerializer;
+ private string FFmpegPath;
+ private string FFprobePath;
protected readonly IServerConfigurationManager ConfigurationManager;
protected readonly IFileSystem FileSystem;
- protected readonly ILiveTvManager LiveTvManager;
- protected readonly IIsoManager IsoManager;
- protected readonly ILibraryManager LibraryManager;
- protected readonly IChannelManager ChannelManager;
- protected readonly ISessionManager SessionManager;
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
protected readonly Func<IMediaSourceManager> MediaSourceManager;
- private readonly IHttpClient _httpClient;
- private readonly IZipClient _zipClient;
private readonly IProcessFactory _processFactory;
+ private readonly 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>();
- private readonly bool _hasExternalEncoder;
- private readonly string _originalFFMpegPath;
- private readonly string _originalFFProbePath;
- private readonly int DefaultImageExtractionTimeoutMs;
public MediaEncoder(
ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer,
- string ffMpegPath,
- string ffProbePath,
- bool hasExternalEncoder,
+ string startupOptionsFFmpegPath,
+ string startupOptionsFFprobePath,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
- ILiveTvManager liveTvManager,
- IIsoManager isoManager,
- ILibraryManager libraryManager,
- IChannelManager channelManager,
- ISessionManager sessionManager,
Func<ISubtitleEncoder> subtitleEncoder,
Func<IMediaSourceManager> mediaSourceManager,
- IHttpClient httpClient,
- IZipClient zipClient,
IProcessFactory processFactory,
int defaultImageExtractionTimeoutMs)
{
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
_jsonSerializer = jsonSerializer;
+ StartupOptionFFmpegPath = startupOptionsFFmpegPath;
+ StartupOptionFFprobePath = startupOptionsFFprobePath;
ConfigurationManager = configurationManager;
FileSystem = fileSystem;
- LiveTvManager = liveTvManager;
- IsoManager = isoManager;
- LibraryManager = libraryManager;
- ChannelManager = channelManager;
- SessionManager = sessionManager;
SubtitleEncoder = subtitleEncoder;
- MediaSourceManager = mediaSourceManager;
- _httpClient = httpClient;
- _zipClient = zipClient;
_processFactory = processFactory;
DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
- FFProbePath = ffProbePath;
- FFMpegPath = ffMpegPath;
- _originalFFProbePath = ffProbePath;
- _originalFFMpegPath = ffMpegPath;
- _hasExternalEncoder = hasExternalEncoder;
}
- public string EncoderLocationType
+ /// <summary>
+ /// Run at startup or if the user removes a Custom path from transcode page.
+ /// Sets global variables FFmpegPath.
+ /// Precedence is: Config > CLI > $PATH
+ /// </summary>
+ public void SetFFmpegPath()
{
- get
+ // ToDo - Finalise removal of the --ffprobe switch
+ if (!string.IsNullOrEmpty(StartupOptionFFprobePath))
{
- if (_hasExternalEncoder)
- {
- return "External";
- }
-
- if (string.IsNullOrWhiteSpace(FFMpegPath))
- {
- return null;
- }
-
- if (IsSystemInstalledPath(FFMpegPath))
- {
- return "System";
- }
-
- return "Custom";
+ _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release");
}
- }
- private bool IsSystemInstalledPath(string path)
- {
- if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1)
+ // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
+ if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
{
- return true;
+ // 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;
+ }
+ }
}
- return false;
- }
-
- public void Init()
- {
- InitPaths();
+ // 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);
- if (!string.IsNullOrWhiteSpace(FFMpegPath))
+ // Only if mpeg path is set, try and set path to probe
+ if (FFmpegPath != null)
{
- var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath);
+ // Determine a probe path from the mpeg path
+ FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
+
+ // Interrogate to understand what coders are supported
+ var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath);
SetAvailableDecoders(result.decoders);
SetAvailableEncoders(result.encoders);
}
- }
-
- private void InitPaths()
- {
- ConfigureEncoderPaths();
-
- if (_hasExternalEncoder)
- {
- LogPaths();
- return;
- }
-
- // If the path was passed in, save it into config now.
- var encodingOptions = GetEncodingOptions();
- var appPath = encodingOptions.EncoderAppPath;
-
- var valueToSave = FFMpegPath;
-
- if (!string.IsNullOrWhiteSpace(valueToSave))
- {
- // if using system variable, don't save this.
- if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder)
- {
- valueToSave = null;
- }
- }
- if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal))
- {
- encodingOptions.EncoderAppPath = valueToSave;
- ConfigurationManager.SaveConfiguration("encoding", encodingOptions);
- }
+ _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty);
}
+ /// <summary>
+ /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
+ /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
+ /// </summary>
+ /// <param name="path"></param>
+ /// <param name="pathType"></param>
public void UpdateEncoderPath(string path, string pathType)
{
- if (_hasExternalEncoder)
- {
- return;
- }
+ string newPath;
_logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty);
- Tuple<string, string> newPaths;
-
- if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase))
- {
- path = "ffmpeg";
-
- newPaths = TestForInstalledVersions();
- }
- else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
+ if (!string.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);
+ throw new ArgumentException("Unexpected pathType value");
}
- else
+ else if (string.IsNullOrWhiteSpace(path))
{
- throw new ArgumentException("Unexpected pathType value");
+ // User had cleared the custom path in UI
+ newPath = string.Empty;
}
-
- if (string.IsNullOrWhiteSpace(newPaths.Item1))
+ else if (File.Exists(path))
{
- throw new ResourceNotFoundException("ffmpeg not found");
+ newPath = path;
}
- if (string.IsNullOrWhiteSpace(newPaths.Item2))
+ else if (Directory.Exists(path))
{
- throw new ResourceNotFoundException("ffprobe not found");
+ // Given path is directory, so resolve down to filename
+ newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
}
-
- path = newPaths.Item1;
-
- if (!ValidateVersion(path, true))
+ else
{
- throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required.");
+ throw new ResourceNotFoundException();
}
- var config = GetEncodingOptions();
- config.EncoderAppPath = path;
+ // Write the new ffmpeg path to the xml as <EncoderAppPath>
+ // This ensures its not lost on next startup
+ var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
+ config.EncoderAppPath = newPath;
ConfigurationManager.SaveConfiguration("encoding", config);
- Init();
+ // Trigger SetFFmpegPath so we validate the new path and setup probe path
+ SetFFmpegPath();
}
- private bool ValidateVersion(string path, bool logOutput)
- {
- return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput);
- }
-
- private void ConfigureEncoderPaths()
+ /// <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)
{
- if (_hasExternalEncoder)
- {
- return;
- }
-
- var appPath = GetEncodingOptions().EncoderAppPath;
+ bool rc = false;
- if (string.IsNullOrWhiteSpace(appPath))
+ if (!string.IsNullOrEmpty(path))
{
- 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;
- }
+ if (File.Exists(path))
+ {
+ rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true);
- LogPaths();
- }
+ if (!rc)
+ {
+ _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path);
+ }
- private Tuple<string, string> GetEncoderPaths(string configuredPath)
- {
- var appPath = configuredPath;
+ // ToDo - Enable the ffmpeg validator. At the moment any version can be used.
+ rc = true;
- if (!string.IsNullOrWhiteSpace(appPath))
- {
- if (Directory.Exists(appPath))
- {
- return GetPathsFromDirectory(appPath);
+ FFmpegPath = path;
+ EncoderLocation = location;
}
-
- if (File.Exists(appPath))
+ else
{
- return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath));
+ _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path);
}
}
- return new Tuple<string, string>(null, null);
+ return rc;
}
- private Tuple<string, string> TestForInstalledVersions()
+ private string GetEncoderPathFromDirectory(string path, string filename)
{
- string encoderPath = null;
- string probePath = null;
-
- if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true))
+ try
{
- encoderPath = _originalFFMpegPath;
- probePath = _originalFFProbePath;
- }
+ var files = FileSystem.GetFilePaths(path);
- if (string.IsNullOrWhiteSpace(encoderPath))
+ 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 (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false))
- {
- encoderPath = "ffmpeg";
- probePath = "ffprobe";
- }
+ // Trap all exceptions, like DirNotExists, and return null
+ return null;
}
-
- return new Tuple<string, string>(encoderPath, probePath);
}
- private Tuple<string, string> GetPathsFromDirectory(string path)
+ /// <summary>
+ /// Search the system $PATH environment variable looking for given filename.
+ /// </summary>
+ /// <param name="fileName"></param>
+ /// <returns></returns>
+ private string ExistsOnSystemPath(string filename)
{
- // 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 values = Environment.GetEnvironmentVariable("PATH");
- 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))
+ foreach (var path in values.Split(Path.PathSeparator))
{
- files = FileSystem.GetFilePaths(path, true);
-
- ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
+ var candidatePath = GetEncoderPathFromDirectory(path, filename);
- if (!string.IsNullOrWhiteSpace(ffmpegPath))
+ if (!string.IsNullOrEmpty(candidatePath))
{
- ffprobePath = GetProbePathFromEncoderPath(ffmpegPath);
+ return candidatePath;
}
}
-
- return new Tuple<string, string>(ffmpegPath, ffprobePath);
- }
-
- private string GetProbePathFromEncoderPath(string appPath)
- {
- return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath))
- .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
- }
-
- private void LogPaths()
- {
- _logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found");
- _logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found");
- }
-
- private EncodingOptions GetEncodingOptions()
- {
- return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
+ return null;
}
private List<string> _encoders = new List<string>();
@@ -413,12 +294,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <summary>
- /// Gets the encoder path.
- /// </summary>
- /// <value>The encoder path.</value>
- public string EncoderPath => FFMpegPath;
-
- /// <summary>
/// Gets the media info.
/// </summary>
/// <param name="request">The request.</param>
@@ -489,7 +364,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true,
- FileName = FFProbePath,
+ FileName = FFprobePath,
Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
IsHidden = true,
@@ -691,10 +566,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
CreateNoWindow = true,
UseShellExecute = false,
- FileName = FFMpegPath,
+ FileName = FFmpegPath,
Arguments = args,
IsHidden = true,
- ErrorDialog = false
+ ErrorDialog = false,
+ EnableRaisingEvents = true
});
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
@@ -813,10 +689,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
CreateNoWindow = true,
UseShellExecute = false,
- FileName = FFMpegPath,
+ FileName = FFmpegPath,
Arguments = args,
IsHidden = true,
- ErrorDialog = false
+ ErrorDialog = false,
+ EnableRaisingEvents = true
});
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
diff --git a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs
index 6a5162b8d..a7e3f6197 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -29,17 +30,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IServerConfigurationManager _config;
- private readonly IEncryptionManager _encryption;
private readonly IJsonSerializer _json;
private readonly IFileSystem _fileSystem;
- public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem)
+ public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem)
{
_logger = loggerFactory.CreateLogger(GetType().Name);
_httpClient = httpClient;
_config = config;
- _encryption = encryption;
_json = json;
_fileSystem = fileSystem;
@@ -63,16 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
!string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
!options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
{
- options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash);
+ options.OpenSubtitlesPasswordHash = EncodePassword(options.OpenSubtitlesPasswordHash);
}
}
- private string EncryptPassword(string password)
+ private static string EncodePassword(string password)
{
- return PasswordHashPrefix + _encryption.EncryptString(password);
+ var bytes = Encoding.UTF8.GetBytes(password);
+ return PasswordHashPrefix + Convert.ToBase64String(bytes);
}
- private string DecryptPassword(string password)
+ private static string DecodePassword(string password)
{
if (password == null ||
!password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
@@ -80,7 +80,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return string.Empty;
}
- return _encryption.DecryptString(password.Substring(2));
+ var bytes = Convert.FromBase64String(password.Substring(2));
+ return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
public string Name => "Open Subtitles";
@@ -186,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var options = GetOptions();
var user = options.OpenSubtitlesUsername ?? string.Empty;
- var password = DecryptPassword(options.OpenSubtitlesPasswordHash);
+ var password = DecodePassword(options.OpenSubtitlesPasswordHash);
var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 8584bd3dd..285ff4ba5 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration
public bool EnableThrottling { get; set; }
public int ThrottleDelaySeconds { get; set; }
public string HardwareAccelerationType { get; set; }
+ /// <summary>
+ /// FFmpeg path as set by the user via the UI
+ /// </summary>
public string EncoderAppPath { get; set; }
+ /// <summary>
+ /// The current FFmpeg path being used by the system and displayed on the transcode page
+ /// </summary>
+ public string EncoderAppPathDisplay { get; set; }
public string VaapiDevice { get; set; }
public int H264Crf { get; set; }
public string H264Preset { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index ed5800329..0ba36b4b9 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration
public string[] LocalNetworkSubnets { get; set; }
public string[] LocalNetworkAddresses { get; set; }
public string[] CodecsUsed { get; set; }
+ public bool IgnoreVirtualInterfaces { get; set; }
public bool EnableExternalContentInSuggestions { get; set; }
public bool RequireHttps { get; set; }
public bool IsBehindProxy { get; set; }
@@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration
CodecsUsed = Array.Empty<string>();
ImageExtractionTimeoutMs = 0;
PathSubstitutions = Array.Empty<PathSubstitution>();
+ IgnoreVirtualInterfaces = false;
EnableSimpleArtistDetection = true;
DisplaySpecialsWithinSeasons = true;
diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
index b027d2ad0..5988112c2 100644
--- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
+++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Collections.Generic;
namespace MediaBrowser.Model.Cryptography
{
@@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
byte[] ComputeMD5(Stream str);
byte[] ComputeMD5(byte[] bytes);
byte[] ComputeSHA1(byte[] bytes);
+ IEnumerable<string> GetSupportedHashMethods();
+ byte[] ComputeHash(string HashMethod, byte[] bytes);
+ byte[] ComputeHashWithDefaultMethod(byte[] bytes);
+ byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt);
+ byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
+ byte[] ComputeHash(PasswordHash hash);
+ byte[] GenerateSalt();
+ string DefaultHashMethod { get; }
}
}
diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs
new file mode 100644
index 000000000..a9d0f6744
--- /dev/null
+++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs
@@ -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;
+ }
+ }
+
+}
diff --git a/MediaBrowser.Model/Net/IpAddressInfo.cs b/MediaBrowser.Model/Net/IpAddressInfo.cs
index 7a278d4d4..87fa55bca 100644
--- a/MediaBrowser.Model/Net/IpAddressInfo.cs
+++ b/MediaBrowser.Model/Net/IpAddressInfo.cs
@@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Net
public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
public string Address { get; set; }
+ public IpAddressInfo SubnetMask { get; set; }
public IpAddressFamily AddressFamily { get; set; }
public IpAddressInfo(string address, IpAddressFamily addressFamily)
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index 581a1069c..6482f2c84 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -5,6 +5,21 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Model.System
{
/// <summary>
+ /// Enum describing the location of the FFmpeg tool.
+ /// </summary>
+ public enum FFmpegLocation
+ {
+ /// <summary>No path to FFmpeg found.</summary>
+ NotFound,
+ /// <summary>Path supplied via command line using switch --ffmpeg.</summary>
+ SetByArgument,
+ /// <summary>User has supplied path via Transcoding UI page.</summary>
+ Custom,
+ /// <summary>FFmpeg tool found on system $PATH.</summary>
+ System
+ };
+
+ /// <summary>
/// Class SystemInfo
/// </summary>
public class SystemInfo : PublicSystemInfo
@@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
public bool HasUpdateAvailable { get; set; }
- public string EncoderLocationType { get; set; }
+ public FFmpegLocation EncoderLocation { get; set; }
public Architecture SystemArchitecture { get; set; }
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 23805b79f..27ce23778 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users
public UserPolicy()
{
- EnableContentDeletion = true;
+ EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
EnableSyncTranscoding = true;
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 77028e526..f0716f201 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
localImagesFailed = true;
- if (!(item is IItemByName))
- {
- Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
- }
+ Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
}
var metadataResult = new MetadataResult<TItemType>
diff --git a/README.md b/README.md
index d869c8978..1f635bdd2 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
<a href="https://github.com/jellyfin/jellyfin"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin.svg"/></a>
<a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a>
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
-<a href="https://cloud.drone.io/jellyfin/jellyfin"><img alt="Build Status" src="https://cloud.drone.io/api/badges/jellyfin/jellyfin/status.svg"/></a>
+<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"><img alt="Azure DevOps builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"></a>
<a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a>
</br>
<a href="https://opencollective.com/jellyfin"><img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/></a>
diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs
index ef75f997f..c99d684a1 100644
--- a/RSSDP/ISsdpCommunicationsServer.cs
+++ b/RSSDP/ISsdpCommunicationsServer.cs
@@ -45,8 +45,8 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Sends a message to the SSDP multicast address and port.
/// </summary>
- Task SendMulticastMessage(string message, CancellationToken cancellationToken);
- Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken);
+ Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
+ Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
#endregion
@@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure
#endregion
}
-} \ No newline at end of file
+}
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index f06d4687b..456a93aa8 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -3,6 +3,7 @@
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
</ItemGroup>
<PropertyGroup>
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index 04e76ef59..d9a4b6ac0 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Configuration;
namespace Rssdp.Infrastructure
{
@@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure
private readonly ILogger _logger;
private ISocketFactory _SocketFactory;
private readonly INetworkManager _networkManager;
+ private readonly IServerConfigurationManager _config;
private int _LocalPort;
private int _MulticastTtl;
@@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure
/// Minimum constructor.
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
- public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
+ public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory,
+ INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
{
+ _config = config;
}
/// <summary>
@@ -236,15 +240,15 @@ namespace Rssdp.Infrastructure
}
}
- public Task SendMulticastMessage(string message, CancellationToken cancellationToken)
+ public Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
{
- return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken);
+ return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken);
}
/// <summary>
/// Sends a message to the SSDP multicast address and port.
/// </summary>
- public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken)
+ public async Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
{
if (message == null) throw new ArgumentNullException(nameof(message));
@@ -264,7 +268,7 @@ namespace Rssdp.Infrastructure
IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
Port = SsdpConstants.MulticastPort
- }, cancellationToken).ConfigureAwait(false);
+ }, fromLocalIpAddress, cancellationToken).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
@@ -332,14 +336,15 @@ namespace Rssdp.Infrastructure
#region Private Methods
- private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken)
+ private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
{
var sockets = _sendSockets;
if (sockets != null)
{
sockets = sockets.ToList();
- var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
+ var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress)))
+ .Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
return Task.WhenAll(tasks);
}
@@ -363,11 +368,11 @@ namespace Rssdp.Infrastructure
if (_enableMultiSocketBinding)
{
- foreach (var address in _networkManager.GetLocalIpAddresses())
+ foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces))
{
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
{
- // Not supported ?
+ // Not support IPv6 right now
continue;
}
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index 128bdfcbb..e17e14c1a 100644
--- a/RSSDP/SsdpDeviceLocator.cs
+++ b/RSSDP/SsdpDeviceLocator.cs
@@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
- return _CommunicationsServer.SendMulticastMessage(message, cancellationToken);
+ return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
}
private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index ce64ba117..921f33c21 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
+using MediaBrowser.Common.Net;
using Rssdp;
namespace Rssdp.Infrastructure
@@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure
/// </summary>
public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
{
+ private readonly INetworkManager _networkManager;
private ISsdpCommunicationsServer _CommsServer;
private string _OSName;
private string _OSVersion;
+ private bool _sendOnlyMatchedHost;
private bool _SupportPnpRootDevice;
@@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Default constructor.
/// </summary>
- public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion)
+ public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager,
+ string osName, string osVersion, bool sendOnlyMatchedHost)
{
if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer));
+ if (networkManager == null) throw new ArgumentNullException(nameof(networkManager));
if (osName == null) throw new ArgumentNullException(nameof(osName));
if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName));
if (osVersion == null) throw new ArgumentNullException(nameof(osVersion));
@@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
_Random = new Random();
+ _networkManager = networkManager;
_CommsServer = communicationsServer;
_CommsServer.RequestReceived += CommsServer_RequestReceived;
_OSName = osName;
_OSVersion = osVersion;
+ _sendOnlyMatchedHost = sendOnlyMatchedHost;
_CommsServer.BeginListeningForBroadcasts();
}
@@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure
foreach (var device in deviceList)
{
- SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
+ if (!_sendOnlyMatchedHost ||
+ _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.IpAddress, device.ToRootDevice().SubnetMask))
+ {
+ SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
+ }
}
}
else
@@ -427,7 +438,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
- _CommsServer.SendMulticastMessage(message, cancellationToken);
+ _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken);
//WriteTrace(String.Format("Sent alive notification"), device);
}
@@ -472,7 +483,7 @@ namespace Rssdp.Infrastructure
var sendCount = IsDisposed ? 1 : 3;
WriteTrace(String.Format("Sent byebye notification"), device);
- return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken);
+ return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
private void DisposeRebroadcastTimer()
diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs
index a2b0f60f5..d918b9040 100644
--- a/RSSDP/SsdpRootDevice.cs
+++ b/RSSDP/SsdpRootDevice.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text;
using System.Xml;
using Rssdp.Infrastructure;
+using MediaBrowser.Model.Net;
namespace Rssdp
{
@@ -52,6 +53,15 @@ namespace Rssdp
/// </summary>
public Uri Location { get; set; }
+ /// <summary>
+ /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required.
+ /// </summary>
+ public IpAddressInfo Address { get; set; }
+
+ /// <summary>
+ /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required.
+ /// </summary>
+ public IpAddressInfo SubnetMask { get; set; }
/// <summary>
/// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.
diff --git a/SharedVersion.cs b/SharedVersion.cs
index 41eda393a..785ba9301 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.2.1")]
-[assembly: AssemblyFileVersion("10.2.1")]
+[assembly: AssemblyVersion("10.2.2")]
+[assembly: AssemblyFileVersion("10.2.2")]
diff --git a/build b/build
index 3b4167dae..51d4b79a2 100755
--- a/build
+++ b/build
@@ -26,7 +26,7 @@ usage() {
echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
echo -e ""
echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
- echo -e "The web_branch defaults to the same branch name as the current main branch."
+ echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
echo -e "To build all platforms, use 'all'."
echo -e "To perform all build actions, use 'all'."
echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
@@ -164,37 +164,39 @@ for target_platform in ${platform[@]}; do
fi
done
-# Initialize submodules
-git submodule update --init --recursive
+if [[ ${web_branch} != 'local' ]]; then
+ # Initialize submodules
+ git submodule update --init --recursive
-# configure branch
-pushd MediaBrowser.WebDashboard/jellyfin-web
+ # configure branch
+ pushd MediaBrowser.WebDashboard/jellyfin-web
-if ! git diff-index --quiet HEAD --; then
- 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."
+ 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
- }
-# Otherwise, just check out the local branch (for testing, etc.)
-else
- git checkout ${web_branch} || {
- echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid."
- exit 1
- }
+ fi
+
+ git 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
fi
-popd
# Execute each platform and action in order, if said action is enabled
pushd deployment/
@@ -217,7 +219,7 @@ for target_platform in ${platform[@]}; do
done
if [[ -d pkg-dist/ ]]; then
echo -e ">> Collecting build artifacts"
- target_dir="../../../jellyfin-build/${target_platform}"
+ target_dir="../../../bin/${target_platform}"
mkdir -p ${target_dir}
mv pkg-dist/* ${target_dir}/
fi
diff --git a/build.yaml b/build.yaml
new file mode 100644
index 000000000..b0d2502d5
--- /dev/null
+++ b/build.yaml
@@ -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
diff --git a/deployment/common.build.sh b/deployment/common.build.sh
index c191ec2a1..d028e3a66 100755
--- a/deployment/common.build.sh
+++ b/deployment/common.build.sh
@@ -15,7 +15,6 @@ DEFAULT_CONFIG="Release"
DEFAULT_OUTPUT_DIR="dist/jellyfin-git"
DEFAULT_PKG_DIR="pkg-dist"
DEFAULT_DOCKERFILE="Dockerfile"
-DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD`
DEFAULT_ARCHIVE_CMD="tar -xvzf"
# Parse the version from the AssemblyVersion
@@ -36,9 +35,9 @@ build_jellyfin()
echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
if [[ $DOTNETRUNTIME == 'framework' ]]; then
- dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}"
+ dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
else
- dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME}
+ dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
fi
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
@@ -53,7 +52,7 @@ build_jellyfin_docker()
(
BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
- IMAGE_TAG=${3-$DEFAULT_IMAGE_TAG}
+ IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"}
echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}"
docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT}
diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/debian-package-armhf/Dockerfile.amd64
new file mode 100644
index 000000000..0d62352e0
--- /dev/null
+++ b/deployment/debian-package-armhf/Dockerfile.amd64
@@ -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"]
diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf
new file mode 100644
index 000000000..eb4152116
--- /dev/null
+++ b/deployment/debian-package-armhf/Dockerfile.armhf
@@ -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"]
diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh
new file mode 100755
index 000000000..3898110af
--- /dev/null
+++ b/deployment/debian-package-armhf/clean.sh
@@ -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
diff --git a/deployment/debian-package-armhf/dependencies.txt b/deployment/debian-package-armhf/dependencies.txt
new file mode 100644
index 000000000..bdb967096
--- /dev/null
+++ b/deployment/debian-package-armhf/dependencies.txt
@@ -0,0 +1 @@
+docker
diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh
new file mode 100755
index 000000000..45e68f0c6
--- /dev/null
+++ b/deployment/debian-package-armhf/docker-build.sh
@@ -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/
diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh
new file mode 100755
index 000000000..0ec0dc95c
--- /dev/null
+++ b/deployment/debian-package-armhf/package.sh
@@ -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}"
diff --git a/deployment/debian-package-armhf/pkg-src b/deployment/debian-package-armhf/pkg-src
new file mode 120000
index 000000000..4c695fea1
--- /dev/null
+++ b/deployment/debian-package-armhf/pkg-src
@@ -0,0 +1 @@
+../debian-package-x64/pkg-src \ No newline at end of file
diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog
index 7b7efff27..349e8787f 100644
--- a/deployment/debian-package-x64/pkg-src/changelog
+++ b/deployment/debian-package-x64/pkg-src/changelog
@@ -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:
diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin
index b052b2ec6..58fe79332 100644
--- a/deployment/debian-package-x64/pkg-src/conf/jellyfin
+++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin
@@ -21,9 +21,9 @@ JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin"
# Restart script for in-app server control
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
-# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
-#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
-#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe"
+# ffmpeg binary paths, overriding the system values
+JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg"
+JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe"
# [OPTIONAL] run Jellyfin as a headless service
#JELLYFIN_SERVICE_OPT="--service"
diff --git a/deployment/debian-package-x64/pkg-src/control b/deployment/debian-package-x64/pkg-src/control
index 88d10438b..d96660590 100644
--- a/deployment/debian-package-x64/pkg-src/control
+++ b/deployment/debian-package-x64/pkg-src/control
@@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Architecture: any
Depends: at,
libsqlite3-0,
- ffmpeg (<7:4.1) | jellyfin-ffmpeg,
+ jellyfin-ffmpeg,
libfontconfig1,
libfreetype6,
libssl1.0.0 | libssl1.0.2
diff --git a/deployment/debian-package-x64/pkg-src/rules b/deployment/debian-package-x64/pkg-src/rules
index ce98cb8f8..62f75bc6b 100644
--- a/deployment/debian-package-x64/pkg-src/rules
+++ b/deployment/debian-package-x64/pkg-src/rules
@@ -2,7 +2,23 @@
CONFIG := Release
TERM := xterm
SHELL := /bin/bash
-DOTNETRUNTIME := debian-x64
+
+HOST_ARCH := $(shell arch)
+BUILD_ARCH := ${DEB_HOST_MULTIARCH}
+ifeq ($(HOST_ARCH),x86_64)
+ ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
+ # Cross-building ARM on AMD64
+ DOTNETRUNTIME := debian-arm
+ else
+ # Building AMD64
+ DOTNETRUNTIME := debian-x64
+ endif
+endif
+ifeq ($(HOST_ARCH),armv7l)
+ # Building ARM
+ DOTNETRUNTIME := debian-arm
+endif
+
export DH_VERBOSE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
@@ -16,7 +32,8 @@ override_dh_auto_test:
override_dh_clistrip:
override_dh_auto_build:
- dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server
+ dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
+ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
override_dh_auto_clean:
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile
index 8bb1d527d..397c944ea 100644
--- a/deployment/fedora-package-x64/Dockerfile
+++ b/deployment/fedora-package-x64/Dockerfile
@@ -13,7 +13,7 @@ RUN dnf update -y \
&& dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
&& dnf copr enable -y @dotnet-sig/dotnet \
&& rpmdev-setuptree \
- && dnf install -y dotnet-sdk-${SDK_VERSION} \
+ && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \
&& ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
&& mkdir -p ${SOURCE_DIR}/SPECS \
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec
index 146486428..e24bd2fcb 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec
+++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec
@@ -7,7 +7,7 @@
%endif
Name: jellyfin
-Version: 10.2.1
+Version: 10.2.2
Release: 1%{?dist}
Summary: The Free Software Media Browser
License: GPLv2
@@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet
-BuildRequires: dotnet-sdk-2.2
+BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2
# RPMfusion free
Requires: ffmpeg
@@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an
%install
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
-dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} Jellyfin.Server
+dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
+ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
%{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
@@ -73,7 +74,6 @@ EOF
%{_libdir}/%{name}/jellyfin-web/*
%attr(755,root,root) %{_bindir}/%{name}
%{_libdir}/%{name}/*.json
-%{_libdir}/%{name}/*.pdb
%{_libdir}/%{name}/*.dll
%{_libdir}/%{name}/*.so
%{_libdir}/%{name}/*.a
@@ -140,6 +140,19 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
+* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
+- jellyfin:
+- PR968 Release 10.2.z copr autobuild
+- PR964 Install the dotnet runtime package in Fedora build
+- PR979 Build Package releases without debug turned on
+- PR990 Fix slow local image validation
+- PR991 Fix the ffmpeg compatibility
+- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
+- PR998 Set EnableRaisingEvents to true for processes that require it
+- PR1017 Set ffmpeg+ffprobe paths in Docker container
+- jellyfin-web:
+- PR152 Go back on Media stop
+- PR156 Fix volume slider not working on nowplayingbar
* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- jellyfin:
- PR920 Fix cachedir missing from Docker container
diff --git a/deployment/win-x64/package.sh b/deployment/win-x64/package.sh
index d21e3b532..b438c28e4 100755
--- a/deployment/win-x64/package.sh
+++ b/deployment/win-x64/package.sh
@@ -21,8 +21,8 @@ package_win64() (
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
rm -r ${TEMP_DIR}
- cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
- cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
+ cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
+ cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
mkdir -p ${PKG_DIR}
pushd ${OUTPUT_DIR}
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
diff --git a/deployment/win-x86/package.sh b/deployment/win-x86/package.sh
index 3cc4eb623..8752d92a8 100755
--- a/deployment/win-x86/package.sh
+++ b/deployment/win-x86/package.sh
@@ -20,8 +20,8 @@ package_win32() (
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
rm -r ${TEMP_DIR}
- cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
- cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
+ cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
+ cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
mkdir -p ${PKG_DIR}
pushd ${OUTPUT_DIR}
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .