Selaa lähdekoodia

Merge remote-tracking branch 'upstream/master' into library_scan_speed

Gary Wilber 4 vuotta sitten
vanhempi
sitoutus
e6d8c02944
100 muutettua tiedostoa jossa 865 lisäystä ja 686 poistoa
  1. 3 0
      .ci/azure-pipelines-abi.yml
  2. 78 0
      .ci/azure-pipelines-api-client.yml
  3. 34 2
      .ci/azure-pipelines-package.yml
  4. 1 1
      .ci/azure-pipelines-test.yml
  5. 9 0
      .ci/azure-pipelines.yml
  6. 1 0
      .gitignore
  7. 3 0
      .npmrc
  8. 1 0
      CONTRIBUTORS.md
  9. 18 18
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  10. 1 1
      Emby.Dlna/Didl/DidlBuilder.cs
  11. 3 3
      Emby.Dlna/DlnaManager.cs
  12. 1 1
      Emby.Dlna/Eventing/DlnaEventManager.cs
  13. 2 2
      Emby.Dlna/Main/DlnaEntryPoint.cs
  14. 16 2
      Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
  15. 10 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  16. 66 56
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
  17. 41 7
      Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
  18. 6 6
      Emby.Dlna/PlayTo/PlayToController.cs
  19. 9 9
      Emby.Dlna/PlayTo/PlayToManager.cs
  20. 5 11
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  21. 26 19
      Emby.Dlna/Service/BaseControlHandler.cs
  22. 3 3
      Emby.Drawing/ImageProcessor.cs
  23. 1 1
      Emby.Drawing/NullImageEncoder.cs
  24. 4 13
      Emby.Naming/AudioBook/AudioBookResolver.cs
  25. 4 1
      Emby.Notifications/NotificationEntryPoint.cs
  26. 1 1
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  27. 39 96
      Emby.Server.Implementations/ApplicationHost.cs
  28. 7 12
      Emby.Server.Implementations/Channels/ChannelManager.cs
  29. 1 1
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  30. 2 1
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  31. 72 60
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  32. 6 3
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  33. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  34. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  35. 4 3
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  36. 6 5
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  37. 1 1
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  38. 4 3
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  39. 76 69
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  40. 3 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  41. 5 12
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  42. 1 1
      Emby.Server.Implementations/Images/ArtistImageProvider.cs
  43. 12 1
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  44. 7 2
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  45. 15 0
      Emby.Server.Implementations/Library/LibraryManager.cs
  46. 17 34
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  47. 2 2
      Emby.Server.Implementations/Library/MusicManager.cs
  48. 4 2
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  49. 33 34
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  50. 13 1
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  51. 2 1
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  52. 14 14
      Emby.Server.Implementations/Library/SearchEngine.cs
  53. 1 1
      Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
  54. 1 1
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  55. 1 1
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  56. 7 7
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  57. 4 4
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  58. 11 11
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  59. 1 1
      Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
  60. 5 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  61. 4 2
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  62. 18 8
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  63. 7 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  64. 0 1
      Emby.Server.Implementations/Localization/Core/af.json
  65. 0 1
      Emby.Server.Implementations/Localization/Core/ar.json
  66. 0 1
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  67. 0 1
      Emby.Server.Implementations/Localization/Core/bn.json
  68. 0 1
      Emby.Server.Implementations/Localization/Core/ca.json
  69. 3 2
      Emby.Server.Implementations/Localization/Core/cs.json
  70. 0 1
      Emby.Server.Implementations/Localization/Core/da.json
  71. 3 2
      Emby.Server.Implementations/Localization/Core/de.json
  72. 0 1
      Emby.Server.Implementations/Localization/Core/el.json
  73. 0 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  74. 2 1
      Emby.Server.Implementations/Localization/Core/en-US.json
  75. 0 1
      Emby.Server.Implementations/Localization/Core/es-AR.json
  76. 0 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  77. 1 2
      Emby.Server.Implementations/Localization/Core/es.json
  78. 0 1
      Emby.Server.Implementations/Localization/Core/es_419.json
  79. 0 1
      Emby.Server.Implementations/Localization/Core/es_DO.json
  80. 0 1
      Emby.Server.Implementations/Localization/Core/fa.json
  81. 20 21
      Emby.Server.Implementations/Localization/Core/fi.json
  82. 0 1
      Emby.Server.Implementations/Localization/Core/fil.json
  83. 0 1
      Emby.Server.Implementations/Localization/Core/fr-CA.json
  84. 3 2
      Emby.Server.Implementations/Localization/Core/fr.json
  85. 0 1
      Emby.Server.Implementations/Localization/Core/gsw.json
  86. 0 1
      Emby.Server.Implementations/Localization/Core/he.json
  87. 3 0
      Emby.Server.Implementations/Localization/Core/hi.json
  88. 61 60
      Emby.Server.Implementations/Localization/Core/hr.json
  89. 0 1
      Emby.Server.Implementations/Localization/Core/hu.json
  90. 0 1
      Emby.Server.Implementations/Localization/Core/id.json
  91. 0 1
      Emby.Server.Implementations/Localization/Core/is.json
  92. 3 2
      Emby.Server.Implementations/Localization/Core/it.json
  93. 4 3
      Emby.Server.Implementations/Localization/Core/ja.json
  94. 0 1
      Emby.Server.Implementations/Localization/Core/kk.json
  95. 4 3
      Emby.Server.Implementations/Localization/Core/ko.json
  96. 0 1
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  97. 0 1
      Emby.Server.Implementations/Localization/Core/lv.json
  98. 0 1
      Emby.Server.Implementations/Localization/Core/mk.json
  99. 0 1
      Emby.Server.Implementations/Localization/Core/mr.json
  100. 0 1
      Emby.Server.Implementations/Localization/Core/ms.json

+ 3 - 0
.ci/azure-pipelines-abi.yml

@@ -62,6 +62,7 @@ jobs:
 
       - task: DownloadPipelineArtifact@2
         displayName: 'Download Reference Assembly Build Artifact'
+        enabled: false
         inputs:
           source: "specific"
           artifact: "$(NugetPackageName)"
@@ -73,6 +74,7 @@ jobs:
 
       - task: CopyFiles@2
         displayName: 'Copy Reference Assembly Build Artifact'
+        enabled: false
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
           contents: '**/*.dll'
@@ -83,6 +85,7 @@ jobs:
 
       - task: DotNetCoreCLI@2
         displayName: 'Execute ABI Compatibility Check Tool'
+        enabled: false
         inputs:
           command: custom
           custom: compat

+ 78 - 0
.ci/azure-pipelines-api-client.yml

@@ -0,0 +1,78 @@
+parameters:
+  - name: LinuxImage
+    type: string
+    default: "ubuntu-latest"
+  - name: GeneratorVersion
+    type: string
+    default: "5.0.0-beta2"
+
+jobs:
+- job: GenerateApiClients
+  displayName: 'Generate Api Clients'
+  dependsOn: Test
+
+  pool:
+    vmImage: "${{ parameters.LinuxImage }}"
+
+  steps:
+    - task: DownloadPipelineArtifact@2
+      displayName: 'Download OpenAPI Spec Artifact'
+      inputs:
+        source: 'current'
+        artifact: "OpenAPI Spec"
+        path: "$(System.ArtifactsDirectory)/openapispec"
+        runVersion: "latest"
+
+    - task: CmdLine@2
+      displayName: 'Download OpenApi Generator'
+      inputs:
+        script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
+
+## Authenticate with npm registry
+    - task: npmAuthenticate@0
+      inputs:
+        workingFile: ./.npmrc
+        customEndpoint: 'jellyfin-bot for NPM'
+
+## Generate npm api client
+# Unstable
+    - task: CmdLine@2
+      displayName: 'Build unstable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+      inputs:
+        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
+
+# Stable
+    - task: CmdLine@2
+      displayName: 'Build stable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+      inputs:
+        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
+
+## Run npm install
+    - task: Npm@1
+      displayName: 'Install npm dependencies'
+      inputs:
+        command: install
+        workingDir: ./apiclient/generated/typescript/axios
+
+## Publish npm packages
+# Unstable
+    - task: Npm@1
+      displayName: 'Publish unstable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+      inputs:
+        command: publish
+        publishRegistry: useFeed
+        publishFeed: 'jellyfin/unstable'
+        workingDir: ./apiclient/generated/typescript/axios
+
+# Stable
+    - task: Npm@1
+      displayName: 'Publish stable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+      inputs:
+        command: publish
+        publishRegistry: useExternalRegistry
+        publishEndpoint: 'jellyfin-bot for NPM'
+        workingDir: ./apiclient/generated/typescript/axios

+ 34 - 2
.ci/azure-pipelines-package.yml

@@ -65,6 +65,38 @@ jobs:
       contents: '**'
       targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
 
+- job: OpenAPISpec
+  dependsOn: Test
+  condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
+  displayName: 'Push OpenAPI Spec to repository'
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: DownloadPipelineArtifact@2
+    displayName: 'Download OpenAPI Spec'
+    inputs:
+      source: 'current'
+      artifact: "OpenAPI Spec"
+      path: "$(System.ArtifactsDirectory)/openapispec"
+      runVersion: "latest"
+
+  - task: SSH@0
+    displayName: 'Create target directory on repository server'
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
+
+  - task: CopyFilesOverSSH@0
+    displayName: 'Upload artifacts to repository server'
+    inputs:
+      sshEndpoint: repository
+      sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
+      contents: 'openapi.json'
+      targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
+
 - job: BuildDocker
   displayName: 'Build Docker'
 
@@ -135,7 +167,7 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
+      commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
 
   - task: SSH@0
     displayName: 'Update Stable Repository'
@@ -144,7 +176,7 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+      commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
 
 - job: PublishNuget
   displayName: 'Publish NuGet packages'

+ 1 - 1
.ci/azure-pipelines-test.yml

@@ -56,7 +56,7 @@ jobs:
         inputs:
           command: "test"
           projects: ${{ parameters.TestProjects }}
-          arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+          arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
           publishTestResults: true
           testRunTitle: $(Agent.JobName)
           workingDirectory: "$(Build.SourcesDirectory)"

+ 9 - 0
.ci/azure-pipelines.yml

@@ -34,6 +34,12 @@ jobs:
         Linux: 'ubuntu-latest'
         Windows: 'windows-latest'
         macOS: 'macos-latest'
+        
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+  - template: azure-pipelines-test.yml
+    parameters:
+      ImageNames:
+        Linux: 'ubuntu-latest'
 
 - ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-abi.yml
@@ -55,3 +61,6 @@ jobs:
 
 - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
   - template: azure-pipelines-package.yml
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+  - template: azure-pipelines-api-client.yml

+ 1 - 0
.gitignore

@@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
 web/
 web-src.*
 MediaBrowser.WebDashboard/jellyfin-web
+apiclient/generated

+ 3 - 0
.npmrc

@@ -0,0 +1,3 @@
+registry=https://registry.npmjs.org/
+@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
+always-auth=true

+ 1 - 0
CONTRIBUTORS.md

