aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml6
-rw-r--r--.github/workflows/codeql-analysis.yml8
-rw-r--r--.github/workflows/commands.yml6
-rw-r--r--.github/workflows/openapi.yml6
-rw-r--r--.github/workflows/repo-stale.yaml2
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs215
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs3
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs9
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs3
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs3
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs2
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs3
-rw-r--r--Emby.Server.Implementations/Images/PlaylistImageProvider.cs3
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs57
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs18
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ka.json17
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json30
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json14
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs18
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs252
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs15
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs6
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs87
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs26
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs10
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs30
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs51
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs21
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs20
-rw-r--r--Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs10
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs3
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj10
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj4
-rw-r--r--Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs16
-rw-r--r--Jellyfin.Server/Program.cs7
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs5
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs2
-rw-r--r--MediaBrowser.Controller/IServerApplicationPaths.cs18
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs10
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs3
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj2
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs20
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs3
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs7
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs17
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs8
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs24
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj3
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs4
-rw-r--r--README.md8
-rw-r--r--debian/control2
-rw-r--r--debian/postinst2
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.docker.amd642
-rw-r--r--deployment/Dockerfile.docker.arm642
-rw-r--r--deployment/Dockerfile.docker.armhf2
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.linux.amd64-musl2
-rw-r--r--deployment/Dockerfile.linux.arm642
-rw-r--r--deployment/Dockerfile.linux.armhf2
-rw-r--r--deployment/Dockerfile.linux.musl-linux-arm642
-rw-r--r--deployment/Dockerfile.macos.amd642
-rw-r--r--deployment/Dockerfile.macos.arm642
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rwxr-xr-xdeployment/build.centos.amd644
-rwxr-xr-xdeployment/build.debian.amd644
-rwxr-xr-xdeployment/build.debian.arm644
-rwxr-xr-xdeployment/build.debian.armhf4
-rwxr-xr-xdeployment/build.fedora.amd644
-rwxr-xr-xdeployment/build.ubuntu.amd644
-rwxr-xr-xdeployment/build.ubuntu.arm644
-rwxr-xr-xdeployment/build.ubuntu.armhf4
-rwxr-xr-xdeployment/build.windows.amd642
-rw-r--r--fedora/jellyfin.spec4
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj5
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs24
-rw-r--r--tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs2
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs2
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs2
112 files changed, 409 insertions, 908 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 83504fefe..1618237f1 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -32,8 +32,10 @@ jobs:
BuildConfiguration: linux.armhf
Windows.amd64:
BuildConfiguration: windows.amd64
- MacOS:
- BuildConfiguration: macos
+ MacOS.amd64:
+ BuildConfiguration: macos.amd64
+ MacOS.arm64:
+ BuildConfiguration: macos.arm64
Portable:
BuildConfiguration: portable
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f385aecb6..5aebbae4d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+ uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # v2
+ uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # v2
+ uses: github/codeql-action/autobuild@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # v2
+ uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index f7fbc4706..f62ae853d 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,13 +24,13 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+ uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
+ uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+ uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index d7ace118b..f426357a9 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+ uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
+ uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
with:
ref: ${{ github.base_ref }}
- name: Setup .NET
@@ -95,7 +95,7 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 # v2
+ uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 1c6fe1492..c1f5e718b 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
+ - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
diff --git a/Dockerfile b/Dockerfile
index 7b69a186f..304f79463 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# 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
ARG JELLYFIN_WEB_VERSION=master
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 84ddf499a..bbb84a461 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# 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
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index d4ae5802c..5572586ae 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# 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
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index fa9d7dea2..9bdc4e5c8 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -5,9 +5,11 @@
using System;
using System.Buffers.Text;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -2461,6 +2463,7 @@ namespace Emby.Server.Implementations.Data
if (query.SearchTerm.Length > 1)
{
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");
@@ -2557,8 +2560,6 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- var now = DateTime.UtcNow;
-
// Hack for right now since we currently don't support filtering out these duplicates within a query
if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
{
@@ -2580,28 +2581,24 @@ namespace Emby.Server.Implementations.Data
}
var commandText = commandTextBuilder.ToString();
- int count;
+
+ using (new QueryTimeLogger(Logger, commandText))
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)
@@ -2610,8 +2607,6 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- var now = DateTime.UtcNow;
-
// Hack for right now since we currently don't support filtering out these duplicates within a query
if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
{
@@ -2655,61 +2650,58 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
+ using (new QueryTimeLogger(Logger, commandText))
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;
}
@@ -2762,26 +2754,6 @@ namespace Emby.Server.Implementations.Data
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)
{
ArgumentNullException.ThrowIfNull(query);
@@ -2797,8 +2769,6 @@ namespace Emby.Server.Implementations.Data
returnList);
}
- var now = DateTime.UtcNow;
-
// Hack for right now since we currently don't support filtering out these duplicates within a query
if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
{
@@ -2899,6 +2869,7 @@ namespace Emby.Server.Implementations.Data
if (!isReturningZeroItems)
{
+ using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
using (var statement = itemQueryStatement)
{
if (EnableJoinUserData(query))
@@ -2929,13 +2900,11 @@ namespace Emby.Server.Implementations.Data
}
}
}
-
- LogQueryTime("GetItems.ItemQuery", itemQuery, now);
}
- now = DateTime.UtcNow;
if (query.EnableTotalRecordCount)
{
+ using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
using (var statement = totalRecordCountQueryStatement)
{
if (EnableJoinUserData(query))
@@ -2951,8 +2920,6 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
-
- LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
}
},
ReadTransactionMode);
@@ -3170,8 +3137,6 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- var now = DateTime.UtcNow;
-
var columns = new List<string> { "guid" };
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 256)
@@ -3208,29 +3173,27 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
+ using (new QueryTimeLogger(Logger, commandText))
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;
}
@@ -5111,8 +5074,6 @@ AND Type = @InternalPersonType)");
{
CheckDisposed();
- var now = DateTime.UtcNow;
-
var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
if (itemValueTypes.Length == 1)
{
@@ -5144,6 +5105,7 @@ AND Type = @InternalPersonType)");
var commandText = stringBuilder.ToString();
var list = new List<string>();
+ using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
@@ -5156,7 +5118,6 @@ AND Type = @InternalPersonType)");
}
}
- LogQueryTime("GetItemValueNames", commandText, now);
return list;
}
@@ -5171,8 +5132,6 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- var now = DateTime.UtcNow;
-
var typeClause = itemValueTypes.Length == 1 ?
("Type=" + itemValueTypes[0]) :
("Type in (" + string.Join(',', itemValueTypes) + ")");
@@ -5346,6 +5305,7 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>();
+ using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection(true))
{
connection.RunInTransaction(
@@ -5419,8 +5379,6 @@ AND Type = @InternalPersonType)");
ReadTransactionMode);
}
- LogQueryTime("GetItemValues", commandText, now);
-
if (result.TotalRecordCount == 0)
{
result.TotalRecordCount = list.Count;
@@ -6245,5 +6203,48 @@ AND Type = @InternalPersonType)");
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);
+ }
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 0c6c31982..5103b1fbf 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -574,8 +574,7 @@ namespace Emby.Server.Implementations.Dto
.Where(i => user is null ?
true :
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);
for (var i = 0; i < people.Count; i++)
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index e3d430c69..f46affc73 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/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.Configuration.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="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 25a7029c9..05d0a9b79 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -282,19 +282,16 @@ namespace Emby.Server.Implementations.EntryPoints
{
// Remove dupes in case some were saved multiple times
var foldersAddedTo = _foldersAddedTo
- .GroupBy(x => x.Id)
- .Select(x => x.First())
+ .DistinctBy(x => x.Id)
.ToList();
var foldersRemovedFrom = _foldersRemovedFrom
- .GroupBy(x => x.Id)
- .Select(x => x.First())
+ .DistinctBy(x => x.Id)
.ToList();
var itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
- .GroupBy(x => x.Id)
- .Select(x => x.First())
+ .DistinctBy(x => x.Id)
.ToList();
SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 42c8f24a1..e724618b3 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -123,8 +123,7 @@ namespace Emby.Server.Implementations.EntryPoints
var user = _userManager.GetUserById(userId);
var dtoList = changedItems
- .GroupBy(x => x.Id)
- .Select(x => x.First())
+ .DistinctBy(x => x.Id)
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index ec8590929..0ad81b653 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -133,8 +133,7 @@ namespace Emby.Server.Implementations.IO
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAffectedBaseItem)
.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)
{
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 4b999d40b..f67a02be8 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.IO
.OfType<Folder>()
.SelectMany(f => f.PhysicalLocations)
.Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(i => i);
+ .Order();
foreach (var path in paths)
{
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 82690f8a9..0bd5fdce0 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -81,8 +81,7 @@ namespace Emby.Server.Implementations.Images
}
return i;
- }).GroupBy(x => x.Id)
- .Select(x => x.First());
+ }).DistinctBy(x => x.Id);
List<BaseItem> returnItems;
if (isUsingCollectionStrip)
diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
index 580151287..3326d21ac 100644
--- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
@@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Images
return null;
})
.Where(i => i is not null)
- .GroupBy(x => x.Id)
- .Select(x => x.First())
+ .DistinctBy(x => x.Id)
.ToList();
}
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 70439d258..a3c66dc79 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1154,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
.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
{
@@ -1175,29 +1175,29 @@ namespace Emby.Server.Implementations.Library
}
})
.Where(i => i is not null)
- .OrderBy(i => i)
+ .Order()
.ToArray(),
CollectionType = GetCollectionType(dir)
};
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)
{
- 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);
if (refreshQueue is not null)
{
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";
}
}
@@ -1999,38 +1999,35 @@ namespace Emby.Server.Implementations.Library
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)
{
var parent = item.GetParent();
- if (parent is null || parent is AggregateFolder)
+ if (parent is AggregateFolder)
{
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)
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index cb377136a..e8615e7db 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -163,17 +163,15 @@ namespace Emby.Server.Implementations.Library.Resolvers
try
{
// 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)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 5f1a3ec6d..1522cd3ae 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
return false;
- }).OrderBy(i => i).ToList();
+ }).Order().ToList();
// If different video types were found, don't allow this
if (videoTypes.Distinct().Count() > 1)
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 7afc7959c..4003468d0 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -2392,8 +2392,7 @@ namespace Emby.Server.Implementations.LiveTv
.Select(i => _libraryManager.FindByPath(i, true))
.Where(i => i is not null && i.IsVisibleStandalone(user))
.SelectMany(i => _libraryManager.GetCollectionFolders(i))
- .GroupBy(x => x.Id)
- .Select(x => x.First())
+ .DistinctBy(x => x.Id)
.OrderBy(i => i.SortName)
.ToList();
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index ada3c7730..4508363b0 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -12,7 +12,7 @@
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
- "Favorites": "مفضلات",
+ "Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
@@ -91,13 +91,13 @@
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
"ValueSpecialEpisodeName": "حلقه خاصه - {0}",
- "VersionNumber": "النسخة {0}",
+ "VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
- "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
+ "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل",
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
- "External": "خارجي"
+ "External": "خارجي",
+ "HearingImpaired": "ضعاف السمع"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index c0ed01fdf..50c574d67 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -118,7 +118,7 @@
"TaskCleanActivityLog": "Buidar Registre d'Activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
- "Default": "Defecte",
+ "Default": "Per defecte",
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
"TaskOptimizeDatabase": "Optimitzar la base de dades",
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json
index 3a8b89f44..dbbc81eeb 100644
--- a/Emby.Server.Implementations/Localization/Core/ka.json
+++ b/Emby.Server.Implementations/Localization/Core/ka.json
@@ -108,5 +108,20 @@
"UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
"UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
- "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა."
+ "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
+ "TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
+ "NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
+ "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
+ "StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
+ "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
+ "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
+ "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.",
+ "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
+ "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.",
+ "TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
+ "TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
+ "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
+ "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
+ "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
+ "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index a4b2e75b3..67dcf5b04 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "데이터베이스 최적화",
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
"TaskKeyframeExtractor": "키프레임 추출",
- "External": "외부"
+ "External": "외부",
+ "HearingImpaired": "청각 장애"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index c05114f01..e03747cbe 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -92,37 +92,37 @@
"ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
"ValueSpecialEpisodeName": "Speciaal - {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.",
"TaskRefreshChannels": "Vernieuw Kanalen",
"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.",
- "TaskRefreshPeople": "Vernieuw Personen",
+ "TaskRefreshPeople": "Personen vernieuwen",
"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.",
- "TaskRefreshChapterImages": "Hoofdstukafbeeldingen Uitpakken",
+ "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
"TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
- "TaskCleanCache": "Cache Folder Opschonen",
+ "TaskCleanCache": "Cache-map opschonen",
"TasksChannelsCategory": "Internet Kanalen",
- "TasksApplicationCategory": "Applicatie",
+ "TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
"TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
- "TaskCleanActivityLog": "Leeg activiteiten logboek",
+ "TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
"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.",
"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",
"HearingImpaired": "Slechthorend"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index d0b458a8f..d4c15ac87 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność.",
"External": "Zewnętrzny",
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
- "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych"
+ "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
+ "HearingImpaired": "Niedosłyszący"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 5413346d4..7fe0c4c4b 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -102,8 +102,8 @@
"LabelIpAddressValue": "آئ پی ایڈریس {0}",
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
- "Inherit": "وراثت میں",
- "HomeVideos": "ہوم ویڈیو",
+ "Inherit": "وراثت",
+ "HomeVideos": "ہوم ویڈیوز",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
"FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
@@ -115,5 +115,13 @@
"AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
"Forced": "جَبری",
"Undefined": "غير وضاحتى",
- "Default": "طے شدہ"
+ "Default": "طے شدہ",
+ "TaskKeyframeExtractorDescription": "زیادہ درست HLS پلے لسٹس بنانے کے لیے ویڈیو فائلوں سے کلیدی فریم نکالتا ہے۔ یہ کام طویل عرصے تک چل سکتا ہے۔",
+ "TaskOptimizeDatabase": "ڈیٹا بیس کو بہتر بنائیں",
+ "TaskOptimizeDatabaseDescription": "ڈیٹا بیس کو کمپیکٹ کرتا ہے اور خالی جگہ کو چھوٹا کرتا ہے۔ لائبریری کو اسکین کرنے یا دیگر تبدیلیاں کرنے کے بعد اس کام کو چلانے سے کارکردگی بہتر ہو سکتی ہے۔",
+ "TaskKeyframeExtractor": "کی فریم ایکسٹریکٹر",
+ "TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔",
+ "External": "بیرونی",
+ "HearingImpaired": "قوت سماعت سے محروم",
+ "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں۔"
}
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index 369a2b0d8..725df98da 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -83,24 +83,6 @@ namespace Emby.Server.Implementations
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>
/// Gets the path to the user configuration directory.
/// </summary>
/// <value>The user configuration directory path.</value>
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index b6780ee20..17d136384 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -92,25 +92,25 @@ namespace Jellyfin.Api.Controllers
Years = itemList.Select(i => i.ProductionYear ?? -1)
.Where(i => i > 0)
.Distinct()
- .OrderBy(i => i)
+ .Order()
.ToArray(),
Genres = itemList.SelectMany(i => i.Genres)
.DistinctNames()
- .OrderBy(i => i)
+ .Order()
.ToArray(),
Tags = itemList
.SelectMany(i => i.Tags)
.Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(i => i)
+ .Order()
.ToArray(),
OfficialRatings = itemList
.Select(i => i.OfficialRating)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(i => i)
+ .Order()
.ToArray()
};
}
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
deleted file mode 100644
index c54851b96..000000000
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index ab2020830..196d509fb 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -770,8 +770,7 @@ namespace Jellyfin.Api.Controllers
Name = i.Name,
DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
})
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
result.MetadataReaders = plugins
@@ -781,8 +780,7 @@ namespace Jellyfin.Api.Controllers
Name = i.Name,
DefaultEnabled = true
})
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
result.SubtitleFetchers = plugins
@@ -792,8 +790,7 @@ namespace Jellyfin.Api.Controllers
Name = i.Name,
DefaultEnabled = true
})
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
var typeOptions = new List<LibraryTypeOptionsDto>();
@@ -814,8 +811,7 @@ namespace Jellyfin.Api.Controllers
Name = i.Name,
DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
})
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
ImageFetchers = plugins
@@ -826,8 +822,7 @@ namespace Jellyfin.Api.Controllers
Name = i.Name,
DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
})
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
SupportedImageTypes = plugins
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 03f864b4a..3cf079362 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -200,8 +200,7 @@ namespace Jellyfin.Api.Controllers
IsMovie = true,
EnableGroupByMetadataKey = true,
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)
.ToList();
@@ -240,8 +239,7 @@ namespace Jellyfin.Api.Controllers
IsMovie = true,
EnableGroupByMetadataKey = true,
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)
.ToList();
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 420630cdf..a28556476 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,12 +1,5 @@
-using System;
using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using System.Threading;
using Jellyfin.Api.Constants;
-using Jellyfin.Api.Models.NotificationDtos;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Notifications;
@@ -23,41 +16,14 @@ namespace Jellyfin.Api.Controllers
public class NotificationsController : BaseJellyfinApiController
{
private readonly INotificationManager _notificationManager;
- private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="NotificationsController" /> class.
/// </summary>
/// <param name="notificationManager">The notification manager.</param>
- /// <param name="userManager">The user manager.</param>
- public NotificationsController(INotificationManager notificationManager, IUserManager userManager)
+ public NotificationsController(INotificationManager notificationManager)
{
_notificationManager = notificationManager;
- _userManager = userManager;
- }
-
- /// <summary>
- /// Gets a user's notifications.
- /// </summary>
- /// <response code="200">Notifications returned.</response>
- /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
- [HttpGet("{userId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<NotificationResultDto> GetNotifications()
- {
- return new NotificationResultDto();
- }
-
- /// <summary>
- /// Gets a user's notification summary.
- /// </summary>
- /// <response code="200">Summary of user's notifications returned.</response>
- /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
- [HttpGet("{userId}/Summary")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<NotificationsSummaryDto> GetNotificationsSummary()
- {
- return new NotificationsSummaryDto();
}
/// <summary>
@@ -83,56 +49,5 @@ namespace Jellyfin.Api.Controllers
{
return _notificationManager.GetNotificationServices();
}
-
- /// <summary>
- /// Sends a notification to all admins.
- /// </summary>
- /// <param name="notificationDto">The notification request.</param>
- /// <response code="204">Notification sent.</response>
- /// <returns>A <cref see="NoContentResult"/>.</returns>
- [HttpPost("Admin")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
- {
- var notification = new NotificationRequest
- {
- Name = notificationDto.Name,
- Description = notificationDto.Description,
- Url = notificationDto.Url,
- Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
- UserIds = _userManager.Users
- .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
- .Select(user => user.Id)
- .ToArray(),
- Date = DateTime.UtcNow,
- };
-
- _notificationManager.SendNotification(notification, CancellationToken.None);
- return NoContent();
- }
-
- /// <summary>
- /// Sets notifications as read.
- /// </summary>
- /// <response code="204">Notifications set as read.</response>
- /// <returns>A <cref see="NoContentResult"/>.</returns>
- [HttpPost("{userId}/Read")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetRead()
- {
- return NoContent();
- }
-
- /// <summary>
- /// Sets notifications as unread.
- /// </summary>
- /// <response code="204">Notifications set as unread.</response>
- /// <returns>A <cref see="NoContentResult"/>.</returns>
- [HttpPost("{userId}/Unread")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetUnread()
- {
- return NoContent();
- }
}
}
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 77d88475f..6dbcdae22 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Quick connect request successfully created.</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>
- [HttpGet("Initiate")]
+ [HttpPost("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
{
@@ -67,6 +68,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>
/// Attempts to retrieve authentication information.
/// </summary>
/// <param name="secret">Secret previously returned from the Initiate endpoint.</param>
@@ -96,6 +107,7 @@ namespace Jellyfin.Api.Controllers
/// Authorizes a pending quick connect request.
/// </summary>
/// <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="403">Unknown user id.</response>
/// <returns>Boolean indicating if the authorization was successful.</returns>
@@ -103,17 +115,19 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[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
{
- return await _quickConnect.AuthorizeRequest(userId, code).ConfigureAwait(false);
+ return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
}
catch (AuthenticationException)
{
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 002327d74..568224a42 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -157,7 +157,6 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The user id.</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="403">Sha1-hashed password only is not allowed.</response>
/// <response code="404">User not found.</response>
@@ -166,10 +165,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Authenticate with username instead")]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
[FromRoute, Required] Guid userId,
- [FromQuery, Required] string pw,
- [FromQuery] string? password)
+ [FromQuery, Required] string pw)
{
var user = _userManager.GetUserById(userId);
@@ -178,11 +177,6 @@ namespace Jellyfin.Api.Controllers
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
{
Username = user.Username,
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 0317a2f2b..889f7dc9a 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,7 +17,7 @@
</PropertyGroup>
<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="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
diff --git a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
deleted file mode 100644
index 2c3a6282f..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// The admin notification dto.
- /// </summary>
- public class AdminNotificationDto
- {
- /// <summary>
- /// Gets or sets the notification name.
- /// </summary>
- public string? Name { get; set; }
-
- /// <summary>
- /// Gets or sets the notification description.
- /// </summary>
- public string? Description { get; set; }
-
- /// <summary>
- /// Gets or sets the notification level.
- /// </summary>
- public NotificationLevel? NotificationLevel { get; set; }
-
- /// <summary>
- /// Gets or sets the notification url.
- /// </summary>
- public string? Url { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
deleted file mode 100644
index af5239ec2..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// The notification DTO.
- /// </summary>
- public class NotificationDto
- {
- /// <summary>
- /// Gets or sets the notification ID. Defaults to an empty string.
- /// </summary>
- public string Id { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification's user ID. Defaults to an empty string.
- /// </summary>
- public string UserId { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification date.
- /// </summary>
- public DateTime Date { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether the notification has been read. Defaults to false.
- /// </summary>
- public bool IsRead { get; set; } = false;
-
- /// <summary>
- /// Gets or sets the notification's name. Defaults to an empty string.
- /// </summary>
- public string Name { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification's description. Defaults to an empty string.
- /// </summary>
- public string Description { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification's URL. Defaults to an empty string.
- /// </summary>
- public string Url { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification level.
- /// </summary>
- public NotificationLevel Level { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
deleted file mode 100644
index 64e92bd83..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// A list of notifications with the total record count for pagination.
- /// </summary>
- public class NotificationResultDto
- {
- /// <summary>
- /// Gets or sets the current page of notifications.
- /// </summary>
- public IReadOnlyList<NotificationDto> Notifications { get; set; } = Array.Empty<NotificationDto>();
-
- /// <summary>
- /// Gets or sets the total number of notifications.
- /// </summary>
- public int TotalRecordCount { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
deleted file mode 100644
index 0568dea66..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// The notification summary DTO.
- /// </summary>
- public class NotificationsSummaryDto
- {
- /// <summary>
- /// Gets or sets the number of unread notifications.
- /// </summary>
- public int UnreadCount { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum unread notification level.
- /// </summary>
- public NotificationLevel? MaxUnreadNotificationLevel { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
index 41f7b169e..31208264f 100644
--- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
+++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
@@ -1,6 +1,4 @@
-using System;
-
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos
{
/// <summary>
/// The authenticate user by name request body.
@@ -16,11 +14,5 @@ namespace Jellyfin.Api.Models.UserDtos
/// Gets or sets the plain text password.
/// </summary>
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; }
}
}
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 366428e78..5520e2f04 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -1301,7 +1301,8 @@ namespace Jellyfin.Networking.Manager
var extResult = _interfaceAddresses
.Exclude(_bindExclusions, false)
.Where(p => !IsInLocalNetwork(p))
- .OrderBy(p => p.Tag);
+ .OrderBy(p => p.Tag)
+ .ToList();
if (extResult.Any())
{
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index d233c00a0..e98290673 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,15 +26,15 @@
</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="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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 58b416480..ac2086935 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -37,8 +37,8 @@
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" 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.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
index 1c25696cd..531897cd4 100644
--- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
@@ -40,25 +40,25 @@ namespace Jellyfin.Server.Middleware
/// <returns>Task.</returns>
public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
{
- var watch = new Stopwatch();
- watch.Start();
+ var startTimestamp = Stopwatch.GetTimestamp();
+
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
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}",
context.Request.GetDisplayUrl(),
context.GetNormalizedRemoteIp(),
- watch.Elapsed,
+ responseTime,
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;
});
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 46f45b9ad..7052f4d2b 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -94,8 +94,7 @@ namespace Jellyfin.Server
private static async Task StartApp(StartupOptions options)
{
- var stopWatch = new Stopwatch();
- stopWatch.Start();
+ var startTimestamp = Stopwatch.GetTimestamp();
// Log all uncaught exceptions to std error
static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
@@ -217,9 +216,7 @@ namespace Jellyfin.Server
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
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 32fe1b3b0..49dd151f3 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -2451,6 +2451,11 @@ namespace MediaBrowser.Controller.Entities
return Task.FromResult(true);
}
+ if (video.OwnerId.Equals(default))
+ {
+ video.OwnerId = this.Id;
+ }
+
return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken);
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 7dc7f774d..5ac619d8f 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -355,8 +355,7 @@ namespace MediaBrowser.Controller.Entities
return PhysicalLocations
.Where(i => !FileSystem.AreEqual(i, Path))
.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)
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 02312757c..e7a8a773e 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -283,7 +283,7 @@ namespace MediaBrowser.Controller.Entities.TV
// This depends on settings for that series
// 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)
diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs
index 1890dbb36..608286cd8 100644
--- a/MediaBrowser.Controller/IServerApplicationPaths.cs
+++ b/MediaBrowser.Controller/IServerApplicationPaths.cs
@@ -51,24 +51,6 @@ namespace MediaBrowser.Controller
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>
/// Gets the path to the user configuration directory.
/// </summary>
/// <value>The user configuration directory path.</value>
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 5905c25a5..f34e3d68d 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -429,10 +429,16 @@ namespace MediaBrowser.Controller.Library
/// Gets the collection folders.
/// </summary>
/// <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> 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);
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 919570e89..ee37fb2dc 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -10,8 +10,7 @@ namespace MediaBrowser.Controller.Library
public static class NameExtensions
{
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)
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 7e3d7a981..4a66edb16 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -19,7 +19,7 @@
<ItemGroup>
<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="System.Threading.Tasks.Dataflow" Version="7.0.0" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 32a7951f6..7e0a69586 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Providers
/// <returns>Task{HttpResponseInfo}.</returns>
Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
- Dictionary<Guid, Guid> GetRefreshQueue();
+ HashSet<Guid> GetRefreshQueue();
void OnRefreshStart(BaseItem item);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index ffaf5246d..91bf42b15 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -415,8 +415,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
}
- var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
-
return GetMediaInfoInternal(
GetInputArgument(inputFile, request.MediaSource),
request.MediaSource.Path,
@@ -425,7 +423,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
analyzeDuration,
request.MediaType == DlnaProfileType.Audio,
request.MediaSource.VideoType,
- forceEnableLogging,
cancellationToken);
}
@@ -473,7 +470,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
string probeSizeArgument,
bool isAudio,
VideoType? videoType,
- bool forceEnableLogging,
CancellationToken cancellationToken)
{
var args = extractChapters
@@ -488,7 +484,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
CreateNoWindow = true,
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,
FileName = _ffprobePath,
@@ -500,21 +496,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
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))
{
await using var memoryStream = new MemoryStream();
- _logger.LogDebug("Starting ffprobe with args {Args}", args);
StartProcess(processWrapper);
- await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken: cancellationToken);
+ await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Seek(0, SeekOrigin.Begin);
InternalMediaInfoResult result;
try
@@ -522,7 +510,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
memoryStream,
_jsonSerializerOptions,
- cancellationToken: cancellationToken).ConfigureAwait(false);
+ cancellationToken).ConfigureAwait(false);
}
catch
{
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 375041490..7404c2868 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -27,7 +27,7 @@
<ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.2" />
- <PackageReference Include="libse" Version="3.6.5" />
+ <PackageReference Include="libse" Version="3.6.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.5.1" />
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 7a3462b97..18e248a1b 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/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, "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)
info.Album = tags.GetValueOrDefault("album");
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index c4d313bdb..81f2f02bc 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -45,6 +45,8 @@ namespace MediaBrowser.Model.Configuration
public bool EnableEmbeddedTitles { get; set; }
+ public bool EnableEmbeddedExtrasTitles { get; set; }
+
public bool EnableEmbeddedEpisodeInfos { get; set; }
public int AutomaticRefreshIntervalDays { get; set; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 2f919b045..bb41c9979 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1477,15 +1477,16 @@ namespace MediaBrowser.Model.Dlna
private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
{
- // Don't restrict by bitrate if coming from an external domain
+ // Don't restrict bitrate if item is remote.
if (item.IsRemote)
{
return false;
}
- 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;
if (itemBitrate > requestedMaxBitrate)
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
index a5a6b18aa..c6d1f3900 100644
--- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -24,24 +24,27 @@ namespace MediaBrowser.Model.Extensions
requestedLanguage = "en";
}
- var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
-
return remoteImageInfos.OrderByDescending(i =>
{
+ // Image priority ordering:
+ // - Images that match the requested language
+ // - Images with no language
+ // - TODO: Images that match the original language
+ // - Images in English
+ // - Images that don't match the requested language
+
if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
{
- return 3;
+ return 4;
}
if (string.IsNullOrEmpty(i.Language))
{
- // Assume empty image language is likely to be English.
- return isRequestedLanguageEn ? 3 : 2;
+ return 3;
}
- if (!isRequestedLanguageEn && string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
{
- // Prioritize English over non-requested languages.
return 2;
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 98179e486..284e89f1c 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -40,7 +40,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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>
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 5b5ca0fca..96ef46239 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -444,8 +444,8 @@ namespace MediaBrowser.Providers.Manager
}
}
- if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) ||
- (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
+ if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
+ || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
{
updateType |= ItemUpdateType.MetadataEdit;
}
@@ -465,7 +465,7 @@ namespace MediaBrowser.Providers.Manager
.Distinct(StringComparer.OrdinalIgnoreCase)
.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;
}
@@ -486,7 +486,7 @@ namespace MediaBrowser.Providers.Manager
.Distinct(StringComparer.OrdinalIgnoreCase)
.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;
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index d3ac2f6cd..914da33a9 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -31,7 +31,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
-using Priority_Queue;
using Book = MediaBrowser.Controller.Entities.Book;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@@ -58,7 +57,7 @@ namespace MediaBrowser.Providers.Manager
private readonly IBaseItemManager _baseItemManager;
private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = 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 IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
@@ -897,18 +896,11 @@ namespace MediaBrowser.Providers.Manager
}
/// <inheritdoc/>
- public Dictionary<Guid, Guid> GetRefreshQueue()
+ public HashSet<Guid> GetRefreshQueue()
{
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;
}
- _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(itemId, options), (int)priority);
+ _refreshQueue.Enqueue((itemId, options), priority);
lock (_refreshQueueLock)
{
@@ -992,7 +984,7 @@ namespace MediaBrowser.Providers.Manager
var cancellationToken = _disposeCancellationTokenSource.Token;
- while (_refreshQueue.TryDequeue(out Tuple<Guid, MetadataRefreshOptions> refreshItem))
+ while (_refreshQueue.TryDequeue(out var refreshItem, out _))
{
if (_disposed)
{
@@ -1001,15 +993,15 @@ namespace MediaBrowser.Providers.Manager
try
{
- var item = libraryManager.GetItemById(refreshItem.Item1);
+ var item = libraryManager.GetItemById(refreshItem.ItemId);
if (item is null)
{
continue;
}
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);
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index d91402f51..dbacc2a82 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -22,8 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<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="TMDbLib" Version="1.9.2" />
</ItemGroup>
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 58b23a36d..751135a2c 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -484,8 +484,8 @@ namespace MediaBrowser.Providers.MediaInfo
{
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;
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 40c489885..1565a8c51 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -149,11 +149,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
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);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
Directory.CreateDirectory(Path.GetDirectoryName(path));
var fileStreamOptions = AsyncFile.WriteOptions;
diff --git a/README.md b/README.md
index 1963ba526..2362741b4 100644
--- a/README.md
+++ b/README.md
@@ -47,16 +47,16 @@
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
-For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html).
+For further details, please see [our documentation page](https://jellyfin.org/docs/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://jellyfin.org/docs/general/getting-help). For more information about the project, please see our [about page](https://jellyfin.org/docs/general/about).
<strong>Want to get started?</strong><br/>
-Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://docs.jellyfin.org/general/administration/installing.html">installation guide</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>. You can also <a href="https://docs.jellyfin.org/general/administration/building.html">build from source</a>.<br/>
+Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://jellyfin.org/docs/general/installation/">installation guide</a>, then see our <a href="https://jellyfin.org/docs/general/quick-start">quick start guide</a>. You can also <a href="https://jellyfin.org/docs/general/installation/source">build from source</a>.<br/>
<strong>Something not working right?</strong><br/>
-Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a> on GitHub.<br/>
+Open an <a href="https://jellyfin.org/docs/general/contributing/issues">Issue</a> on GitHub.<br/>
<strong>Want to contribute?</strong><br/>
-Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://docs.jellyfin.org/general/contributing/index.html">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/>
+Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://jellyfin.org/docs/general/contributing/">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/>
<strong>New idea or improvement?</strong><br/>
Check out our <a href="https://features.jellyfin.org/?view=most-wanted">feature request hub</a>.<br/>
diff --git a/debian/control b/debian/control
index dea48d948..08c0dcda6 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
Build-Depends: debhelper (>= 9),
- dotnet-sdk-6.0,
+ dotnet-sdk-7.0,
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
diff --git a/debian/postinst b/debian/postinst
index 2f9c4cffb..47173855f 100644
--- a/debian/postinst
+++ b/debian/postinst
@@ -83,7 +83,7 @@ fi
# End automatically added section
# Automatically added by dh_installinit
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
deb-systemd-invoke start jellyfin >/dev/null || true
elif [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index fcb880283..f7b7e3025 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/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
# 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 \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index c7bb5f768..1e1f6e54e 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index a0ca9b3f3..bbed2c534 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 42a55ebfe..79373519c 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
index 3fd3fa33c..3a6ad95e8 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/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 ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index e3cc92bcb..ca7239304 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/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 ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index 3a5df2e24..26cce1958 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/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 ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index c18db7213..666937e5c 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/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
# 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 \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 14b580d11..39169bd2a 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index 672c3f269..636a34544 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index f2a178be3..ba8ce82f0 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 025716f45..d771e9991 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64
index 2da72e4ae..846561181 100644
--- a/deployment/Dockerfile.linux.musl-linux-arm64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64
index 62f807687..7ebf35442 100644
--- a/deployment/Dockerfile.macos.amd64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64
index 2dfbab9b3..5041ff967 100644
--- a/deployment/Dockerfile.macos.arm64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index e48e2d41a..822b66ee6 100644
--- a/deployment/Dockerfile.portable
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 01402184a..0ad0132cc 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# 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 \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 6af22eed9..4f7ac2099 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# 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 \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index a7e70a35a..af439e6eb 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# 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 \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index 655300d47..805c63f8c 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/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
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64
index bfdc6e591..0374624d8 100755
--- a/deployment/build.centos.amd64
+++ b/deployment/build.centos.amd64
@@ -9,7 +9,7 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
cp -a jellyfin.spec /tmp/spec.orig
@@ -52,7 +52,7 @@ if [[ ${IS_DOCKER} == YES ]]; then
cp -a /tmp/spec.orig jellyfin.spec
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
-
+
popd
fi
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index b2bbf9c29..d92953ad1 100755
--- a/deployment/build.debian.amd64
+++ b/deployment/build.debian.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
- sed -i '/dotnet-sdk-6.0,/d' debian/control
+ sed -i '/dotnet-sdk-7.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
index 02f84471e..618a121b6 100755
--- a/deployment/build.debian.arm64
+++ b/deployment/build.debian.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
- sed -i '/dotnet-sdk-6.0,/d' debian/control
+ sed -i '/dotnet-sdk-7.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
index 92779cb59..d1631d022 100755
--- a/deployment/build.debian.armhf
+++ b/deployment/build.debian.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
- sed -i '/dotnet-sdk-6.0,/d' debian/control
+ sed -i '/dotnet-sdk-7.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
index 23c5ed86a..1b629289f 100755
--- a/deployment/build.fedora.amd64
+++ b/deployment/build.fedora.amd64
@@ -9,7 +9,7 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
cp -a jellyfin.spec /tmp/spec.orig
@@ -52,7 +52,7 @@ if [[ ${IS_DOCKER} == YES ]]; then
cp -a /tmp/spec.orig jellyfin.spec
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
-
+
popd
fi
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
index c36978c9e..4254103fa 100755
--- a/deployment/build.ubuntu.amd64
+++ b/deployment/build.ubuntu.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
- sed -i '/dotnet-sdk-6.0,/d' debian/control
+ sed -i '/dotnet-sdk-7.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
index 76d51e321..42f111a01 100755
--- a/deployment/build.ubuntu.arm64
+++ b/deployment/build.ubuntu.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
- sed -i '/dotnet-sdk-6.0,/d' debian/control
+ sed -i '/dotnet-sdk-7.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
index 0ff5ab066..357d63626 100755
--- a/deployment/build.ubuntu.armhf
+++ b/deployment/build.ubuntu.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
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
- sed -i '/dotnet-sdk-6.0,/d' debian/control
+ sed -i '/dotnet-sdk-7.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
index 30b252beb..0786358bd 100755
--- a/deployment/build.windows.amd64
+++ b/deployment/build.windows.amd64
@@ -8,7 +8,7 @@ set -o xtrace
# Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad"
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
pushd ${SOURCE_DIR}
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index a6771e389..416d88360 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -27,7 +27,7 @@ BuildRequires: systemd
BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
# Requirements not packaged in RHEL 7 main repos, added via Makefile
# 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}
# 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
# Jellyfin files
%{__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
%{__install} -D %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index eaf2bc35c..9fed8cbd9 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -27,6 +27,11 @@
<Compile Include="../../SharedVersion.cs" />
</ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Diacritics" Version="3.3.14" />
+ </ItemGroup>
+
<!-- Code Analyzers-->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index b19be071b..f30b63945 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -20,23 +20,8 @@ namespace Jellyfin.Extensions
/// <param name="text">The string to act on.</param>
/// <returns>The string without diacritics character.</returns>
public static string RemoveDiacritics(this string text)
- {
- string withDiactritics = _nonConformingUnicode
- .Replace(text, string.Empty)
- .Normalize(NormalizationForm.FormD);
-
- var withoutDiactritics = new StringBuilder();
- foreach (char c in withDiactritics)
- {
- UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(c);
- if (uc != UnicodeCategory.NonSpacingMark)
- {
- withoutDiactritics.Append(c);
- }
- }
-
- return withoutDiactritics.ToString().Normalize(NormalizationForm.FormC);
- }
+ => Diacritics.Extensions.StringExtensions.RemoveDiacritics(
+ _nonConformingUnicode.Replace(text, string.Empty));
/// <summary>
/// Checks whether or not the specified string has diacritics in it.
@@ -44,9 +29,8 @@ namespace Jellyfin.Extensions
/// <param name="text">The string to check.</param>
/// <returns>True if the string has diacritics, false otherwise.</returns>
public static bool HasDiacritics(this string text)
- {
- return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
- }
+ => Diacritics.Extensions.StringExtensions.HasDiacritics(text)
+ || _nonConformingUnicode.IsMatch(text);
/// <summary>
/// Counts the number of occurrences of [needle] in the string.
diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
index 7730841a1..2a7e8fafd 100644
--- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Extensions.Tests
{
var copy = strings.Reverse().ToArray();
Array.Sort(copy, new AlphanumericComparator());
- Assert.True(strings.SequenceEqual(copy));
+ Assert.Equal(strings, copy);
}
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
index af9227de2..16c69ca48 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
@@ -57,7 +57,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
Assert.Equal(json, res);
}
- private class TestContainer
+ private sealed class TestContainer
{
public TestContainer(CollectionTypeOptions? collectionType)
{
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
index 903d88caa..69d20bd3f 100644
--- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -9,12 +9,15 @@ namespace Jellyfin.Extensions.Tests
[InlineData("", "")] // Identity edge-case (no diactritics)
[InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics)
[InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping
+ [InlineData("åäö", "aao")] // Issue #7484
[InlineData("Jön", "Jon")] // Issue #7484
[InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484
[InlineData("Kieślowski", "Kieslowski")] // Issue #7450
[InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560
[InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support)
[InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393
+ [InlineData("Le cœur a ses raisons", "Le coeur a ses raisons")] // Issue #8893
+ [InlineData("Béla Tarr", "Bela Tarr")] // Issue #8893
public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult)
{
string result = input.RemoveDiacritics();
@@ -25,12 +28,15 @@ namespace Jellyfin.Extensions.Tests
[InlineData("", false)] // Identity edge-case (no diactritics)
[InlineData("Indiana Jones", false)] // Identity (no diactritics)
[InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping
+ [InlineData("åäö", true)] // Issue #7484
[InlineData("Jön", true)] // Issue #7484
[InlineData("Jönssonligan", true)] // Issue #7484
[InlineData("Kieślowski", true)] // Issue #7450
[InlineData("Cidadão Kane", true)] // Issue #7560
[InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support)
[InlineData("애타는 로맨스", false)] // Issue #6393
+ [InlineData("Le cœur a ses raisons", true)] // Issue #8893
+ [InlineData("Béla Tarr", true)] // Issue #8893
public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult)
{
bool result = input.HasDiacritics();
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index c0c363d3d..1b27e344b 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -32,7 +32,7 @@ namespace Jellyfin.MediaEncoding.Tests
Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
}
- private class GetFFmpegVersionTestData : TheoryData<string, Version?>
+ private sealed class GetFFmpegVersionTestData : TheoryData<string, Version?>
{
public GetFFmpegVersionTestData()
{
diff --git a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs
index a1ace8476..2a62ab74c 100644
--- a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs
+++ b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs
@@ -186,7 +186,7 @@ namespace Jellyfin.Model.Tests.Entities
Assert.Null(nullProvider.ProviderIds);
}
- private class ProviderIdsExtensionsTestsObject : IHasProviderIds
+ private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds
{
public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject();
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index 4c7c56311..7d92e7b26 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -282,7 +282,7 @@ namespace Jellyfin.Server.Implementations.Tests.Data
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>();
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs
index ef8f7cd90..f01611819 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs
+++ b/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));
}
- internal class BufferSegment : ReadOnlySequenceSegment<byte>
+ internal sealed class BufferSegment : ReadOnlySequenceSegment<byte>
{
public BufferSegment(Memory<byte> memory)
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index c21871297..286ba0405 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -60,7 +60,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
}
- private class EpisodeResolverMock : EpisodeResolver
+ private sealed class EpisodeResolverMock : EpisodeResolver
{
public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
index 59d82678e..1dd49b2cf 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
+++ b/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));
}
- private class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
+ private sealed class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
{
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()
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 79c11a865..9eb0beda4 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -48,7 +48,7 @@ namespace Jellyfin.Server.Integration.Tests
headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
}
- private class AuthenticationResultDto
+ private sealed class AuthenticationResultDto
{
public string AccessToken { get; set; } = string.Empty;