2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'remotes/upstream/master' into kestrel_poc

Claus Vium 6 жил өмнө
parent
commit
0abe57e930
94 өөрчлөгдсөн 1757 нэмэгдсэн , 1228 устгасан
  1. 183 0
      .ci/azure-pipelines.yml
  2. 3 0
      CONTRIBUTORS.md
  3. 13 5
      Dockerfile
  4. 13 6
      Dockerfile.arm
  5. 13 6
      Dockerfile.arm64
  6. 8 8
      DvdLib/Ifo/Dvd.cs
  7. 2 0
      Emby.Dlna/Configuration/DlnaOptions.cs
  8. 2 2
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  9. 1 2
      Emby.Dlna/Didl/DidlBuilder.cs
  10. 11 10
      Emby.Dlna/DlnaManager.cs
  11. 12 9
      Emby.Dlna/Main/DlnaEntryPoint.cs
  12. 19 0
      Emby.Dlna/PlayTo/TransportCommands.cs
  13. 20 12
      Emby.Naming/TV/EpisodePathParser.cs
  14. 6 1
      Emby.Server.Implementations/Activity/ActivityManager.cs
  15. 17 88
      Emby.Server.Implementations/ApplicationHost.cs
  16. 3 8
      Emby.Server.Implementations/Channels/ChannelManager.cs
  17. 18 30
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  18. 129 0
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  19. 3 4
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  20. 2 2
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  21. 34 0
      Emby.Server.Implementations/Data/SqliteUserRepository.cs
  22. 2 13
      Emby.Server.Implementations/Dto/DtoService.cs
  23. 0 24
      Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs
  24. 0 17
      Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs
  25. 0 132
      Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs
  26. 120 21
      Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
  27. 14 23
      Emby.Server.Implementations/Library/UserManager.cs
  28. 24 128
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  29. 8 0
      Emby.Server.Implementations/Localization/Ratings/au.csv
  30. 6 0
      Emby.Server.Implementations/Localization/Ratings/be.csv
  31. 10 0
      Emby.Server.Implementations/Localization/Ratings/de.csv
  32. 5 0
      Emby.Server.Implementations/Localization/Ratings/ru.csv
  33. 66 6
      Emby.Server.Implementations/Networking/NetworkManager.cs
  34. 0 57
      Emby.Server.Implementations/Security/EncryptionManager.cs
  35. 66 95
      Emby.Server.Implementations/Services/ServicePath.cs
  36. 25 27
      Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
  37. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs
  38. 20 19
      Emby.Server.Implementations/SocketSharp/RequestMono.cs
  39. 40 22
      Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
  40. 24 3
      Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs
  41. 1 1
      Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
  42. 1 1
      Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
  43. 1 1
      Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
  44. 2 2
      Jellyfin.Server/StartupOptions.cs
  45. 3 10
      MediaBrowser.Api/BaseApiService.cs
  46. 2 1
      MediaBrowser.Api/FilterService.cs
  47. 1 0
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  48. 13 22
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  49. 4 1
      MediaBrowser.Common/Net/INetworkManager.cs
  50. 2 12
      MediaBrowser.Controller/Dto/DtoOptions.cs
  51. 1 3
      MediaBrowser.Controller/Dto/IDtoService.cs
  52. 3 21
      MediaBrowser.Controller/Entities/Folder.cs
  53. 1 1
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  54. 3 2
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  55. 6 5
      MediaBrowser.Controller/MediaEncoding/JobLogger.cs
  56. 0 19
      MediaBrowser.Controller/Security/IEncryptionManager.cs
  57. 7 0
      MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
  58. 105 8
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  59. 131 254
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  60. 10 9
      MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs
  61. 7 0
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  62. 2 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  63. 9 0
      MediaBrowser.Model/Cryptography/ICryptoProvider.cs
  64. 153 0
      MediaBrowser.Model/Cryptography/PasswordHash.cs
  65. 1 0
      MediaBrowser.Model/Net/IpAddressInfo.cs
  66. 16 1
      MediaBrowser.Model/System/SystemInfo.cs
  67. 1 1
      MediaBrowser.Model/Users/UserPolicy.cs
  68. 1 4
      MediaBrowser.Providers/Manager/MetadataService.cs
  69. 1 1
      README.md
  70. 3 3
      RSSDP/ISsdpCommunicationsServer.cs
  71. 1 0
      RSSDP/RSSDP.csproj
  72. 14 9
      RSSDP/SsdpCommunicationsServer.cs
  73. 1 1
      RSSDP/SsdpDeviceLocator.cs
  74. 15 4
      RSSDP/SsdpDevicePublisher.cs
  75. 10 0
      RSSDP/SsdpRootDevice.cs
  76. 2 2
      SharedVersion.cs
  77. 31 29
      build
  78. 15 0
      build.yaml
  79. 3 4
      deployment/common.build.sh
  80. 42 0
      deployment/debian-package-armhf/Dockerfile.amd64
  81. 34 0
      deployment/debian-package-armhf/Dockerfile.armhf
  82. 29 0
      deployment/debian-package-armhf/clean.sh
  83. 1 0
      deployment/debian-package-armhf/dependencies.txt
  84. 20 0
      deployment/debian-package-armhf/docker-build.sh
  85. 42 0
      deployment/debian-package-armhf/package.sh
  86. 1 0
      deployment/debian-package-armhf/pkg-src
  87. 17 0
      deployment/debian-package-x64/pkg-src/changelog
  88. 3 3
      deployment/debian-package-x64/pkg-src/conf/jellyfin
  89. 1 1
      deployment/debian-package-x64/pkg-src/control
  90. 19 2
      deployment/debian-package-x64/pkg-src/rules
  91. 1 1
      deployment/fedora-package-x64/Dockerfile
  92. 17 4
      deployment/fedora-package-x64/pkg-src/jellyfin.spec
  93. 2 2
      deployment/win-x64/package.sh
  94. 2 2
      deployment/win-x86/package.sh

+ 183 - 0
.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
+
+

+ 3 - 0
CONTRIBUTORS.md

@@ -21,6 +21,9 @@
  - [WillWill56](https://github.com/WillWill56)
  - [WillWill56](https://github.com/WillWill56)
  - [Liggy](https://github.com/Liggy)
  - [Liggy](https://github.com/Liggy)
  - [fruhnow](https://github.com/fruhnow)
  - [fruhnow](https://github.com/fruhnow)
+ - [Lynxy](https://github.com/Lynxy)
+ - [fasheng](https://github.com/fasheng)
+ - [ploughpuff](https://github.com/ploughpuff) 
 
 
 # Emby Contributors
 # Emby Contributors
 
 

+ 13 - 5
Dockerfile

@@ -4,10 +4,8 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
 WORKDIR /repo
 WORKDIR /repo
 COPY . .
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-RUN dotnet publish \
-    --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 jellyfin/ffmpeg as ffmpeg
 FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
 FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
@@ -22,6 +20,16 @@ RUN apt-get update \
  && chmod 777 /cache /config /media
  && chmod 777 /cache /config /media
 COPY --from=ffmpeg / /
 COPY --from=ffmpeg / /
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=builder /jellyfin /jellyfin
+
+ARG JELLYFIN_WEB_VERSION=10.2.2
+RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && rm -rf /jellyfin/jellyfin-web \
+ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+
 EXPOSE 8096
 EXPOSE 8096
 VOLUME /cache /config /media
 VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+    --datadir /config \
+    --cachedir /cache \
+    --ffmpeg /usr/local/bin/ffmpeg \
+    --ffprobe /usr/local/bin/ffprobe

+ 13 - 6
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
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
 # Build
-RUN dotnet publish \
-    -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
 FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
@@ -31,6 +28,16 @@ RUN apt-get update \
  && mkdir -p /cache /config /media \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media
  && chmod 777 /cache /config /media
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=builder /jellyfin /jellyfin
+
+ARG JELLYFIN_WEB_VERSION=10.2.2
+RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && rm -rf /jellyfin/jellyfin-web \
+ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+
 EXPOSE 8096
 EXPOSE 8096
 VOLUME /cache /config /media
 VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+    --datadir /config \
+    --cachedir /cache \
+    --ffmpeg /usr/bin/ffmpeg \
+    --ffprobe /usr/bin/ffprobe

+ 13 - 6
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
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
 # Build
-RUN dotnet publish \
-    -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
 FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
@@ -32,6 +29,16 @@ RUN apt-get update \
  && mkdir -p /cache /config /media \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media
  && chmod 777 /cache /config /media
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=builder /jellyfin /jellyfin
+
+ARG JELLYFIN_WEB_VERSION=10.2.2
+RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && rm -rf /jellyfin/jellyfin-web \
+ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+
 EXPOSE 8096
 EXPOSE 8096
 VOLUME /cache /config /media
 VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+    --datadir /config \
+    --cachedir /cache \
+    --ffmpeg /usr/bin/ffmpeg \
+    --ffprobe /usr/bin/ffprobe

+ 8 - 8
DvdLib/Ifo/Dvd.cs

@@ -26,17 +26,17 @@ namespace DvdLib.Ifo
 
 
             if (vmgPath == null)
             if (vmgPath == null)
             {
             {
-                var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase));
-
-                foreach (var ifo in 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);
                         ReadVTS(ifoNumber, ifo.FullName);
-                        numbersRead.Add(ifoNumber);
                     }
                     }
                 }
                 }
             }
             }
@@ -76,7 +76,7 @@ namespace DvdLib.Ifo
             }
             }
         }
         }
 
 
-        private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles)
+        private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
         {
         {
             var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
             var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
 
 

+ 2 - 0
Emby.Dlna/Configuration/DlnaOptions.cs

@@ -7,6 +7,7 @@ namespace Emby.Dlna.Configuration
         public bool EnableServer { get; set; }
         public bool EnableServer { get; set; }
         public bool EnableDebugLog { get; set; }
         public bool EnableDebugLog { get; set; }
         public bool BlastAliveMessages { get; set; }
         public bool BlastAliveMessages { get; set; }
+        public bool SendOnlyMatchedHost { get; set; }
         public int ClientDiscoveryIntervalSeconds { get; set; }
         public int ClientDiscoveryIntervalSeconds { get; set; }
         public int BlastAliveMessageIntervalSeconds { get; set; }
         public int BlastAliveMessageIntervalSeconds { get; set; }
         public string DefaultUserId { get; set; }
         public string DefaultUserId { get; set; }
@@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration
             EnablePlayTo = true;
             EnablePlayTo = true;
             EnableServer = true;
             EnableServer = true;
             BlastAliveMessages = true;
             BlastAliveMessages = true;
+            SendOnlyMatchedHost = true;
             ClientDiscoveryIntervalSeconds = 60;
             ClientDiscoveryIntervalSeconds = 60;
             BlastAliveMessageIntervalSeconds = 1800;
             BlastAliveMessageIntervalSeconds = 1800;
         }
         }

+ 2 - 2
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory
 
 
                     if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
                     if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
                     {
                     {
-                        var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
+                        var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
 
 
                         _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
                         _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
                     }
                     }
@@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory
                 }
                 }
                 else
                 else
                 {
                 {
-                    var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
+                    var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
                     totalCount = childrenResult.TotalRecordCount;
                     totalCount = childrenResult.TotalRecordCount;
 
 
                     provided = childrenResult.Items.Length;
                     provided = childrenResult.Items.Length;

+ 1 - 2
Emby.Dlna/Didl/DidlBuilder.cs

@@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl
         {
         {
             AddCommonFields(item, itemStubType, context, writer, filter);
             AddCommonFields(item, itemStubType, context, writer, filter);
 
 
-            var hasArtists = item as IHasArtist;
             var hasAlbumArtists = item as IHasAlbumArtist;
             var hasAlbumArtists = item as IHasAlbumArtist;
 
 
-            if (hasArtists != null)
+            if (item is IHasArtist hasArtists)
             {
             {
                 foreach (var artist in hasArtists.Artists)
                 foreach (var artist in hasArtists.Artists)
                 {
                 {

+ 11 - 10
Emby.Dlna/DlnaManager.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Reflection;
 using System.Text;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Reflection;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -31,7 +31,7 @@ namespace Emby.Dlna
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
-        private readonly IAssemblyInfo _assemblyInfo;
+        private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
 
 
         private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
         private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
 
 
@@ -41,8 +41,7 @@ namespace Emby.Dlna
             IApplicationPaths appPaths,
             IApplicationPaths appPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            IServerApplicationHost appHost,
-            IAssemblyInfo assemblyInfo)
+            IServerApplicationHost appHost)
         {
         {
             _xmlSerializer = xmlSerializer;
             _xmlSerializer = xmlSerializer;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -50,7 +49,6 @@ namespace Emby.Dlna
             _logger = loggerFactory.CreateLogger("Dlna");
             _logger = loggerFactory.CreateLogger("Dlna");
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _appHost = appHost;
             _appHost = appHost;
-            _assemblyInfo = assemblyInfo;
         }
         }
 
 
         public async Task InitProfilesAsync()
         public async Task InitProfilesAsync()
@@ -367,15 +365,18 @@ namespace Emby.Dlna
 
 
             var systemProfilesPath = SystemProfilesPath;
             var systemProfilesPath = SystemProfilesPath;
 
 
-            foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType())
-                .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 filename = Path.GetFileName(name).Substring(namespaceName.Length);
 
 
                 var path = Path.Combine(systemProfilesPath, filename);
                 var path = Path.Combine(systemProfilesPath, filename);
 
 
-                using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name))
+                using (var stream = _assembly.GetManifestResourceStream(name))
                 {
                 {
                     var fileInfo = _fileSystem.GetFileInfo(path);
                     var fileInfo = _fileSystem.GetFileInfo(path);
 
 
@@ -513,7 +514,7 @@ namespace Emby.Dlna
             return new ImageStream
             return new ImageStream
             {
             {
                 Format = format,
                 Format = format,
-                Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource)
+                Stream = _assembly.GetManifestResourceStream(resource)
             };
             };
         }
         }
     }
     }