@@ -137,6 +137,7 @@
  - [KristupasSavickas](https://github.com/KristupasSavickas)
  - [Pusta](https://github.com/pusta)
  - [nielsvanvelzen](https://github.com/nielsvanvelzen)
+ - [skyfrk](https://github.com/skyfrk)
 
 # Emby Contributors
 

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

@@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
                 User = user,
                 Recursive = true,
                 IsMissing = false,
-                ExcludeItemTypes = new[] { typeof(Book).Name },
+                ExcludeItemTypes = new[] { nameof(Book) },
                 IsFolder = isFolder,
                 MediaTypes = mediaTypes,
                 DtoOptions = GetDtoOptions()
@@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
                 Limit = limit,
                 StartIndex = startIndex,
                 IsVirtualItem = false,
-                ExcludeItemTypes = new[] { typeof(Book).Name },
+                ExcludeItemTypes = new[] { nameof(Book) },
                 IsPlaceHolder = false,
                 DtoOptions = GetDtoOptions()
             };
@@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
                 StartIndex = startIndex,
                 Limit = limit,
             };
-            query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
+            query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
 
             SetSorting(query, sort, false);
 
@@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
             // query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+            query.IncludeItemTypes = new[] { nameof(BoxSet) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+            query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Audio).Name };
+            query.IncludeItemTypes = new[] { nameof(Audio) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Audio).Name };
+            query.IncludeItemTypes = new[] { nameof(Audio) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Episode).Name };
+            query.IncludeItemTypes = new[] { nameof(Episode) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+            query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
                 {
                     UserId = user.Id,
                     Limit = 50,
-                    IncludeItemTypes = new[] { typeof(Episode).Name },
+                    IncludeItemTypes = new[] { nameof(Episode) },
                     ParentId = parent == null ? Guid.Empty : parent.Id,
                     GroupItems = false
                 },
@@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 ParentId = parentId,
                 ArtistIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+                IncludeItemTypes = new[] { nameof(MusicAlbum) },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()
@@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 ParentId = parentId,
                 GenreIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+                IncludeItemTypes = new[] { nameof(MusicAlbum) },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()
@@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
             {
                 if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
                 {
-                    stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
-                    id = id.Split(new[] { '_' }, 2)[1];
+                    stubType = Enum.Parse<StubType>(name, true);
+                    id = id.Split('_', 2)[1];
 
                     break;
                 }

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

@@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
         {
             foreach (var att in profile.XmlRootAttributes)
             {
-                var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+                var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
                 if (parts.Length == 2)
                 {
                     writer.WriteAttributeString(parts[0], parts[1], null, att.Value);

+ 3 - 3
Emby.Dlna/DlnaManager.cs

@@ -383,9 +383,9 @@ namespace Emby.Dlna
                     continue;
                 }
 
-                var filename = Path.GetFileName(name).Substring(namespaceName.Length);
-
-                var path = Path.Combine(systemProfilesPath, filename);
+                var path = Path.Join(
+                    systemProfilesPath,
+                    Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
 
                 using (var stream = _assembly.GetManifestResourceStream(name))
                 {

+ 1 - 1
Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
 
             builder.Append("</e:propertyset>");
 
-            using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"),  subscription.CallbackUrl);
+            using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
             options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
             options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
             options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");

+ 2 - 2
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
 
         private async Task RegisterServerEndpoints()
         {
-            var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
+            var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
 
             var udn = CreateUuid(_appHost.SystemId);
+            var descriptorUri = "/dlna/" + udn + "/description.xml";
 
             foreach (var address in addresses)
             {
@@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
 
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
 
-                var descriptorUri = "/dlna/" + udn + "/description.xml";
                 var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
 
                 var device = new SsdpRootDevice

+ 16 - 2
Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Xml;
@@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
+    /// <summary>
+    /// Defines the <see cref="ControlHandler" />.
+    /// </summary>
     public class ControlHandler : BaseControlHandler
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ControlHandler"/> class.
+        /// </summary>
+        /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+        /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
         public ControlHandler(IServerConfigurationManager config, ILogger logger)
             : base(config, logger)
         {
@@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
         }
 
+        /// <summary>
+        /// Records that the handle is authorized in the xml stream.
+        /// </summary>
+        /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
         private static void HandleIsAuthorized(XmlWriter xmlWriter)
             => xmlWriter.WriteElementString("Result", "1");
 
+        /// <summary>
+        /// Records that the handle is validated in the xml stream.
+        /// </summary>
+        /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
         private static void HandleIsValidated(XmlWriter xmlWriter)
             => xmlWriter.WriteElementString("Result", "1");
     }

+ 10 - 3
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
@@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
+    /// <summary>
+    /// Defines the <see cref="MediaReceiverRegistrarService" />.
+    /// </summary>
     public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
     {
         private readonly IServerConfigurationManager _config;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
+        /// </summary>
+        /// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
+        /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
+        /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
         public MediaReceiverRegistrarService(
             ILogger<MediaReceiverRegistrarService> logger,
             IHttpClientFactory httpClientFactory,
@@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
         /// <inheritdoc />
         public string GetServiceXml()
         {
-            return new MediaReceiverRegistrarXmlBuilder().GetXml();
+            return MediaReceiverRegistrarXmlBuilder.GetXml();
         }
 
         /// <inheritdoc />

+ 66 - 56
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs

@@ -1,79 +1,89 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using Emby.Dlna.Service;
+using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
-    public class MediaReceiverRegistrarXmlBuilder
+    /// <summary>
+    /// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
+    /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
+    /// </summary>
+    public static class MediaReceiverRegistrarXmlBuilder
     {
-        public string GetXml()
+        /// <summary>
+        /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
+        /// </summary>
+        /// <returns>An XML representation of this service.</returns>
+        public static string GetXml()
         {
-            return new ServiceXmlBuilder().GetXml(
-                new ServiceActionListBuilder().GetActions(),
-                GetStateVariables());
+            return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
         }
 
+        /// <summary>
+        /// The a list of all the state variables for this invocation.
+        /// </summary>
+        /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
         private static IEnumerable<StateVariable> GetStateVariables()
         {
-            var list = new List<StateVariable>();
-
-            list.Add(new StateVariable
+            var list = new List<StateVariable>
             {
-                Name = "AuthorizationGrantedUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "AuthorizationGrantedUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_DeviceID",
-                DataType = "string",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_DeviceID",
+                    DataType = "string",
+                    SendsEvents = false
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "AuthorizationDeniedUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "AuthorizationDeniedUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "ValidationSucceededUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "ValidationSucceededUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_RegistrationRespMsg",
-                DataType = "bin.base64",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_RegistrationRespMsg",
+                    DataType = "bin.base64",
+                    SendsEvents = false
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_RegistrationReqMsg",
-                DataType = "bin.base64",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_RegistrationReqMsg",
+                    DataType = "bin.base64",
+                    SendsEvents = false
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "ValidationRevokedUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "ValidationRevokedUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_Result",
-                DataType = "int",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_Result",
+                    DataType = "int",
+                    SendsEvents = false
+                }
+            };
 
             return list;
         }

+ 41 - 7
Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs

@@ -1,13 +1,19 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using Emby.Dlna.Common;
+using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
-    public class ServiceActionListBuilder
+    /// <summary>
+    /// Defines the <see cref="ServiceActionListBuilder" />.
+    /// </summary>
+    public static class ServiceActionListBuilder
     {
-        public IEnumerable<ServiceAction> GetActions()
+        /// <summary>
+        /// Returns a list of services that this instance provides.
+        /// </summary>
+        /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
+        public static IEnumerable<ServiceAction> GetActions()
         {
             return new[]
             {
@@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             };
         }
 
+        /// <summary>
+        /// Returns the action details for "IsValidated".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetIsValidated()
         {
             var action = new ServiceAction
@@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
+        /// <summary>
+        /// Returns the action details for "IsAuthorized".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetIsAuthorized()
         {
             var action = new ServiceAction
@@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
+        /// <summary>
+        /// Returns the action details for "RegisterDevice".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetRegisterDevice()
         {
             var action = new ServiceAction
@@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
+        /// <summary>
+        /// Returns the action details for "GetValidationSucceededUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetGetValidationSucceededUpdateID()
         {
             var action = new ServiceAction
@@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
-        private ServiceAction GetGetAuthorizationDeniedUpdateID()
+        /// <summary>
+        /// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
+        private static ServiceAction GetGetAuthorizationDeniedUpdateID()
         {
             var action = new ServiceAction
             {
@@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
-        private ServiceAction GetGetValidationRevokedUpdateID()
+        /// <summary>
+        /// Returns the action details for "GetValidationRevokedUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
+        private static ServiceAction GetGetValidationRevokedUpdateID()
         {
             var action = new ServiceAction
             {
@@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
-        private ServiceAction GetGetAuthorizationGrantedUpdateID()
+        /// <summary>
+        /// Returns the action details for "GetAuthorizationGrantedUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
+        private static ServiceAction GetGetAuthorizationGrantedUpdateID()
         {
             var action = new ServiceAction
             {

+ 6 - 6
Emby.Dlna/PlayTo/PlayToController.cs

@@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
 
         public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
         {
-            _logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
+            _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
 
             var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
 
@@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
             var startIndex = command.StartIndex ?? 0;
             if (startIndex > 0)
             {
-                items = items.Skip(startIndex).ToList();
+                items = items.GetRange(startIndex, items.Count - startIndex);
             }
 
             var playlist = new List<PlaylistItem>();
@@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
         }
 
         /// <inheritdoc />
-        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
+        public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
         {
             if (_disposed)
             {
@@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo
                 return Task.CompletedTask;
             }
 
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
+            if (name == SessionMessageType.Play)
             {
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
             }
 
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
+            if (name == SessionMessageType.PlayState)
             {
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
             }
 
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
+            if (name == SessionMessageType.GeneralCommand)
             {
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);
             }

+ 9 - 9
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo
 
                     SupportedCommands = new[]
                     {
-                        GeneralCommandType.VolumeDown.ToString(),
-                        GeneralCommandType.VolumeUp.ToString(),
-                        GeneralCommandType.Mute.ToString(),
-                        GeneralCommandType.Unmute.ToString(),
-                        GeneralCommandType.ToggleMute.ToString(),
-                        GeneralCommandType.SetVolume.ToString(),
-                        GeneralCommandType.SetAudioStreamIndex.ToString(),
-                        GeneralCommandType.SetSubtitleStreamIndex.ToString(),
-                        GeneralCommandType.PlayMediaSource.ToString()
+                        GeneralCommandType.VolumeDown,
+                        GeneralCommandType.VolumeUp,
+                        GeneralCommandType.Mute,
+                        GeneralCommandType.Unmute,
+                        GeneralCommandType.ToggleMute,
+                        GeneralCommandType.SetVolume,
+                        GeneralCommandType.SetAudioStreamIndex,
+                        GeneralCommandType.SetSubtitleStreamIndex,
+                        GeneralCommandType.PlayMediaSource
                     },
 
                     SupportsMediaControl = true

+ 5 - 11
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -235,13 +235,13 @@ namespace Emby.Dlna.Server
                     .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
                     .Append("</serviceId>");
                 builder.Append("<SCPDURL>")
-                    .Append(BuildUrl(service.ScpdUrl, true))
+                    .Append(BuildUrl(service.ScpdUrl))
                     .Append("</SCPDURL>");
                 builder.Append("<controlURL>")
-                    .Append(BuildUrl(service.ControlUrl, true))
+                    .Append(BuildUrl(service.ControlUrl))
                     .Append("</controlURL>");
                 builder.Append("<eventSubURL>")
-                    .Append(BuildUrl(service.EventSubUrl, true))
+                    .Append(BuildUrl(service.EventSubUrl))
                     .Append("</eventSubURL>");
 
                 builder.Append("</service>");
@@ -250,13 +250,7 @@ namespace Emby.Dlna.Server
             builder.Append("</serviceList>");
         }
 
-        /// <summary>
-        /// Builds a valid url for inclusion in the xml.
-        /// </summary>
-        /// <param name="url">Url to include.</param>
-        /// <param name="absoluteUrl">Optional. When set to true, the absolute url is always used.</param>
-        /// <returns>The url to use for the element.</returns>
-        private string BuildUrl(string url, bool absoluteUrl = false)
+        private string BuildUrl(string url)
         {
             if (string.IsNullOrEmpty(url))
             {
@@ -267,7 +261,7 @@ namespace Emby.Dlna.Server
 
             url = "/dlna/" + _serverUdn + "/" + url;
 
-            if (EnableAbsoluteUrls || absoluteUrl)
+            if (EnableAbsoluteUrls)
             {
                 url = _serverAddress.TrimEnd('/') + url;
             }

+ 26 - 19
Emby.Dlna/Service/BaseControlHandler.cs

@@ -60,10 +60,8 @@ namespace Emby.Dlna.Service
                     Async = true
                 };
 
-                using (var reader = XmlReader.Create(streamReader, readerSettings))
-                {
-                    requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
-                }
+                using var reader = XmlReader.Create(streamReader, readerSettings);
+                requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
             }
 
             Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
@@ -124,10 +122,8 @@ namespace Emby.Dlna.Service
                             {
                                 if (!reader.IsEmptyElement)
                                 {
-                                    using (var subReader = reader.ReadSubtree())
-                                    {
-                                        return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
-                                    }
+                                    using var subReader = reader.ReadSubtree();
+                                    return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
                                 }
                                 else
                                 {
@@ -150,12 +146,12 @@ namespace Emby.Dlna.Service
                 }
             }
 
-            return new ControlRequestInfo();
+            throw new EndOfStreamException("Stream ended but no body tag found.");
         }
 
         private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
         {
-            var result = new ControlRequestInfo();
+            string namespaceURI = null, localName = null;
 
             await reader.MoveToContentAsync().ConfigureAwait(false);
             await reader.ReadAsync().ConfigureAwait(false);
@@ -165,16 +161,15 @@ namespace Emby.Dlna.Service
             {
                 if (reader.NodeType == XmlNodeType.Element)
                 {
-                    result.LocalName = reader.LocalName;
-                    result.NamespaceURI = reader.NamespaceURI;
+                    localName = reader.LocalName;
+                    namespaceURI = reader.NamespaceURI;
 
                     if (!reader.IsEmptyElement)
                     {
-                        using (var subReader = reader.ReadSubtree())
-                        {
-                            await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
-                            return result;
-                        }
+                        var result = new ControlRequestInfo(localName, namespaceURI);
+                        using var subReader = reader.ReadSubtree();
+                        await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
+                        return result;
                     }
                     else
                     {
@@ -187,7 +182,12 @@ namespace Emby.Dlna.Service
                 }
             }
 
-            return result;
+            if (localName != null && namespaceURI != null)
+            {
+                return new ControlRequestInfo(localName, namespaceURI);
+            }
+
+            throw new EndOfStreamException("Stream ended but no control found.");
         }
 
         private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
@@ -234,11 +234,18 @@ namespace Emby.Dlna.Service
 
         private class ControlRequestInfo
         {
+            public ControlRequestInfo(string localName, string namespaceUri)
+            {
+                LocalName = localName;
+                NamespaceURI = namespaceUri;
+                Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            }
+
             public string LocalName { get; set; }
 
             public string NamespaceURI { get; set; }
 
-            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            public Dictionary<string, string> Headers { get; }
         }
     }
 }

+ 3 - 3
Emby.Drawing/ImageProcessor.cs

@@ -36,7 +36,7 @@ namespace Emby.Drawing
         private readonly IImageEncoder _imageEncoder;
         private readonly IMediaEncoder _mediaEncoder;
 
-        private bool _disposed = false;
+        private bool _disposed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
@@ -466,11 +466,11 @@ namespace Emby.Drawing
         }
 
         /// <inheritdoc />
-        public void CreateImageCollage(ImageCollageOptions options)
+        public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
         {
             _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
 
-            _imageEncoder.CreateImageCollage(options);
+            _imageEncoder.CreateImageCollage(options, libraryName);
 
             _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
         }

+ 1 - 1
Emby.Drawing/NullImageEncoder.cs

@@ -38,7 +38,7 @@ namespace Emby.Drawing
         }
 
         /// <inheritdoc />
-        public void CreateImageCollage(ImageCollageOptions options)
+        public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
         {
             throw new NotImplementedException();
         }

+ 4 - 13
Emby.Naming/AudioBook/AudioBookResolver.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
             _options = options;
         }
 
-        public AudioBookFileInfo ParseFile(string path)
+        public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
         {
-            return Resolve(path, false);
-        }
-
-        public AudioBookFileInfo ParseDirectory(string path)
-        {
-            return Resolve(path, true);
-        }
-
-        public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
-        {
-            if (string.IsNullOrEmpty(path))
+            if (path.Length == 0)
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("String can't be empty.", nameof(path));
             }
 
             // TODO

+ 4 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -209,7 +209,10 @@ namespace Emby.Notifications
                 _libraryUpdateTimer = null;
             }
 
-            items = items.Take(10).ToList();
+            if (items.Count > 10)
+            {
+                items = items.GetRange(0, 10);
+            }
 
             foreach (var item in items)
             {

+ 1 - 1
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
         }
 
         /// <inheritdoc />
-        public string VirtualDataPath { get; } = "%AppDataPath%";
+        public string VirtualDataPath => "%AppDataPath%";
 
         /// <summary>
         /// Gets the image cache path.

+ 39 - 96
Emby.Server.Implementations/ApplicationHost.cs

@@ -4,7 +4,6 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Devices;
 using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer.Security;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Library;
@@ -97,6 +95,7 @@ using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
+using MediaBrowser.Providers.Plugins.Tmdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Mvc;
@@ -127,7 +126,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private IHttpClientFactory _httpClientFactory;
-        private IWebSocketManager _webSocketManager;
 
         private string[] _urlPrefixes;
 
@@ -258,8 +256,8 @@ namespace Emby.Server.Implementations
             IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
-            _jsonSerializer = new JsonSerializer();            
-            
+            _jsonSerializer = new JsonSerializer();
+
             ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
@@ -339,7 +337,7 @@ namespace Emby.Server.Implementations
         /// Gets the email address for use within a comment section of a user agent field.
         /// Presently used to provide contact information to MusicBrainz service.
         /// </summary>
-        public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
+        public string ApplicationUserAgentAddress => "team@jellyfin.org";
 
         /// <summary>
         /// Gets the current application name.
@@ -403,7 +401,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Resolves this instance.
         /// </summary>
-        /// <typeparam name="T">The type</typeparam>
+        /// <typeparam name="T">The type.</typeparam>
         /// <returns>``0.</returns>
         public T Resolve<T>() => ServiceProvider.GetService<T>();
 
@@ -537,6 +535,7 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton(_fileSystemManager);
             ServiceCollection.AddSingleton<TvdbClientManager>();
+            ServiceCollection.AddSingleton<TmdbClientManager>();
 
             ServiceCollection.AddSingleton(_networkManager);
 
@@ -665,7 +664,6 @@ namespace Emby.Server.Implementations
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
             _httpClientFactory = Resolve<IHttpClientFactory>();
-            _webSocketManager = Resolve<IWebSocketManager>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
@@ -786,7 +784,6 @@ namespace Emby.Server.Implementations
                         .ToArray();
 
             _urlPrefixes = GetUrlPrefixes().ToArray();
-            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
@@ -819,38 +816,6 @@ namespace Emby.Server.Implementations
         {
             try
             {
-                if (plugin is IPluginAssembly assemblyPlugin)
-                {
-                    var assembly = plugin.GetType().Assembly;
-                    var assemblyName = assembly.GetName();
-                    var assemblyFilePath = assembly.Location;
-
-                    var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-
-                    assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
-
-                    try
-                    {
-                        var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
-                        if (idAttributes.Length > 0)
-                        {
-                            var attribute = (GuidAttribute)idAttributes[0];
-                            var assemblyId = new Guid(attribute.Value);
-
-                            assemblyPlugin.SetId(assemblyId);
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
-                    }
-                }
-
-                if (plugin is IHasPluginConfiguration hasPluginConfiguration)
-                {
-                    hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
-                }
-
                 plugin.RegisterServices(ServiceCollection);
             }
             catch (Exception ex)
@@ -1026,80 +991,54 @@ namespace Emby.Server.Implementations
 
         protected abstract void RestartInternal();
 
-        /// <summary>
-        /// Comparison function used in <see cref="GetPlugins" />.
-        /// </summary>
-        /// <param name="a">Item to compare.</param>
-        /// <param name="b">Item to compare with.</param>
-        /// <returns>Boolean result of the operation.</returns>
-        private static int VersionCompare(
-            (Version PluginVersion, string Name, string Path) a,
-            (Version PluginVersion, string Name, string Path) b)
-        {
-            int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
-
-            if (compare == 0)
-            {
-                return a.PluginVersion.CompareTo(b.PluginVersion);
-            }
-
-            return compare;
-        }
-
-        /// <summary>
-        /// Returns a list of plugins to install.
-        /// </summary>
-        /// <param name="path">Path to check.</param>
-        /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
-        /// <returns>Enumerable list of dlls to load.</returns>
-        private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+        /// <inheritdoc/>
+        public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
         {
-            var dllList = new List<string>();
-            var versions = new List<(Version PluginVersion, string Name, string Path)>();
+            var minimumVersion = new Version(0, 0, 0, 1);
+            var versions = new List<LocalPlugin>();
             var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
-            string metafile;
 
             foreach (var dir in directories)
             {
                 try
                 {
-                    metafile = Path.Combine(dir, "meta.json");
+                    var metafile = Path.Combine(dir, "meta.json");
                     if (File.Exists(metafile))
                     {
                         var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
 
                         if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
                         {
-                            targetAbi = new Version(0, 0, 0, 1);
+                            targetAbi = minimumVersion;
                         }
 
                         if (!Version.TryParse(manifest.Version, out var version))
                         {
-                            version = new Version(0, 0, 0, 1);
+                            version = minimumVersion;
                         }
 
                         if (ApplicationVersion >= targetAbi)
                         {
                             // Only load Plugins if the plugin is built for this version or below.
-                            versions.Add((version, manifest.Name, dir));
+                            versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
                         }
                     }
                     else
                     {
                         // No metafile, so lets see if the folder is versioned.
                         metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
-                        
+
                         int versionIndex = dir.LastIndexOf('_');
-                        if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+                        if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
                         {
                             // Versioned folder.
-                            versions.Add((ver, metafile, dir));
+                            versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
                         }
                         else
                         {
-                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.                        
-                            versions.Add((new Version(0, 0, 0, 1), metafile, dir));
-                        }   
+                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.
+                            versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
+                        }
                     }
                 }
                 catch
@@ -1109,14 +1048,14 @@ namespace Emby.Server.Implementations
             }
 
             string lastName = string.Empty;
-            versions.Sort(VersionCompare);
+            versions.Sort(LocalPlugin.Compare);
             // Traverse backwards through the list.
             // The first item will be the latest version.
             for (int x = versions.Count - 1; x >= 0; x--)
             {
                 if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
                 {
-                    dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+                    versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
                     lastName = versions[x].Name;
                     continue;
                 }
@@ -1124,6 +1063,7 @@ namespace Emby.Server.Implementations
                 if (!string.IsNullOrEmpty(lastName) && cleanup)
                 {
                     // Attempt a cleanup of old folders.
+                    versions.RemoveAt(x);
                     try
                     {
                         Logger.LogDebug("Deleting {Path}", versions[x].Path);
@@ -1136,7 +1076,7 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            return dllList;
+            return versions;
         }
 
         /// <summary>
@@ -1147,21 +1087,24 @@ namespace Emby.Server.Implementations
         {
             if (Directory.Exists(ApplicationPaths.PluginsPath))
             {
-                foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
+                foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
                 {
-                    Assembly plugAss;
-                    try
-                    {
-                        plugAss = Assembly.LoadFrom(file);
-                    }
-                    catch (FileLoadException ex)
+                    foreach (var file in plugin.DllFiles)
                     {
-                        Logger.LogError(ex, "Failed to load assembly {Path}", file);
-                        continue;
-                    }
+                        Assembly plugAss;
+                        try
+                        {
+                            plugAss = Assembly.LoadFrom(file);
+                        }
+                        catch (FileLoadException ex)
+                        {
+                            Logger.LogError(ex, "Failed to load assembly {Path}", file);
+                            continue;
+                        }
 
-                    Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
-                    yield return plugAss;
+                        Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
+                        yield return plugAss;
+                    }
                 }
             }
 

+ 7 - 12
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
             var all = channels;
             var totalCount = all.Count;
 
-            if (query.StartIndex.HasValue)
+            if (query.StartIndex.HasValue || query.Limit.HasValue)
             {
-                all = all.Skip(query.StartIndex.Value).ToList();
+                int startIndex = query.StartIndex ?? 0;
+                int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
+                all = all.GetRange(startIndex, count);
             }
 
-            if (query.Limit.HasValue)
-            {
-                all = all.Take(query.Limit.Value).ToList();
-            }
-
-            var returnItems = all.ToArray();
-
             if (query.RefreshLatestChannelItems)
             {
-                foreach (var item in returnItems)
+                foreach (var item in all)
                 {
                     RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
                 }
@@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
 
             return new QueryResult<Channel>
             {
-                Items = returnItems,
+                Items = all,
                 TotalRecordCount = totalCount
             };
         }
@@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
             return _libraryManager.GetItemIds(
                 new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(Channel).Name },
+                    IncludeItemTypes = new[] { nameof(Channel) },
                     OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
                 }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
         }

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
 
             var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Channel).Name },
+                IncludeItemTypes = new[] { nameof(Channel) },
                 ExcludeItemIds = installedChannelIds.ToArray()
             });
 

+ 2 - 1
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
 
         protected bool TableExists(ManagedConnection connection, string name)
         {
-            return connection.RunInTransaction(db =>
+            return connection.RunInTransaction(
+            db =>
             {
                 using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
                 {

+ 72 - 60
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
             {
                 connection.RunQueries(queries);
 
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var existingColumnNames = GetColumnNames(db, "AncestorIds");
                     AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
                     {
@@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     SaveItemsInTranscation(db, tuples);
                 }, TransactionMode);
@@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     // First delete chapters
                     db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@@ -2921,7 +2925,8 @@ namespace Emby.Server.Implementations.Data
             var result = new QueryResult<BaseItem>();
             using (var connection = GetConnection(true))
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var statements = PrepareAll(db, statementTexts);
 
@@ -3324,7 +3329,8 @@ namespace Emby.Server.Implementations.Data
             var result = new QueryResult<Guid>();
             using (var connection = GetConnection(true))
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var statements = PrepareAll(db, statementTexts);
 
@@ -3908,7 +3914,7 @@ namespace Emby.Server.Implementations.Data
                 if (query.IsPlayed.HasValue)
                 {
                     // We should probably figure this out for all folders, but for right now, this is the only place where we need it
-                    if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase))
+                    if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
                     {
                         if (query.IsPlayed.Value)
                         {
@@ -4749,29 +4755,29 @@ namespace Emby.Server.Implementations.Data
         {
             var list = new List<string>();
 
-            if (IsTypeInQuery(typeof(Person).Name, query))
+            if (IsTypeInQuery(nameof(Person), query))
             {
-                list.Add(typeof(Person).Name);
+                list.Add(nameof(Person));
             }
 
-            if (IsTypeInQuery(typeof(Genre).Name, query))
+            if (IsTypeInQuery(nameof(Genre), query))
             {
-                list.Add(typeof(Genre).Name);
+                list.Add(nameof(Genre));
             }
 
-            if (IsTypeInQuery(typeof(MusicGenre).Name, query))
+            if (IsTypeInQuery(nameof(MusicGenre), query))
             {
-                list.Add(typeof(MusicGenre).Name);
+                list.Add(nameof(MusicGenre));
             }
 
-            if (IsTypeInQuery(typeof(MusicArtist).Name, query))
+            if (IsTypeInQuery(nameof(MusicArtist), query))
             {
-                list.Add(typeof(MusicArtist).Name);
+                list.Add(nameof(MusicArtist));
             }
 
-            if (IsTypeInQuery(typeof(Studio).Name, query))
+            if (IsTypeInQuery(nameof(Studio), query))
             {
-                list.Add(typeof(Studio).Name);
+                list.Add(nameof(Studio));
             }
 
             return list;
@@ -4826,12 +4832,12 @@ namespace Emby.Server.Implementations.Data
 
             var types = new[]
             {
-                typeof(Episode).Name,
-                typeof(Video).Name,
-                typeof(Movie).Name,
-                typeof(MusicVideo).Name,
-                typeof(Series).Name,
-                typeof(Season).Name
+                nameof(Episode),
+                nameof(Video),
+                nameof(Movie),
+                nameof(MusicVideo),
+                nameof(Series),
+                nameof(Season)
             };
 
             if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@@ -4899,7 +4905,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     connection.ExecuteAll(sql);
                 }, TransactionMode);
@@ -4950,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var idBlob = id.ToByteArray();
 
@@ -4994,26 +5002,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             CheckDisposed();
 
-            var commandText = "select Distinct Name from People";
+            var commandText = new StringBuilder("select Distinct p.Name from People p");
+
+            if (query.User != null && query.IsFavorite.HasValue)
+            {
+                commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
+                commandText.Append(typeof(Person).FullName);
+                commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
+            }
 
             var whereClauses = GetPeopleWhereClauses(query, null);
 
             if (whereClauses.Count != 0)
             {
-                commandText += "  where " + string.Join(" AND ", whereClauses);
+                commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
             }
 
-            commandText += " order by ListOrder";
+            commandText.Append(" order by ListOrder");
 
             if (query.Limit > 0)
             {
-                commandText += " LIMIT " + query.Limit;
+                commandText.Append(" LIMIT ").Append(query.Limit);
             }
 
             using (var connection = GetConnection(true))
             {
                 var list = new List<string>();
-                using (var statement = PrepareStatement(connection, commandText))
+                using (var statement = PrepareStatement(connection, commandText.ToString()))
                 {
                     // Run this again to bind the params
                     GetPeopleWhereClauses(query, statement);
@@ -5079,19 +5094,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             if (!query.ItemId.Equals(Guid.Empty))
             {
                 whereClauses.Add("ItemId=@ItemId");
-                if (statement != null)
-                {
-                    statement.TryBind("@ItemId", query.ItemId.ToByteArray());
-                }
+                statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
             }
 
             if (!query.AppearsInItemId.Equals(Guid.Empty))
             {
-                whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
-                if (statement != null)
-                {
-                    statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
-                }
+                whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
+                statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
             }
 
             var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@@ -5099,10 +5108,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             if (queryPersonTypes.Count == 1)
             {
                 whereClauses.Add("PersonType=@PersonType");
-                if (statement != null)
-                {
-                    statement.TryBind("@PersonType", queryPersonTypes[0]);
-                }
+                statement?.TryBind("@PersonType", queryPersonTypes[0]);
             }
             else if (queryPersonTypes.Count > 1)
             {
@@ -5116,10 +5122,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             if (queryExcludePersonTypes.Count == 1)
             {
                 whereClauses.Add("PersonType<>@PersonType");
-                if (statement != null)
-                {
-                    statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
-                }
+                statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
             }
             else if (queryExcludePersonTypes.Count > 1)
             {
@@ -5131,19 +5134,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             if (query.MaxListOrder.HasValue)
             {
                 whereClauses.Add("ListOrder<=@MaxListOrder");
-                if (statement != null)
-                {
-                    statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
-                }
+                statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
             }
 
             if (!string.IsNullOrWhiteSpace(query.NameContains))
             {
-                whereClauses.Add("Name like @NameContains");
-                if (statement != null)
-                {
-                    statement.TryBind("@NameContains", "%" + query.NameContains + "%");
-                }
+                whereClauses.Add("p.Name like @NameContains");
+                statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
+            }
+
+            if (query.IsFavorite.HasValue)
+            {
+                whereClauses.Add("isFavorite=@IsFavorite");
+                statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
+            }
+
+            if (query.User != null)
+            {
+                statement?.TryBind("@UserId", query.User.InternalId);
             }
 
             return whereClauses;
@@ -5357,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                 itemCountColumns = new Dictionary<string, string>()
                 {
-                    { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"}
+                    { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
                 };
             }
 
@@ -5412,6 +5420,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 NameStartsWithOrGreater = query.NameStartsWithOrGreater,
                 Tags = query.Tags,
                 OfficialRatings = query.OfficialRatings,
+                StudioIds = query.StudioIds,
                 GenreIds = query.GenreIds,
                 Genres = query.Genres,
                 Years = query.Years,
@@ -5744,7 +5753,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var itemIdBlob = itemId.ToByteArray();
 
@@ -5898,7 +5908,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var itemIdBlob = id.ToByteArray();
 
@@ -6232,7 +6243,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var itemIdBlob = id.ToByteArray();
 

+ 6 - 3
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
 
                 var users = userDatasTableExists ? null : userManager.Users;
 
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     db.ExecuteAll(string.Join(";", new[] {
 
@@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     SaveUserData(db, internalUserId, key, userData);
                 }, TransactionMode);
@@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     foreach (var userItemData in userDataList)
                     {

+ 1 - 1
Emby.Server.Implementations/Dto/DtoService.cs

@@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
             {
                 var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+                    IncludeItemTypes = new[] { nameof(MusicAlbum) },
                     Name = item.Album,
                     Limit = 1
                 });

+ 4 - 4
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -32,10 +32,10 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
     <PackageReference Include="Mono.Nat" Version="3.0.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />

+ 4 - 3
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -17,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             try
             {
-                _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
+                _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
             }
             catch
             {
@@ -124,7 +125,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
                 try
                 {
-                    _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
+                    _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
                 }
                 catch
                 {
@@ -348,7 +349,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
                 try
                 {
-                    await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
+                    await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {

+ 6 - 5
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
         }
 
         private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
         }
 
         private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
         }
 
         private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
         }
 
-        private async Task SendMessage(string name, TimerEventInfo info)
+        private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
         {
             var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
 

+ 1 - 1
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
         {
-            return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
+            return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
         }
 
         private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)

+ 4 - 3
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
 
@@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
         public AuthorizationInfo Authenticate(HttpRequest request)
         {
             var auth = _authorizationContext.GetAuthorizationInfo(request);
-            if (auth?.User == null)
+            if (!auth.IsAuthenticated)
             {
-                return null;
+                throw new AuthenticationException("Invalid token.");
             }
 
-            if (auth.User.HasPermission(PermissionKind.IsDisabled))
+            if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
             {
                 throw new SecurityException("User account has been disabled.");
             }

+ 76 - 69
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
         {
             var auth = GetAuthorizationDictionary(requestContext);
-            var (authInfo, _) =
-                GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+            var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
             return authInfo;
         }
 
@@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private AuthorizationInfo GetAuthorization(HttpContext httpReq)
         {
             var auth = GetAuthorizationDictionary(httpReq);
-            var (authInfo, originalAuthInfo) =
-                GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
-
-            if (originalAuthInfo != null)
-            {
-                httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
-            }
+            var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
 
             httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             return authInfo;
         }
 
-        private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+        private AuthorizationInfo GetAuthorizationInfoFromDictionary(
             in Dictionary<string, string> auth,
             in IHeaderDictionary headers,
             in IQueryCollection queryString)
@@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 Device = device,
                 DeviceId = deviceId,
                 Version = version,
-                Token = token
+                Token = token,
+                IsAuthenticated = false
             };
 
-            AuthenticationInfo originalAuthenticationInfo = null;
-            if (!string.IsNullOrWhiteSpace(token))
+            if (string.IsNullOrWhiteSpace(token))
             {
-                var result = _authRepo.Get(new AuthenticationInfoQuery
-                {
-                    AccessToken = token
-                });
+                // Request doesn't contain a token.
+                return authInfo;
+            }
 
-                originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
+            var result = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                AccessToken = token
+            });
 
-                if (originalAuthenticationInfo != null)
-                {
-                    var updateToken = false;
+            if (result.Items.Count > 0)
+            {
+                authInfo.IsAuthenticated = true;
+            }
 
-                    // TODO: Remove these checks for IsNullOrWhiteSpace
-                    if (string.IsNullOrWhiteSpace(authInfo.Client))
-                    {
-                        authInfo.Client = originalAuthenticationInfo.AppName;
-                    }
+            var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 
-                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
-                    {
-                        authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
-                    }
+            if (originalAuthenticationInfo != null)
+            {
+                var updateToken = false;
 
-                    // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                    var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+                // TODO: Remove these checks for IsNullOrWhiteSpace
+                if (string.IsNullOrWhiteSpace(authInfo.Client))
+                {
+                    authInfo.Client = originalAuthenticationInfo.AppName;
+                }
 
-                    if (string.IsNullOrWhiteSpace(authInfo.Device))
-                    {
-                        authInfo.Device = originalAuthenticationInfo.DeviceName;
-                    }
-                    else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
-                    {
-                        if (allowTokenInfoUpdate)
-                        {
-                            updateToken = true;
-                            originalAuthenticationInfo.DeviceName = authInfo.Device;
-                        }
-                    }
+                if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                {
+                    authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
+                }
 
-                    if (string.IsNullOrWhiteSpace(authInfo.Version))
-                    {
-                        authInfo.Version = originalAuthenticationInfo.AppVersion;
-                    }
-                    else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+                // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+                var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+
+                if (string.IsNullOrWhiteSpace(authInfo.Device))
+                {
+                    authInfo.Device = originalAuthenticationInfo.DeviceName;
+                }
+                else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+                {
+                    if (allowTokenInfoUpdate)
                     {
-                        if (allowTokenInfoUpdate)
-                        {
-                            updateToken = true;
-                            originalAuthenticationInfo.AppVersion = authInfo.Version;
-                        }
+                        updateToken = true;
+                        originalAuthenticationInfo.DeviceName = authInfo.Device;
                     }
+                }
 
-                    if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+                if (string.IsNullOrWhiteSpace(authInfo.Version))
+                {
+                    authInfo.Version = originalAuthenticationInfo.AppVersion;
+                }
+                else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+                {
+                    if (allowTokenInfoUpdate)
                     {
-                        originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
                         updateToken = true;
+                        originalAuthenticationInfo.AppVersion = authInfo.Version;
                     }
+                }
 
-                    if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
-                    {
-                        authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
+                if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+                {
+                    originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
+                    updateToken = true;
+                }
 
-                        if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
-                        {
-                            originalAuthenticationInfo.UserName = authInfo.User.Username;
-                            updateToken = true;
-                        }
-                    }
+                if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
+                {
+                    authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
 
-                    if (updateToken)
+                    if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
                     {
-                        _authRepo.Update(originalAuthenticationInfo);
+                        originalAuthenticationInfo.UserName = authInfo.User.Username;
+                        updateToken = true;
                     }
+
+                    authInfo.IsApiKey = true;
+                }
+                else
+                {
+                    authInfo.IsApiKey = false;
+                }
+
+                if (updateToken)
+                {
+                    _authRepo.Update(originalAuthenticationInfo);
                 }
             }
 
-            return (authInfo, originalAuthenticationInfo);
+            return authInfo;
         }
 
         /// <summary>
@@ -267,7 +274,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 if (param.Length == 2)
                 {
                     var value = NormalizeValue(param[1].Trim(new[] { '"' }));
-                    result.Add(param[0], value);
+                    result[param[0]] = value;
                 }
             }
 

+ 3 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -11,6 +11,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
                 Connection = this
             };
 
-            if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+            if (info.MessageType == SessionMessageType.KeepAlive)
             {
                 await SendKeepAliveResponse().ConfigureAwait(false);
             }
@@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
                 new WebSocketMessage<string>
                 {
                     MessageId = Guid.NewGuid(),
-                    MessageType = "KeepAlive"
+                    MessageType = SessionMessageType.KeepAlive
                 }, CancellationToken.None);
         }
 

+ 5 - 12
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -2,7 +2,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Net.WebSockets;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
@@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
 {
     public class WebSocketManager : IWebSocketManager
     {
+        private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
         private readonly ILogger<WebSocketManager> _logger;
         private readonly ILoggerFactory _loggerFactory;
 
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
 
         public WebSocketManager(
+            Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
             ILogger<WebSocketManager> logger,
             ILoggerFactory loggerFactory)
         {
+            _webSocketListeners = webSocketListeners;
             _logger = logger;
             _loggerFactory = loggerFactory;
         }
@@ -68,15 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        /// <summary>
-        /// Adds the rest handlers.
-        /// </summary>
-        /// <param name="listeners">The web socket listeners.</param>
-        public void Init(IEnumerable<IWebSocketListener> listeners)
-        {
-            _webSocketListeners = listeners.ToArray();
-        }
-
         /// <summary>
         /// Processes the web socket message received.
         /// </summary>
@@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
 
             IEnumerable<Task> GetTasks()
             {
-                foreach (var x in _webSocketListeners)
+                var listeners = _webSocketListeners.Value;
+                foreach (var x in listeners)
                 {
                     yield return x.ProcessMessageAsync(result);
                 }

+ 1 - 1
Emby.Server.Implementations/Images/ArtistImageProvider.cs

@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
             // return _libraryManager.GetItemList(new InternalItemsQuery
             // {
             //    ArtistIds = new[] { item.Id },
-            //    IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+            //    IncludeItemTypes = new[] { nameof(MusicAlbum) },
             //    OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
             //    Limit = 4,
             //    Recursive = true,

+ 12 - 1
Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs

@@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
 
         protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
         {
+            var useBackdrop = primaryItem is CollectionFolder;
             return items
                 .Select(i =>
                 {
+                    // Use Backdrop instead of Primary image for Library images.
+                    if (useBackdrop)
+                    {
+                        var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
+                        if (backdrop != null && backdrop.IsLocalFile)
+                        {
+                            return backdrop.Path;
+                        }
+                    }
+
                     var image = i.GetImageInfo(ImageType.Primary, 0);
                     if (image != null && image.IsLocalFile)
                     {
@@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
                 return null;
             }
 
-            ImageProcessor.CreateImageCollage(options);
+            ImageProcessor.CreateImageCollage(options, primaryItem.Name);
             return outputPath;
         }
 

+ 7 - 2
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
             return _libraryManager.GetItemList(new InternalItemsQuery
             {
                 Genres = new[] { item.Name },
-                IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name },
+                IncludeItemTypes = new[]
+                {
+                    nameof(MusicAlbum),
+                    nameof(MusicVideo),
+                    nameof(Audio)
+                },
                 OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
                 Limit = 4,
                 Recursive = true,
@@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
             return _libraryManager.GetItemList(new InternalItemsQuery
             {
                 Genres = new[] { item.Name },
-                IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
+                IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
                 OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
                 Limit = 4,
                 Recursive = true,

+ 15 - 0
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
             new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
         }
 
+        public BaseItem GetParentItem(string parentId, Guid? userId)
+        {
+            if (!string.IsNullOrEmpty(parentId))
+            {
+                return GetItemById(new Guid(parentId));
+            }
+
+            if (userId.HasValue && userId != Guid.Empty)
+            {
+                return GetUserRootFolder();
+            }
+
+            return RootFolder;
+        }
+
         /// <inheritdoc />
         public bool IsVideoFile(string path)
         {

+ 17 - 34
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
 
-        private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+        private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
 
         private IMediaSourceProvider[] _providers;
@@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
             mediaSource.InferTotalBitrate();
         }
 
-        public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
+        public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
         {
-            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
+            var info = _openStreams.Values.FirstOrDefault(i =>
             {
-                var info = _openStreams.Values.FirstOrDefault(i =>
+                var liveStream = i as ILiveStream;
+                if (liveStream != null)
                 {
-                    var liveStream = i as ILiveStream;
-                    if (liveStream != null)
-                    {
-                        return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
-                    }
+                    return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
+                }
 
-                    return false;
-                });
+                return false;
+            });
 
-                return info as IDirectStreamProvider;
-            }
-            finally
-            {
-                _liveStreamSemaphore.Release();
-            }
+            return Task.FromResult(info as IDirectStreamProvider);
         }
 
         public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library
             return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
         }
 
-        private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+        private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
         {
             if (string.IsNullOrEmpty(id))
             {
                 throw new ArgumentNullException(nameof(id));
             }
 
-            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
+            if (_openStreams.TryGetValue(id, out ILiveStream info))
             {
-                if (_openStreams.TryGetValue(id, out ILiveStream info))
-                {
-                    return info;
-                }
-                else
-                {
-                    throw new ResourceNotFoundException();
-                }
+                return Task.FromResult(info);
             }
-            finally
+            else
             {
-                _liveStreamSemaphore.Release();
+                return Task.FromException<ILiveStream>(new ResourceNotFoundException());
             }
         }
 
@@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
 
                     if (liveStream.ConsumerCount <= 0)
                     {
-                        _openStreams.Remove(id);
+                        _openStreams.TryRemove(id, out _);
 
                         _logger.LogInformation("Closing live stream {0}", id);
 

+ 2 - 2
Emby.Server.Implementations/Library/MusicManager.cs

@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
             var genres = item
                .GetRecursiveChildren(user, new InternalItemsQuery(user)
                {
-                   IncludeItemTypes = new[] { typeof(Audio).Name },
+                   IncludeItemTypes = new[] { nameof(Audio) },
                    DtoOptions = dtoOptions
                })
                .Cast<Audio>()
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
         {
             return _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(Audio).Name },
+                IncludeItemTypes = new[] { nameof(Audio) },
 
                 GenreIds = genreIds.ToArray(),
 

+ 4 - 2
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <value>The priority.</value>
         public override ResolverPriority Priority => ResolverPriority.Fourth;
 
-        public MultiItemResolverResult ResolveMultiple(Folder parent,
+        public MultiItemResolverResult ResolveMultiple(
+            Folder parent,
             List<FileSystemMetadata> files,
             string collectionType,
             IDirectoryService directoryService)
@@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             return result;
         }
 
-        private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+        private MultiItemResolverResult ResolveMultipleInternal(
+            Folder parent,
             List<FileSystemMetadata> files,
             string collectionType,
             IDirectoryService directoryService)

+ 33 - 34
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -1,5 +1,8 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
 using Emby.Naming.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             IFileSystem fileSystem,
             ILibraryManager libraryManager)
         {
+            // check for audio files before digging down into directories
+            var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+            if (foundAudioFile)
+            {
+                // at least one audio file exists
+                return true;
+            }
+
+            if (!allowSubfolders)
+            {
+                // not music since no audio file exists and we're not looking into subfolders
+                return false;
+            }
+
             var discSubfolderCount = 0;
-            var notMultiDisc = false;
 
             var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
             var parser = new AlbumParser(namingOptions);
-            foreach (var fileSystemInfo in list)
+
+            var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
+
+            var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
             {
-                if (fileSystemInfo.IsDirectory)
+                var path = fileSystemInfo.FullName;
+                var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+
+                if (hasMusic)
                 {
-                    if (allowSubfolders)
+                    if (parser.IsMultiPart(path))
                     {
-                        if (notMultiDisc)
-                        {
-                            continue;
-                        }
-
-                        var path = fileSystemInfo.FullName;
-                        var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
-
-                        if (hasMusic)
-                        {
-                            if (parser.IsMultiPart(path))
-                            {
-                                logger.LogDebug("Found multi-disc folder: " + path);
-                                discSubfolderCount++;
-                            }
-                            else
-                            {
-                                // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
-                                notMultiDisc = true;
-                            }
-                        }
+                        logger.LogDebug("Found multi-disc folder: " + path);
+                        Interlocked.Increment(ref discSubfolderCount);
                     }
-                }
-                else
-                {
-                    var fullName = fileSystemInfo.FullName;
-
-                    if (libraryManager.IsAudioFile(fullName))
+                    else
                     {
-                        return true;
+                        // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+                        state.Stop();
                     }
                 }
-            }
+            });
 
-            if (notMultiDisc)
+            if (!result.IsCompleted)
             {
                 return false;
             }

+ 13 - 1
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using System.Threading.Tasks;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
 
             // If we contain an album assume we are an artist folder
-            return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
+            var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
+
+            var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
+            {
+                if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+                {
+                    // stop once we see a music album
+                    state.Stop();
+                }
+            });
+
+            return !result.IsCompleted ? new MusicArtist() : null;
         }
     }
 }

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs

@@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
                 var fileExtension = Path.GetExtension(f.FullName) ??
                                     string.Empty;
 
-                return _validExtensions.Contains(fileExtension,
+                return _validExtensions.Contains(
+                    fileExtension,
                                                 StringComparer
                                                     .OrdinalIgnoreCase);
             }).ToList();

+ 14 - 14
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
             var excludeItemTypes = query.ExcludeItemTypes.ToList();
             var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
 
-            excludeItemTypes.Add(typeof(Year).Name);
-            excludeItemTypes.Add(typeof(Folder).Name);
+            excludeItemTypes.Add(nameof(Year));
+            excludeItemTypes.Add(nameof(Folder));
 
             if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(Genre).Name);
-                    AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
+                    AddIfMissing(includeItemTypes, nameof(Genre));
+                    AddIfMissing(includeItemTypes, nameof(MusicGenre));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(Genre).Name);
-                AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
+                AddIfMissing(excludeItemTypes, nameof(Genre));
+                AddIfMissing(excludeItemTypes, nameof(MusicGenre));
             }
 
             if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(Person).Name);
+                    AddIfMissing(includeItemTypes, nameof(Person));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(Person).Name);
+                AddIfMissing(excludeItemTypes, nameof(Person));
             }
 
             if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(Studio).Name);
+                    AddIfMissing(includeItemTypes, nameof(Studio));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(Studio).Name);
+                AddIfMissing(excludeItemTypes, nameof(Studio));
             }
 
             if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
+                    AddIfMissing(includeItemTypes, nameof(MusicArtist));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
+                AddIfMissing(excludeItemTypes, nameof(MusicArtist));
             }
 
-            AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
-            AddIfMissing(excludeItemTypes, typeof(Folder).Name);
+            AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
+            AddIfMissing(excludeItemTypes, nameof(Folder));
             var mediaTypes = query.MediaTypes.ToList();
 
             if (includeItemTypes.Count > 0)

+ 1 - 1
Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(MusicArtist).Name },
+                IncludeItemTypes = new[] { nameof(MusicArtist) },
                 IsDeadArtist = true,
                 IsLocked = false
             }).Cast<MusicArtist>().ToList();

+ 1 - 1
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Person).Name },
+                IncludeItemTypes = new[] { nameof(Person) },
                 IsDeadPerson = true,
                 IsLocked = false
             });

+ 1 - 1
Emby.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Studio).Name },
+                IncludeItemTypes = new[] { nameof(Studio) },
                 IsDeadStudio = true,
                 IsLocked = false
             });

+ 7 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             {
                 var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                    IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                     Limit = 1,
                     ExternalId = timer.ProgramId,
                     DtoOptions = new DtoOptions(true)
@@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             var query = new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                 Limit = 1,
                 DtoOptions = new DtoOptions(true)
                 {
@@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             var query = new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                 ExternalSeriesId = seriesTimer.SeriesId,
                 DtoOptions = new DtoOptions(true)
                 {
@@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     channel = _libraryManager.GetItemList(
                         new InternalItemsQuery
                         {
-                            IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+                            IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
                             ItemIds = new[] { parent.ChannelId },
                             DtoOptions = new DtoOptions()
                         }).FirstOrDefault() as LiveTvChannel;
@@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     channel = _libraryManager.GetItemList(
                         new InternalItemsQuery
                         {
-                            IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+                            IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
                             ItemIds = new[] { programInfo.ChannelId },
                             DtoOptions = new DtoOptions()
                         }).FirstOrDefault() as LiveTvChannel;
@@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 var seriesIds = _libraryManager.GetItemIds(
                     new InternalItemsQuery
                     {
-                        IncludeItemTypes = new[] { typeof(Series).Name },
+                        IncludeItemTypes = new[] { nameof(Series) },
                         Name = program.Name
                     }).ToArray();
 
@@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                     var result = _libraryManager.GetItemIds(new InternalItemsQuery
                     {
-                        IncludeItemTypes = new[] { typeof(Episode).Name },
+                        IncludeItemTypes = new[] { nameof(Episode) },
                         ParentIndexNumber = program.SeasonNumber.Value,
                         IndexNumber = program.EpisodeNumber.Value,
                         AncestorIds = seriesIds,

+ 4 - 4
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(Series).Name },
+                IncludeItemTypes = new string[] { nameof(Series) },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(Series).Name },
+                IncludeItemTypes = new string[] { nameof(Series) },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var program = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(Series).Name },
+                IncludeItemTypes = new string[] { nameof(Series) },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Primary },
@@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 program = _libraryManager.GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                    IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                     ExternalSeriesId = programSeriesId,
                     Limit = 1,
                     ImageTypes = new ImageType[] { ImageType.Primary },

+ 11 - 11
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
                 IsKids = query.IsKids,
                 IsSports = query.IsSports,
                 IsSeries = query.IsSeries,
-                IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvChannel) },
                 TopParentIds = new[] { topFolder.Id },
                 IsFavorite = query.IsFavorite,
                 IsLiked = query.IsLiked,
@@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var internalQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                 MinEndDate = query.MinEndDate,
                 MinStartDate = query.MinStartDate,
                 MaxEndDate = query.MaxEndDate,
@@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var internalQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                 IsAiring = query.IsAiring,
                 HasAired = query.HasAired,
                 IsNews = query.IsNews,
@@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
 
             if (cleanDatabase)
             {
-                CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
-                CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
+                CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
+                CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
             }
 
             var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
 
                     var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
                     {
-                        IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                        IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                         ChannelIds = new Guid[] { currentChannel.Id },
                         DtoOptions = new DtoOptions(true)
                     }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 if (query.IsMovie.Value)
                 {
-                    includeItemTypes.Add(typeof(Movie).Name);
+                    includeItemTypes.Add(nameof(Movie));
                 }
                 else
                 {
-                    excludeItemTypes.Add(typeof(Movie).Name);
+                    excludeItemTypes.Add(nameof(Movie));
                 }
             }
 
@@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 if (query.IsSeries.Value)
                 {
-                    includeItemTypes.Add(typeof(Episode).Name);
+                    includeItemTypes.Add(nameof(Episode));
                 }
                 else
                 {
-                    excludeItemTypes.Add(typeof(Episode).Name);
+                    excludeItemTypes.Add(nameof(Episode));
                 }
             }
 
@@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                 ChannelIds = channelIds,
                 MaxStartDate = now,
                 MinEndDate = now,

+ 1 - 1
Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs

@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
             return new[]
             {
                 // Every so often
-                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
             };
         }
 

+ 5 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             await taskCompletionSource.Task.ConfigureAwait(false);
         }
 
+        public string GetFilePath()
+        {
+            return TempFilePath;
+        }
+
         private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
         {
             return Task.Run(async () =>

+ 4 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var channelIdPrefix = GetFullChannelIdPrefix(info);
 
-            return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+            return await new M3uParser(Logger, _httpClientFactory, _appHost)
+                .Parse(info, channelIdPrefix, cancellationToken)
+                .ConfigureAwait(false);
         }
 
         public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task Validate(TunerHostInfo info)
         {
-            using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+            using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
             {
             }
         }

+ 18 - 8
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.LiveTv;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
@@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             _appHost = appHost;
         }
 
-        public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
+        public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
         {
             // Read the file and display it line by line.
-            using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
+            using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
             {
-                return GetChannels(reader, channelIdPrefix, tunerHostId);
+                return GetChannels(reader, channelIdPrefix, info.Id);
             }
         }
 
@@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
         }
 
-        public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
+        public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
         {
-            if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
-                return _httpClientFactory.CreateClient(NamedClient.Default)
-                    .GetStreamAsync(url);
+                using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+                if (!string.IsNullOrEmpty(info.UserAgent))
+                {
+                    requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
+                }
+
+                var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(requestMessage, cancellationToken)
+                    .ConfigureAwait(false);
+                response.EnsureSuccessStatusCode();
+                return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             }
 
-            return Task.FromResult((Stream)File.OpenRead(url));
+            return File.OpenRead(info.Url);
         }
 
         private const string ExtInfPrefix = "#EXTINF:";

+ 7 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var typeName = GetType().Name;
             Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
 
-            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+            // Response stream is disposed manually.
+            var response = await _httpClientFactory.CreateClient(NamedClient.Default)
                 .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
                 .ConfigureAwait(false);
 
@@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
         }
 
+        public string GetFilePath()
+        {
+            return TempFilePath;
+        }
+
         private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
         {
             return Task.Run(async () =>

+ 0 - 1
Emby.Server.Implementations/Localization/Core/af.json

@@ -85,7 +85,6 @@
     "ItemAddedWithName": "{0} is in die versameling",
     "HomeVideos": "Tuis opnames",
     "HeaderRecordingGroups": "Groep Opnames",
-    "HeaderCameraUploads": "Kamera Oplaai",
     "Genres": "Genres",
     "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
     "ChapterNameValue": "Hoofstuk",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ar.json

@@ -16,7 +16,6 @@
     "Folders": "المجلدات",
     "Genres": "التضنيفات",
     "HeaderAlbumArtists": "فناني الألبومات",
-    "HeaderCameraUploads": "تحميلات الكاميرا",
     "HeaderContinueWatching": "استئناف",
     "HeaderFavoriteAlbums": "الألبومات المفضلة",
     "HeaderFavoriteArtists": "الفنانون المفضلون",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -16,7 +16,6 @@
     "Folders": "Папки",
     "Genres": "Жанрове",
     "HeaderAlbumArtists": "Изпълнители на албуми",
-    "HeaderCameraUploads": "Качени от камера",
     "HeaderContinueWatching": "Продължаване на гледането",
     "HeaderFavoriteAlbums": "Любими албуми",
     "HeaderFavoriteArtists": "Любими изпълнители",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/bn.json

@@ -14,7 +14,6 @@
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
-    "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
     "HeaderAlbumArtists": "এলবাম শিল্পী",
     "Genres": "জেনার",
     "Folders": "ফোল্ডারগুলো",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ca.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetes",
     "Genres": "Gèneres",
     "HeaderAlbumArtists": "Artistes del Àlbum",
-    "HeaderCameraUploads": "Pujades de Càmera",
     "HeaderContinueWatching": "Continua Veient",
     "HeaderFavoriteAlbums": "Àlbums Preferits",
     "HeaderFavoriteArtists": "Artistes Preferits",

+ 3 - 2
Emby.Server.Implementations/Localization/Core/cs.json

@@ -16,7 +16,6 @@
     "Folders": "Složky",
     "Genres": "Žánry",
     "HeaderAlbumArtists": "Umělci alba",
-    "HeaderCameraUploads": "Nahrané fotografie",
     "HeaderContinueWatching": "Pokračovat ve sledování",
     "HeaderFavoriteAlbums": "Oblíbená alba",
     "HeaderFavoriteArtists": "Oblíbení interpreti",
@@ -114,5 +113,7 @@
     "TasksChannelsCategory": "Internetové kanály",
     "TasksApplicationCategory": "Aplikace",
     "TasksLibraryCategory": "Knihovna",
-    "TasksMaintenanceCategory": "Údržba"
+    "TasksMaintenanceCategory": "Údržba",
+    "TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
+    "TaskCleanActivityLog": "Smazat záznam aktivity"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/da.json

@@ -16,7 +16,6 @@
     "Folders": "Mapper",
     "Genres": "Genrer",
     "HeaderAlbumArtists": "Albumkunstnere",
-    "HeaderCameraUploads": "Kamera Uploads",
     "HeaderContinueWatching": "Fortsæt Afspilning",
     "HeaderFavoriteAlbums": "Favoritalbummer",
     "HeaderFavoriteArtists": "Favoritkunstnere",

+ 3 - 2
Emby.Server.Implementations/Localization/Core/de.json

@@ -16,7 +16,6 @@
     "Folders": "Verzeichnisse",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album-Interpreten",
-    "HeaderCameraUploads": "Kamera-Uploads",
     "HeaderContinueWatching": "Fortsetzen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
     "HeaderFavoriteArtists": "Lieblings-Interpreten",
@@ -114,5 +113,7 @@
     "TasksChannelsCategory": "Internet Kanäle",
     "TasksApplicationCategory": "Anwendung",
     "TasksLibraryCategory": "Bibliothek",
-    "TasksMaintenanceCategory": "Wartung"
+    "TasksMaintenanceCategory": "Wartung",
+    "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
+    "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/el.json

@@ -16,7 +16,6 @@
     "Folders": "Φάκελοι",
     "Genres": "Είδη",
     "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
-    "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
     "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
     "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
     "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/en-GB.json

@@ -16,7 +16,6 @@
     "Folders": "Folders",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album Artists",
-    "HeaderCameraUploads": "Camera Uploads",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favourite Albums",
     "HeaderFavoriteArtists": "Favourite Artists",

+ 2 - 1
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -16,7 +16,6 @@
     "Folders": "Folders",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album Artists",
-    "HeaderCameraUploads": "Camera Uploads",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favorite Albums",
     "HeaderFavoriteArtists": "Favorite Artists",
@@ -96,6 +95,8 @@
     "TasksLibraryCategory": "Library",
     "TasksApplicationCategory": "Application",
     "TasksChannelsCategory": "Internet Channels",
+    "TaskCleanActivityLog": "Clean Activity Log",
+    "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
     "TaskCleanCache": "Clean Cache Directory",
     "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
     "TaskRefreshChapterImages": "Extract Chapter Images",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es-AR.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas de álbum",
-    "HeaderCameraUploads": "Subidas de cámara",
     "HeaderContinueWatching": "Seguir viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas del álbum",
-    "HeaderCameraUploads": "Subidas desde la cámara",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

+ 1 - 2
Emby.Server.Implementations/Localization/Core/es.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas del álbum",
-    "HeaderCameraUploads": "Subidas desde la cámara",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
@@ -78,7 +77,7 @@
     "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
     "Sync": "Sincronizar",
     "System": "Sistema",
-    "TvShows": "Programas de televisión",
+    "TvShows": "Series",
     "User": "Usuario",
     "UserCreatedWithName": "El usuario {0} ha sido creado",
     "UserDeletedWithName": "El usuario {0} ha sido borrado",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es_419.json

@@ -105,7 +105,6 @@
     "Inherit": "Heredar",
     "HomeVideos": "Videos caseros",
     "HeaderRecordingGroups": "Grupos de grabación",
-    "HeaderCameraUploads": "Subidas desde la cámara",
     "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
     "DeviceOnlineWithName": "{0} está conectado",
     "DeviceOfflineWithName": "{0} se ha desconectado",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es_DO.json

@@ -12,7 +12,6 @@
     "Application": "Aplicación",
     "AppDeviceValues": "App: {0}, Dispositivo: {1}",
     "HeaderContinueWatching": "Continuar Viendo",
-    "HeaderCameraUploads": "Subidas de Cámara",
     "HeaderAlbumArtists": "Artistas del Álbum",
     "Genres": "Géneros",
     "Folders": "Carpetas",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/fa.json

@@ -16,7 +16,6 @@
     "Folders": "پوشه‌ها",
     "Genres": "ژانرها",
     "HeaderAlbumArtists": "هنرمندان آلبوم",
-    "HeaderCameraUploads": "آپلودهای دوربین",
     "HeaderContinueWatching": "ادامه تماشا",
     "HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
     "HeaderFavoriteArtists": "هنرمندان مورد علاقه",

+ 20 - 21
Emby.Server.Implementations/Localization/Core/fi.json

@@ -1,7 +1,7 @@
 {
     "HeaderLiveTV": "Live-TV",
     "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
-    "NameSeasonUnknown": "Tuntematon Kausi",
+    "NameSeasonUnknown": "Tuntematon kausi",
     "NameSeasonNumber": "Kausi {0}",
     "NameInstallFailed": "{0} asennus epäonnistui",
     "MusicVideos": "Musiikkivideot",
@@ -19,24 +19,23 @@
     "ItemAddedWithName": "{0} lisättiin kirjastoon",
     "Inherit": "Periytyä",
     "HomeVideos": "Kotivideot",
-    "HeaderRecordingGroups": "Nauhoiteryhmät",
+    "HeaderRecordingGroups": "Tallennusryhmät",
     "HeaderNextUp": "Seuraavaksi",
-    "HeaderFavoriteSongs": "Lempikappaleet",
-    "HeaderFavoriteShows": "Lempisarjat",
-    "HeaderFavoriteEpisodes": "Lempijaksot",
-    "HeaderCameraUploads": "Kamerasta Lähetetyt",
-    "HeaderFavoriteArtists": "Lempiartistit",
-    "HeaderFavoriteAlbums": "Lempialbumit",
+    "HeaderFavoriteSongs": "Suosikkikappaleet",
+    "HeaderFavoriteShows": "Suosikkisarjat",
+    "HeaderFavoriteEpisodes": "Suosikkijaksot",
+    "HeaderFavoriteArtists": "Suosikkiartistit",
+    "HeaderFavoriteAlbums": "Suosikkialbumit",
     "HeaderContinueWatching": "Jatka katsomista",
-    "HeaderAlbumArtists": "Albumin esittäjä",
+    "HeaderAlbumArtists": "Albumin artistit",
     "Genres": "Tyylilajit",
     "Folders": "Kansiot",
     "Favorites": "Suosikit",
     "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
     "DeviceOnlineWithName": "{0} on yhdistetty",
-    "DeviceOfflineWithName": "{0} on katkaissut yhteytensä",
+    "DeviceOfflineWithName": "{0} yhteys on katkaistu",
     "Collections": "Kokoelmat",
-    "ChapterNameValue": "Luku: {0}",
+    "ChapterNameValue": "Jakso: {0}",
     "Channels": "Kanavat",
     "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
     "Books": "Kirjat",
@@ -62,25 +61,25 @@
     "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
     "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
     "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
-    "UserOfflineFromDevice": "{0} yhteys katkaistu {1}",
+    "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
     "UserLockedOutWithName": "Käyttäjä {0} lukittu",
     "UserDownloadingItemWithValues": "{0} lataa {1}",
     "UserDeletedWithName": "Käyttäjä {0} poistettu",
     "UserCreatedWithName": "Käyttäjä {0} luotu",
-    "TvShows": "TV-sarjat",
+    "TvShows": "TV-ohjelmat",
     "Sync": "Synkronoi",
-    "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry",
-    "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
+    "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
     "Songs": "Kappaleet",
-    "Shows": "Sarjat",
-    "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
+    "Shows": "Ohjelmat",
+    "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
     "ProviderValue": "Tarjoaja: {0}",
     "Plugin": "Liitännäinen",
     "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
     "NotificationOptionVideoPlayback": "Videota toistetaan",
     "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
     "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
-    "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen",
+    "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
     "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
     "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
     "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@@ -105,10 +104,10 @@
     "TaskRefreshPeople": "Päivitä henkilöt",
     "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
     "TaskCleanLogs": "Puhdista lokihakemisto",
-    "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
+    "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
     "TaskRefreshLibrary": "Skannaa mediakirjasto",
-    "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
-    "TaskRefreshChapterImages": "Eristä lukujen kuvat",
+    "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
+    "TaskRefreshChapterImages": "Pura jakson kuvat",
     "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
     "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
     "TasksChannelsCategory": "Internet kanavat",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/fil.json

@@ -73,7 +73,6 @@
     "HeaderFavoriteArtists": "Paboritong Artista",
     "HeaderFavoriteAlbums": "Paboritong Albums",
     "HeaderContinueWatching": "Ituloy Manood",
-    "HeaderCameraUploads": "Camera Uploads",
     "HeaderAlbumArtists": "Artista ng Album",
     "Genres": "Kategorya",
     "Folders": "Folders",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/fr-CA.json

@@ -16,7 +16,6 @@
     "Folders": "Dossiers",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Artistes de l'album",
-    "HeaderCameraUploads": "Photos transférées",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes favoris",

+ 3 - 2
Emby.Server.Implementations/Localization/Core/fr.json

@@ -16,7 +16,6 @@
     "Folders": "Dossiers",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Artistes",
-    "HeaderCameraUploads": "Photos transférées",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes préférés",
@@ -114,5 +113,7 @@
     "TaskCleanCache": "Vider le répertoire cache",
     "TasksApplicationCategory": "Application",
     "TasksLibraryCategory": "Bibliothèque",
-    "TasksMaintenanceCategory": "Maintenance"
+    "TasksMaintenanceCategory": "Maintenance",
+    "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
+    "TaskCleanActivityLog": "Nettoyer le journal d'activité"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/gsw.json

@@ -16,7 +16,6 @@
     "Folders": "Ordner",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album-Künstler",
-    "HeaderCameraUploads": "Kamera-Uploads",
     "HeaderContinueWatching": "weiter schauen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
     "HeaderFavoriteArtists": "Lieblings-Künstler",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/he.json

@@ -16,7 +16,6 @@
     "Folders": "תיקיות",
     "Genres": "ז'אנרים",
     "HeaderAlbumArtists": "אמני האלבום",
-    "HeaderCameraUploads": "העלאות ממצלמה",
     "HeaderContinueWatching": "המשך לצפות",
     "HeaderFavoriteAlbums": "אלבומים מועדפים",
     "HeaderFavoriteArtists": "אמנים מועדפים",

+ 3 - 0
Emby.Server.Implementations/Localization/Core/hi.json

@@ -0,0 +1,3 @@
+{
+    "Albums": "आल्बुम्"
+}

+ 61 - 60
Emby.Server.Implementations/Localization/Core/hr.json

@@ -5,18 +5,17 @@
     "Artists": "Izvođači",
     "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
     "Books": "Knjige",
-    "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
+    "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
     "Channels": "Kanali",
     "ChapterNameValue": "Poglavlje {0}",
     "Collections": "Kolekcije",
-    "DeviceOfflineWithName": "{0} se odspojilo",
-    "DeviceOnlineWithName": "{0} je spojeno",
-    "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
+    "DeviceOfflineWithName": "{0} je prekinuo vezu",
+    "DeviceOnlineWithName": "{0} je povezan",
+    "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
     "Favorites": "Favoriti",
     "Folders": "Mape",
     "Genres": "Žanrovi",
     "HeaderAlbumArtists": "Izvođači na albumu",
-    "HeaderCameraUploads": "Uvoz sa kamere",
     "HeaderContinueWatching": "Nastavi gledati",
     "HeaderFavoriteAlbums": "Omiljeni albumi",
     "HeaderFavoriteArtists": "Omiljeni izvođači",
@@ -24,95 +23,97 @@
     "HeaderFavoriteShows": "Omiljene serije",
     "HeaderFavoriteSongs": "Omiljene pjesme",
     "HeaderLiveTV": "TV uživo",
-    "HeaderNextUp": "Sljedeće je",
+    "HeaderNextUp": "Slijedi",
     "HeaderRecordingGroups": "Grupa snimka",
-    "HomeVideos": "Kućni videi",
+    "HomeVideos": "Kućni video",
     "Inherit": "Naslijedi",
     "ItemAddedWithName": "{0} je dodano u biblioteku",
-    "ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
+    "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
     "LabelIpAddressValue": "IP adresa: {0}",
     "LabelRunningTimeValue": "Vrijeme rada: {0}",
     "Latest": "Najnovije",
-    "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
-    "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
-    "MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
+    "MessageApplicationUpdated": "Jellyfin server je ažuriran",
+    "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
+    "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
     "MixedContent": "Miješani sadržaj",
     "Movies": "Filmovi",
     "Music": "Glazba",
     "MusicVideos": "Glazbeni spotovi",
     "NameInstallFailed": "{0} neuspješnih instalacija",
     "NameSeasonNumber": "Sezona {0}",
-    "NameSeasonUnknown": "Nepoznata sezona",
+    "NameSeasonUnknown": "Sezona nepoznata",
     "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
-    "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
-    "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
-    "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
-    "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
-    "NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
-    "NotificationOptionInstallationFailed": "Instalacija neuspješna",
-    "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
-    "NotificationOptionPluginError": "Dodatak otkazao",
+    "NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
+    "NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
+    "NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
+    "NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
+    "NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
+    "NotificationOptionInstallationFailed": "Instalacija nije uspjela",
+    "NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
+    "NotificationOptionPluginError": "Dodatak zakazao",
     "NotificationOptionPluginInstalled": "Dodatak instaliran",
-    "NotificationOptionPluginUninstalled": "Dodatak uklonjen",
-    "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak",
-    "NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera",
-    "NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen",
+    "NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
+    "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
+    "NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
+    "NotificationOptionTaskFailed": "Greška zakazanog zadatka",
     "NotificationOptionUserLockedOut": "Korisnik zaključan",
-    "NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
-    "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
-    "Photos": "Slike",
-    "Playlists": "Popis za reprodukciju",
+    "NotificationOptionVideoPlayback": "Reprodukcija videa započela",
+    "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
+    "Photos": "Fotografije",
+    "Playlists": "Popisi za reprodukciju",
     "Plugin": "Dodatak",
     "PluginInstalledWithName": "{0} je instalirano",
     "PluginUninstalledWithName": "{0} je deinstalirano",
     "PluginUpdatedWithName": "{0} je ažurirano",
-    "ProviderValue": "Pružitelj: {0}",
+    "ProviderValue": "Pružatelj: {0}",
     "ScheduledTaskFailedWithName": "{0} neuspjelo",
     "ScheduledTaskStartedWithName": "{0} pokrenuto",
-    "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
+    "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
     "Shows": "Serije",
     "Songs": "Pjesme",
-    "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
+    "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
     "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
-    "SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
-    "Sync": "Sink.",
-    "System": "Sistem",
+    "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
+    "Sync": "Sinkronizacija",
+    "System": "Sustav",
     "TvShows": "Serije",
     "User": "Korisnik",
-    "UserCreatedWithName": "Korisnik {0} je stvoren",
+    "UserCreatedWithName": "Korisnik {0} je kreiran",
     "UserDeletedWithName": "Korisnik {0} je obrisan",
-    "UserDownloadingItemWithValues": "{0} se preuzima {1}",
+    "UserDownloadingItemWithValues": "{0} preuzima {1}",
     "UserLockedOutWithName": "Korisnik {0} je zaključan",
-    "UserOfflineFromDevice": "{0} se odspojilo od {1}",
-    "UserOnlineFromDevice": "{0} je online od {1}",
+    "UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
+    "UserOnlineFromDevice": "{0} povezan od {1}",
     "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
-    "UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
-    "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
-    "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
+    "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
+    "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
+    "UserStoppedPlayingItemWithValues": "{0} je zavio reprodukciju {1} na {2}",
     "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
-    "ValueSpecialEpisodeName": "Specijal - {0}",
+    "ValueSpecialEpisodeName": "Posebno - {0}",
     "VersionNumber": "Verzija {0}",
-    "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
-    "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
-    "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
-    "TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
-    "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
-    "TaskCleanCache": "Očisti priručnu memoriju",
+    "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
+    "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
+    "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
+    "TaskRefreshChapterImages": "Izdvoji slike poglavlja",
+    "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
+    "TaskCleanCache": "Očisti mapu predmemorije",
     "TasksApplicationCategory": "Aplikacija",
     "TasksMaintenanceCategory": "Održavanje",
-    "TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
-    "TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
-    "TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
+    "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
+    "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
+    "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
     "TaskRefreshChannels": "Osvježi kanale",
-    "TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
-    "TaskCleanTranscode": "Očisti direktorij za transkodiranje",
-    "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
+    "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
+    "TaskCleanTranscode": "Očisti mapu transkodiranja",
+    "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
     "TaskUpdatePlugins": "Ažuriraj dodatke",
-    "TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
-    "TaskRefreshPeople": "Osvježi ljude",
-    "TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
-    "TaskCleanLogs": "Očisti direktorij sa logovima",
+    "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
+    "TaskRefreshPeople": "Osvježi osobe",
+    "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
+    "TaskCleanLogs": "Očisti mapu dnevnika zapisa",
     "TasksChannelsCategory": "Internet kanali",
-    "TasksLibraryCategory": "Biblioteka"
+    "TasksLibraryCategory": "Biblioteka",
+    "TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
+    "TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -16,7 +16,6 @@
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
     "HeaderAlbumArtists": "Album előadók",
-    "HeaderCameraUploads": "Kamera feltöltések",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderFavoriteAlbums": "Kedvenc albumok",
     "HeaderFavoriteArtists": "Kedvenc előadók",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/id.json

@@ -20,7 +20,6 @@
     "HeaderFavoriteArtists": "Artis Favorit",
     "HeaderFavoriteAlbums": "Album Favorit",
     "HeaderContinueWatching": "Lanjut Menonton",
-    "HeaderCameraUploads": "Unggahan Kamera",
     "HeaderAlbumArtists": "Album Artis",
     "Genres": "Aliran",
     "Folders": "Folder",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/is.json

@@ -13,7 +13,6 @@
     "HeaderFavoriteArtists": "Uppáhalds Listamenn",
     "HeaderFavoriteAlbums": "Uppáhalds Plötur",
     "HeaderContinueWatching": "Halda áfram að horfa",
-    "HeaderCameraUploads": "Myndavéla upphal",
     "HeaderAlbumArtists": "Höfundur plötu",
     "Genres": "Tegundir",
     "Folders": "Möppur",

+ 3 - 2
Emby.Server.Implementations/Localization/Core/it.json

@@ -16,7 +16,6 @@
     "Folders": "Cartelle",
     "Genres": "Generi",
     "HeaderAlbumArtists": "Artisti degli Album",
-    "HeaderCameraUploads": "Caricamenti Fotocamera",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteArtists": "Artisti Preferiti",
@@ -114,5 +113,7 @@
     "TasksChannelsCategory": "Canali su Internet",
     "TasksApplicationCategory": "Applicazione",
     "TasksLibraryCategory": "Libreria",
-    "TasksMaintenanceCategory": "Manutenzione"
+    "TasksMaintenanceCategory": "Manutenzione",
+    "TaskCleanActivityLog": "Attività di Registro Completate",
+    "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata."
 }

+ 4 - 3
Emby.Server.Implementations/Localization/Core/ja.json

@@ -16,7 +16,6 @@
     "Folders": "フォルダー",
     "Genres": "ジャンル",
     "HeaderAlbumArtists": "アルバムアーティスト",
-    "HeaderCameraUploads": "カメラアップロード",
     "HeaderContinueWatching": "視聴を続ける",
     "HeaderFavoriteAlbums": "お気に入りのアルバム",
     "HeaderFavoriteArtists": "お気に入りのアーティスト",
@@ -97,7 +96,7 @@
     "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
     "TaskRefreshLibrary": "メディアライブラリのスキャン",
     "TaskCleanCacheDescription": "不要なキャッシュを消去します。",
-    "TaskCleanCache": "キャッシュの掃除",
+    "TaskCleanCache": "キャッシュを消去",
     "TasksChannelsCategory": "ネットチャンネル",
     "TasksApplicationCategory": "アプリケーション",
     "TasksLibraryCategory": "ライブラリ",
@@ -113,5 +112,7 @@
     "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
     "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
     "TaskRefreshChapterImages": "チャプター画像を抽出する",
-    "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
+    "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
+    "TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
+    "TaskCleanActivityLog": "アクティビティの履歴を消去"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/kk.json

@@ -16,7 +16,6 @@
     "Folders": "Qaltalar",
     "Genres": "Janrlar",
     "HeaderAlbumArtists": "Álbom oryndaýshylary",
-    "HeaderCameraUploads": "Kameradan júktelgender",
     "HeaderContinueWatching": "Qaraýdy jalǵastyrý",
     "HeaderFavoriteAlbums": "Tańdaýly álbomdar",
     "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",

+ 4 - 3
Emby.Server.Implementations/Localization/Core/ko.json

@@ -16,7 +16,6 @@
     "Folders": "폴더",
     "Genres": "장르",
     "HeaderAlbumArtists": "앨범 아티스트",
-    "HeaderCameraUploads": "카메라 업로드",
     "HeaderContinueWatching": "계속 시청하기",
     "HeaderFavoriteAlbums": "즐겨찾는 앨범",
     "HeaderFavoriteArtists": "즐겨찾는 아티스트",
@@ -28,7 +27,7 @@
     "HeaderRecordingGroups": "녹화 그룹",
     "HomeVideos": "홈 비디오",
     "Inherit": "상속",
-    "ItemAddedWithName": "{0}가 라이브러리에 추가",
+    "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
     "ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
     "LabelIpAddressValue": "IP 주소: {0}",
     "LabelRunningTimeValue": "상영 시간: {0}",
@@ -114,5 +113,7 @@
     "TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
     "TaskCleanCache": "캐시 폴더 청소",
     "TasksChannelsCategory": "인터넷 채널",
-    "TasksLibraryCategory": "라이브러리"
+    "TasksLibraryCategory": "라이브러리",
+    "TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
+    "TaskCleanActivityLog": "활동내역청소"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/lt-LT.json

@@ -16,7 +16,6 @@
     "Folders": "Katalogai",
     "Genres": "Žanrai",
     "HeaderAlbumArtists": "Albumo atlikėjai",
-    "HeaderCameraUploads": "Kameros",
     "HeaderContinueWatching": "Žiūrėti toliau",
     "HeaderFavoriteAlbums": "Mėgstami Albumai",
     "HeaderFavoriteArtists": "Mėgstami Atlikėjai",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/lv.json

@@ -72,7 +72,6 @@
     "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
     "HeaderLiveTV": "Tiešraides TV",
     "HeaderContinueWatching": "Turpināt Skatīšanos",
-    "HeaderCameraUploads": "Kameras augšupielādes",
     "HeaderAlbumArtists": "Albumu Izpildītāji",
     "Genres": "Žanri",
     "Folders": "Mapes",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/mk.json

@@ -51,7 +51,6 @@
     "HeaderFavoriteArtists": "Омилени Изведувачи",
     "HeaderFavoriteAlbums": "Омилени Албуми",
     "HeaderContinueWatching": "Продолжи со гледање",
-    "HeaderCameraUploads": "Поставувања од камера",
     "HeaderAlbumArtists": "Изведувачи од Албуми",
     "Genres": "Жанрови",
     "Folders": "Папки",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/mr.json

@@ -54,7 +54,6 @@
     "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
     "HomeVideos": "घरचे व्हिडीयो",
     "HeaderRecordingGroups": "रेकॉर्डिंग गट",
-    "HeaderCameraUploads": "कॅमेरा अपलोड",
     "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
     "Application": "अ‍ॅप्लिकेशन",
     "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ms.json

@@ -16,7 +16,6 @@
     "Folders": "Fail-fail",
     "Genres": "Genre-genre",
     "HeaderAlbumArtists": "Album Artis-artis",
-    "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderContinueWatching": "Terus Menonton",
     "HeaderFavoriteAlbums": "Album-album Kegemaran",
     "HeaderFavoriteArtists": "Artis-artis Kegemaran",

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä