瀏覽代碼

Merge branch 'master' into network-rewrite

Shadowghost 2 年之前
父節點
當前提交
ef085483b2
共有 93 個文件被更改,包括 345 次插入654 次删除
  1. 4 2
      .ci/azure-pipelines-package.yml
  2. 4 4
      .github/workflows/codeql-analysis.yml
  3. 3 3
      .github/workflows/commands.yml
  4. 2 2
      .github/workflows/openapi.yml
  5. 1 1
      Dockerfile
  6. 1 1
      Dockerfile.arm
  7. 1 1
      Dockerfile.arm64
  8. 108 107
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  9. 1 2
      Emby.Server.Implementations/Dto/DtoService.cs
  10. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  11. 3 6
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  12. 1 2
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  13. 1 2
      Emby.Server.Implementations/IO/FileRefresher.cs
  14. 1 1
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  15. 1 2
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  16. 1 2
      Emby.Server.Implementations/Images/PlaylistImageProvider.cs
  17. 27 30
      Emby.Server.Implementations/Library/LibraryManager.cs
  18. 8 10
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  19. 1 1
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  20. 1 2
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  21. 15 15
      Emby.Server.Implementations/Localization/Core/nl.json
  22. 11 3
      Emby.Server.Implementations/Localization/Core/ur_PK.json
  23. 0 18
      Emby.Server.Implementations/ServerApplicationPaths.cs
  24. 4 4
      Jellyfin.Api/Controllers/FilterController.cs
  25. 0 252
      Jellyfin.Api/Controllers/ImageByNameController.cs
  26. 5 10
      Jellyfin.Api/Controllers/LibraryController.cs
  27. 2 4
      Jellyfin.Api/Controllers/MoviesController.cs
  28. 20 6
      Jellyfin.Api/Controllers/QuickConnectController.cs
  29. 2 8
      Jellyfin.Api/Controllers/UserController.cs
  30. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  31. 1 9
      Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
  32. 5 5
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  33. 2 2
      Jellyfin.Server/Jellyfin.Server.csproj
  34. 8 8
      Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
  35. 2 5
      Jellyfin.Server/Program.cs
  36. 5 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  37. 1 2
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  38. 1 1
      MediaBrowser.Controller/Entities/TV/Series.cs
  39. 0 18
      MediaBrowser.Controller/IServerApplicationPaths.cs
  40. 8 2
      MediaBrowser.Controller/Library/ILibraryManager.cs
  41. 1 2
      MediaBrowser.Controller/Library/NameExtensions.cs
  42. 1 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  43. 1 1
      MediaBrowser.Controller/Providers/IProviderManager.cs
  44. 4 16
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  45. 2 1
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  46. 2 0
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  47. 4 3
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  48. 1 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  49. 2 2
      MediaBrowser.Providers/Manager/MetadataService.cs
  50. 8 16
      MediaBrowser.Providers/Manager/ProviderManager.cs
  51. 1 2
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  52. 2 2
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  53. 2 2
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
  54. 1 1
      debian/control
  55. 1 1
      debian/postinst
  56. 1 1
      deployment/Dockerfile.centos.amd64
  57. 1 1
      deployment/Dockerfile.debian.amd64
  58. 1 1
      deployment/Dockerfile.debian.arm64
  59. 1 1
      deployment/Dockerfile.debian.armhf
  60. 1 1
      deployment/Dockerfile.docker.amd64
  61. 1 1
      deployment/Dockerfile.docker.arm64
  62. 1 1
      deployment/Dockerfile.docker.armhf
  63. 1 1
      deployment/Dockerfile.fedora.amd64
  64. 1 1
      deployment/Dockerfile.linux.amd64
  65. 1 1
      deployment/Dockerfile.linux.amd64-musl
  66. 1 1
      deployment/Dockerfile.linux.arm64
  67. 1 1
      deployment/Dockerfile.linux.armhf
  68. 1 1
      deployment/Dockerfile.linux.musl-linux-arm64
  69. 1 1
      deployment/Dockerfile.macos.amd64
  70. 1 1
      deployment/Dockerfile.macos.arm64
  71. 1 1
      deployment/Dockerfile.portable
  72. 1 1
      deployment/Dockerfile.ubuntu.amd64
  73. 1 1
      deployment/Dockerfile.ubuntu.arm64
  74. 1 1
      deployment/Dockerfile.ubuntu.armhf
  75. 1 1
      deployment/Dockerfile.windows.amd64
  76. 2 2
      deployment/build.centos.amd64
  77. 2 2
      deployment/build.debian.amd64
  78. 2 2
      deployment/build.debian.arm64
  79. 2 2
      deployment/build.debian.armhf
  80. 2 2
      deployment/build.fedora.amd64
  81. 2 2
      deployment/build.ubuntu.amd64
  82. 2 2
      deployment/build.ubuntu.arm64
  83. 2 2
      deployment/build.ubuntu.armhf
  84. 1 1
      deployment/build.windows.amd64
  85. 2 2
      fedora/jellyfin.spec
  86. 1 1
      tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
  87. 1 1
      tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
  88. 1 1
      tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs
  89. 1 1
      tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
  90. 1 1
      tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs
  91. 1 1
      tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
  92. 2 2
      tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
  93. 1 1
      tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs

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

@@ -32,8 +32,10 @@ jobs:
         BuildConfiguration: linux.armhf
         BuildConfiguration: linux.armhf
       Windows.amd64:
       Windows.amd64:
         BuildConfiguration: windows.amd64
         BuildConfiguration: windows.amd64
-      MacOS:
-        BuildConfiguration: macos
+      MacOS.amd64:
+        BuildConfiguration: macos.amd64
+      MacOS.arm64:
+        BuildConfiguration: macos.arm64
       Portable:
       Portable:
         BuildConfiguration: portable
         BuildConfiguration: portable
 
 

+ 4 - 4
.github/workflows/codeql-analysis.yml

@@ -20,18 +20,18 @@ jobs:
 
 
     steps:
     steps:
     - name: Checkout repository
     - name: Checkout repository
-      uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+      uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
     - name: Setup .NET
     - name: Setup .NET
       uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
       uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
       with:
       with:
         dotnet-version: '7.0.x'
         dotnet-version: '7.0.x'
 
 
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # v2
+      uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         queries: +security-extended
         queries: +security-extended
     - name: Autobuild
     - name: Autobuild
-      uses: github/codeql-action/autobuild@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # v2
+      uses: github/codeql-action/autobuild@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # v2
+      uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2

+ 3 - 3
.github/workflows/commands.yml

@@ -24,13 +24,13 @@ jobs:
           reactions: '+1'
           reactions: '+1'
 
 
       - name: Checkout the latest code
       - name: Checkout the latest code
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
           fetch-depth: 0
 
 
       - name: Automatic Rebase
       - name: Automatic Rebase
-        uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
+        uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
 
 
@@ -51,7 +51,7 @@ jobs:
           reactions: eyes
           reactions: eyes
 
 
       - name: Checkout the latest code
       - name: Checkout the latest code
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
           fetch-depth: 0

+ 2 - 2
.github/workflows/openapi.yml

@@ -14,7 +14,7 @@ jobs:
     permissions: read-all
     permissions: read-all
     steps:
     steps:
       - name: Checkout repository
       - name: Checkout repository
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
         with:
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +39,7 @@ jobs:
     permissions: read-all
     permissions: read-all
     steps:
     steps:
       - name: Checkout repository
       - name: Checkout repository
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
         with:
         with:
           ref: ${{ github.base_ref }}
           ref: ${{ github.base_ref }}
       - name: Setup .NET
       - name: Setup .NET

+ 1 - 1
Dockerfile

@@ -2,7 +2,7 @@
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=6.0
+ARG DOTNET_VERSION=7.0
 
 
 FROM node:lts-alpine as web-builder
 FROM node:lts-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 ARG JELLYFIN_WEB_VERSION=master

+ 1 - 1
Dockerfile.arm

@@ -2,7 +2,7 @@
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=6.0
+ARG DOTNET_VERSION=7.0
 
 
 
 
 FROM node:lts-alpine as web-builder
 FROM node:lts-alpine as web-builder

+ 1 - 1
Dockerfile.arm64

@@ -2,7 +2,7 @@
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=6.0
+ARG DOTNET_VERSION=7.0
 
 
 
 
 FROM node:lts-alpine as web-builder
 FROM node:lts-alpine as web-builder

+ 108 - 107
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -5,9 +5,11 @@
 using System;
 using System;
 using System.Buffers.Text;
 using System.Buffers.Text;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Runtime.CompilerServices;
 using System.Text;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
@@ -2461,6 +2463,7 @@ namespace Emby.Server.Implementations.Data
                 if (query.SearchTerm.Length > 1)
                 if (query.SearchTerm.Length > 1)
                 {
                 {
                     builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
                     builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
+                    builder.Append("+ ((Tags not null and Tags like @SearchTermContains) * 5)");
                 }
                 }
 
 
                 builder.Append(") as SearchScore");
                 builder.Append(") as SearchScore");
@@ -2557,8 +2560,6 @@ namespace Emby.Server.Implementations.Data
 
 
             CheckDisposed();
             CheckDisposed();
 
 
-            var now = DateTime.UtcNow;
-
             // Hack for right now since we currently don't support filtering out these duplicates within a query
             // Hack for right now since we currently don't support filtering out these duplicates within a query
             if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
             if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
             {
             {
@@ -2580,28 +2581,24 @@ namespace Emby.Server.Implementations.Data
             }
             }
 
 
             var commandText = commandTextBuilder.ToString();
             var commandText = commandTextBuilder.ToString();
-            int count;
+
+            using (new QueryTimeLogger(Logger, commandText))
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
+            using (var statement = PrepareStatement(connection, commandText))
             {
             {
-                using (var statement = PrepareStatement(connection, commandText))
+                if (EnableJoinUserData(query))
                 {
                 {
-                    if (EnableJoinUserData(query))
-                    {
-                        statement.TryBind("@UserId", query.User.InternalId);
-                    }
+                    statement.TryBind("@UserId", query.User.InternalId);
+                }
 
 
-                    BindSimilarParams(query, statement);
-                    BindSearchParams(query, statement);
+                BindSimilarParams(query, statement);
+                BindSearchParams(query, statement);
 
 
-                    // Running this again will bind the params
-                    GetWhereClauses(query, statement);
+                // Running this again will bind the params
+                GetWhereClauses(query, statement);
 
 
-                    count = statement.ExecuteQuery().SelectScalarInt().First();
-                }
+                return statement.ExecuteQuery().SelectScalarInt().First();
             }
             }
-
-            LogQueryTime("GetCount", commandText, now);
-            return count;
         }
         }
 
 
         public List<BaseItem> GetItemList(InternalItemsQuery query)
         public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -2610,8 +2607,6 @@ namespace Emby.Server.Implementations.Data
 
 
             CheckDisposed();
             CheckDisposed();
 
 
-            var now = DateTime.UtcNow;
-
             // Hack for right now since we currently don't support filtering out these duplicates within a query
             // Hack for right now since we currently don't support filtering out these duplicates within a query
             if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
             if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
             {
             {
@@ -2655,61 +2650,58 @@ namespace Emby.Server.Implementations.Data
 
 
             var commandText = commandTextBuilder.ToString();
             var commandText = commandTextBuilder.ToString();
             var items = new List<BaseItem>();
             var items = new List<BaseItem>();
+            using (new QueryTimeLogger(Logger, commandText))
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
+            using (var statement = PrepareStatement(connection, commandText))
             {
             {
-                using (var statement = PrepareStatement(connection, commandText))
+                if (EnableJoinUserData(query))
                 {
                 {
-                    if (EnableJoinUserData(query))
-                    {
-                        statement.TryBind("@UserId", query.User.InternalId);
-                    }
+                    statement.TryBind("@UserId", query.User.InternalId);
+                }
 
 
-                    BindSimilarParams(query, statement);
-                    BindSearchParams(query, statement);
+                BindSimilarParams(query, statement);
+                BindSearchParams(query, statement);
 
 
-                    // Running this again will bind the params
-                    GetWhereClauses(query, statement);
+                // Running this again will bind the params
+                GetWhereClauses(query, statement);
 
 
-                    var hasEpisodeAttributes = HasEpisodeAttributes(query);
-                    var hasServiceName = HasServiceName(query);
-                    var hasProgramAttributes = HasProgramAttributes(query);
-                    var hasStartDate = HasStartDate(query);
-                    var hasTrailerTypes = HasTrailerTypes(query);
-                    var hasArtistFields = HasArtistFields(query);
-                    var hasSeriesFields = HasSeriesFields(query);
+                var hasEpisodeAttributes = HasEpisodeAttributes(query);
+                var hasServiceName = HasServiceName(query);
+                var hasProgramAttributes = HasProgramAttributes(query);
+                var hasStartDate = HasStartDate(query);
+                var hasTrailerTypes = HasTrailerTypes(query);
+                var hasArtistFields = HasArtistFields(query);
+                var hasSeriesFields = HasSeriesFields(query);
 
 
-                    foreach (var row in statement.ExecuteQuery())
+                foreach (var row in statement.ExecuteQuery())
+                {
+                    var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+                    if (item is not null)
                     {
                     {
-                        var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
-                        if (item is not null)
-                        {
-                            items.Add(item);
-                        }
+                        items.Add(item);
                     }
                     }
                 }
                 }
+            }
 
 
-                // Hack for right now since we currently don't support filtering out these duplicates within a query
-                if (query.EnableGroupByMetadataKey)
+            // Hack for right now since we currently don't support filtering out these duplicates within a query
+            if (query.EnableGroupByMetadataKey)
+            {
+                var limit = query.Limit ?? int.MaxValue;
+                limit -= 4;
+                var newList = new List<BaseItem>();
+
+                foreach (var item in items)
                 {
                 {
-                    var limit = query.Limit ?? int.MaxValue;
-                    limit -= 4;
-                    var newList = new List<BaseItem>();
+                    AddItem(newList, item);
 
 
-                    foreach (var item in items)
+                    if (newList.Count >= limit)
                     {
                     {
-                        AddItem(newList, item);
-
-                        if (newList.Count >= limit)
-                        {
-                            break;
-                        }
+                        break;
                     }
                     }
-
-                    items = newList;
                 }
                 }
-            }
 
 
-            LogQueryTime("GetItemList", commandText, now);
+                items = newList;
+            }
 
 
             return items;
             return items;
         }
         }
@@ -2762,26 +2754,6 @@ namespace Emby.Server.Implementations.Data
             items.Add(newItem);
             items.Add(newItem);
         }
         }
 
 
-        private void LogQueryTime(string methodName, string commandText, DateTime startDate)
-        {
-            var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
-
-#if DEBUG
-            const int SlowThreshold = 100;
-#else
-            const int SlowThreshold = 10;
-#endif
-
-            if (elapsed >= SlowThreshold)
-            {
-                Logger.LogDebug(
-                    "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
-                    methodName,
-                    elapsed,
-                    commandText);
-            }
-        }
-
         public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
         public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
         {
         {
             ArgumentNullException.ThrowIfNull(query);
             ArgumentNullException.ThrowIfNull(query);
@@ -2797,8 +2769,6 @@ namespace Emby.Server.Implementations.Data
                     returnList);
                     returnList);
             }
             }
 
 
-            var now = DateTime.UtcNow;
-
             // Hack for right now since we currently don't support filtering out these duplicates within a query
             // Hack for right now since we currently don't support filtering out these duplicates within a query
             if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
             if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
             {
             {
@@ -2899,6 +2869,7 @@ namespace Emby.Server.Implementations.Data
 
 
                         if (!isReturningZeroItems)
                         if (!isReturningZeroItems)
                         {
                         {
+                            using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
                             using (var statement = itemQueryStatement)
                             using (var statement = itemQueryStatement)
                             {
                             {
                                 if (EnableJoinUserData(query))
                                 if (EnableJoinUserData(query))
@@ -2929,13 +2900,11 @@ namespace Emby.Server.Implementations.Data
                                     }
                                     }
                                 }
                                 }
                             }
                             }
-
-                            LogQueryTime("GetItems.ItemQuery", itemQuery, now);
                         }
                         }
 
 
-                        now = DateTime.UtcNow;
                         if (query.EnableTotalRecordCount)
                         if (query.EnableTotalRecordCount)
                         {
                         {
+                            using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
                             using (var statement = totalRecordCountQueryStatement)
                             using (var statement = totalRecordCountQueryStatement)
                             {
                             {
                                 if (EnableJoinUserData(query))
                                 if (EnableJoinUserData(query))
@@ -2951,8 +2920,6 @@ namespace Emby.Server.Implementations.Data
 
 
                                 result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
                                 result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
                             }
                             }
-
-                            LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
                         }
                         }
                     },
                     },
                     ReadTransactionMode);
                     ReadTransactionMode);
@@ -3170,8 +3137,6 @@ namespace Emby.Server.Implementations.Data
 
 
             CheckDisposed();
             CheckDisposed();
 
 
-            var now = DateTime.UtcNow;
-
             var columns = new List<string> { "guid" };
             var columns = new List<string> { "guid" };
             SetFinalColumnsToSelect(query, columns);
             SetFinalColumnsToSelect(query, columns);
             var commandTextBuilder = new StringBuilder("select ", 256)
             var commandTextBuilder = new StringBuilder("select ", 256)
@@ -3208,29 +3173,27 @@ namespace Emby.Server.Implementations.Data
 
 
             var commandText = commandTextBuilder.ToString();
             var commandText = commandTextBuilder.ToString();
             var list = new List<Guid>();
             var list = new List<Guid>();
+            using (new QueryTimeLogger(Logger, commandText))
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
+            using (var statement = PrepareStatement(connection, commandText))
             {
             {
-                using (var statement = PrepareStatement(connection, commandText))
+                if (EnableJoinUserData(query))
                 {
                 {
-                    if (EnableJoinUserData(query))
-                    {
-                        statement.TryBind("@UserId", query.User.InternalId);
-                    }
+                    statement.TryBind("@UserId", query.User.InternalId);
+                }
 
 
-                    BindSimilarParams(query, statement);
-                    BindSearchParams(query, statement);
+                BindSimilarParams(query, statement);
+                BindSearchParams(query, statement);
 
 
-                    // Running this again will bind the params
-                    GetWhereClauses(query, statement);
+                // Running this again will bind the params
+                GetWhereClauses(query, statement);
 
 
-                    foreach (var row in statement.ExecuteQuery())
-                    {
-                        list.Add(row[0].ReadGuidFromBlob());
-                    }
+                foreach (var row in statement.ExecuteQuery())
+                {
+                    list.Add(row[0].ReadGuidFromBlob());
                 }
                 }
             }
             }
 
 
-            LogQueryTime("GetItemList", commandText, now);
             return list;
             return list;
         }
         }
 
 
@@ -5111,8 +5074,6 @@ AND Type = @InternalPersonType)");
         {
         {
             CheckDisposed();
             CheckDisposed();
 
 
-            var now = DateTime.UtcNow;
-
             var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
             var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
             if (itemValueTypes.Length == 1)
             if (itemValueTypes.Length == 1)
             {
             {
@@ -5144,6 +5105,7 @@ AND Type = @InternalPersonType)");
             var commandText = stringBuilder.ToString();
             var commandText = stringBuilder.ToString();
 
 
             var list = new List<string>();
             var list = new List<string>();
+            using (new QueryTimeLogger(Logger, commandText))
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
             using (var statement = PrepareStatement(connection, commandText))
             using (var statement = PrepareStatement(connection, commandText))
             {
             {
@@ -5156,7 +5118,6 @@ AND Type = @InternalPersonType)");
                 }
                 }
             }
             }
 
 
-            LogQueryTime("GetItemValueNames", commandText, now);
             return list;
             return list;
         }
         }
 
 
@@ -5171,8 +5132,6 @@ AND Type = @InternalPersonType)");
 
 
             CheckDisposed();
             CheckDisposed();
 
 
-            var now = DateTime.UtcNow;
-
             var typeClause = itemValueTypes.Length == 1 ?
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0]) :
                 ("Type=" + itemValueTypes[0]) :
                 ("Type in (" + string.Join(',', itemValueTypes) + ")");
                 ("Type in (" + string.Join(',', itemValueTypes) + ")");
@@ -5346,6 +5305,7 @@ AND Type = @InternalPersonType)");
 
 
             var list = new List<(BaseItem, ItemCounts)>();
             var list = new List<(BaseItem, ItemCounts)>();
             var result = new QueryResult<(BaseItem, ItemCounts)>();
             var result = new QueryResult<(BaseItem, ItemCounts)>();
+            using (new QueryTimeLogger(Logger, commandText))
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
             {
             {
                 connection.RunInTransaction(
                 connection.RunInTransaction(
@@ -5419,8 +5379,6 @@ AND Type = @InternalPersonType)");
                     ReadTransactionMode);
                     ReadTransactionMode);
             }
             }
 
 
-            LogQueryTime("GetItemValues", commandText, now);
-
             if (result.TotalRecordCount == 0)
             if (result.TotalRecordCount == 0)
             {
             {
                 result.TotalRecordCount = list.Count;
                 result.TotalRecordCount = list.Count;
@@ -6245,5 +6203,48 @@ AND Type = @InternalPersonType)");
 
 
             return item;
             return item;
         }
         }
+
+#nullable enable
+
+        private readonly struct QueryTimeLogger : IDisposable
+        {
+            private readonly ILogger _logger;
+            private readonly string _commandText;
+            private readonly string _methodName;
+            private readonly long _startTimestamp;
+
+            public QueryTimeLogger(ILogger logger, string commandText, [CallerMemberName] string methodName = "")
+            {
+                _logger = logger;
+                _commandText = commandText;
+                _methodName = methodName;
+                _startTimestamp = logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : -1;
+            }
+
+            public void Dispose()
+            {
+                if (_startTimestamp == -1)
+                {
+                    return;
+                }
+
+                var elapsedMs = Stopwatch.GetElapsedTime(_startTimestamp).TotalMilliseconds;
+
+#if DEBUG
+                const int SlowThreshold = 100;
+#else
+                const int SlowThreshold = 10;
+#endif
+
+                if (elapsedMs >= SlowThreshold)
+                {
+                    _logger.LogDebug(
+                        "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
+                        _methodName,
+                        elapsedMs,
+                        _commandText);
+                }
+            }
+        }
     }
     }
 }
 }

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

@@ -574,8 +574,7 @@ namespace Emby.Server.Implementations.Dto
                 .Where(i => user is null ?
                 .Where(i => user is null ?
                     true :
                     true :
                     i.IsVisible(user))
                     i.IsVisible(user))
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
+                .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
                 .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
                 .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
 
 
             for (var i = 0; i < people.Count; i++)
             for (var i = 0; i < people.Count; i++)

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

@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.1" />
     <PackageReference Include="Mono.Nat" Version="3.0.4" />
     <PackageReference Include="Mono.Nat" Version="3.0.4" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />

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

@@ -282,19 +282,16 @@ namespace Emby.Server.Implementations.EntryPoints
             {
             {
                 // Remove dupes in case some were saved multiple times
                 // Remove dupes in case some were saved multiple times
                 var foldersAddedTo = _foldersAddedTo
                 var foldersAddedTo = _foldersAddedTo
-                                        .GroupBy(x => x.Id)
-                                        .Select(x => x.First())
+                                        .DistinctBy(x => x.Id)
                                         .ToList();
                                         .ToList();
 
 
                 var foldersRemovedFrom = _foldersRemovedFrom
                 var foldersRemovedFrom = _foldersRemovedFrom
-                                            .GroupBy(x => x.Id)
-                                            .Select(x => x.First())
+                                            .DistinctBy(x => x.Id)
                                             .ToList();
                                             .ToList();
 
 
                 var itemsUpdated = _itemsUpdated
                 var itemsUpdated = _itemsUpdated
                                     .Where(i => !_itemsAdded.Contains(i))
                                     .Where(i => !_itemsAdded.Contains(i))
-                                    .GroupBy(x => x.Id)
-                                    .Select(x => x.First())
+                                    .DistinctBy(x => x.Id)
                                     .ToList();
                                     .ToList();
 
 
                 SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
                 SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();

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

@@ -123,8 +123,7 @@ namespace Emby.Server.Implementations.EntryPoints
             var user = _userManager.GetUserById(userId);
             var user = _userManager.GetUserById(userId);
 
 
             var dtoList = changedItems
             var dtoList = changedItems
-                .GroupBy(x => x.Id)
-                .Select(x => x.First())
+                .DistinctBy(x => x.Id)
                 .Select(i =>
                 .Select(i =>
                 {
                 {
                     var dto = _userDataManager.GetUserDataDto(i, user);
                     var dto = _userDataManager.GetUserDataDto(i, user);

+ 1 - 2
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -133,8 +133,7 @@ namespace Emby.Server.Implementations.IO
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(GetAffectedBaseItem)
                 .Select(GetAffectedBaseItem)
                 .Where(item => item is not null)
                 .Where(item => item is not null)
-                .GroupBy(x => x!.Id) // Removed null values in the previous .Where()
-                .Select(x => x.First())!;
+                .DistinctBy(x => x!.Id)!;  // Removed null values in the previous .Where()
 
 
             foreach (var item in itemsToRefresh)
             foreach (var item in itemsToRefresh)
             {
             {

+ 1 - 1
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.IO
                 .OfType<Folder>()
                 .OfType<Folder>()
                 .SelectMany(f => f.PhysicalLocations)
                 .SelectMany(f => f.PhysicalLocations)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i);
+                .Order();
 
 
             foreach (var path in paths)
             foreach (var path in paths)
             {
             {

+ 1 - 2
Emby.Server.Implementations/Images/DynamicImageProvider.cs

@@ -81,8 +81,7 @@ namespace Emby.Server.Implementations.Images
                 }
                 }
 
 
                 return i;
                 return i;
-            }).GroupBy(x => x.Id)
-            .Select(x => x.First());
+            }).DistinctBy(x => x.Id);
 
 
             List<BaseItem> returnItems;
             List<BaseItem> returnItems;
             if (isUsingCollectionStrip)
             if (isUsingCollectionStrip)

+ 1 - 2
Emby.Server.Implementations/Images/PlaylistImageProvider.cs

@@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Images
                     return null;
                     return null;
                 })
                 })
                 .Where(i => i is not null)
                 .Where(i => i is not null)
-                .GroupBy(x => x.Id)
-                .Select(x => x.First())
+                .DistinctBy(x => x.Id)
                 .ToList();
                 .ToList();
         }
         }
     }
     }

+ 27 - 30
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1154,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
                 .ToList();
                 .ToList();
         }
         }
 
 
-        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, Dictionary<Guid, Guid> refreshQueue)
+        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid> refreshQueue)
         {
         {
             var info = new VirtualFolderInfo
             var info = new VirtualFolderInfo
             {
             {
@@ -1175,29 +1175,29 @@ namespace Emby.Server.Implementations.Library
                         }
                         }
                     })
                     })
                     .Where(i => i is not null)
                     .Where(i => i is not null)
-                    .OrderBy(i => i)
+                    .Order()
                     .ToArray(),
                     .ToArray(),
 
 
                 CollectionType = GetCollectionType(dir)
                 CollectionType = GetCollectionType(dir)
             };
             };
 
 
             var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase));
             var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase));
-
-            if (libraryFolder is not null && libraryFolder.HasImage(ImageType.Primary))
-            {
-                info.PrimaryImageItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
-            }
-
             if (libraryFolder is not null)
             if (libraryFolder is not null)
             {
             {
-                info.ItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
+                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
+                info.ItemId = libraryFolderId;
+                if (libraryFolder.HasImage(ImageType.Primary))
+                {
+                    info.PrimaryImageItemId = libraryFolderId;
+                }
+
                 info.LibraryOptions = GetLibraryOptions(libraryFolder);
                 info.LibraryOptions = GetLibraryOptions(libraryFolder);
 
 
                 if (refreshQueue is not null)
                 if (refreshQueue is not null)
                 {
                 {
                     info.RefreshProgress = libraryFolder.GetRefreshProgress();
                     info.RefreshProgress = libraryFolder.GetRefreshProgress();
 
 
-                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.ContainsKey(libraryFolder.Id) ? "Queued" : "Idle";
+                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.Id) ? "Queued" : "Idle";
                 }
                 }
             }
             }
 
 
@@ -1998,39 +1998,36 @@ namespace Emby.Server.Implementations.Library
         }
         }
 
 
         public List<Folder> GetCollectionFolders(BaseItem item)
         public List<Folder> GetCollectionFolders(BaseItem item)
+        {
+            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
+        }
+
+        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
         {
         {
             while (item is not null)
             while (item is not null)
             {
             {
                 var parent = item.GetParent();
                 var parent = item.GetParent();
 
 
-                if (parent is null || parent is AggregateFolder)
+                if (parent is AggregateFolder)
                 {
                 {
                     break;
                     break;
                 }
                 }
 
 
-                item = parent;
-            }
-
-            if (item is null)
-            {
-                return new List<Folder>();
-            }
-
-            return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
-        }
+                if (parent is null)
+                {
+                    var owner = item.GetOwner();
 
 
-        public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
-        {
-            while (item is not null)
-            {
-                var parent = item.GetParent();
+                    if (owner is null)
+                    {
+                        break;
+                    }
 
 
-                if (parent is null || parent is AggregateFolder)
+                    item = owner;
+                }
+                else
                 {
                 {
-                    break;
+                    item = parent;
                 }
                 }
-
-                item = parent;
             }
             }
 
 
             if (item is null)
             if (item is null)

+ 8 - 10
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -163,17 +163,15 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     try
                     try
                     {
                     {
                         // use disc-utils, both DVDs and BDs use UDF filesystem
                         // use disc-utils, both DVDs and BDs use UDF filesystem
-                        using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
-                        using (UdfReader udfReader = new UdfReader(videoFileStream))
+                        using var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read, FileShare.Read);
+                        using UdfReader udfReader = new UdfReader(videoFileStream);
+                        if (udfReader.DirectoryExists("VIDEO_TS"))
                         {
                         {
-                            if (udfReader.DirectoryExists("VIDEO_TS"))
-                            {
-                                video.IsoType = IsoType.Dvd;
-                            }
-                            else if (udfReader.DirectoryExists("BDMV"))
-                            {
-                                video.IsoType = IsoType.BluRay;
-                            }
+                            video.IsoType = IsoType.Dvd;
+                        }
+                        else if (udfReader.DirectoryExists("BDMV"))
+                        {
+                            video.IsoType = IsoType.BluRay;
                         }
                         }
                     }
                     }
                     catch (Exception ex)
                     catch (Exception ex)

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 }
                 }
 
 
                 return false;
                 return false;
-            }).OrderBy(i => i).ToList();
+            }).Order().ToList();
 
 
             // If different video types were found, don't allow this
             // If different video types were found, don't allow this
             if (videoTypes.Distinct().Count() > 1)
             if (videoTypes.Distinct().Count() > 1)

+ 1 - 2
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -2392,8 +2392,7 @@ namespace Emby.Server.Implementations.LiveTv
                 .Select(i => _libraryManager.FindByPath(i, true))
                 .Select(i => _libraryManager.FindByPath(i, true))
                 .Where(i => i is not null && i.IsVisibleStandalone(user))
                 .Where(i => i is not null && i.IsVisibleStandalone(user))
                 .SelectMany(i => _libraryManager.GetCollectionFolders(i))
                 .SelectMany(i => _libraryManager.GetCollectionFolders(i))
-                .GroupBy(x => x.Id)
-                .Select(x => x.First())
+                .DistinctBy(x => x.Id)
                 .OrderBy(i => i.SortName)
                 .OrderBy(i => i.SortName)
                 .ToList();
                 .ToList();
 
 

+ 15 - 15
Emby.Server.Implementations/Localization/Core/nl.json

@@ -92,37 +92,37 @@
     "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
     "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
     "ValueSpecialEpisodeName": "Speciaal - {0}",
     "ValueSpecialEpisodeName": "Speciaal - {0}",
     "VersionNumber": "Versie {0}",
     "VersionNumber": "Versie {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar missende ondertiteling gebaseerd op metadata configuratie.",
-    "TaskDownloadMissingSubtitles": "Download missende ondertiteling",
+    "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
+    "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
     "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
     "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
     "TaskRefreshChannels": "Vernieuw Kanalen",
     "TaskRefreshChannels": "Vernieuw Kanalen",
     "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
     "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
-    "TaskCleanLogs": "Log Folder Opschonen",
-    "TaskCleanTranscode": "Transcode Folder Opschonen",
-    "TaskUpdatePluginsDescription": "Download en installeert updates voor plugins waar automatisch updaten aan staat.",
-    "TaskUpdatePlugins": "Update Plugins",
+    "TaskCleanLogs": "Logboekmap opschonen",
+    "TaskCleanTranscode": "Transcoderingsmap opschonen",
+    "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
+    "TaskUpdatePlugins": "Plug-ins bijwerken",
     "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
     "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
-    "TaskRefreshPeople": "Vernieuw Personen",
+    "TaskRefreshPeople": "Personen vernieuwen",
     "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
     "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
-    "TaskRefreshLibraryDescription": "Scant de media bibliotheek voor nieuwe bestanden en vernieuwt de metadata.",
-    "TaskRefreshLibrary": "Scan Media Bibliotheek",
+    "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
+    "TaskRefreshLibrary": "Mediabibliotheek scannen",
     "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
     "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
-    "TaskRefreshChapterImages": "Hoofdstukafbeeldingen Uitpakken",
+    "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
     "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
     "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
-    "TaskCleanCache": "Cache Folder Opschonen",
+    "TaskCleanCache": "Cache-map opschonen",
     "TasksChannelsCategory": "Internet Kanalen",
     "TasksChannelsCategory": "Internet Kanalen",
-    "TasksApplicationCategory": "Applicatie",
+    "TasksApplicationCategory": "Toepassing",
     "TasksLibraryCategory": "Bibliotheek",
     "TasksLibraryCategory": "Bibliotheek",
     "TasksMaintenanceCategory": "Onderhoud",
     "TasksMaintenanceCategory": "Onderhoud",
     "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
     "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
-    "TaskCleanActivityLog": "Leeg activiteiten logboek",
+    "TaskCleanActivityLog": "Activiteitenlogboek legen",
     "Undefined": "Niet gedefinieerd",
     "Undefined": "Niet gedefinieerd",
     "Forced": "Geforceerd",
     "Forced": "Geforceerd",
     "Default": "Standaard",
     "Default": "Standaard",
     "TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
     "TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
     "TaskOptimizeDatabase": "Database optimaliseren",
     "TaskOptimizeDatabase": "Database optimaliseren",
-    "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS afspeellijsten te maken. Dit kan lang duren.",
-    "TaskKeyframeExtractor": "Keyframe Extractor",
+    "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
+    "TaskKeyframeExtractor": "Keyframe-uitpakker",
     "External": "Extern",
     "External": "Extern",
     "HearingImpaired": "Slechthorend"
     "HearingImpaired": "Slechthorend"
 }
 }

+ 11 - 3
Emby.Server.Implementations/Localization/Core/ur_PK.json

@@ -102,8 +102,8 @@
     "LabelIpAddressValue": "آئ پی ایڈریس {0}",
     "LabelIpAddressValue": "آئ پی ایڈریس {0}",
     "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
     "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
     "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
     "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
-    "Inherit": "وراثت میں",
-    "HomeVideos": "ہوم ویڈیو",
+    "Inherit": "وراثت",
+    "HomeVideos": "ہوم ویڈیوز",
     "HeaderRecordingGroups": "ریکارڈنگ گروپس",
     "HeaderRecordingGroups": "ریکارڈنگ گروپس",
     "FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
     "FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
     "DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
     "DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
@@ -115,5 +115,13 @@
     "AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
     "AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
     "Forced": "جَبری",
     "Forced": "جَبری",
     "Undefined": "غير وضاحتى",
     "Undefined": "غير وضاحتى",
-    "Default": "طے شدہ"
+    "Default": "طے شدہ",
+    "TaskKeyframeExtractorDescription": "زیادہ درست HLS پلے لسٹس بنانے کے لیے ویڈیو فائلوں سے کلیدی فریم نکالتا ہے۔ یہ کام طویل عرصے تک چل سکتا ہے۔",
+    "TaskOptimizeDatabase": "ڈیٹا بیس کو بہتر بنائیں",
+    "TaskOptimizeDatabaseDescription": "ڈیٹا بیس کو کمپیکٹ کرتا ہے اور خالی جگہ کو چھوٹا کرتا ہے۔ لائبریری کو اسکین کرنے یا دیگر تبدیلیاں کرنے کے بعد اس کام کو چلانے سے کارکردگی بہتر ہو سکتی ہے۔",
+    "TaskKeyframeExtractor": "کی فریم ایکسٹریکٹر",
+    "TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔",
+    "External": "بیرونی",
+    "HearingImpaired": "قوت سماعت سے محروم",
+    "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں۔"
 }
 }

+ 0 - 18
Emby.Server.Implementations/ServerApplicationPaths.cs

@@ -82,24 +82,6 @@ namespace Emby.Server.Implementations
         /// <value>The year path.</value>
         /// <value>The year path.</value>
         public string YearPath => Path.Combine(InternalMetadataPath, "Year");
         public string YearPath => Path.Combine(InternalMetadataPath, "Year");
 
 
-        /// <summary>
-        /// Gets the path to the General IBN directory.
-        /// </summary>
-        /// <value>The general path.</value>
-        public string GeneralPath => Path.Combine(InternalMetadataPath, "general");
-
-        /// <summary>
-        /// Gets the path to the Ratings IBN directory.
-        /// </summary>
-        /// <value>The ratings path.</value>
-        public string RatingsPath => Path.Combine(InternalMetadataPath, "ratings");
-
-        /// <summary>
-        /// Gets the media info images path.
-        /// </summary>
-        /// <value>The media info images path.</value>
-        public string MediaInfoImagesPath => Path.Combine(InternalMetadataPath, "mediainfo");
-
         /// <summary>
         /// <summary>
         /// Gets the path to the user configuration directory.
         /// Gets the path to the user configuration directory.
         /// </summary>
         /// </summary>

+ 4 - 4
Jellyfin.Api/Controllers/FilterController.cs

@@ -92,25 +92,25 @@ namespace Jellyfin.Api.Controllers
                 Years = itemList.Select(i => i.ProductionYear ?? -1)
                 Years = itemList.Select(i => i.ProductionYear ?? -1)
                     .Where(i => i > 0)
                     .Where(i => i > 0)
                     .Distinct()
                     .Distinct()
-                    .OrderBy(i => i)
+                    .Order()
                     .ToArray(),
                     .ToArray(),
 
 
                 Genres = itemList.SelectMany(i => i.Genres)
                 Genres = itemList.SelectMany(i => i.Genres)
                     .DistinctNames()
                     .DistinctNames()
-                    .OrderBy(i => i)
+                    .Order()
                     .ToArray(),
                     .ToArray(),
 
 
                 Tags = itemList
                 Tags = itemList
                     .SelectMany(i => i.Tags)
                     .SelectMany(i => i.Tags)
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .Distinct(StringComparer.OrdinalIgnoreCase)
-                    .OrderBy(i => i)
+                    .Order()
                     .ToArray(),
                     .ToArray(),
 
 
                 OfficialRatings = itemList
                 OfficialRatings = itemList
                     .Select(i => i.OfficialRating)
                     .Select(i => i.OfficialRating)
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .Distinct(StringComparer.OrdinalIgnoreCase)
-                    .OrderBy(i => i)
+                    .Order()
                     .ToArray()
                     .ToArray()
             };
             };
         }
         }

+ 0 - 252
Jellyfin.Api/Controllers/ImageByNameController.cs

@@ -1,252 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.IO;
-using System.Linq;
-using System.Net.Mime;
-using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers
-{
-    /// <summary>
-    ///     Images By Name Controller.
-    /// </summary>
-    [Route("Images")]
-    public class ImageByNameController : BaseJellyfinApiController
-    {
-        private readonly IServerApplicationPaths _applicationPaths;
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        ///     Initializes a new instance of the <see cref="ImageByNameController" /> class.
-        /// </summary>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param>
-        /// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param>
-        public ImageByNameController(
-            IServerConfigurationManager serverConfigurationManager,
-            IFileSystem fileSystem)
-        {
-            _applicationPaths = serverConfigurationManager.ApplicationPaths;
-            _fileSystem = fileSystem;
-        }
-
-        /// <summary>
-        ///     Get all general images.
-        /// </summary>
-        /// <response code="200">Retrieved list of images.</response>
-        /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
-        [HttpGet("General")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
-        {
-            return GetImageList(_applicationPaths.GeneralPath, false);
-        }
-
-        /// <summary>
-        ///     Get General Image.
-        /// </summary>
-        /// <param name="name">The name of the image.</param>
-        /// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
-        /// <response code="200">Image stream retrieved.</response>
-        /// <response code="404">Image not found.</response>
-        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        [HttpGet("General/{name}/{type}")]
-        [AllowAnonymous]
-        [Produces(MediaTypeNames.Application.Octet)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesImageFile]
-        public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type)
-        {
-            var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
-                ? "folder"
-                : type;
-
-            var path = BaseItem.SupportedImageExtensions
-                .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
-                .FirstOrDefault(System.IO.File.Exists);
-
-            if (path is null)
-            {
-                return NotFound();
-            }
-
-            if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture))
-            {
-                return BadRequest("Invalid image path.");
-            }
-
-            var contentType = MimeTypes.GetMimeType(path);
-            return File(AsyncFile.OpenRead(path), contentType);
-        }
-
-        /// <summary>
-        ///     Get all general images.
-        /// </summary>
-        /// <response code="200">Retrieved list of images.</response>
-        /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
-        [HttpGet("Ratings")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
-        {
-            return GetImageList(_applicationPaths.RatingsPath, false);
-        }
-
-        /// <summary>
-        ///     Get rating image.
-        /// </summary>
-        /// <param name="theme">The theme to get the image from.</param>
-        /// <param name="name">The name of the image.</param>
-        /// <response code="200">Image stream retrieved.</response>
-        /// <response code="404">Image not found.</response>
-        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        [HttpGet("Ratings/{theme}/{name}")]
-        [AllowAnonymous]
-        [Produces(MediaTypeNames.Application.Octet)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesImageFile]
-        public ActionResult GetRatingImage(
-            [FromRoute, Required] string theme,
-            [FromRoute, Required] string name)
-        {
-            return GetImageFile(_applicationPaths.RatingsPath, theme, name);
-        }
-
-        /// <summary>
-        ///     Get all media info images.
-        /// </summary>
-        /// <response code="200">Image list retrieved.</response>
-        /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
-        [HttpGet("MediaInfo")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
-        {
-            return GetImageList(_applicationPaths.MediaInfoImagesPath, false);
-        }
-
-        /// <summary>
-        ///     Get media info image.
-        /// </summary>
-        /// <param name="theme">The theme to get the image from.</param>
-        /// <param name="name">The name of the image.</param>
-        /// <response code="200">Image stream retrieved.</response>
-        /// <response code="404">Image not found.</response>
-        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        [HttpGet("MediaInfo/{theme}/{name}")]
-        [AllowAnonymous]
-        [Produces(MediaTypeNames.Application.Octet)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesImageFile]
-        public ActionResult GetMediaInfoImage(
-            [FromRoute, Required] string theme,
-            [FromRoute, Required] string name)
-        {
-            return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
-        }
-
-        /// <summary>
-        ///     Internal FileHelper.
-        /// </summary>
-        /// <param name="basePath">Path to begin search.</param>
-        /// <param name="theme">Theme to search.</param>
-        /// <param name="name">File name to search for.</param>
-        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        private ActionResult GetImageFile(string basePath, string theme, string? name)
-        {
-            var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
-
-            if (Directory.Exists(themeFolder))
-            {
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
-                    .FirstOrDefault(System.IO.File.Exists);
-
-                if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
-                {
-                    if (!path.StartsWith(basePath, StringComparison.InvariantCulture))
-                    {
-                        return BadRequest("Invalid image path.");
-                    }
-
-                    var contentType = MimeTypes.GetMimeType(path);
-
-                    return PhysicalFile(path, contentType);
-                }
-            }
-
-            var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
-            if (Directory.Exists(allFolder))
-            {
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
-                    .FirstOrDefault(System.IO.File.Exists);
-
-                if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
-                {
-                    if (!path.StartsWith(basePath, StringComparison.InvariantCulture))
-                    {
-                        return BadRequest("Invalid image path.");
-                    }
-
-                    var contentType = MimeTypes.GetMimeType(path);
-                    return PhysicalFile(path, contentType);
-                }
-            }
-
-            return NotFound();
-        }
-
-        private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
-        {
-            try
-            {
-                return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
-                    .Select(i => new ImageByNameInfo
-                    {
-                        Name = _fileSystem.GetFileNameWithoutExtension(i),
-                        FileLength = i.Length,
-
-                        // For themeable images, use the Theme property
-                        // For general images, the same object structure is fine,
-                        // but it's not owned by a theme, so call it Context
-                        Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
-                        Context = supportsThemes ? null : GetThemeName(i.FullName, path),
-                        Format = i.Extension.ToLowerInvariant().TrimStart('.')
-                    })
-                    .OrderBy(i => i.Name)
-                    .ToList();
-            }
-            catch (IOException)
-            {
-                return new List<ImageByNameInfo>();
-            }
-        }
-
-        private string? GetThemeName(string path, string rootImagePath)
-        {
-            var parentName = Path.GetDirectoryName(path);
-
-            if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
-            {
-                return null;
-            }
-
-            parentName = Path.GetFileName(parentName);
-
-            return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName;
-        }
-    }
-}

+ 5 - 10
Jellyfin.Api/Controllers/LibraryController.cs

@@ -770,8 +770,7 @@ namespace Jellyfin.Api.Controllers
                     Name = i.Name,
                     Name = i.Name,
                     DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
                     DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
                 })
                 })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
+                .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                 .ToArray();
                 .ToArray();
 
 
             result.MetadataReaders = plugins
             result.MetadataReaders = plugins
@@ -781,8 +780,7 @@ namespace Jellyfin.Api.Controllers
                     Name = i.Name,
                     Name = i.Name,
                     DefaultEnabled = true
                     DefaultEnabled = true
                 })
                 })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
+                .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                 .ToArray();
                 .ToArray();
 
 
             result.SubtitleFetchers = plugins
             result.SubtitleFetchers = plugins
@@ -792,8 +790,7 @@ namespace Jellyfin.Api.Controllers
                     Name = i.Name,
                     Name = i.Name,
                     DefaultEnabled = true
                     DefaultEnabled = true
                 })
                 })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
+                .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                 .ToArray();
                 .ToArray();
 
 
             var typeOptions = new List<LibraryTypeOptionsDto>();
             var typeOptions = new List<LibraryTypeOptionsDto>();
@@ -814,8 +811,7 @@ namespace Jellyfin.Api.Controllers
                         Name = i.Name,
                         Name = i.Name,
                         DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
                         DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
                     })
                     })
-                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                    .Select(x => x.First())
+                    .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                     .ToArray(),
                     .ToArray(),
 
 
                     ImageFetchers = plugins
                     ImageFetchers = plugins
@@ -826,8 +822,7 @@ namespace Jellyfin.Api.Controllers
                         Name = i.Name,
                         Name = i.Name,
                         DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
                         DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
                     })
                     })
-                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                    .Select(x => x.First())
+                    .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                     .ToArray(),
                     .ToArray(),
 
 
                     SupportedImageTypes = plugins
                     SupportedImageTypes = plugins

+ 2 - 4
Jellyfin.Api/Controllers/MoviesController.cs

@@ -200,8 +200,7 @@ namespace Jellyfin.Api.Controllers
                         IsMovie = true,
                         IsMovie = true,
                         EnableGroupByMetadataKey = true,
                         EnableGroupByMetadataKey = true,
                         DtoOptions = dtoOptions
                         DtoOptions = dtoOptions
-                    }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
-                    .Select(x => x.First())
+                    }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
                     .Take(itemLimit)
                     .Take(itemLimit)
                     .ToList();
                     .ToList();
 
 
@@ -240,8 +239,7 @@ namespace Jellyfin.Api.Controllers
                     IsMovie = true,
                     IsMovie = true,
                     EnableGroupByMetadataKey = true,
                     EnableGroupByMetadataKey = true,
                     DtoOptions = dtoOptions
                     DtoOptions = dtoOptions
-                }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
-                    .Select(x => x.First())
+                }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
                     .Take(itemLimit)
                     .Take(itemLimit)
                     .ToList();
                     .ToList();
 
 

+ 20 - 6
Jellyfin.Api/Controllers/QuickConnectController.cs

@@ -1,3 +1,4 @@
+using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
@@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Quick connect request successfully created.</response>
         /// <response code="200">Quick connect request successfully created.</response>
         /// <response code="401">Quick connect is not active on this server.</response>
         /// <response code="401">Quick connect is not active on this server.</response>
         /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
         /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
-        [HttpGet("Initiate")]
+        [HttpPost("Initiate")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
         public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
         {
         {
@@ -66,6 +67,16 @@ namespace Jellyfin.Api.Controllers
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Old version of <see cref="InitiateQuickConnect" /> using a GET method.
+        /// Still available to avoid breaking compatibility.
+        /// </summary>
+        /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns>
+        [Obsolete("Use POST request instead")]
+        [HttpGet("Initiate")]
+        [ApiExplorerSettings(IgnoreApi = true)]
+        public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect();
+
         /// <summary>
         /// <summary>
         /// Attempts to retrieve authentication information.
         /// Attempts to retrieve authentication information.
         /// </summary>
         /// </summary>
@@ -96,6 +107,7 @@ namespace Jellyfin.Api.Controllers
         /// Authorizes a pending quick connect request.
         /// Authorizes a pending quick connect request.
         /// </summary>
         /// </summary>
         /// <param name="code">Quick connect code to authorize.</param>
         /// <param name="code">Quick connect code to authorize.</param>
+        /// <param name="userId">The user the authorize. Access to the requested user is required.</param>
         /// <response code="200">Quick connect result authorized successfully.</response>
         /// <response code="200">Quick connect result authorized successfully.</response>
         /// <response code="403">Unknown user id.</response>
         /// <response code="403">Unknown user id.</response>
         /// <returns>Boolean indicating if the authorization was successful.</returns>
         /// <returns>Boolean indicating if the authorization was successful.</returns>
@@ -103,17 +115,19 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
-        public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code)
+        public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null)
         {
         {
-            var userId = User.GetUserId();
-            if (userId.Equals(default))
+            var currentUserId = User.GetUserId();
+            var actualUserId = userId ?? currentUserId;
+
+            if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
             {
             {
-                return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
+                return Forbid("Unknown user id");
             }
             }
 
 
             try
             try
             {
             {
-                return await _quickConnect.AuthorizeRequest(userId, code).ConfigureAwait(false);
+                return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
             }
             }
             catch (AuthenticationException)
             catch (AuthenticationException)
             {
             {

+ 2 - 8
Jellyfin.Api/Controllers/UserController.cs

@@ -157,7 +157,6 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// </summary>
         /// <param name="userId">The user id.</param>
         /// <param name="userId">The user id.</param>
         /// <param name="pw">The password as plain text.</param>
         /// <param name="pw">The password as plain text.</param>
-        /// <param name="password">The password sha1-hash.</param>
         /// <response code="200">User authenticated.</response>
         /// <response code="200">User authenticated.</response>
         /// <response code="403">Sha1-hashed password only is not allowed.</response>
         /// <response code="403">Sha1-hashed password only is not allowed.</response>
         /// <response code="404">User not found.</response>
         /// <response code="404">User not found.</response>
@@ -166,10 +165,10 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [Obsolete("Authenticate with username instead")]
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
             [FromRoute, Required] Guid userId,
             [FromRoute, Required] Guid userId,
-            [FromQuery, Required] string pw,
-            [FromQuery] string? password)
+            [FromQuery, Required] string pw)
         {
         {
             var user = _userManager.GetUserById(userId);
             var user = _userManager.GetUserById(userId);
 
 
@@ -178,11 +177,6 @@ namespace Jellyfin.Api.Controllers
                 return NotFound("User not found");
                 return NotFound("User not found");
             }
             }
 
 
-            if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
-            {
-                return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
-            }
-
             AuthenticateUserByName request = new AuthenticateUserByName
             AuthenticateUserByName request = new AuthenticateUserByName
             {
             {
                 Username = user.Username,
                 Username = user.Username,

+ 1 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -17,7 +17,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.0" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.1" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />

+ 1 - 9
Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs

@@ -1,6 +1,4 @@
-using System;
-
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos
 {
 {
     /// <summary>
     /// <summary>
     /// The authenticate user by name request body.
     /// The authenticate user by name request body.
@@ -16,11 +14,5 @@ namespace Jellyfin.Api.Models.UserDtos
         /// Gets or sets the plain text password.
         /// Gets or sets the plain text password.
         /// </summary>
         /// </summary>
         public string? Pw { get; set; }
         public string? Pw { get; set; }
-
-        /// <summary>
-        /// Gets or sets the sha1-hashed password.
-        /// </summary>
-        [Obsolete("Send password using pw field")]
-        public string? Password { get; set; }
     }
     }
 }
 }

+ 5 - 5
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -26,15 +26,15 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.0" />
+    <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.1" />
     <PackageReference Include="System.Linq.Async" Version="6.0.1" />
     <PackageReference Include="System.Linq.Async" Version="6.0.1" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.1" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>

+ 2 - 2
Jellyfin.Server/Jellyfin.Server.csproj

@@ -37,8 +37,8 @@
     <PackageReference Include="CommandLineParser" Version="2.9.1" />
     <PackageReference Include="CommandLineParser" Version="2.9.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.1" />
     <PackageReference Include="prometheus-net" Version="7.0.0" />
     <PackageReference Include="prometheus-net" Version="7.0.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />

+ 8 - 8
Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs

@@ -40,25 +40,25 @@ namespace Jellyfin.Server.Middleware
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
         public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
         {
         {
-            var watch = new Stopwatch();
-            watch.Start();
+            var startTimestamp = Stopwatch.GetTimestamp();
+
             var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
             var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
             var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
             var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
             context.Response.OnStarting(() =>
             context.Response.OnStarting(() =>
             {
             {
-                watch.Stop();
-                if (enableWarning && watch.ElapsedMilliseconds > warningThreshold)
+                var responseTime = Stopwatch.GetElapsedTime(startTimestamp);
+                var responseTimeMs = responseTime.TotalMilliseconds;
+                if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
                 {
                 {
-                    _logger.LogWarning(
+                    _logger.LogDebug(
                         "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
                         "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
                         context.Request.GetDisplayUrl(),
                         context.Request.GetDisplayUrl(),
                         context.GetNormalizedRemoteIp(),
                         context.GetNormalizedRemoteIp(),
-                        watch.Elapsed,
+                        responseTime,
                         context.Response.StatusCode);
                         context.Response.StatusCode);
                 }
                 }
 
 
-                var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
-                context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
+                context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture);
                 return Task.CompletedTask;
                 return Task.CompletedTask;
             });
             });
 
 

+ 2 - 5
Jellyfin.Server/Program.cs

@@ -94,8 +94,7 @@ namespace Jellyfin.Server
 
 
         private static async Task StartApp(StartupOptions options)
         private static async Task StartApp(StartupOptions options)
         {
         {
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
+            var startTimestamp = Stopwatch.GetTimestamp();
 
 
             // Log all uncaught exceptions to std error
             // Log all uncaught exceptions to std error
             static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
             static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
@@ -217,9 +216,7 @@ namespace Jellyfin.Server
 
 
                 await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
                 await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
 
 
-                stopWatch.Stop();
-
-                _logger.LogInformation("Startup complete {Time:g}", stopWatch.Elapsed);
+                _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(startTimestamp));
 
 
                 // Block main thread until shutdown
                 // Block main thread until shutdown
                 await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
                 await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);

+ 5 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -2451,6 +2451,11 @@ namespace MediaBrowser.Controller.Entities
                 return Task.FromResult(true);
                 return Task.FromResult(true);
             }
             }
 
 
+            if (video.OwnerId.Equals(default))
+            {
+                video.OwnerId = this.Id;
+            }
+
             return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken);
             return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken);
         }
         }
 
 

+ 1 - 2
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -355,8 +355,7 @@ namespace MediaBrowser.Controller.Entities
             return PhysicalLocations
             return PhysicalLocations
                     .Where(i => !FileSystem.AreEqual(i, Path))
                     .Where(i => !FileSystem.AreEqual(i, Path))
                     .SelectMany(i => GetPhysicalParents(i, rootChildren))
                     .SelectMany(i => GetPhysicalParents(i, rootChildren))
-                    .GroupBy(x => x.Id)
-                    .Select(x => x.First());
+                    .DistinctBy(x => x.Id);
         }
         }
 
 
         private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren)
         private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren)

+ 1 - 1
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -283,7 +283,7 @@ namespace MediaBrowser.Controller.Entities.TV
             // This depends on settings for that series
             // This depends on settings for that series
             // When this happens, remove the duplicate from season 0
             // When this happens, remove the duplicate from season 0
 
 
-            return allEpisodes.GroupBy(i => i.Id).Select(x => x.First()).Reverse();
+            return allEpisodes.DistinctBy(i => i.Id).Reverse();
         }
         }
 
 
         public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
         public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)

+ 0 - 18
MediaBrowser.Controller/IServerApplicationPaths.cs

@@ -50,24 +50,6 @@ namespace MediaBrowser.Controller
         /// <value>The year path.</value>
         /// <value>The year path.</value>
         string YearPath { get; }
         string YearPath { get; }
 
 
-        /// <summary>
-        /// Gets the path to the General IBN directory.
-        /// </summary>
-        /// <value>The general path.</value>
-        string GeneralPath { get; }
-
-        /// <summary>
-        /// Gets the path to the Ratings IBN directory.
-        /// </summary>
-        /// <value>The ratings path.</value>
-        string RatingsPath { get; }
-
-        /// <summary>
-        /// Gets the media info images path.
-        /// </summary>
-        /// <value>The media info images path.</value>
-        string MediaInfoImagesPath { get; }
-
         /// <summary>
         /// <summary>
         /// Gets the path to the user configuration directory.
         /// Gets the path to the user configuration directory.
         /// </summary>
         /// </summary>

+ 8 - 2
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -429,10 +429,16 @@ namespace MediaBrowser.Controller.Library
         /// Gets the collection folders.
         /// Gets the collection folders.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <returns>IEnumerable&lt;Folder&gt;.</returns>
+        /// <returns>The folders that contain the item.</returns>
         List<Folder> GetCollectionFolders(BaseItem item);
         List<Folder> GetCollectionFolders(BaseItem item);
 
 
-        List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren);
+        /// <summary>
+        /// Gets the collection folders.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="allUserRootChildren">The root folders to consider.</param>
+        /// <returns>The folders that contain the item.</returns>
+        List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren);
 
 
         LibraryOptions GetLibraryOptions(BaseItem item);
         LibraryOptions GetLibraryOptions(BaseItem item);
 
 

+ 1 - 2
MediaBrowser.Controller/Library/NameExtensions.cs

@@ -10,8 +10,7 @@ namespace MediaBrowser.Controller.Library
     public static class NameExtensions
     public static class NameExtensions
     {
     {
         public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
         public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
-            => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First());
+            => names.DistinctBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase);
 
 
         private static string RemoveDiacritics(string? name)
         private static string RemoveDiacritics(string? name)
         {
         {

+ 1 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -19,7 +19,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.1" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
     <PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
     <PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
   </ItemGroup>
   </ItemGroup>

+ 1 - 1
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Providers
         /// <returns>Task{HttpResponseInfo}.</returns>
         /// <returns>Task{HttpResponseInfo}.</returns>
         Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
         Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
 
 
-        Dictionary<Guid, Guid> GetRefreshQueue();
+        HashSet<Guid> GetRefreshQueue();
 
 
         void OnRefreshStart(BaseItem item);
         void OnRefreshStart(BaseItem item);
 
 

+ 4 - 16
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -415,8 +415,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
                 analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
             }
             }
 
 
-            var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
-
             return GetMediaInfoInternal(
             return GetMediaInfoInternal(
                 GetInputArgument(inputFile, request.MediaSource),
                 GetInputArgument(inputFile, request.MediaSource),
                 request.MediaSource.Path,
                 request.MediaSource.Path,
@@ -425,7 +423,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 analyzeDuration,
                 analyzeDuration,
                 request.MediaType == DlnaProfileType.Audio,
                 request.MediaType == DlnaProfileType.Audio,
                 request.MediaSource.VideoType,
                 request.MediaSource.VideoType,
-                forceEnableLogging,
                 cancellationToken);
                 cancellationToken);
         }
         }
 
 
@@ -473,7 +470,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             string probeSizeArgument,
             string probeSizeArgument,
             bool isAudio,
             bool isAudio,
             VideoType? videoType,
             VideoType? videoType,
-            bool forceEnableLogging,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
             var args = extractChapters
             var args = extractChapters
@@ -488,7 +484,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     CreateNoWindow = true,
                     CreateNoWindow = true,
                     UseShellExecute = false,
                     UseShellExecute = false,
 
 
-                    // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
+                    // Must consume both or ffmpeg may hang due to deadlocks.
                     RedirectStandardOutput = true,
                     RedirectStandardOutput = true,
 
 
                     FileName = _ffprobePath,
                     FileName = _ffprobePath,
@@ -500,21 +496,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 EnableRaisingEvents = true
                 EnableRaisingEvents = true
             };
             };
 
 
-            if (forceEnableLogging)
-            {
-                _logger.LogInformation("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments);
-            }
-            else
-            {
-                _logger.LogDebug("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments);
-            }
+            _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 
 
             using (var processWrapper = new ProcessWrapper(process, this))
             using (var processWrapper = new ProcessWrapper(process, this))
             {
             {
                 await using var memoryStream = new MemoryStream();
                 await using var memoryStream = new MemoryStream();
-                _logger.LogDebug("Starting ffprobe with args {Args}", args);
                 StartProcess(processWrapper);
                 StartProcess(processWrapper);
-                await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken: cancellationToken);
+                await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken);
                 memoryStream.Seek(0, SeekOrigin.Begin);
                 memoryStream.Seek(0, SeekOrigin.Begin);
                 InternalMediaInfoResult result;
                 InternalMediaInfoResult result;
                 try
                 try
@@ -522,7 +510,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
                     result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
                                         memoryStream,
                                         memoryStream,
                                         _jsonSerializerOptions,
                                         _jsonSerializerOptions,
-                                        cancellationToken: cancellationToken).ConfigureAwait(false);
+                                        cancellationToken).ConfigureAwait(false);
                 }
                 }
                 catch
                 catch
                 {
                 {

+ 2 - 1
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -144,7 +144,8 @@ namespace MediaBrowser.MediaEncoding.Probing
                 FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
                 FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
                 FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
                 FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
                 FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ??
                 FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ??
-                FFProbeHelpers.GetDictionaryDateTime(tags, "date");
+                FFProbeHelpers.GetDictionaryDateTime(tags, "date") ??
+                FFProbeHelpers.GetDictionaryDateTime(tags, "creation_time");
 
 
             // Set common metadata for music (audio) and music videos (video)
             // Set common metadata for music (audio) and music videos (video)
             info.Album = tags.GetValueOrDefault("album");
             info.Album = tags.GetValueOrDefault("album");

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

@@ -45,6 +45,8 @@ namespace MediaBrowser.Model.Configuration
 
 
         public bool EnableEmbeddedTitles { get; set; }
         public bool EnableEmbeddedTitles { get; set; }
 
 
+        public bool EnableEmbeddedExtrasTitles { get; set; }
+
         public bool EnableEmbeddedEpisodeInfos { get; set; }
         public bool EnableEmbeddedEpisodeInfos { get; set; }
 
 
         public int AutomaticRefreshIntervalDays { get; set; }
         public int AutomaticRefreshIntervalDays { get; set; }

+ 4 - 3
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -1450,15 +1450,16 @@ namespace MediaBrowser.Model.Dlna
 
 
         private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
         private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
         {
         {
-            // Don't restrict by bitrate if coming from an external domain
+            // Don't restrict bitrate if item is remote.
             if (item.IsRemote)
             if (item.IsRemote)
             {
             {
                 return true;
                 return true;
             }
             }
 
 
-            long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000;
+            // If no maximum bitrate is set, default to no maximum bitrate.
+            long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : int.MaxValue;
 
 
-            // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps
+            // If we don't know the item bitrate, then force a transcode if requested max bitrate is under 40 mbps
             int itemBitrate = item.Bitrate ?? 40000000;
             int itemBitrate = item.Bitrate ?? 40000000;
 
 
             if (itemBitrate > requestedMaxBitrate)
             if (itemBitrate > requestedMaxBitrate)

+ 1 - 1
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -40,7 +40,7 @@
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>
     <PackageReference Include="System.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Globalization" Version="4.3.0" />
-    <PackageReference Include="System.Text.Json" Version="7.0.0" />
+    <PackageReference Include="System.Text.Json" Version="7.0.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 2 - 2
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -465,7 +465,7 @@ namespace MediaBrowser.Providers.Manager
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .ToArray();
                     .ToArray();
 
 
-                if (currentList.Length != item.Genres.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Genres.OrderBy(i => i), StringComparer.OrdinalIgnoreCase))
+                if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), StringComparer.OrdinalIgnoreCase))
                 {
                 {
                     updateType |= ItemUpdateType.MetadataEdit;
                     updateType |= ItemUpdateType.MetadataEdit;
                 }
                 }
@@ -486,7 +486,7 @@ namespace MediaBrowser.Providers.Manager
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .ToArray();
                     .ToArray();
 
 
-                if (currentList.Length != item.Studios.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Studios.OrderBy(i => i), StringComparer.OrdinalIgnoreCase))
+                if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order(), StringComparer.OrdinalIgnoreCase))
                 {
                 {
                     updateType |= ItemUpdateType.MetadataEdit;
                     updateType |= ItemUpdateType.MetadataEdit;
                 }
                 }

+ 8 - 16
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -31,7 +31,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
-using Priority_Queue;
 using Book = MediaBrowser.Controller.Entities.Book;
 using Book = MediaBrowser.Controller.Entities.Book;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@@ -58,7 +57,7 @@ namespace MediaBrowser.Providers.Manager
         private readonly IBaseItemManager _baseItemManager;
         private readonly IBaseItemManager _baseItemManager;
         private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
         private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
         private readonly CancellationTokenSource _disposeCancellationTokenSource = new();
         private readonly CancellationTokenSource _disposeCancellationTokenSource = new();
-        private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue = new();
+        private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new();
 
 
         private IImageProvider[] _imageProviders = Array.Empty<IImageProvider>();
         private IImageProvider[] _imageProviders = Array.Empty<IImageProvider>();
         private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
         private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
@@ -897,18 +896,11 @@ namespace MediaBrowser.Providers.Manager
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public Dictionary<Guid, Guid> GetRefreshQueue()
+        public HashSet<Guid> GetRefreshQueue()
         {
         {
             lock (_refreshQueueLock)
             lock (_refreshQueueLock)
             {
             {
-                var dict = new Dictionary<Guid, Guid>();
-
-                foreach (var item in _refreshQueue)
-                {
-                    dict[item.Item1] = item.Item1;
-                }
-
-                return dict;
+                return _refreshQueue.UnorderedItems.Select(x => x.Element.ItemId).ToHashSet();
             }
             }
         }
         }
 
 
@@ -969,7 +961,7 @@ namespace MediaBrowser.Providers.Manager
                 return;
                 return;
             }
             }
 
 
-            _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(itemId, options), (int)priority);
+            _refreshQueue.Enqueue((itemId, options), priority);
 
 
             lock (_refreshQueueLock)
             lock (_refreshQueueLock)
             {
             {
@@ -992,7 +984,7 @@ namespace MediaBrowser.Providers.Manager
 
 
             var cancellationToken = _disposeCancellationTokenSource.Token;
             var cancellationToken = _disposeCancellationTokenSource.Token;
 
 
-            while (_refreshQueue.TryDequeue(out Tuple<Guid, MetadataRefreshOptions> refreshItem))
+            while (_refreshQueue.TryDequeue(out var refreshItem, out _))
             {
             {
                 if (_disposed)
                 if (_disposed)
                 {
                 {
@@ -1001,15 +993,15 @@ namespace MediaBrowser.Providers.Manager
 
 
                 try
                 try
                 {
                 {
-                    var item = libraryManager.GetItemById(refreshItem.Item1);
+                    var item = libraryManager.GetItemById(refreshItem.ItemId);
                     if (item is null)
                     if (item is null)
                     {
                     {
                         continue;
                         continue;
                     }
                     }
 
 
                     var task = item is MusicArtist artist
                     var task = item is MusicArtist artist
-                        ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
-                        : RefreshItem(item, refreshItem.Item2, cancellationToken);
+                        ? RefreshArtist(artist, refreshItem.RefreshOptions, cancellationToken)
+                        : RefreshItem(item, refreshItem.RefreshOptions, cancellationToken);
 
 
                     await task.ConfigureAwait(false);
                     await task.ConfigureAwait(false);
                 }
                 }

+ 1 - 2
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -22,8 +22,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
     <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
-    <PackageReference Include="OptimizedPriorityQueue" Version="5.1.0" />
-    <PackageReference Include="PlaylistsNET" Version="1.2.1" />
+    <PackageReference Include="PlaylistsNET" Version="1.3.1" />
     <PackageReference Include="TagLibSharp" Version="2.3.0" />
     <PackageReference Include="TagLibSharp" Version="2.3.0" />
     <PackageReference Include="TMDbLib" Version="1.9.2" />
     <PackageReference Include="TMDbLib" Version="1.9.2" />
   </ItemGroup>
   </ItemGroup>

+ 2 - 2
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -484,8 +484,8 @@ namespace MediaBrowser.Providers.MediaInfo
             {
             {
                 if (!string.IsNullOrWhiteSpace(data.Name) && libraryOptions.EnableEmbeddedTitles)
                 if (!string.IsNullOrWhiteSpace(data.Name) && libraryOptions.EnableEmbeddedTitles)
                 {
                 {
-                    // Don't use the embedded name for extras because it will often be the same name as the movie
-                    if (!video.ExtraType.HasValue)
+                    // Separate option to use the embedded name for extras because it will often be the same name as the movie
+                    if (!video.ExtraType.HasValue || libraryOptions.EnableEmbeddedExtrasTitles)
                     {
                     {
                         video.Name = data.Name;
                         video.Name = data.Name;
                     }
                     }

+ 2 - 2
MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs

@@ -149,11 +149,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
 
             var url = BaseUrl + "/artist-mb.php?i=" + musicBrainzId;
             var url = BaseUrl + "/artist-mb.php?i=" + musicBrainzId;
 
 
-            var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
-
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
 
 
+            var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
             Directory.CreateDirectory(Path.GetDirectoryName(path));
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
 
             var fileStreamOptions = AsyncFile.WriteOptions;
             var fileStreamOptions = AsyncFile.WriteOptions;

+ 1 - 1
debian/control

@@ -3,7 +3,7 @@ Section: misc
 Priority: optional
 Priority: optional
 Maintainer: Jellyfin Team <team@jellyfin.org>
 Maintainer: Jellyfin Team <team@jellyfin.org>
 Build-Depends:  debhelper (>= 9),
 Build-Depends:  debhelper (>= 9),
-                dotnet-sdk-6.0,
+                dotnet-sdk-7.0,
                 libc6-dev,
                 libc6-dev,
                 libcurl4-openssl-dev,
                 libcurl4-openssl-dev,
                 libfontconfig1-dev,
                 libfontconfig1-dev,

+ 1 - 1
debian/postinst

@@ -83,7 +83,7 @@ fi
 # End automatically added section
 # End automatically added section
 # Automatically added by dh_installinit
 # Automatically added by dh_installinit
 if [[ "$1" == "configure" ]] || [[ "$1" == "abort-upgrade" ]]; then
 if [[ "$1" == "configure" ]] || [[ "$1" == "abort-upgrade" ]]; then
-  if [[ -d "/run/systemd/systemd" ]]; then
+  if [[ -d "/run/systemd/system" ]]; then
     systemctl --system daemon-reload >/dev/null || true
     systemctl --system daemon-reload >/dev/null || true
     deb-systemd-invoke start jellyfin >/dev/null || true
     deb-systemd-invoke start jellyfin >/dev/null || true
   elif [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
   elif [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then

+ 1 - 1
deployment/Dockerfile.centos.amd64

@@ -13,7 +13,7 @@ RUN yum update -yq \
   && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
   && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
 
 
 # Install DotNET SDK
 # Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
   && mkdir -p dotnet-sdk \
   && mkdir -p dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.debian.amd64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.debian.arm64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.debian.armhf

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.docker.amd64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 
 
 ARG SOURCE_DIR=/src
 ARG SOURCE_DIR=/src
 ARG ARTIFACT_DIR=/jellyfin
 ARG ARTIFACT_DIR=/jellyfin

+ 1 - 1
deployment/Dockerfile.docker.arm64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 
 
 ARG SOURCE_DIR=/src
 ARG SOURCE_DIR=/src
 ARG ARTIFACT_DIR=/jellyfin
 ARG ARTIFACT_DIR=/jellyfin

+ 1 - 1
deployment/Dockerfile.docker.armhf

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 
 
 ARG SOURCE_DIR=/src
 ARG SOURCE_DIR=/src
 ARG ARTIFACT_DIR=/jellyfin
 ARG ARTIFACT_DIR=/jellyfin

+ 1 - 1
deployment/Dockerfile.fedora.amd64

@@ -12,7 +12,7 @@ RUN dnf update -yq \
   && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
   && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
 
 
 # Install DotNET SDK
 # Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
   && mkdir -p dotnet-sdk \
   && mkdir -p dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.linux.amd64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.linux.amd64-musl

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.linux.arm64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.linux.armhf

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.linux.musl-linux-arm64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.macos.amd64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.macos.arm64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.portable

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 1 - 1
deployment/Dockerfile.ubuntu.amd64

@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
     libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
     libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
 
 
 # Install dotnet repository
 # Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
   && mkdir -p dotnet-sdk \
   && mkdir -p dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.arm64

@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
     mmv build-essential lsb-release
     mmv build-essential lsb-release
 
 
 # Install dotnet repository
 # Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
   && mkdir -p dotnet-sdk \
   && mkdir -p dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.armhf

@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
     mmv build-essential lsb-release
     mmv build-essential lsb-release
 
 
 # Install dotnet repository
 # Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
   && mkdir -p dotnet-sdk \
   && mkdir -p dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
   && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.windows.amd64

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
 # Docker build arguments
 # Docker build arguments
 ARG SOURCE_DIR=/jellyfin
 ARG SOURCE_DIR=/jellyfin
 ARG ARTIFACT_DIR=/dist
 ARG ARTIFACT_DIR=/dist

+ 2 - 2
deployment/build.centos.amd64

@@ -9,7 +9,7 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+    # Remove BuildRequires for dotnet, since it's installed manually
     pushd fedora
     pushd fedora
 
 
     cp -a jellyfin.spec /tmp/spec.orig
     cp -a jellyfin.spec /tmp/spec.orig
@@ -52,7 +52,7 @@ if [[ ${IS_DOCKER} == YES ]]; then
 
 
     cp -a /tmp/spec.orig jellyfin.spec
     cp -a /tmp/spec.orig jellyfin.spec
     chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
     chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
-    
+
     popd
     popd
 fi
 fi
 
 

+ 2 - 2
deployment/build.debian.amd64

@@ -9,9 +9,9 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
+    # Remove build-dep for dotnet-sdk-7.0, since it's installed manually
     cp -a debian/control /tmp/control.orig
     cp -a debian/control /tmp/control.orig
-    sed -i '/dotnet-sdk-6.0,/d' debian/control
+    sed -i '/dotnet-sdk-7.0,/d' debian/control
 fi
 fi
 
 
 # Modify changelog to unstable configuration if IS_UNSTABLE
 # Modify changelog to unstable configuration if IS_UNSTABLE

+ 2 - 2
deployment/build.debian.arm64

@@ -9,9 +9,9 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
+    # Remove build-dep for dotnet-sdk-7.0, since it's installed manually
     cp -a debian/control /tmp/control.orig
     cp -a debian/control /tmp/control.orig
-    sed -i '/dotnet-sdk-6.0,/d' debian/control
+    sed -i '/dotnet-sdk-7.0,/d' debian/control
 fi
 fi
 
 
 # Modify changelog to unstable configuration if IS_UNSTABLE
 # Modify changelog to unstable configuration if IS_UNSTABLE

+ 2 - 2
deployment/build.debian.armhf

@@ -9,9 +9,9 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
+    # Remove build-dep for dotnet-sdk-7.0, since it's installed manually
     cp -a debian/control /tmp/control.orig
     cp -a debian/control /tmp/control.orig
-    sed -i '/dotnet-sdk-6.0,/d' debian/control
+    sed -i '/dotnet-sdk-7.0,/d' debian/control
 fi
 fi
 
 
 # Modify changelog to unstable configuration if IS_UNSTABLE
 # Modify changelog to unstable configuration if IS_UNSTABLE

+ 2 - 2
deployment/build.fedora.amd64

@@ -9,7 +9,7 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+    # Remove BuildRequires for dotnet, since it's installed manually
     pushd fedora
     pushd fedora
 
 
     cp -a jellyfin.spec /tmp/spec.orig
     cp -a jellyfin.spec /tmp/spec.orig
@@ -52,7 +52,7 @@ if [[ ${IS_DOCKER} == YES ]]; then
 
 
     cp -a /tmp/spec.orig jellyfin.spec
     cp -a /tmp/spec.orig jellyfin.spec
     chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
     chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
-    
+
     popd
     popd
 fi
 fi
 
 

+ 2 - 2
deployment/build.ubuntu.amd64

@@ -9,9 +9,9 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
+    # Remove build-dep for dotnet-sdk-7.0, since it's installed manually
     cp -a debian/control /tmp/control.orig
     cp -a debian/control /tmp/control.orig
-    sed -i '/dotnet-sdk-6.0,/d' debian/control
+    sed -i '/dotnet-sdk-7.0,/d' debian/control
 fi
 fi
 
 
 # Modify changelog to unstable configuration if IS_UNSTABLE
 # Modify changelog to unstable configuration if IS_UNSTABLE

+ 2 - 2
deployment/build.ubuntu.arm64

@@ -9,9 +9,9 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
+    # Remove build-dep for dotnet-sdk-7.0, since it's installed manually
     cp -a debian/control /tmp/control.orig
     cp -a debian/control /tmp/control.orig
-    sed -i '/dotnet-sdk-6.0,/d' debian/control
+    sed -i '/dotnet-sdk-7.0,/d' debian/control
 fi
 fi
 
 
 # Modify changelog to unstable configuration if IS_UNSTABLE
 # Modify changelog to unstable configuration if IS_UNSTABLE

+ 2 - 2
deployment/build.ubuntu.armhf

@@ -9,9 +9,9 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 if [[ ${IS_DOCKER} == YES ]]; then
 if [[ ${IS_DOCKER} == YES ]]; then
-    # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
+    # Remove build-dep for dotnet-sdk-7.0, since it's installed manually
     cp -a debian/control /tmp/control.orig
     cp -a debian/control /tmp/control.orig
-    sed -i '/dotnet-sdk-6.0,/d' debian/control
+    sed -i '/dotnet-sdk-7.0,/d' debian/control
 fi
 fi
 
 
 # Modify changelog to unstable configuration if IS_UNSTABLE
 # Modify changelog to unstable configuration if IS_UNSTABLE

+ 1 - 1
deployment/build.windows.amd64

@@ -8,7 +8,7 @@ set -o xtrace
 # Version variables
 # Version variables
 NSSM_VERSION="nssm-2.24-101-g897c7ad"
 NSSM_VERSION="nssm-2.24-101-g897c7ad"
 NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
 NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip";
+FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg-portable_win64.zip";
 
 
 # Move to source directory
 # Move to source directory
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}

+ 2 - 2
fedora/jellyfin.spec

@@ -27,7 +27,7 @@ BuildRequires:  systemd
 BuildRequires:  libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
 BuildRequires:  libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
 # Requirements not packaged in RHEL 7 main repos, added via Makefile
 # Requirements not packaged in RHEL 7 main repos, added via Makefile
 # https://packages.microsoft.com/rhel/7/prod/
 # https://packages.microsoft.com/rhel/7/prod/
-BuildRequires:  dotnet-runtime-6.0, dotnet-sdk-6.0
+BuildRequires:  dotnet-runtime-7.0, dotnet-sdk-7.0
 Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
 Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
 
 
 # Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471
 # Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471
@@ -74,7 +74,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
 %install
 %install
 # Jellyfin files
 # Jellyfin files
 %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
 %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
-%{__cp} -r Jellyfin.Server/bin/Release/net6.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
+%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
 ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
 ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
 %{__install} -D %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
 %{__install} -D %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
 
 

+ 1 - 1
tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs

@@ -57,7 +57,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
             Assert.Equal(json, res);
             Assert.Equal(json, res);
         }
         }
 
 
-        private class TestContainer
+        private sealed class TestContainer
         {
         {
             public TestContainer(CollectionTypeOptions? collectionType)
             public TestContainer(CollectionTypeOptions? collectionType)
             {
             {

+ 1 - 1
tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs

@@ -32,7 +32,7 @@ namespace Jellyfin.MediaEncoding.Tests
             Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
             Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
         }
         }
 
 
-        private class GetFFmpegVersionTestData : TheoryData<string, Version?>
+        private sealed class GetFFmpegVersionTestData : TheoryData<string, Version?>
         {
         {
             public GetFFmpegVersionTestData()
             public GetFFmpegVersionTestData()
             {
             {

+ 1 - 1
tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs

@@ -186,7 +186,7 @@ namespace Jellyfin.Model.Tests.Entities
             Assert.Null(nullProvider.ProviderIds);
             Assert.Null(nullProvider.ProviderIds);
         }
         }
 
 
-        private class ProviderIdsExtensionsTestsObject : IHasProviderIds
+        private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds
         {
         {
             public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject();
             public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject();
 
 

+ 1 - 1
tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs

@@ -282,7 +282,7 @@ namespace Jellyfin.Server.Implementations.Tests.Data
             Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values));
             Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values));
         }
         }
 
 
-        private class ProviderIdsExtensionsTestsObject : IHasProviderIds
+        private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds
         {
         {
             public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
             public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
         }
         }

+ 1 - 1
tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs

@@ -48,7 +48,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer
             Assert.Throws<JsonException>(() => con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed));
             Assert.Throws<JsonException>(() => con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed));
         }
         }
 
 
-        internal class BufferSegment : ReadOnlySequenceSegment<byte>
+        internal sealed class BufferSegment : ReadOnlySequenceSegment<byte>
         {
         {
             public BufferSegment(Memory<byte> memory)
             public BufferSegment(Memory<byte> memory)
             {
             {

+ 1 - 1
tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs

@@ -60,7 +60,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
             Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
             Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
         }
         }
 
 
-        private class EpisodeResolverMock : EpisodeResolver
+        private sealed class EpisodeResolverMock : EpisodeResolver
         {
         {
             public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
             public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
             {
             {

+ 2 - 2
tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs

@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
             Assert.Equal(-expected, cmp.Compare(y, x));
             Assert.Equal(-expected, cmp.Compare(y, x));
         }
         }
 
 
-        private class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
+        private sealed class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
         {
         {
             public EpisodeBadData()
             public EpisodeBadData()
             {
             {
@@ -36,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
             }
             }
         }
         }
 
 
-        private class EpisodeTestData : TheoryData<BaseItem, BaseItem, int>
+        private sealed class EpisodeTestData : TheoryData<BaseItem, BaseItem, int>
         {
         {
             public EpisodeTestData()
             public EpisodeTestData()
             {
             {

+ 1 - 1
tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs

@@ -48,7 +48,7 @@ namespace Jellyfin.Server.Integration.Tests
             headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
             headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
         }
         }
 
 
-        private class AuthenticationResultDto
+        private sealed class AuthenticationResultDto
         {
         {
             public string AccessToken { get; set; } = string.Empty;
             public string AccessToken { get; set; } = string.Empty;