+ 12 - 9
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -169,9 +169,10 @@ namespace Emby.Dlna.Main
             {
             {
                 if (_communicationsServer == null)
                 if (_communicationsServer == null)
                 {
                 {
-                    var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
+                    var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows ||
+                                                   _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux;
 
 
-                    _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
+                    _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
                     {
                     {
                         IsShared = true
                         IsShared = true
                     };
                     };
@@ -229,7 +230,7 @@ namespace Emby.Dlna.Main
 
 
             try
             try
             {
             {
-                _Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion);
+                _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
                 _Publisher.LogFunction = LogMessage;
                 _Publisher.LogFunction = LogMessage;
                 _Publisher.SupportPnpRootDevice = false;
                 _Publisher.SupportPnpRootDevice = false;
 
 
@@ -245,17 +246,17 @@ namespace Emby.Dlna.Main
 
 
         private async Task RegisterServerEndpoints()
         private async Task RegisterServerEndpoints()
         {
         {
-            var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList();
+            var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
 
 
             var udn = CreateUuid(_appHost.SystemId);
             var udn = CreateUuid(_appHost.SystemId);
 
 
             foreach (var address in addresses)
             foreach (var address in addresses)
             {
             {
-                // TODO: Remove this condition on platforms that support it
-                //if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
-                //{
-                //    continue;
-                //}
+                if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
+                {
+                   // Not support IPv6 right now
+                   continue;
+                }
 
 
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
 
@@ -268,6 +269,8 @@ namespace Emby.Dlna.Main
                 {
                 {
                     CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
                     CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
+                    Address = address,
+                    SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
                     FriendlyName = "Jellyfin",
                     FriendlyName = "Jellyfin",
                     Manufacturer = "Jellyfin",
                     Manufacturer = "Jellyfin",
                     ModelName = "Jellyfin Server",
                     ModelName = "Jellyfin Server",

+ 19 - 0
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo
             foreach (var arg in action.ArgumentList)
             foreach (var arg in action.ArgumentList)
             {
             {
                 if (arg.Direction == "out")
                 if (arg.Direction == "out")
+                {
                     continue;
                     continue;
+                }
 
 
                 if (arg.Name == "InstanceID")
                 if (arg.Name == "InstanceID")
+                {
                     stateString += BuildArgumentXml(arg, "0");
                     stateString += BuildArgumentXml(arg, "0");
+                }
                 else
                 else
+                {
                     stateString += BuildArgumentXml(arg, null);
                     stateString += BuildArgumentXml(arg, null);
+                }
             }
             }
 
 
             return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
             return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
@@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo
             foreach (var arg in action.ArgumentList)
             foreach (var arg in action.ArgumentList)
             {
             {
                 if (arg.Direction == "out")
                 if (arg.Direction == "out")
+                {
                     continue;
                     continue;
+                }
+
                 if (arg.Name == "InstanceID")
                 if (arg.Name == "InstanceID")
+                {
                     stateString += BuildArgumentXml(arg, "0");
                     stateString += BuildArgumentXml(arg, "0");
+                }
                 else
                 else
+                {
                     stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
                     stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
+                }
             }
             }
 
 
             return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
             return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
@@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo
             foreach (var arg in action.ArgumentList)
             foreach (var arg in action.ArgumentList)
             {
             {
                 if (arg.Name == "InstanceID")
                 if (arg.Name == "InstanceID")
+                {
                     stateString += BuildArgumentXml(arg, "0");
                     stateString += BuildArgumentXml(arg, "0");
+                }
                 else if (dictionary.ContainsKey(arg.Name))
                 else if (dictionary.ContainsKey(arg.Name))
+                {
                     stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
                     stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
+                }
                 else
                 else
+                {
                     stateString += BuildArgumentXml(arg, value.ToString());
                     stateString += BuildArgumentXml(arg, value.ToString());
+                }
             }
             }
 
 
             return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
             return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);

+ 20 - 12
Emby.Naming/TV/EpisodePathParser.cs

@@ -2,7 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
-using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 
 
 namespace Emby.Naming.TV
 namespace Emby.Naming.TV
@@ -22,7 +21,9 @@ namespace Emby.Naming.TV
             // There were no failed tests without this block, but to be safe, we can keep it until
             // There were no failed tests without this block, but to be safe, we can keep it until
             // the regex which require file extensions are modified so that they don't need them.
             // the regex which require file extensions are modified so that they don't need them.
             if (IsDirectory)
             if (IsDirectory)
+            {
                 path += ".mp4";
                 path += ".mp4";
+            }
 
 
             EpisodePathParserResult result = null;
             EpisodePathParserResult result = null;
 
 
@@ -35,6 +36,7 @@ namespace Emby.Naming.TV
                         continue;
                         continue;
                     }
                     }
                 }
                 }
+
                 if (isNamed.HasValue)
                 if (isNamed.HasValue)
                 {
                 {
                     if (expression.IsNamed != isNamed.Value)
                     if (expression.IsNamed != isNamed.Value)
@@ -42,6 +44,7 @@ namespace Emby.Naming.TV
                         continue;
                         continue;
                     }
                     }
                 }
                 }
+
                 if (isOptimistic.HasValue)
                 if (isOptimistic.HasValue)
                 {
                 {
                     if (expression.IsOptimistic != isOptimistic.Value)
                     if (expression.IsOptimistic != isOptimistic.Value)
@@ -191,13 +194,20 @@ namespace Emby.Naming.TV
 
 
         private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
         private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
         {
         {
-            var results = expressions
-                .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))
                 if (string.IsNullOrEmpty(info.SeriesName))
                 {
                 {
                     info.SeriesName = result.SeriesName;
                     info.SeriesName = result.SeriesName;
@@ -208,12 +218,10 @@ namespace Emby.Naming.TV
                     info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
                     info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
                 }
                 }
 
 
-                if (!string.IsNullOrEmpty(info.SeriesName))
+                if (!string.IsNullOrEmpty(info.SeriesName)
+                    && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
                 {
                 {
-                    if (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)
-                    {
-                        break;
-                    }
+                    break;
                 }
                 }
             }
             }
         }
         }

+ 6 - 1
Emby.Server.Implementations/Activity/ActivityManager.cs

@@ -39,8 +39,13 @@ namespace Emby.Server.Implementations.Activity
         {
         {
             var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
             var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
 
 
-            foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty)))
+            foreach (var item in result.Items)
             {
             {
+                if (item.UserId == Guid.Empty)
+                {
+                    continue;
+                }
+
                 var user = _userManager.GetUserById(item.UserId);
                 var user = _userManager.GetUserById(item.UserId);
 
 
                 if (user != null)
                 if (user != null)

+ 17 - 88
Emby.Server.Implementations/ApplicationHost.cs

@@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Devices;
 using Emby.Server.Implementations.Devices;
 using Emby.Server.Implementations.Diagnostics;
 using Emby.Server.Implementations.Diagnostics;
 using Emby.Server.Implementations.Dto;
 using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.FFMpeg;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer.Security;
 using Emby.Server.Implementations.HttpServer.Security;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.IO;
@@ -541,7 +540,7 @@ namespace Emby.Server.Implementations
 
 
             ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
             ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
 
 
-            MediaEncoder.Init();
+            MediaEncoder.SetFFmpegPath();
 
 
             //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
             //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
             //{
             //{
@@ -813,10 +812,8 @@ namespace Emby.Server.Implementations
             TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
             TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
             serviceCollection.AddSingleton(TVSeriesManager);
             serviceCollection.AddSingleton(TVSeriesManager);
 
 
-            var encryptionManager = new EncryptionManager();
-            serviceCollection.AddSingleton<IEncryptionManager>(encryptionManager);
-
             DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
             DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
+
             serviceCollection.AddSingleton(DeviceManager);
             serviceCollection.AddSingleton(DeviceManager);
 
 
             MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
             MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
@@ -838,7 +835,7 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton(SessionManager);
             serviceCollection.AddSingleton(SessionManager);
 
 
             serviceCollection.AddSingleton<IDlnaManager>(
             serviceCollection.AddSingleton<IDlnaManager>(
-                new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo));
+                new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this));
 
 
             CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
             CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
             serviceCollection.AddSingleton(CollectionManager);
             serviceCollection.AddSingleton(CollectionManager);
@@ -861,7 +858,18 @@ namespace Emby.Server.Implementations
             ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
             ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
             serviceCollection.AddSingleton(ChapterManager);
             serviceCollection.AddSingleton(ChapterManager);
 
 
-            RegisterMediaEncoder(serviceCollection);
+            MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
+                LoggerFactory,
+                JsonSerializer,
+                StartupOptions.FFmpegPath,
+                StartupOptions.FFprobePath,
+                ServerConfigurationManager,
+                FileSystemManager,
+                () => SubtitleEncoder,
+                () => MediaSourceManager,
+                ProcessFactory,
+                5000);
+            serviceCollection.AddSingleton(MediaEncoder);
 
 
             EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
             EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
             serviceCollection.AddSingleton(EncodingManager);
             serviceCollection.AddSingleton(EncodingManager);
@@ -970,85 +978,6 @@ namespace Emby.Server.Implementations
             return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
             return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
         }
         }
 
 
-        protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()
-        {
-            var info = new FFMpegInstallInfo();
-
-            // Windows builds: http://ffmpeg.zeranoe.com/builds/
-            // Linux builds: http://johnvansickle.com/ffmpeg/
-            // OS X builds: http://ffmpegmac.net/
-            // OS X x64: http://www.evermeet.cx/ffmpeg/
-
-            if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux)
-            {
-                info.FFMpegFilename = "ffmpeg";
-                info.FFProbeFilename = "ffprobe";
-                info.ArchiveType = "7z";
-                info.Version = "20170308";
-            }
-            else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
-            {
-                info.FFMpegFilename = "ffmpeg.exe";
-                info.FFProbeFilename = "ffprobe.exe";
-                info.Version = "20170308";
-                info.ArchiveType = "7z";
-            }
-            else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
-            {
-                info.FFMpegFilename = "ffmpeg";
-                info.FFProbeFilename = "ffprobe";
-                info.ArchiveType = "7z";
-                info.Version = "20170308";
-            }
-
-            return info;
-        }
-
-        protected FFMpegInfo GetFFMpegInfo()
-        {
-            return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
-                .GetFFMpegInfo(StartupOptions);
-        }
-
-        /// <summary>
-        /// Registers the media encoder.
-        /// </summary>
-        /// <returns>Task.</returns>
-        private void RegisterMediaEncoder(IServiceCollection serviceCollection)
-        {
-            string encoderPath = null;
-            string probePath = null;
-
-            var info = GetFFMpegInfo();
-
-            encoderPath = info.EncoderPath;
-            probePath = info.ProbePath;
-            var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
-
-            var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
-                LoggerFactory,
-                JsonSerializer,
-                encoderPath,
-                probePath,
-                hasExternalEncoder,
-                ServerConfigurationManager,
-                FileSystemManager,
-                LiveTvManager,
-                IsoManager,
-                LibraryManager,
-                ChannelManager,
-                SessionManager,
-                () => SubtitleEncoder,
-                () => MediaSourceManager,
-                HttpClient,
-                ZipClient,
-                ProcessFactory,
-                5000);
-
-            MediaEncoder = mediaEncoder;
-            serviceCollection.AddSingleton(MediaEncoder);
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the user repository.
         /// Gets the user repository.
         /// </summary>
         /// </summary>
@@ -1481,7 +1410,7 @@ namespace Emby.Server.Implementations
                 ServerName = FriendlyName,
                 ServerName = FriendlyName,
                 LocalAddress = localAddress,
                 LocalAddress = localAddress,
                 SupportsLibraryMonitor = true,
                 SupportsLibraryMonitor = true,
-                EncoderLocationType = MediaEncoder.EncoderLocationType,
+                EncoderLocation = MediaEncoder.EncoderLocation,
                 SystemArchitecture = EnvironmentInfo.SystemArchitecture,
                 SystemArchitecture = EnvironmentInfo.SystemArchitecture,
                 SystemUpdateLevel = SystemUpdateLevel,
                 SystemUpdateLevel = SystemUpdateLevel,
                 PackageName = StartupOptions.PackageName
                 PackageName = StartupOptions.PackageName
@@ -1598,7 +1527,7 @@ namespace Emby.Server.Implementations
 
 
             if (addresses.Count == 0)
             if (addresses.Count == 0)
             {
             {
-                addresses.AddRange(NetworkManager.GetLocalIpAddresses());
+                addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
             }
             }
 
 
             var resultList = new List<IpAddressInfo>();
             var resultList = new List<IpAddressInfo>();

+ 3 - 8
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels
             {
             {
                 foreach (var item in returnItems)
                 foreach (var item in returnItems)
                 {
                 {
-                    var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None);
-                    Task.WaitAll(task);
+                    RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
                 }
                 }
             }
             }
 
 
@@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels
                 }
                 }
 
 
                 numComplete++;
                 numComplete++;
-                double percent = numComplete;
-                percent /= allChannelsList.Count;
-
+                double percent = (double)numComplete / allChannelsList.Count;
                 progress.Report(100 * percent);
                 progress.Report(100 * percent);
             }
             }
 
 
@@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels
 
 
             foreach (var item in result.Items)
             foreach (var item in result.Items)
             {
             {
-                var folder = item as Folder;
-
-                if (folder != null)
+                if (item is Folder folder)
                 {
                 {
                     await GetChannelItemsInternal(new InternalItemsQuery
                     await GetChannelItemsInternal(new InternalItemsQuery
                     {
                     {

+ 18 - 30
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels
         public static string GetUserDistinctValue(User user)
         public static string GetUserDistinctValue(User user)
         {
         {
             var channels = user.Policy.EnabledChannels
             var channels = user.Policy.EnabledChannels
-                .OrderBy(i => i)
-                .ToList();
+                .OrderBy(i => i);
 
 
-            return string.Join("|", channels.ToArray());
+            return string.Join("|", channels);
         }
         }
 
 
         private void CleanDatabase(CancellationToken cancellationToken)
         private void CleanDatabase(CancellationToken cancellationToken)
         {
         {
             var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
             var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
 
 
-            var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery
+            var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
             {
             {
-                IncludeItemTypes = new[] { typeof(Channel).Name }
+                IncludeItemTypes = new[] { typeof(Channel).Name },
+                ExcludeItemIds = installedChannelIds.ToArray()
             });
             });
 
 
-            var invalidIds = databaseIds
-                .Except(installedChannelIds)
-                .ToList();
-
-            foreach (var id in invalidIds)
+            foreach (var channel in uninstalledChannels)
             {
             {
                 cancellationToken.ThrowIfCancellationRequested();
                 cancellationToken.ThrowIfCancellationRequested();
 
 
-                CleanChannel(id, cancellationToken);
+                CleanChannel((Channel)channel, cancellationToken);
             }
             }
         }
         }
 
 
-        private void CleanChannel(Guid id, CancellationToken cancellationToken)
+        private void CleanChannel(Channel channel, CancellationToken cancellationToken)
         {
         {
-            _logger.LogInformation("Cleaning channel {0} from database", id);
+            _logger.LogInformation("Cleaning channel {0} from database", channel.Id);
 
 
             // Delete all channel items
             // Delete all channel items
-            var allIds = _libraryManager.GetItemIds(new InternalItemsQuery
+            var items = _libraryManager.GetItemList(new InternalItemsQuery
             {
             {
-                ChannelIds = new[] { id }
+                ChannelIds = new[] { channel.Id }
             });
             });
 
 
-            foreach (var deleteId in allIds)
+            foreach (var item in items)
             {
             {
                 cancellationToken.ThrowIfCancellationRequested();
                 cancellationToken.ThrowIfCancellationRequested();
 
 
-                DeleteItem(deleteId);
-            }
-
-            // 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
                 DeleteFileLocation = false
 
 

+ 129 - 0
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

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

+ 3 - 4
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)
         private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
         {
-            "Audio",
-            "MusicAlbum",
-            "MusicVideo",
+            "Book",
             "AudioBook",
             "AudioBook",
-            "AudioPodcast"
+            "Episode",
+            "Season"
         };
         };
 
 
         private bool HasSeriesFields(InternalItemsQuery query)
         private bool HasSeriesFields(InternalItemsQuery query)

+ 2 - 2
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data
                     {
                     {
                         list.Add(row[0].ReadGuidFromBlob());
                         list.Add(row[0].ReadGuidFromBlob());
                     }
                     }
-                    catch
+                    catch (Exception ex)
                     {
                     {
-
+                        Logger.LogError(ex, "Error while getting user");
                     }
                     }
                 }
                 }
             }
             }

+ 34 - 0
Emby.Server.Implementations/Data/SqliteUserRepository.cs

@@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
                 {
                 {
                     TryMigrateToLocalUsersTable(connection);
                     TryMigrateToLocalUsersTable(connection);
                 }
                 }
+
+                RemoveEmptyPasswordHashes();
             }
             }
         }
         }
 
 
@@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
             }
             }
         }
         }
 
 
+        private void RemoveEmptyPasswordHashes()
+        {
+            foreach (var user in RetrieveAllUsers())
+            {
+                // If the user password is the sha1 hash of the empty string, remove it
+                if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
+                    || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
+                {
+                    continue;
+                }
+
+                user.Password = null;
+                var serialized = _jsonSerializer.SerializeToBytes(user);
+
+                using (WriteLock.Write())
+                using (var connection = CreateConnection())
+                {
+                    connection.RunInTransaction(db =>
+                    {
+                        using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
+                        {
+                            statement.TryBind("@InternalId", user.InternalId);
+                            statement.TryBind("@data", serialized);
+                            statement.MoveNext();
+                        }
+
+                    }, TransactionMode);
+                }
+            }
+
+        }
+
         /// <summary>
         /// <summary>
         /// Save a user in the repo
         /// Save a user in the repo
         /// </summary>
         /// </summary>

+ 2 - 13
Emby.Server.Implementations/Dto/DtoService.cs

@@ -5,8 +5,6 @@ using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto
             return GetBaseItemDto(item, options, user, owner);
             return GetBaseItemDto(item, options, user, owner);
         }
         }
 
 
-        public BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
-        {
-            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)
         public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null)
         {
         {

+ 0 - 24
Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs

@@ -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; }
-    }
-}

+ 0 - 17
Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs

@@ -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";
-        }
-    }
-}

+ 0 - 132
Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs

@@ -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;
-        }
-    }
-}

+ 120 - 21
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Linq;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
@@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
         public string Name => "Default";
         public string Name => "Default";
 
 
         public bool IsEnabled => true;
         public bool IsEnabled => true;
-
+        
+        // This is dumb and an artifact of the backwards way auth providers were designed.
+        // This version of authenticate was never meant to be called, but needs to be here for interface compat
+        // Only the providers that don't provide local user support use this
         public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
         public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
-
+        
+        // This is the verson that we need to use for local users. Because reasons.
         public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
         public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
         {
         {
+            bool success = false;
             if (resolvedUser == null)
             if (resolvedUser == null)
             {
             {
                 throw new Exception("Invalid username or password");
                 throw new Exception("Invalid username or password");
             }
             }
 
 
-            var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
+            // As long as jellyfin supports passwordless users, we need this little block here to accomodate
+            if (IsPasswordEmpty(resolvedUser, password))
+            {
+                return Task.FromResult(new ProviderAuthenticationResult
+                {
+                    Username = username
+                });
+            }
+
+            ConvertPasswordFormat(resolvedUser);
+            byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
+
+            PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
+            byte[] calculatedHash;
+            string calculatedHashString;
+            if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
+            {
+                if (string.IsNullOrEmpty(readyHash.Salt))
+                {
+                    calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
+                    calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
+                }
+                else
+                {
+                    calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
+                    calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
+                }
+
+                if (calculatedHashString == readyHash.Hash)
+                {
+                    success = true;
+                    // throw new Exception("Invalid username or password");
+                }
+            }
+            else
+            {
+                throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
+            }
+
+            // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
 
 
             if (!success)
             if (!success)
             {
             {
@@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
             });
             });
         }
         }
 
 
+        // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
+        // but at least they are in the new format.
+        private void ConvertPasswordFormat(User user)
+        {
+            if (string.IsNullOrEmpty(user.Password))
+            {
+                return;
+            }
+
+            if (!user.Password.Contains("$"))
+            {
+                string hash = user.Password;
+                user.Password = string.Format("$SHA1${0}", hash);
+            }
+            
+            if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
+            {
+                string hash = user.EasyPassword;
+                user.EasyPassword = string.Format("$SHA1${0}", hash);
+            }
+        }
+
         public Task<bool> HasPassword(User user)
         public Task<bool> HasPassword(User user)
         {
         {
             var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
             var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
             return Task.FromResult(hasConfiguredPassword);
             return Task.FromResult(hasConfiguredPassword);
         }
         }
 
 
-        private bool IsPasswordEmpty(User user, string passwordHash)
+        private bool IsPasswordEmpty(User user, string password)
         {
         {
-            return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
+            return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
         }
         }
 
 
         public Task ChangePassword(User user, string newPassword)
         public Task ChangePassword(User user, string newPassword)
         {
         {
-            string newPasswordHash = null;
+            ConvertPasswordFormat(user);
+            // This is needed to support changing a no password user to a password user
+            if (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;
             return Task.CompletedTask;
         }
         }
 
 
         public string GetPasswordHash(User user)
         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>
         /// <summary>
@@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
         /// </summary>
         /// </summary>
         public string GetHashedString(User user, string str)
         public string GetHashedString(User user, string str)
         {
         {
-            var salt = user.Salt;
-            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)));
+            }
         }
         }
     }
     }
 }
 }

+ 14 - 23
Emby.Server.Implementations/Library/UserManager.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
@@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
             }
             }
         }
         }
 
 
-        public bool IsValidUsername(string username)
+        public static bool IsValidUsername(string username)
         {
         {
-            // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
-            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)
         private static bool IsValidUsernameCharacter(char i)
         {
         {
-            return !char.Equals(i, '<') && !char.Equals(i, '>');
+            return IsValidUsername(i.ToString());
         }
         }
 
 
         public string MakeValidUsername(string username)
         public string MakeValidUsername(string username)
@@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
         private string GetLocalPasswordHash(User user)
         private string GetLocalPasswordHash(User user)
         {
         {
             return string.IsNullOrEmpty(user.EasyPassword)
             return string.IsNullOrEmpty(user.EasyPassword)
-                ? _defaultAuthenticationProvider.GetEmptyHashedString(user)
+                ? null
                 : user.EasyPassword;
                 : user.EasyPassword;
         }
         }
 
 
-        private bool IsPasswordEmpty(User user, string passwordHash)
-        {
-            return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
-        }
-
         /// <summary>
         /// <summary>
         /// Loads the users from the repository
         /// Loads the users from the repository
         /// </summary>
         /// </summary>
@@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(user));
                 throw new ArgumentNullException(nameof(user));
             }
             }
 
 
-            var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
-            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 :
                 hasConfiguredEasyPassword :
                 hasConfiguredPassword;
                 hasConfiguredPassword;
 
 
-            var dto = new UserDto
+            UserDto dto = new UserDto
             {
             {
                 Id = user.Id,
                 Id = user.Id,
                 Name = user.Name,
                 Name = user.Name,
@@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
                 dto.EnableAutoLogin = true;
                 dto.EnableAutoLogin = true;
             }
             }
 
 
-            var image = user.GetImageInfo(ImageType.Primary, 0);
+            ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
 
 
             if (image != null)
             if (image != null)
             {
             {
@@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
 
 
             if (!IsValidUsername(name))
             if (!IsValidUsername(name))
             {
             {
-                throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
             }
             }
 
 
             if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
             if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))

+ 24 - 128
Emby.Server.Implementations/Localization/LocalizationManager.cs

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

+ 8 - 0
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

+ 6 - 0
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

+ 10 - 0
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

+ 5 - 0
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

+ 66 - 6
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -79,13 +79,13 @@ namespace Emby.Server.Implementations.Networking
         private IpAddressInfo[] _localIpAddresses;
         private IpAddressInfo[] _localIpAddresses;
         private readonly object _localIpAddressSyncLock = new object();
         private readonly object _localIpAddressSyncLock = new object();
 
 
-        public IpAddressInfo[] GetLocalIpAddresses()
+        public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
         {
         {
             lock (_localIpAddressSyncLock)
             lock (_localIpAddressSyncLock)
             {
             {
                 if (_localIpAddresses == null)
                 if (_localIpAddresses == null)
                 {
                 {
-                    var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray();
+                    var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray();
 
 
                     _localIpAddresses = addresses;
                     _localIpAddresses = addresses;
 
 
@@ -95,9 +95,9 @@ namespace Emby.Server.Implementations.Networking
             }
             }
         }
         }
 
 
-        private async Task<List<IPAddress>> GetLocalIpAddressesInternal()
+        private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool ignoreVirtualInterface)
         {
         {
-            var list = GetIPsDefault()
+            var list = GetIPsDefault(ignoreVirtualInterface)
                 .ToList();
                 .ToList();
 
 
             if (list.Count == 0)
             if (list.Count == 0)
@@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking
             return Dns.GetHostAddressesAsync(hostName);
             return Dns.GetHostAddressesAsync(hostName);
         }
         }
 
 
-        private List<IPAddress> GetIPsDefault()
+        private List<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
         {
         {
             NetworkInterface[] interfaces;
             NetworkInterface[] interfaces;
 
 
@@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking
                     // Try to exclude virtual adapters
                     // Try to exclude virtual adapters
                     // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
                     // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
                     var addr = ipProperties.GatewayAddresses.FirstOrDefault();
                     var addr = ipProperties.GatewayAddresses.FirstOrDefault();
-                    if (addr == null || string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
+                    if (addr == null || ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
                     {
                     {
                         return new List<IPAddress>();
                         return new List<IPAddress>();
                     }
                     }
@@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking
             return false;
             return false;
         }
         }
 
 
+        public bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask)
+        {
+             IPAddress network1 = GetNetworkAddress(ToIPAddress(address1), ToIPAddress(subnetMask));
+             IPAddress network2 = GetNetworkAddress(ToIPAddress(address2), ToIPAddress(subnetMask));
+             return network1.Equals(network2);
+        }
+
+        private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
+        {
+            byte[] ipAdressBytes = address.GetAddressBytes();
+            byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
+
+            if (ipAdressBytes.Length != subnetMaskBytes.Length)
+            {
+                throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
+            }
+
+            byte[] broadcastAddress = new byte[ipAdressBytes.Length];
+            for (int i = 0; i < broadcastAddress.Length; i++)
+            {
+                broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
+            }
+            return new IPAddress(broadcastAddress);
+        }
+
+        public IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address)
+        {
+            NetworkInterface[] interfaces;
+            IPAddress ipaddress = ToIPAddress(address);
+
+            try
+            {
+                var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
+
+                interfaces = NetworkInterface.GetAllNetworkInterfaces()
+                    .Where(i => validStatuses.Contains(i.OperationalStatus))
+                    .ToArray();
+            }
+            catch (Exception ex)
+            {
+                Logger.LogError(ex, "Error in GetAllNetworkInterfaces");
+                return null;
+            }
+
+            foreach (NetworkInterface ni in interfaces)
+            {
+                if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null)
+                {
+                    foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
+                    {
+                        if (ip.Address.Equals(ipaddress) && ip.IPv4Mask != null)
+                        {
+                           return ToIpAddressInfo(ip.IPv4Mask);
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+
         public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
         public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
         {
         {
             if (endpoint == null)
             if (endpoint == null)

+ 0 - 57
Emby.Server.Implementations/Security/EncryptionManager.cs

@@ -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);
-        }
-    }
-}

+ 66 - 95
Emby.Server.Implementations/Services/ServicePath.cs

@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Services
         private const char ComponentSeperator = '.';
         private const char ComponentSeperator = '.';
         private const string VariablePrefix = "{";
         private const string VariablePrefix = "{";
 
 
-        readonly bool[] componentsWithSeparators;
+        private readonly bool[] componentsWithSeparators;
 
 
         private readonly string restPath;
         private readonly string restPath;
         public bool IsWildCardPath { get; private set; }
         public bool IsWildCardPath { get; private set; }
@@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services
         public string Description { get; private set; }
         public string Description { get; private set; }
         public bool IsHidden { get; private set; }
         public bool IsHidden { get; private set; }
 
 
-        public int Priority { get; set; } //passed back to RouteAttribute
-
-        public IEnumerable<string> PathVariables => this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e));
-
         public static string[] GetPathPartsForMatching(string pathInfo)
         public static string[] GetPathPartsForMatching(string pathInfo)
         {
         {
             return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
             return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
@@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services
             {
             {
                 list.Add(hashPrefix + part);
                 list.Add(hashPrefix + part);
 
 
-                var subParts = part.Split(ComponentSeperator);
-                if (subParts.Length == 1) continue;
+                if (part.IndexOf(ComponentSeperator) == -1)
+                {
+                    continue;
+                }
 
 
+                var subParts = part.Split(ComponentSeperator);
                 foreach (var subPart in subParts)
                 foreach (var subPart in subParts)
                 {
                 {
                     list.Add(hashPrefix + subPart);
                     list.Add(hashPrefix + subPart);
@@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services
             {
             {
                 if (string.IsNullOrEmpty(component)) continue;
                 if (string.IsNullOrEmpty(component)) continue;
 
 
-                if (StringContains(component, VariablePrefix)
+                if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
                     && component.IndexOf(ComponentSeperator) != -1)
                     && component.IndexOf(ComponentSeperator) != -1)
                 {
                 {
                     hasSeparators.Add(true);
                     hasSeparators.Add(true);
@@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services
 
 
             for (var i = 0; i < components.Length - 1; i++)
             for (var i = 0; i < components.Length - 1; i++)
             {
             {
-                if (!this.isWildcard[i]) continue;
+                if (!this.isWildcard[i])
+                {
+                    continue;
+                }
+
                 if (this.literalsToMatch[i + 1] == null)
                 if (this.literalsToMatch[i + 1] == null)
                 {
                 {
                     throw new ArgumentException(
                     throw new ArgumentException(
@@ -173,7 +176,7 @@ namespace Emby.Server.Implementations.Services
                 }
                 }
             }
             }
 
 
-            this.wildcardCount = this.isWildcard.Count(x => x);
+            this.wildcardCount = this.isWildcard.Length;
             this.IsWildCardPath = this.wildcardCount > 0;
             this.IsWildCardPath = this.wildcardCount > 0;
 
 
             this.FirstMatchHashKey = !this.IsWildCardPath
             this.FirstMatchHashKey = !this.IsWildCardPath
@@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services
                 : WildCardChar + PathSeperator + firstLiteralMatch;
                 : WildCardChar + PathSeperator + firstLiteralMatch;
 
 
             this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
             this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
-            RegisterCaseInsenstivePropertyNameMappings();
-        }
 
 
-        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",
             "IgnoreDataMemberAttribute",
             "JsonIgnoreAttribute"
             "JsonIgnoreAttribute"
         };
         };
@@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services
 
 
         private static Type excludeType = typeof(Stream);
         private static Type excludeType = typeof(Stream);
 
 
-        internal static List<PropertyInfo> GetSerializableProperties(Type type)
+        internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
         {
         {
-            var list = new List<PropertyInfo>();
-            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;
                     continue;
                 }
                 }
@@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services
 
 
                 if (!ignored)
                 if (!ignored)
                 {
                 {
-                    list.Add(prop);
+                    yield return prop;
                 }
                 }
             }
             }
-
-            // else return those properties that are not decorated with IgnoreDataMember
-            return list;
         }
         }
 
 
-        private static List<PropertyInfo> GetPublicProperties(Type type)
+        private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
         {
         {
-            if (type.GetTypeInfo().IsInterface)
+            if (type.IsInterface)
             {
             {
                 var propertyInfos = new List<PropertyInfo>();
                 var propertyInfos = new List<PropertyInfo>();
-
-                var considered = new List<Type>();
+                var considered = new List<Type>()
+                {
+                    type
+                };
                 var queue = new Queue<Type>();
                 var queue = new Queue<Type>();
-                considered.Add(type);
                 queue.Enqueue(type);
                 queue.Enqueue(type);
 
 
                 while (queue.Count > 0)
                 while (queue.Count > 0)
@@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services
                     var subType = queue.Dequeue();
                     var subType = queue.Dequeue();
                     foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
                     foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
                     {
                     {
-                        if (considered.Contains(subInterface)) continue;
+                        if (considered.Contains(subInterface))
+                        {
+                            continue;
+                        }
 
 
                         considered.Add(subInterface);
                         considered.Add(subInterface);
                         queue.Enqueue(subInterface);
                         queue.Enqueue(subInterface);
                     }
                     }
 
 
-                    var typeProperties = GetTypesPublicProperties(subType);
-
-                    var newPropertyInfos = typeProperties
+                    var newPropertyInfos = GetTypesPublicProperties(subType)
                         .Where(x => !propertyInfos.Contains(x));
                         .Where(x => !propertyInfos.Contains(x));
 
 
                     propertyInfos.InsertRange(0, newPropertyInfos);
                     propertyInfos.InsertRange(0, newPropertyInfos);
@@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services
                 return propertyInfos;
                 return propertyInfos;
             }
             }
 
 
-            var list = new List<PropertyInfo>();
-
-            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())
             foreach (var pi in subType.GetRuntimeProperties())
             {
             {
                 var mi = pi.GetMethod ?? pi.SetMethod;
                 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>
         /// <summary>
@@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services
 
 
         private readonly StringMapTypeDeserializer typeDeserializer;
         private readonly StringMapTypeDeserializer typeDeserializer;
 
 
-        private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
+        private readonly HashSet<string> _propertyNamesMap;
 
 
         public int MatchScore(string httpMethod, string[] withPathInfoParts)
         public int MatchScore(string httpMethod, string[] withPathInfoParts)
         {
         {
@@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services
                 return -1;
                 return -1;
             }
             }
 
 
-            var score = 0;
-
             //Routes with least wildcard matches get the highest score
             //Routes with least wildcard matches get the highest score
-            score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
-
-            //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
             //Exact verb match is better than ANY
             if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
             if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
@@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services
             return score;
             return score;
         }
         }
 
 
-        private bool StringContains(string str1, string str2)
-        {
-            return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1;
-        }
-
         /// <summary>
         /// <summary>
         /// For performance withPathInfoParts should already be a lower case string
         /// For performance withPathInfoParts should already be a lower case string
         /// to minimize redundant matching operations.
         /// to minimize redundant matching operations.
@@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services
                     if (i < this.TotalComponentsCount - 1)
                     if (i < this.TotalComponentsCount - 1)
                     {
                     {
                         // Continue to consume up until a match with the next literal
                         // Continue to consume up until a match with the next literal
-                        while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1]))
+                        while (pathIx < withPathInfoParts.Length
+                            && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
                         {
                         {
                             pathIx++;
                             pathIx++;
                             wildcardMatchCount++;
                             wildcardMatchCount++;
@@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services
                         continue;
                         continue;
                     }
                     }
 
 
-                    if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
+                    if (withPathInfoParts.Length <= pathIx
+                        || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
                     {
                     {
                         return false;
                         return false;
                     }
                     }
+
                     pathIx++;
                     pathIx++;
                 }
                 }
             }
             }
@@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services
             return pathIx == withPathInfoParts.Length;
             return pathIx == withPathInfoParts.Length;
         }
         }
 
 
-        private static bool LiteralsEqual(string str1, string str2)
-        {
-            // Most cases
-            if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase))
-            {
-                return true;
-            }
-
-            // Handle turkish i
-            str1 = str1.ToUpperInvariant();
-            str2 = str2.ToUpperInvariant();
-
-            // Invariant IgnoreCase would probably be better but it's not available in PCL
-            return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase);
-        }
-
         private bool ExplodeComponents(ref string[] withPathInfoParts)
         private bool ExplodeComponents(ref string[] withPathInfoParts)
         {
         {
             var totalComponents = new List<string>();
             var totalComponents = new List<string>();
             for (var i = 0; i < withPathInfoParts.Length; i++)
             for (var i = 0; i < withPathInfoParts.Length; i++)
             {
             {
                 var component = withPathInfoParts[i];
                 var component = withPathInfoParts[i];
-                if (string.IsNullOrEmpty(component)) continue;
+                if (string.IsNullOrEmpty(component))
+                {
+                    continue;
+                }
 
 
                 if (this.PathComponentsCount != this.TotalComponentsCount
                 if (this.PathComponentsCount != this.TotalComponentsCount
                     && this.componentsWithSeparators[i])
                     && this.componentsWithSeparators[i])
                 {
                 {
                     var subComponents = component.Split(ComponentSeperator);
                     var subComponents = component.Split(ComponentSeperator);
-                    if (subComponents.Length < 2) return false;
+                    if (subComponents.Length < 2)
+                    {
+                        return false;
+                    }
+
                     totalComponents.AddRange(subComponents);
                     totalComponents.AddRange(subComponents);
                 }
                 }
                 else
                 else
@@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services
                     continue;
                     continue;
                 }
                 }
 
 
-                if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest))
+                if (!this._propertyNamesMap.Contains(variableName))
                 {
                 {
                     if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
                     if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
                     {
                     {
@@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services
                         {
                         {
                             sb.Append(PathSeperatorChar + requestComponents[j]);
                             sb.Append(PathSeperatorChar + requestComponents[j]);
                         }
                         }
+
                         value = sb.ToString();
                         value = sb.ToString();
                     }
                     }
                     else
                     else
@@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services
                         var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
                         var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
                         if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
                         if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
                         {
                         {
-                            var sb = new StringBuilder();
-                            sb.Append(value);
+                            var sb = new StringBuilder(value);
                             pathIx++;
                             pathIx++;
                             while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
                             while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
                             {
                             {
                                 sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
                                 sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
                             }
                             }
+
                             value = sb.ToString();
                             value = sb.ToString();
                         }
                         }
                         else
                         else
@@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services
                     pathIx++;
                     pathIx++;
                 }
                 }
 
 
-                requestKeyValuesMap[propertyNameOnRequest] = value;
+                requestKeyValuesMap[variableName] = value;
             }
             }
 
 
             if (queryStringAndFormData != null)
             if (queryStringAndFormData != null)

+ 25 - 27
Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs

@@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services
     {
     {
         internal class PropertySerializerEntry
         internal class PropertySerializerEntry
         {
         {
-            public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn)
+            public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
             {
             {
                 PropertySetFn = propertySetFn;
                 PropertySetFn = propertySetFn;
                 PropertyParseStringFn = propertyParseStringFn;
                 PropertyParseStringFn = propertyParseStringFn;
+                PropertyType = PropertyType;
             }
             }
 
 
-            public Action<object, object> PropertySetFn;
-            public 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;
         private readonly Type type;
@@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services
         public Func<string, object> GetParseFn(Type propertyType)
         public Func<string, object> GetParseFn(Type propertyType)
         {
         {
             if (propertyType == typeof(string))
             if (propertyType == typeof(string))
+            {
                 return s => s;
                 return s => s;
+            }
 
 
             return _GetParseFn(propertyType);
             return _GetParseFn(propertyType);
         }
         }
@@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services
                 var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
                 var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
                 var propertyType = propertyInfo.PropertyType;
                 var propertyType = propertyInfo.PropertyType;
                 var propertyParseStringFn = GetParseFn(propertyType);
                 var propertyParseStringFn = GetParseFn(propertyType);
-                var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
+                var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
 
 
                 propertySetterMap[propertyInfo.Name] = propertySerializer;
                 propertySetterMap[propertyInfo.Name] = propertySerializer;
             }
             }
@@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services
 
 
         public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
         public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
         {
         {
-            string propertyName = null;
-            string propertyTextValue = null;
             PropertySerializerEntry propertySerializerEntry = null;
             PropertySerializerEntry propertySerializerEntry = null;
 
 
             if (instance == null)
             if (instance == null)
+            {
                 instance = _CreateInstanceFn(type);
                 instance = _CreateInstanceFn(type);
+            }
 
 
             foreach (var pair in keyValuePairs)
             foreach (var pair in keyValuePairs)
             {
             {
-                propertyName = pair.Key;
-                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;
                     continue;
                 }
                 }
@@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services
                 {
                 {
                     continue;
                     continue;
                 }
                 }
+
                 propertySerializerEntry.PropertySetFn(instance, value);
                 propertySerializerEntry.PropertySetFn(instance, value);
             }
             }
 
 
@@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services
 
 
         public static string LeftPart(string strVal, char needle)
         public static string LeftPart(string strVal, char needle)
         {
         {
-            if (strVal == null) return null;
+            if (strVal == null)
+            {
+                return null;
+            }
+
             var pos = strVal.IndexOf(needle);
             var pos = strVal.IndexOf(needle);
             return pos == -1
             return pos == -1
                 ? strVal
                 ? strVal
@@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services
     {
     {
         public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
         public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
         {
         {
-            if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) return null;
+            if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
+            {
+                return null;
+            }
 
 
             var setMethodInfo = propertyInfo.SetMethod;
             var setMethodInfo = propertyInfo.SetMethod;
             return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
             return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });

+ 1 - 1
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1090,7 +1090,7 @@ namespace Emby.Server.Implementations.Session
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
-        private IList<BaseItem> TranslateItemForPlayback(Guid id, User user)
+        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);
 
 

+ 20 - 19
Emby.Server.Implementations/SocketSharp/RequestMono.cs

@@ -13,9 +13,9 @@ namespace Emby.Server.Implementations.SocketSharp
 {
 {
     public partial class WebSocketSharpRequest : IHttpRequest
     public partial class WebSocketSharpRequest : IHttpRequest
     {
     {
-        internal static string GetParameter(string header, string attr)
+        internal static string GetParameter(ReadOnlySpan<char> header, string attr)
         {
         {
-            int ap = header.IndexOf(attr, StringComparison.Ordinal);
+            int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal);
             if (ap == -1)
             if (ap == -1)
             {
             {
                 return null;
                 return null;
@@ -33,18 +33,19 @@ namespace Emby.Server.Implementations.SocketSharp
                 ending = ' ';
                 ending = ' ';
             }
             }
 
 
-            int end = header.IndexOf(ending, ap + 1);
+            var slice = header.Slice(ap + 1);
+            int end = slice.IndexOf(ending);
             if (end == -1)
             if (end == -1)
             {
             {
-                return ending == '"' ? null : header.Substring(ap);
+                return ending == '"' ? null : header.Slice(ap).ToString();
             }
             }
 
 
-            return header.Substring(ap + 1, end - ap - 1);
+            return slice.Slice(0, end - ap - 1).ToString();
         }
         }
 
 
         private async Task LoadMultiPart(WebROCollection form)
         private async Task LoadMultiPart(WebROCollection form)
         {
         {
-            string boundary = GetParameter(ContentType, "; boundary=");
+            string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
             if (boundary == null)
             if (boundary == null)
             {
             {
                 return;
                 return;
@@ -377,17 +378,17 @@ namespace Emby.Server.Implementations.SocketSharp
                 }
                 }
 
 
                 var elem = new Element();
                 var elem = new Element();
-                string header;
-                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.Name = GetContentDispositionAttribute(header, "name");
                         elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
                         elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
                     }
                     }
-                    else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
+                    else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        elem.ContentType = header.Substring("Content-Type:".Length).Trim();
+                        elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
                         elem.Encoding = GetEncoding(elem.ContentType);
                         elem.Encoding = GetEncoding(elem.ContentType);
                     }
                     }
                 }
                 }
@@ -435,16 +436,16 @@ namespace Emby.Server.Implementations.SocketSharp
                 return sb.ToString();
                 return sb.ToString();
             }
             }
 
 
-            private static string GetContentDispositionAttribute(string l, string name)
+            private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name)
             {
             {
-                int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
+                int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
                 if (idx < 0)
                 if (idx < 0)
                 {
                 {
                     return null;
                     return null;
                 }
                 }
 
 
                 int begin = idx + name.Length + "=\"".Length;
                 int begin = idx + name.Length + "=\"".Length;
-                int end = l.IndexOf('"', begin);
+                int end = l.Slice(begin).IndexOf('"');
                 if (end < 0)
                 if (end < 0)
                 {
                 {
                     return null;
                     return null;
@@ -455,19 +456,19 @@ namespace Emby.Server.Implementations.SocketSharp
                     return string.Empty;
                     return string.Empty;
                 }
                 }
 
 
-                return l.Substring(begin, end - begin);
+                return l.Slice(begin, end - begin).ToString();
             }
             }
 
 
-            private string GetContentDispositionAttributeWithEncoding(string l, string name)
+            private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name)
             {
             {
-                int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
+                int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
                 if (idx < 0)
                 if (idx < 0)
                 {
                 {
                     return null;
                     return null;
                 }
                 }
 
 
                 int begin = idx + name.Length + "=\"".Length;
                 int begin = idx + name.Length + "=\"".Length;
-                int end = l.IndexOf('"', begin);
+                int end = l.Slice(begin).IndexOf('"');
                 if (end < 0)
                 if (end < 0)
                 {
                 {
                     return null;
                     return null;
@@ -478,7 +479,7 @@ namespace Emby.Server.Implementations.SocketSharp
                     return string.Empty;
                     return string.Empty;
                 }
                 }
 
 
-                string temp = l.Substring(begin, end - begin);
+                ReadOnlySpan<char> temp = l.Slice(begin, end - begin);
                 byte[] source = new byte[temp.Length];
                 byte[] source = new byte[temp.Length];
                 for (int i = temp.Length - 1; i >= 0; i--)
                 for (int i = temp.Length - 1; i >= 0; i--)
                 {
                 {

+ 40 - 22
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();
         public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
 
 
         private string remoteIp;
         private string remoteIp;
+        public string RemoteIp
+        {
+            get
+            {
+                if (remoteIp != null)
+                {
+                    return remoteIp;
+                }
 
 
-        public string RemoteIp =>
-            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 };
         private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
 
 
         // CheckBadChars - throws on invalid chars to be not found in header name/value
         // CheckBadChars - throws on invalid chars to be not found in header name/value
-        internal static string CheckBadChars(string name)
+        internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
         {
         {
-            if (name == null || name.Length == 0)
+            if (name.Length == 0)
             {
             {
                 return name;
                 return name;
             }
             }
@@ -99,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp
                         }
                         }
                         else if (c == 127 || (c < ' ' && c != '\t'))
                         else if (c == 127 || (c < ' ' && c != '\t'))
                         {
                         {
-                            throw new ArgumentException("net_WebHeaderInvalidControlChars");
+                            throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
                         }
                         }
 
 
                         break;
                         break;
@@ -113,7 +131,7 @@ namespace Emby.Server.Implementations.SocketSharp
                             break;
                             break;
                         }
                         }
 
 
-                        throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+                        throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
                     }
                     }
 
 
                     case 2:
                     case 2:
@@ -124,29 +142,29 @@ namespace Emby.Server.Implementations.SocketSharp
                             break;
                             break;
                         }
                         }
 
 
-                        throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+                        throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
                     }
                     }
                 }
                 }
             }
             }
 
 
             if (crlf != 0)
             if (crlf != 0)
             {
             {
-                throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+                throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
             }
             }
 
 
             return name;
             return name;
         }
         }
 
 
-        private string NormalizeIp(string ip)
+        private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
         {
         {
-            if (!string.IsNullOrWhiteSpace(ip))
+            if (ip.Length != 0 && !ip.IsWhiteSpace())
             {
             {
                 // Handle ipv4 mapped to ipv6
                 // Handle ipv4 mapped to ipv6
                 const string srch = "::ffff:";
                 const string srch = "::ffff:";
-                var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+                var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
                 if (index == 0)
                 if (index == 0)
                 {
                 {
-                    ip = ip.Substring(srch.Length);
+                    ip = ip.Slice(srch.Length);
                 }
                 }
             }
             }
 
 
@@ -324,7 +342,7 @@ namespace Emby.Server.Implementations.SocketSharp
                     }
                     }
 
 
                     this.pathInfo = WebUtility.UrlDecode(pathInfo);
                     this.pathInfo = WebUtility.UrlDecode(pathInfo);
-                    this.pathInfo = NormalizePathInfo(pathInfo, mode);
+                    this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
                 }
                 }
 
 
                 return this.pathInfo;
                 return this.pathInfo;
@@ -436,7 +454,7 @@ namespace Emby.Server.Implementations.SocketSharp
 
 
         public static Encoding GetEncoding(string contentTypeHeader)
         public static Encoding GetEncoding(string contentTypeHeader)
         {
         {
-            var param = GetParameter(contentTypeHeader, "charset=");
+            var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
             if (param == null)
             if (param == null)
             {
             {
                 return null;
                 return null;
@@ -488,18 +506,18 @@ namespace Emby.Server.Implementations.SocketSharp
             }
             }
         }
         }
 
 
-        public static string NormalizePathInfo(string pathInfo, string handlerPath)
+        public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
         {
         {
             if (handlerPath != null)
             if (handlerPath != null)
             {
             {
-                var trimmed = pathInfo.TrimStart('/');
-                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();
         }
         }
     }
     }
 }
 }

+ 24 - 3
Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs

@@ -495,9 +495,7 @@ namespace Emby.XmlTv.Classes
                     ParseMovieDbSystem(reader, result);
                     ParseMovieDbSystem(reader, result);
                     break;
                     break;
                 case "SxxExx":
                 case "SxxExx":
-                    // TODO
-                    // <episode-num system="SxxExx">S03E12</episode-num>
-                    reader.Skip();
+                    ParseSxxExxSystem(reader, result);
                     break;
                     break;
                 default: // Handles empty string and nulls
                 default: // Handles empty string and nulls
                     reader.Skip();
                     reader.Skip();
@@ -505,6 +503,29 @@ namespace Emby.XmlTv.Classes
             }
             }
         }
         }
 
 
+        public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result)
+        {
+            // <episode-num system="SxxExx">S012E32</episode-num>
+
+            var value = reader.ReadElementContentAsString();
+            var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase);
+
+            if (res.Success)
+            {
+                int parsedInt;
+
+                if (int.TryParse(res.Groups[1].Value, out parsedInt))
+                {
+                    result.Episode.Series = parsedInt;
+                }
+
+                if (int.TryParse(res.Groups[2].Value, out parsedInt))
+                {
+                    result.Episode.Episode = parsedInt;
+                }   
+            }
+        }
+
         public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result)
         public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result)
         {
         {
             // <episode-num system="thetvdb.com">series/248841</episode-num>
             // <episode-num system="thetvdb.com">series/248841</episode-num>

+ 1 - 1
Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs

@@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia
                 foregroundWidth *= percent;
                 foregroundWidth *= percent;
                 foregroundWidth /= 100;
                 foregroundWidth /= 100;
 
 
-                paint.Color = SKColor.Parse("#FF52B54B");
+                paint.Color = SKColor.Parse("#FF00A4DC");
                 canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
                 canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
             }
             }
         }
         }

+ 1 - 1
Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs

@@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
 
 
             using (var paint = new SKPaint())
             using (var paint = new SKPaint())
             {
             {
-                paint.Color = SKColor.Parse("#CC52B54B");
+                paint.Color = SKColor.Parse("#CC00A4DC");
                 paint.Style = SKPaintStyle.Fill;
                 paint.Style = SKPaintStyle.Fill;
                 canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
                 canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
             }
             }

+ 1 - 1
Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia
 
 
             using (var paint = new SKPaint())
             using (var paint = new SKPaint())
             {
             {
-                paint.Color = SKColor.Parse("#CC52B54B");
+                paint.Color = SKColor.Parse("#CC00A4DC");
                 paint.Style = SKPaintStyle.Fill;
                 paint.Style = SKPaintStyle.Fill;
                 canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
                 canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
             }
             }

+ 2 - 2
Jellyfin.Server/StartupOptions.cs

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

+ 3 - 10
MediaBrowser.Api/BaseApiService.cs

@@ -172,16 +172,9 @@ namespace MediaBrowser.Api
 
 
                 if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
                 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();
                 }
                 }
             }
             }
 
 

+ 2 - 1
MediaBrowser.Api/FilterService.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -180,7 +181,7 @@ namespace MediaBrowser.Api
             return ToOptimizedResult(filters);
             return ToOptimizedResult(filters);
         }
         }
 
 
-        private QueryFiltersLegacy GetFilters(BaseItem[] items)
+        private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
         {
         {
             var result = new QueryFiltersLegacy();
             var result = new QueryFiltersLegacy();
 
 

+ 1 - 0
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.mov", "GET")]
     [Route("/Videos/{Id}/stream.iso", "GET")]
     [Route("/Videos/{Id}/stream.iso", "GET")]
     [Route("/Videos/{Id}/stream.flv", "GET")]
     [Route("/Videos/{Id}/stream.flv", "GET")]
+    [Route("/Videos/{Id}/stream.rm", "GET")]
     [Route("/Videos/{Id}/stream", "GET")]
     [Route("/Videos/{Id}/stream", "GET")]
     [Route("/Videos/{Id}/stream.ts", "HEAD")]
     [Route("/Videos/{Id}/stream.ts", "HEAD")]
     [Route("/Videos/{Id}/stream.webm", "HEAD")]
     [Route("/Videos/{Id}/stream.webm", "HEAD")]

+ 13 - 22
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
@@ -197,29 +198,27 @@ namespace MediaBrowser.Api.UserLibrary
                 request.ParentId = null;
                 request.ParentId = null;
             }
             }
 
 
-            var item = string.IsNullOrEmpty(request.ParentId) ?
-                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)
             if (folder == null)
             {
             {
-                folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
+                folder = _libraryManager.GetUserRootFolder();
             }
             }
 
 
             var hasCollectionType = folder as IHasCollectionType;
             var hasCollectionType = folder as IHasCollectionType;
-            var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase));
-
-            if (isPlaylistQuery)
+            if (hasCollectionType != null
+                && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
             {
             {
                 request.Recursive = true;
                 request.Recursive = true;
                 request.IncludeItemTypes = "Playlist";
                 request.IncludeItemTypes = "Playlist";
@@ -235,20 +234,12 @@ namespace MediaBrowser.Api.UserLibrary
                 };
                 };
             }
             }
 
 
-            if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
-            {
-                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));
                 return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
             }
             }
 
 
             var itemsArray = folder.GetChildren(user, true).ToArray();
             var itemsArray = folder.GetChildren(user, true).ToArray();
-
             return new QueryResult<BaseItem>
             return new QueryResult<BaseItem>
             {
             {
                 Items = itemsArray,
                 Items = itemsArray,

+ 4 - 1
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>
         /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
         bool IsInLocalNetwork(string endpoint);
         bool IsInLocalNetwork(string endpoint);
 
 
-        IpAddressInfo[] GetLocalIpAddresses();
+        IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface);
 
 
         IpAddressInfo ParseIpAddress(string ipAddress);
         IpAddressInfo ParseIpAddress(string ipAddress);
 
 
@@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net
         Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
         Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
 
 
         bool IsAddressInSubnets(string addressString, string[] subnets);
         bool IsAddressInSubnets(string addressString, string[] subnets);
+
+        bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask);
+        IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address);
     }
     }
 }
 }

+ 2 - 12
MediaBrowser.Controller/Dto/DtoOptions.cs

@@ -36,9 +36,7 @@ namespace MediaBrowser.Controller.Dto
             .ToArray();
             .ToArray();
 
 
         public bool ContainsField(ItemFields field)
         public bool ContainsField(ItemFields field)
-        {
-            return AllItemFields.Contains(field);
-        }
+            => Fields.Contains(field);
 
 
         public DtoOptions(bool allFields)
         public DtoOptions(bool allFields)
         {
         {
@@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto
             EnableUserData = true;
             EnableUserData = true;
             AddCurrentProgram = true;
             AddCurrentProgram = true;
 
 
-            if (allFields)
-            {
-                Fields = AllItemFields;
-            }
-            else
-            {
-                Fields = new ItemFields[] { };
-            }
-
+            Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
             ImageTypes = AllImageTypes;
             ImageTypes = AllImageTypes;
         }
         }
 
 

+ 1 - 3
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto
         /// <param name="options">The options.</param>
         /// <param name="options">The options.</param>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         /// <param name="owner">The owner.</param>
         /// <param name="owner">The owner.</param>
-        BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
-
-        BaseItemDto[] GetBaseItemDtos(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>
         /// <summary>
         /// Gets the item by name dto.
         /// Gets the item by name dto.

+ 3 - 21
MediaBrowser.Controller/Entities/Folder.cs

@@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             if (query.ItemIds.Length > 0)
             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);
             return GetItemsInternal(query);
         }
         }
 
 
-        public BaseItem[] GetItemList(InternalItemsQuery query)
+        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
         {
         {
             query.EnableTotalRecordCount = false;
             query.EnableTotalRecordCount = false;
 
 
             if (query.ItemIds.Length > 0)
             if (query.ItemIds.Length > 0)
             {
             {
-                var result = LibraryManager.GetItemList(query);
-
-                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;
             return GetItemsInternal(query).Items;

+ 1 - 1
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
             {
                 flags.Add("+ignidx");
                 flags.Add("+ignidx");
             }
             }
-            if (state.GenPtsInput)
+            if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
             {
             {
                 flags.Add("+genpts");
                 flags.Add("+genpts");
             }
             }

+ 3 - 2
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.System;
 
 
 namespace MediaBrowser.Controller.MediaEncoding
 namespace MediaBrowser.Controller.MediaEncoding
 {
 {
@@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
     /// </summary>
     /// </summary>
     public interface IMediaEncoder : ITranscoderSupport
     public interface IMediaEncoder : ITranscoderSupport
     {
     {
-        string EncoderLocationType { get; }
+        FFmpegLocation EncoderLocation { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the encoder path.
         /// Gets the encoder path.
@@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         string EscapeSubtitleFilterPath(string path);
         string EscapeSubtitleFilterPath(string path);
 
 
-        void Init();
+        void SetFFmpegPath();
 
 
         void UpdateEncoderPath(string path, string pathType);
         void UpdateEncoderPath(string path, string pathType);
         bool SupportsEncoder(string encoder);
         bool SupportsEncoder(string encoder);

+ 6 - 5
MediaBrowser.Controller/MediaEncoding/JobLogger.cs

@@ -32,16 +32,17 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
                         var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
                         var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
 
 
+                        // If ffmpeg process is closed, the state is disposed, so don't write to target in that case
+                        if (!target.CanWrite)
+                        {
+                            break;
+                        }
+
                         await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
                         await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
                         await target.FlushAsync().ConfigureAwait(false);
                         await target.FlushAsync().ConfigureAwait(false);
                     }
                     }
                 }
                 }
             }
             }
-            catch (ObjectDisposedException)
-            {
-                //TODO Investigate and properly fix.
-                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
-            }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 _logger.LogError(ex, "Error reading ffmpeg log");
                 _logger.LogError(ex, "Error reading ffmpeg log");

+ 0 - 19
MediaBrowser.Controller/Security/IEncryptionManager.cs

@@ -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);
-    }
-}

+ 7 - 0
MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
+using System.IO;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images
 
 
             var path = item.ContainingFolderPath;
             var path = item.ContainingFolderPath;
 
 
+            // Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
+            if (!Directory.Exists(path))
+            {
+                return Array.Empty<FileSystemMetadata>();
+            }
+
             if (includeDirectories)
             if (includeDirectories)
             {
             {
                 return directoryService.GetFileSystemEntries(path)
                 return directoryService.GetFileSystemEntries(path)

+ 105 - 8
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -1,6 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Globalization;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Diagnostics;
@@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             _processFactory = processFactory;
             _processFactory = processFactory;
         }
         }
 
 
-        public (IEnumerable<string> decoders, IEnumerable<string> encoders) Validate(string encoderPath)
+        public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath)
         {
         {
             _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
             _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
 
 
@@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
             if (string.IsNullOrWhiteSpace(output))
             if (string.IsNullOrWhiteSpace(output))
             {
             {
+                if (logOutput)
+                {
+                    _logger.LogError("FFmpeg validation: The process returned no result");
+                }
                 return false;
                 return false;
             }
             }
 
 
@@ -55,21 +59,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
             if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
             if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
             {
             {
+                if (logOutput)
+                {
+                    _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
+                }
                 return false;
                 return false;
             }
             }
 
 
-            output = " " + output + " ";
+            // The min and max FFmpeg versions required to run jellyfin successfully
+            var minRequired = new Version(4, 0);
+            var maxRequired = new Version(4, 0);
+
+            // 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[]
         private static readonly string[] requiredDecoders = new[]

+ 131 - 254
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -3,17 +3,14 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.MediaEncoding.Probing;
 using MediaBrowser.MediaEncoding.Probing;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Diagnostics;
@@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.MediaEncoding.Encoder
 namespace MediaBrowser.MediaEncoding.Encoder
@@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder
     public class MediaEncoder : IMediaEncoder, IDisposable
     public class MediaEncoder : IMediaEncoder, IDisposable
     {
     {
         /// <summary>
         /// <summary>
-        /// The _logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// Gets the json serializer.
+        /// Gets the encoder path.
         /// </summary>
         /// </summary>
-        /// <value>The json serializer.</value>
-        private readonly IJsonSerializer _jsonSerializer;
+        /// <value>The encoder path.</value>
+        public string EncoderPath => FFmpegPath;
 
 
         /// <summary>
         /// <summary>
-        /// The _thumbnail resource pool
+        /// The location of the discovered FFmpeg tool.
         /// </summary>
         /// </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 IServerConfigurationManager ConfigurationManager;
         protected readonly IFileSystem FileSystem;
         protected readonly IFileSystem FileSystem;
-        protected readonly ILiveTvManager LiveTvManager;
-        protected readonly IIsoManager IsoManager;
-        protected readonly ILibraryManager LibraryManager;
-        protected readonly IChannelManager ChannelManager;
-        protected readonly ISessionManager SessionManager;
         protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
         protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
         protected readonly Func<IMediaSourceManager> MediaSourceManager;
         protected readonly Func<IMediaSourceManager> MediaSourceManager;
-        private readonly IHttpClient _httpClient;
-        private readonly IZipClient _zipClient;
         private readonly IProcessFactory _processFactory;
         private readonly IProcessFactory _processFactory;
+        private readonly 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 List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
-        private readonly bool _hasExternalEncoder;
-        private readonly string _originalFFMpegPath;
-        private readonly string _originalFFProbePath;
-        private readonly int DefaultImageExtractionTimeoutMs;
 
 
         public MediaEncoder(
         public MediaEncoder(
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            string ffMpegPath,
-            string ffProbePath,
-            bool hasExternalEncoder,
+            string startupOptionsFFmpegPath,
+            string startupOptionsFFprobePath,
             IServerConfigurationManager configurationManager,
             IServerConfigurationManager configurationManager,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            ILiveTvManager liveTvManager,
-            IIsoManager isoManager,
-            ILibraryManager libraryManager,
-            IChannelManager channelManager,
-            ISessionManager sessionManager,
             Func<ISubtitleEncoder> subtitleEncoder,
             Func<ISubtitleEncoder> subtitleEncoder,
             Func<IMediaSourceManager> mediaSourceManager,
             Func<IMediaSourceManager> mediaSourceManager,
-            IHttpClient httpClient,
-            IZipClient zipClient,
             IProcessFactory processFactory,
             IProcessFactory processFactory,
             int defaultImageExtractionTimeoutMs)
             int defaultImageExtractionTimeoutMs)
         {
         {
             _logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
             _logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
+            StartupOptionFFmpegPath = startupOptionsFFmpegPath;
+            StartupOptionFFprobePath = startupOptionsFFprobePath;
             ConfigurationManager = configurationManager;
             ConfigurationManager = configurationManager;
             FileSystem = fileSystem;
             FileSystem = fileSystem;
-            LiveTvManager = liveTvManager;
-            IsoManager = isoManager;
-            LibraryManager = libraryManager;
-            ChannelManager = channelManager;
-            SessionManager = sessionManager;
             SubtitleEncoder = subtitleEncoder;
             SubtitleEncoder = subtitleEncoder;
-            MediaSourceManager = mediaSourceManager;
-            _httpClient = httpClient;
-            _zipClient = zipClient;
             _processFactory = processFactory;
             _processFactory = processFactory;
             DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
             DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
-            FFProbePath = ffProbePath;
-            FFMpegPath = ffMpegPath;
-            _originalFFProbePath = ffProbePath;
-            _originalFFMpegPath = ffMpegPath;
-            _hasExternalEncoder = hasExternalEncoder;
         }
         }
 
 
-        public string EncoderLocationType
+        /// <summary>
+        /// Run at startup or if the user removes a Custom path from transcode page.
+        /// Sets global variables FFmpegPath.
+        /// Precedence is: Config > CLI > $PATH
+        /// </summary>
+        public void SetFFmpegPath()
         {
         {
-            get
+            // ToDo - Finalise removal of the --ffprobe switch
+            if (!string.IsNullOrEmpty(StartupOptionFFprobePath))
             {
             {
-                if (_hasExternalEncoder)
-                {
-                    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);
                 SetAvailableDecoders(result.decoders);
                 SetAvailableEncoders(result.encoders);
                 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)
         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);
             _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);
             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>();
         private List<string> _encoders = new List<string>();
@@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return true;
             return true;
         }
         }
 
 
-        /// <summary>
-        /// Gets the encoder path.
-        /// </summary>
-        /// <value>The encoder path.</value>
-        public string EncoderPath => FFMpegPath;
-
         /// <summary>
         /// <summary>
         /// Gets the media info.
         /// Gets the media info.
         /// </summary>
         /// </summary>
@@ -489,7 +364,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
                 // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
                 // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
                 RedirectStandardOutput = true,
                 RedirectStandardOutput = true,
-                FileName = FFProbePath,
+                FileName = FFprobePath,
                 Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
                 Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
 
 
                 IsHidden = true,
                 IsHidden = true,
@@ -691,10 +566,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
             {
             {
                 CreateNoWindow = true,
                 CreateNoWindow = true,
                 UseShellExecute = false,
                 UseShellExecute = false,
-                FileName = FFMpegPath,
+                FileName = FFmpegPath,
                 Arguments = args,
                 Arguments = args,
                 IsHidden = true,
                 IsHidden = true,
-                ErrorDialog = false
+                ErrorDialog = false,
+                EnableRaisingEvents = true
             });
             });
 
 
             _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
             _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
@@ -813,10 +689,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
             {
             {
                 CreateNoWindow = true,
                 CreateNoWindow = true,
                 UseShellExecute = false,
                 UseShellExecute = false,
-                FileName = FFMpegPath,
+                FileName = FFmpegPath,
                 Arguments = args,
                 Arguments = args,
                 IsHidden = true,
                 IsHidden = true,
-                ErrorDialog = false
+                ErrorDialog = false,
+                EnableRaisingEvents = true
             });
             });
 
 
             _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
             _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);

+ 10 - 9
MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -29,17 +30,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
-        private readonly IEncryptionManager _encryption;
 
 
         private readonly IJsonSerializer _json;
         private readonly IJsonSerializer _json;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
-        public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem)
+        public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem)
         {
         {
             _logger = loggerFactory.CreateLogger(GetType().Name);
             _logger = loggerFactory.CreateLogger(GetType().Name);
             _httpClient = httpClient;
             _httpClient = httpClient;
             _config = config;
             _config = config;
-            _encryption = encryption;
             _json = json;
             _json = json;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
 
 
@@ -63,16 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
                 !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
                 !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
                 !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
             {
             {
-                options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash);
+                options.OpenSubtitlesPasswordHash = EncodePassword(options.OpenSubtitlesPasswordHash);
             }
             }
         }
         }
 
 
-        private string EncryptPassword(string password)
+        private static string EncodePassword(string password)
         {
         {
-            return PasswordHashPrefix + _encryption.EncryptString(password);
+            var bytes = Encoding.UTF8.GetBytes(password);
+            return PasswordHashPrefix + Convert.ToBase64String(bytes);
         }
         }
 
 
-        private string DecryptPassword(string password)
+        private static string DecodePassword(string password)
         {
         {
             if (password == null ||
             if (password == null ||
                 !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
                 !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
@@ -80,7 +80,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
-            return _encryption.DecryptString(password.Substring(2));
+            var bytes = Convert.FromBase64String(password.Substring(2));
+            return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
         }
         }
 
 
         public string Name => "Open Subtitles";
         public string Name => "Open Subtitles";
@@ -186,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             var options = GetOptions();
             var options = GetOptions();
 
 
             var user = options.OpenSubtitlesUsername ?? string.Empty;
             var user = options.OpenSubtitlesUsername ?? string.Empty;
-            var password = DecryptPassword(options.OpenSubtitlesPasswordHash);
+            var password = DecodePassword(options.OpenSubtitlesPasswordHash);
 
 
             var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false);
             var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false);
 
 

+ 7 - 0
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableThrottling { get; set; }
         public bool EnableThrottling { get; set; }
         public int ThrottleDelaySeconds { get; set; }
         public int ThrottleDelaySeconds { get; set; }
         public string HardwareAccelerationType { get; set; }
         public string HardwareAccelerationType { get; set; }
+        /// <summary>
+        /// FFmpeg path as set by the user via the UI
+        /// </summary>
         public string EncoderAppPath { get; set; }
         public string EncoderAppPath { get; set; }
+        /// <summary>
+        /// The current FFmpeg path being used by the system and displayed on the transcode page
+        /// </summary>
+        public string EncoderAppPathDisplay { get; set; }
         public string VaapiDevice { get; set; }
         public string VaapiDevice { get; set; }
         public int H264Crf { get; set; }
         public int H264Crf { get; set; }
         public string H264Preset { get; set; }
         public string H264Preset { get; set; }

+ 2 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration
         public string[] LocalNetworkSubnets { get; set; }
         public string[] LocalNetworkSubnets { get; set; }
         public string[] LocalNetworkAddresses { get; set; }
         public string[] LocalNetworkAddresses { get; set; }
         public string[] CodecsUsed { get; set; }
         public string[] CodecsUsed { get; set; }
+        public bool IgnoreVirtualInterfaces { get; set; }
         public bool EnableExternalContentInSuggestions { get; set; }
         public bool EnableExternalContentInSuggestions { get; set; }
         public bool RequireHttps { get; set; }
         public bool RequireHttps { get; set; }
         public bool IsBehindProxy { get; set; }
         public bool IsBehindProxy { get; set; }
@@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration
             CodecsUsed = Array.Empty<string>();
             CodecsUsed = Array.Empty<string>();
             ImageExtractionTimeoutMs = 0;
             ImageExtractionTimeoutMs = 0;
             PathSubstitutions = Array.Empty<PathSubstitution>();
             PathSubstitutions = Array.Empty<PathSubstitution>();
+            IgnoreVirtualInterfaces = false;
             EnableSimpleArtistDetection = true;
             EnableSimpleArtistDetection = true;
 
 
             DisplaySpecialsWithinSeasons = true;
             DisplaySpecialsWithinSeasons = true;

+ 9 - 0
MediaBrowser.Model/Cryptography/ICryptoProvider.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
+using System.Collections.Generic;
 
 
 namespace MediaBrowser.Model.Cryptography
 namespace MediaBrowser.Model.Cryptography
 {
 {
@@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
         byte[] ComputeMD5(Stream str);
         byte[] ComputeMD5(Stream str);
         byte[] ComputeMD5(byte[] bytes);
         byte[] ComputeMD5(byte[] bytes);
         byte[] ComputeSHA1(byte[] bytes);
         byte[] ComputeSHA1(byte[] bytes);
+        IEnumerable<string> GetSupportedHashMethods();
+        byte[] ComputeHash(string HashMethod, byte[] bytes);
+        byte[] ComputeHashWithDefaultMethod(byte[] bytes);
+        byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt);
+        byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
+        byte[] ComputeHash(PasswordHash hash);
+        byte[] GenerateSalt();
+        string DefaultHashMethod { get; }
     }
     }
 }
 }

+ 153 - 0
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;
+        }
+    }
+
+}

+ 1 - 0
MediaBrowser.Model/Net/IpAddressInfo.cs

@@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Net
         public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
         public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
 
 
         public string Address { get; set; }
         public string Address { get; set; }
+        public IpAddressInfo SubnetMask { get; set; }
         public IpAddressFamily AddressFamily { get; set; }
         public IpAddressFamily AddressFamily { get; set; }
 
 
         public IpAddressInfo(string address, IpAddressFamily addressFamily)
         public IpAddressInfo(string address, IpAddressFamily addressFamily)

+ 16 - 1
MediaBrowser.Model/System/SystemInfo.cs

@@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates;
 
 
 namespace MediaBrowser.Model.System
 namespace MediaBrowser.Model.System
 {
 {
+    /// <summary>
+    /// Enum describing the location of the FFmpeg tool.
+    /// </summary>
+    public enum FFmpegLocation
+    {
+        /// <summary>No path to FFmpeg found.</summary>
+        NotFound,
+        /// <summary>Path supplied via command line using switch --ffmpeg.</summary>
+        SetByArgument,
+        /// <summary>User has supplied path via Transcoding UI page.</summary>
+        Custom,
+        /// <summary>FFmpeg tool found on system $PATH.</summary>
+        System
+    };
+
     /// <summary>
     /// <summary>
     /// Class SystemInfo
     /// Class SystemInfo
     /// </summary>
     /// </summary>
@@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
         /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
         /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
         public bool HasUpdateAvailable { get; set; }
         public bool HasUpdateAvailable { get; set; }
 
 
-        public string EncoderLocationType { get; set; }
+        public FFmpegLocation EncoderLocation { get; set; }
 
 
         public Architecture SystemArchitecture { get; set; }
         public Architecture SystemArchitecture { get; set; }
 
 

+ 1 - 1
MediaBrowser.Model/Users/UserPolicy.cs

@@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users
 
 
         public UserPolicy()
         public UserPolicy()
         {
         {
-            EnableContentDeletion = true;
+            EnableContentDeletion = false;
             EnableContentDeletionFromFolders = Array.Empty<string>();
             EnableContentDeletionFromFolders = Array.Empty<string>();
 
 
             EnableSyncTranscoding = true;
             EnableSyncTranscoding = true;

+ 1 - 4
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 localImagesFailed = true;
                 localImagesFailed = true;
-                if (!(item is IItemByName))
-                {
-                    Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
-                }
+                Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
             }
             }
 
 
             var metadataResult = new MetadataResult<TItemType>
             var metadataResult = new MetadataResult<TItemType>

+ 1 - 1
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"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin.svg"/></a>
 <a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a>
 <a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a>
 <a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
 <a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
-<a href="https://cloud.drone.io/jellyfin/jellyfin"><img alt="Build Status" src="https://cloud.drone.io/api/badges/jellyfin/jellyfin/status.svg"/></a>
+<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"><img alt="Azure DevOps builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"></a>
 <a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a>
 <a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a>
 </br>
 </br>
 <a href="https://opencollective.com/jellyfin"><img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/></a>
 <a href="https://opencollective.com/jellyfin"><img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/></a>

+ 3 - 3
RSSDP/ISsdpCommunicationsServer.cs

@@ -45,8 +45,8 @@ namespace Rssdp.Infrastructure
         /// <summary>
         /// <summary>
         /// Sends a message to the SSDP multicast address and port.
         /// Sends a message to the SSDP multicast address and port.
         /// </summary>
         /// </summary>
-        Task SendMulticastMessage(string message, CancellationToken cancellationToken);
-        Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken);
+        Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
+        Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
 
 
         #endregion
         #endregion
 
 
@@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure
         #endregion
         #endregion
 
 
     }
     }
-}
+}

+ 1 - 0
RSSDP/RSSDP.csproj

@@ -3,6 +3,7 @@
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>

+ 14 - 9
RSSDP/SsdpCommunicationsServer.cs

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Configuration;
 
 
 namespace Rssdp.Infrastructure
 namespace Rssdp.Infrastructure
 {
 {
@@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private ISocketFactory _SocketFactory;
         private ISocketFactory _SocketFactory;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
+        private readonly IServerConfigurationManager _config;
 
 
         private int _LocalPort;
         private int _LocalPort;
         private int _MulticastTtl;
         private int _MulticastTtl;
@@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure
         /// Minimum constructor.
         /// Minimum constructor.
         /// </summary>
         /// </summary>
         /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
         /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
-        public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
+        public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory,
+            INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
             : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
             : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
         {
         {
+            _config = config;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -236,15 +240,15 @@ namespace Rssdp.Infrastructure
             }
             }
         }
         }
 
 
-        public Task SendMulticastMessage(string message, CancellationToken cancellationToken)
+        public Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
         {
         {
-            return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken);
+            return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken);
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Sends a message to the SSDP multicast address and port.
         /// Sends a message to the SSDP multicast address and port.
         /// </summary>
         /// </summary>
-        public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken)
+        public async Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
         {
         {
             if (message == null) throw new ArgumentNullException(nameof(message));
             if (message == null) throw new ArgumentNullException(nameof(message));
 
 
@@ -264,7 +268,7 @@ namespace Rssdp.Infrastructure
                     IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
                     IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
                     Port = SsdpConstants.MulticastPort
                     Port = SsdpConstants.MulticastPort
 
 
-                }, cancellationToken).ConfigureAwait(false);
+                }, fromLocalIpAddress, cancellationToken).ConfigureAwait(false);
 
 
                 await Task.Delay(100, cancellationToken).ConfigureAwait(false);
                 await Task.Delay(100, cancellationToken).ConfigureAwait(false);
             }
             }
@@ -332,14 +336,15 @@ namespace Rssdp.Infrastructure
 
 
         #region Private Methods
         #region Private Methods
 
 
-        private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken)
+        private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
         {
         {
             var sockets = _sendSockets;
             var sockets = _sendSockets;
             if (sockets != null)
             if (sockets != null)
             {
             {
                 sockets = sockets.ToList();
                 sockets = sockets.ToList();
 
 
-                var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
+                var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress)))
+                    .Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
                 return Task.WhenAll(tasks);
                 return Task.WhenAll(tasks);
             }
             }
 
 
@@ -363,11 +368,11 @@ namespace Rssdp.Infrastructure
 
 
             if (_enableMultiSocketBinding)
             if (_enableMultiSocketBinding)
             {
             {
-                foreach (var address in _networkManager.GetLocalIpAddresses())
+                foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces))
                 {
                 {
                     if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
                     if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
                     {
                     {
-                        // Not supported ?
+                        // Not support IPv6 right now
                         continue;
                         continue;
                     }
                     }
 
 

+ 1 - 1
RSSDP/SsdpDeviceLocator.cs

@@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure
 
 
             var message = BuildMessage(header, values);
             var message = BuildMessage(header, values);
 
 
-            return _CommunicationsServer.SendMulticastMessage(message, cancellationToken);
+            return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
         }
         }
 
 
         private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)
         private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)

+ 15 - 4
RSSDP/SsdpDevicePublisher.cs

@@ -7,6 +7,7 @@ using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Common.Net;
 using Rssdp;
 using Rssdp;
 
 
 namespace Rssdp.Infrastructure
 namespace Rssdp.Infrastructure
@@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure
     /// </summary>
     /// </summary>
     public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
     public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
     {
     {
+        private readonly INetworkManager _networkManager;
 
 
         private ISsdpCommunicationsServer _CommsServer;
         private ISsdpCommunicationsServer _CommsServer;
         private string _OSName;
         private string _OSName;
         private string _OSVersion;
         private string _OSVersion;
+        private bool _sendOnlyMatchedHost;
 
 
         private bool _SupportPnpRootDevice;
         private bool _SupportPnpRootDevice;
 
 
@@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure
         /// <summary>
         /// <summary>
         /// Default constructor.
         /// Default constructor.
         /// </summary>
         /// </summary>
-        public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion)
+        public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager,
+            string osName, string osVersion, bool sendOnlyMatchedHost)
         {
         {
             if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer));
             if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer));
+            if (networkManager == null) throw new ArgumentNullException(nameof(networkManager));
             if (osName == null) throw new ArgumentNullException(nameof(osName));
             if (osName == null) throw new ArgumentNullException(nameof(osName));
             if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName));
             if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName));
             if (osVersion == null) throw new ArgumentNullException(nameof(osVersion));
             if (osVersion == null) throw new ArgumentNullException(nameof(osVersion));
@@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure
             _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
             _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
             _Random = new Random();
             _Random = new Random();
 
 
+            _networkManager = networkManager;
             _CommsServer = communicationsServer;
             _CommsServer = communicationsServer;
             _CommsServer.RequestReceived += CommsServer_RequestReceived;
             _CommsServer.RequestReceived += CommsServer_RequestReceived;
             _OSName = osName;
             _OSName = osName;
             _OSVersion = osVersion;
             _OSVersion = osVersion;
+            _sendOnlyMatchedHost = sendOnlyMatchedHost;
 
 
             _CommsServer.BeginListeningForBroadcasts();
             _CommsServer.BeginListeningForBroadcasts();
         }
         }
@@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure
 
 
                     foreach (var device in deviceList)
                     foreach (var device in deviceList)
                     {
                     {
-                        SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
+                        if (!_sendOnlyMatchedHost ||
+                            _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.IpAddress, device.ToRootDevice().SubnetMask))
+                        {
+                            SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
+                        }
                     }
                     }
                 }
                 }
                 else
                 else
@@ -427,7 +438,7 @@ namespace Rssdp.Infrastructure
 
 
             var message = BuildMessage(header, values);
             var message = BuildMessage(header, values);
 
 
-            _CommsServer.SendMulticastMessage(message, cancellationToken);
+            _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken);
 
 
             //WriteTrace(String.Format("Sent alive notification"), device);
             //WriteTrace(String.Format("Sent alive notification"), device);
         }
         }
@@ -472,7 +483,7 @@ namespace Rssdp.Infrastructure
 
 
             var sendCount = IsDisposed ? 1 : 3;
             var sendCount = IsDisposed ? 1 : 3;
             WriteTrace(String.Format("Sent byebye notification"), device);
             WriteTrace(String.Format("Sent byebye notification"), device);
-            return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken);
+            return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
         }
         }
 
 
         private void DisposeRebroadcastTimer()
         private void DisposeRebroadcastTimer()

+ 10 - 0
RSSDP/SsdpRootDevice.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Text;
 using System.Text;
 using System.Xml;
 using System.Xml;
 using Rssdp.Infrastructure;
 using Rssdp.Infrastructure;
+using MediaBrowser.Model.Net;
 
 
 namespace Rssdp
 namespace Rssdp
 {
 {
@@ -52,6 +53,15 @@ namespace Rssdp
         /// </summary>
         /// </summary>
         public Uri Location { get; set; }
         public Uri Location { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required.
+        /// </summary>
+        public IpAddressInfo Address { get; set; }
+
+        /// <summary>
+        /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required.
+        /// </summary>
+        public IpAddressInfo SubnetMask { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.
         /// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.

+ 2 - 2
SharedVersion.cs

@@ -1,4 +1,4 @@
 using System.Reflection;
 using System.Reflection;
 
 
-[assembly: AssemblyVersion("10.2.1")]
-[assembly: AssemblyFileVersion("10.2.1")]
+[assembly: AssemblyVersion("10.2.2")]
+[assembly: AssemblyFileVersion("10.2.2")]

+ 31 - 29
build

@@ -26,7 +26,7 @@ usage() {
     echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
     echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
     echo -e ""
     echo -e ""
     echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
     echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
-    echo -e "The web_branch defaults to the same branch name as the current main branch."
+    echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
     echo -e "To build all platforms, use 'all'."
     echo -e "To build all platforms, use 'all'."
     echo -e "To perform all build actions, use 'all'."
     echo -e "To perform all build actions, use 'all'."
     echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
     echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
@@ -164,37 +164,39 @@ for target_platform in ${platform[@]}; do
     fi
     fi
 done
 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
         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
 fi
-popd
 
 
 # Execute each platform and action in order, if said action is enabled
 # Execute each platform and action in order, if said action is enabled
 pushd deployment/
 pushd deployment/
@@ -217,7 +219,7 @@ for target_platform in ${platform[@]}; do
     done
     done
     if [[ -d pkg-dist/ ]]; then
     if [[ -d pkg-dist/ ]]; then
         echo -e ">> Collecting build artifacts"
         echo -e ">> Collecting build artifacts"
-        target_dir="../../../jellyfin-build/${target_platform}"
+        target_dir="../../../bin/${target_platform}"
         mkdir -p ${target_dir}
         mkdir -p ${target_dir}
         mv pkg-dist/* ${target_dir}/
         mv pkg-dist/* ${target_dir}/
     fi
     fi

+ 15 - 0
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

+ 3 - 4
deployment/common.build.sh

@@ -15,7 +15,6 @@ DEFAULT_CONFIG="Release"
 DEFAULT_OUTPUT_DIR="dist/jellyfin-git"
 DEFAULT_OUTPUT_DIR="dist/jellyfin-git"
 DEFAULT_PKG_DIR="pkg-dist"
 DEFAULT_PKG_DIR="pkg-dist"
 DEFAULT_DOCKERFILE="Dockerfile"
 DEFAULT_DOCKERFILE="Dockerfile"
-DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD`
 DEFAULT_ARCHIVE_CMD="tar -xvzf"
 DEFAULT_ARCHIVE_CMD="tar -xvzf"
 
 
 # Parse the version from the AssemblyVersion
 # Parse the version from the AssemblyVersion
@@ -36,9 +35,9 @@ build_jellyfin()
 
 
     echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
     echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
     if [[ $DOTNETRUNTIME == 'framework' ]]; then
     if [[ $DOTNETRUNTIME == 'framework' ]]; then
-        dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}"
+        dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
     else
     else
-        dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME}
+        dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
     fi    
     fi    
     EXIT_CODE=$?
     EXIT_CODE=$?
     if [ $EXIT_CODE -eq 0 ]; then
     if [ $EXIT_CODE -eq 0 ]; then
@@ -53,7 +52,7 @@ build_jellyfin_docker()
 (
 (
     BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
     BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
     DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
     DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
-    IMAGE_TAG=${3-$DEFAULT_IMAGE_TAG}
+    IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"}
 
 
     echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}"
     echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}"
     docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT}
     docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT}

+ 42 - 0
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"]

+ 34 - 0
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"]

+ 29 - 0
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

+ 1 - 0
deployment/debian-package-armhf/dependencies.txt

@@ -0,0 +1 @@
+docker

+ 20 - 0
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/

+ 42 - 0
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}"

+ 1 - 0
deployment/debian-package-armhf/pkg-src

@@ -0,0 +1 @@
+../debian-package-x64/pkg-src

+ 17 - 0
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 (10.2.1-1) unstable; urgency=medium
 
 
   * jellyfin:
   * jellyfin:

+ 3 - 3
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
 # Restart script for in-app server control
 JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
 JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
 
 
-# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
-#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
 # [OPTIONAL] run Jellyfin as a headless service
 #JELLYFIN_SERVICE_OPT="--service"
 #JELLYFIN_SERVICE_OPT="--service"

+ 1 - 1
deployment/debian-package-x64/pkg-src/control

@@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
 Architecture: any
 Architecture: any
 Depends: at,
 Depends: at,
          libsqlite3-0,
          libsqlite3-0,
-         ffmpeg (<7:4.1) | jellyfin-ffmpeg,
+         jellyfin-ffmpeg,
          libfontconfig1,
          libfontconfig1,
          libfreetype6,
          libfreetype6,
          libssl1.0.0 | libssl1.0.2
          libssl1.0.0 | libssl1.0.2

+ 19 - 2
deployment/debian-package-x64/pkg-src/rules

@@ -2,7 +2,23 @@
 CONFIG := Release
 CONFIG := Release
 TERM := xterm
 TERM := xterm
 SHELL := /bin/bash
 SHELL := /bin/bash
-DOTNETRUNTIME := debian-x64
+
+HOST_ARCH := $(shell arch)
+BUILD_ARCH := ${DEB_HOST_MULTIARCH}
+ifeq ($(HOST_ARCH),x86_64)
+    ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
+        # Cross-building ARM on AMD64
+        DOTNETRUNTIME := debian-arm
+    else
+        # Building AMD64
+        DOTNETRUNTIME := debian-x64
+    endif
+endif
+ifeq ($(HOST_ARCH),armv7l)
+    # Building ARM
+    DOTNETRUNTIME := debian-arm
+endif
+
 export DH_VERBOSE=1
 export DH_VERBOSE=1
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 
@@ -16,7 +32,8 @@ override_dh_auto_test:
 override_dh_clistrip:
 override_dh_clistrip:
 
 
 override_dh_auto_build:
 override_dh_auto_build:
-	dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server
+	dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
+		"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
 
 
 override_dh_auto_clean:
 override_dh_auto_clean:
 	dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
 	dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true

+ 1 - 1
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 install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
  && dnf copr enable -y @dotnet-sig/dotnet \
  && dnf copr enable -y @dotnet-sig/dotnet \
  && rpmdev-setuptree \
  && rpmdev-setuptree \
- && dnf install -y dotnet-sdk-${SDK_VERSION} \
+ && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \
  && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
  && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
  && mkdir -p ${SOURCE_DIR}/SPECS \
  && mkdir -p ${SOURCE_DIR}/SPECS \
  && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
  && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \

+ 17 - 4
deployment/fedora-package-x64/pkg-src/jellyfin.spec

@@ -7,7 +7,7 @@
 %endif
 %endif
 
 
 Name:           jellyfin
 Name:           jellyfin
-Version:        10.2.1
+Version:        10.2.2
 Release:        1%{?dist}
 Release:        1%{?dist}
 Summary:        The Free Software Media Browser
 Summary:        The Free Software Media Browser
 License:        GPLv2
 License:        GPLv2
@@ -27,7 +27,7 @@ BuildRequires:  libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
 Requires:       libcurl, fontconfig, freetype, openssl, glibc libicu
 Requires:       libcurl, fontconfig, freetype, openssl, glibc libicu
 # Requirements not packaged in main repos
 # Requirements not packaged in main repos
 # COPR @dotnet-sig/dotnet
 # COPR @dotnet-sig/dotnet
-BuildRequires:  dotnet-sdk-2.2
+BuildRequires:  dotnet-runtime-2.2, dotnet-sdk-2.2
 # RPMfusion free
 # RPMfusion free
 Requires:       ffmpeg
 Requires:       ffmpeg
 
 
@@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an
 %install
 %install
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
-dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} Jellyfin.Server
+dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
+    "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
 %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
 %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
 %{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
 %{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
 %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
 %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
@@ -73,7 +74,6 @@ EOF
 %{_libdir}/%{name}/jellyfin-web/*
 %{_libdir}/%{name}/jellyfin-web/*
 %attr(755,root,root) %{_bindir}/%{name}
 %attr(755,root,root) %{_bindir}/%{name}
 %{_libdir}/%{name}/*.json
 %{_libdir}/%{name}/*.json
-%{_libdir}/%{name}/*.pdb
 %{_libdir}/%{name}/*.dll
 %{_libdir}/%{name}/*.dll
 %{_libdir}/%{name}/*.so
 %{_libdir}/%{name}/*.so
 %{_libdir}/%{name}/*.a
 %{_libdir}/%{name}/*.a
@@ -140,6 +140,19 @@ fi
 %systemd_postun_with_restart jellyfin.service
 %systemd_postun_with_restart jellyfin.service
 
 
 %changelog
 %changelog
+* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
+- jellyfin:
+- PR968 Release 10.2.z copr autobuild
+- PR964 Install the dotnet runtime package in Fedora build
+- PR979 Build Package releases without debug turned on
+- PR990 Fix slow local image validation
+- PR991 Fix the ffmpeg compatibility
+- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
+- PR998 Set EnableRaisingEvents to true for processes that require it
+- PR1017 Set ffmpeg+ffprobe paths in Docker container
+- jellyfin-web:
+- PR152 Go back on Media stop
+- PR156 Fix volume slider not working on nowplayingbar
 * Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
 * Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
 - jellyfin:
 - jellyfin:
 - PR920 Fix cachedir missing from Docker container
 - PR920 Fix cachedir missing from Docker container

+ 2 - 2
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/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
         cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
         cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
         rm -r ${TEMP_DIR}
         rm -r ${TEMP_DIR}
-        cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
-        cp ${ROOT}/deployment/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}
         mkdir -p ${PKG_DIR}
         pushd ${OUTPUT_DIR} 
         pushd ${OUTPUT_DIR} 
         ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
         ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .

+ 2 - 2
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/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
         cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
         cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
         rm -r ${TEMP_DIR}
         rm -r ${TEMP_DIR}
-        cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
-        cp ${ROOT}/deployment/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}
         mkdir -p ${PKG_DIR}
         pushd ${OUTPUT_DIR} 
         pushd ${OUTPUT_DIR} 
         ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .
         ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .