aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.devcontainer/Dev - Server Ffmpeg/devcontainer.json28
-rw-r--r--.devcontainer/devcontainer.json4
-rw-r--r--.devcontainer/install-ffmpeg.sh (renamed from .devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh)2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--.vscode/extensions.json11
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props12
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json3
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv32
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs40
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs37
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs10
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs2
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs13
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs7
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs10
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs40
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs2
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs20
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs69
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs7
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs13
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs56
-rw-r--r--MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs12
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs10
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs5
-rw-r--r--MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs6
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs1
-rw-r--r--MediaBrowser.Model/Tasks/TaskTriggerInfo.cs22
-rw-r--r--MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs28
-rw-r--r--MediaBrowser.Providers/Lyric/LyricScheduledTask.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs28
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs2
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs2
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs13
-rw-r--r--src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs2
-rw-r--r--src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs2
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs50
61 files changed, 429 insertions, 241 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 02afa3f07..dd484d564 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.11",
+ "version": "9.0.0",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
deleted file mode 100644
index a934512f4..000000000
--- a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "Development Jellyfin Server - FFmpeg",
- "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-jammy",
- // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
- "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
- // reads the extensions list and installs them
- "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
- "features": {
- "ghcr.io/devcontainers/features/dotnet:2": {
- "version": "none",
- "dotnetRuntimeVersions": "9.0",
- "aspNetCoreRuntimeVersions": "9.0"
- },
- "ghcr.io/devcontainers-contrib/features/apt-packages:1": {
- "preserve_apt_list": false,
- "packages": ["libfontconfig1"]
- },
- "ghcr.io/devcontainers/features/docker-in-docker:2": {
- "dockerDashComposeVersion": "v2"
- },
- "ghcr.io/devcontainers/features/github-cli:1": {},
- "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
- },
- "hostRequirements": {
- "memory": "8gb",
- "cpus": 4
- }
-}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 0cf768f1f..228d4a17c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,8 +1,8 @@
{
"name": "Development Jellyfin Server",
- "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-jammy",
+ "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
- "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
+ "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
diff --git a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh
index c867ef538..842a53255 100644
--- a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
+++ b/.devcontainer/install-ffmpeg.sh
@@ -29,4 +29,4 @@ Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
sudo apt update -y
-sudo apt install jellyfin-ffmpeg6 -y
+sudo apt install jellyfin-ffmpeg7 -y
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index e6993d39d..2c88330cb 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
+ uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
+ uses: github/codeql-action/autobuild@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
+ uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 353c47c54..25b4b9f81 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 3be946e44..e4205ce0b 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,12 +1,13 @@
{
- "recommendations": [
+ "recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
- "ms-dotnettools.csdevkit"
- ],
- "unwantedRecommendations": [
+ "ms-dotnettools.csdevkit",
+ "alexcvzz.vscode-sqlite"
+ ],
+ "unwantedRecommendations": [
- ]
+ ]
}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index e44608135..eccc3b0ce 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -192,6 +192,7 @@
- [jaina heartles](https://github.com/heartles)
- [oxixes](https://github.com/oxixes)
- [elfalem](https://github.com/elfalem)
+ - [Kenneth Cochran](https://github.com/kennethcochran)
- [benedikt257](https://github.com/benedikt257)
- [revam](https://github.com/revam)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1f614148a..64245b112 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AsyncKeyedLock" Version="7.1.3" />
+ <PackageVersion Include="AsyncKeyedLock" Version="7.1.4" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -47,8 +47,8 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
- <PackageVersion Include="MimeTypes" Version="2.4.0" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
+ <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
@@ -71,7 +71,7 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="2.0.0.2" />
+ <PackageVersion Include="Svg.Skia" Version="2.0.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
@@ -80,12 +80,12 @@
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.8.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.9.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
- <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
+ <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.2" />
</ItemGroup>
</Project> \ No newline at end of file
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 91791a1c8..a06f6e7fe 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a03c1214d..14798dda6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (args.IsDirectory)
{
- // It's a boxset if the path is a directory with [playlist] in its name
+ // It's a playlist if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index e149f8adf..6da31227d 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -131,5 +131,6 @@
"TaskRefreshTrickplayImages": "Генерирај слики за прегледување (Trickplay)",
"TaskAudioNormalization": "Нормализација на звукот",
"TaskRefreshTrickplayImagesDescription": "Креира трикплеј прегледи за видеа во овозможените библиотеки.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат.",
+ "TaskExtractMediaSegments": "Скенирање на сегменти на содржина"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index bf59e1583..a873c157e 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -77,7 +77,7 @@
"HeaderAlbumArtists": "Artiști album",
"Genres": "Genuri",
"Folders": "Dosare",
- "Favorites": "Favorite",
+ "Favorites": "Preferate",
"FailedLoginAttemptWithUserName": "Încercare de conectare eșuată pentru {0}",
"DeviceOnlineWithName": "{0} este conectat",
"DeviceOfflineWithName": "{0} s-a deconectat",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index e2f768f1f..bc1fd8cb2 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -133,5 +133,6 @@
"TaskDownloadMissingLyricsDescription": "下載歌詞",
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
"TaskAudioNormalization": "音訊同等化",
- "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。"
+ "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
+ "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。"
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv
index d103ddf42..fc91edecd 100644
--- a/Emby.Server.Implementations/Localization/Ratings/us.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/us.csv
@@ -5,23 +5,23 @@ TV-Y,0
TV-Y7,7
TV-Y7-FV,7
PG,10
+TV-PG,10
+TV-PG-D,10
+TV-PG-L,10
+TV-PG-S,10
+TV-PG-V,10
+TV-PG-DL,10
+TV-PG-DS,10
+TV-PG-DV,10
+TV-PG-LS,10
+TV-PG-LV,10
+TV-PG-SV,10
+TV-PG-DLS,10
+TV-PG-DLV,10
+TV-PG-DSV,10
+TV-PG-LSV,10
+TV-PG-DLSV,10
PG-13,13
-TV-PG,13
-TV-PG-D,13
-TV-PG-L,13
-TV-PG-S,13
-TV-PG-V,13
-TV-PG-DL,13
-TV-PG-DS,13
-TV-PG-DV,13
-TV-PG-LS,13
-TV-PG-LV,13
-TV-PG-SV,13
-TV-PG-DLS,13
-TV-PG-DLV,13
-TV-PG-DSV,13
-TV-PG-LSV,13
-TV-PG-DLSV,13
TV-14,14
TV-14-D,14
TV-14-L,14
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 47ff22c0b..daeb7fed8 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists
var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist);
- // Filter out duplicate items, if necessary
- if (!_appConfig.DoPlaylistsAllowDuplicates())
- {
- var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
- newItems = newItems
- .Where(i => !existingIds.Contains(i.Id))
- .Distinct();
- }
+ // Filter out duplicate items
+ var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
+ newItems = newItems
+ .Where(i => !existingIds.Contains(i.Id))
+ .Distinct();
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
@@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists
var idList = entryIds.ToList();
- var removals = children.Where(i => idList.Contains(i.Item1.Id));
+ var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
playlist.LinkedChildren = children.Except(removals)
.Select(i => i.Item1)
@@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
+ public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
throw new ArgumentException("No Playlist exists with the supplied Id");
}
+ var user = _userManager.GetUserById(callingUserId);
var children = playlist.GetManageableItems().ToList();
+ var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray();
- var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase));
+ var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
+ var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
- if (oldIndex == newIndex)
+ if (oldIndexAccessible == newIndex)
{
return;
}
- var item = playlist.LinkedChildren[oldIndex];
+ var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
+ var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
+ var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
+ var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1;
- var newList = playlist.LinkedChildren.ToList();
+ var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
+ if (item is null)
+ {
+ _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
+ return;
+ }
+
+ var newList = playlist.LinkedChildren.ToList();
newList.Remove(item);
if (newIndex >= newList.Count)
@@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists
}
else
{
- newList.Insert(newIndex, item);
+ newList.Insert(adjustedNewIndex, item);
}
playlist.LinkedChildren = [.. newList];
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index e7323d9d0..4c32d5717 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -785,30 +785,27 @@ namespace Emby.Server.Implementations.Plugins
var cleaned = false;
var path = entry.Path;
- if (_config.RemoveOldPlugins)
+ // Attempt a cleanup of old folders.
+ try
{
- // Attempt a cleanup of old folders.
- try
- {
- _logger.LogDebug("Deleting {Path}", path);
- Directory.Delete(path, true);
- cleaned = true;
- }
+ _logger.LogDebug("Deleting {Path}", path);
+ Directory.Delete(path, true);
+ cleaned = true;
+ }
#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception e)
+ catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
- {
- _logger.LogWarning(e, "Unable to delete {Path}", path);
- }
+ {
+ _logger.LogWarning(e, "Unable to delete {Path}", path);
+ }
- if (cleaned)
- {
- versions.RemoveAt(x);
- }
- else
- {
- ChangePluginState(entry, PluginStatus.Deleted);
- }
+ if (cleaned)
+ {
+ versions.RemoveAt(x);
+ }
+ else
+ {
+ ChangePluginState(entry, PluginStatus.Deleted);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 9b342cfbe..fe769baf9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -471,7 +471,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
new()
{
IntervalTicks = TimeSpan.FromDays(1).Ticks,
- Type = TaskTriggerInfo.TriggerInterval
+ Type = TaskTriggerInfoType.IntervalTrigger
}
];
}
@@ -616,7 +616,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
MaxRuntimeTicks = info.MaxRuntimeTicks
};
- if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.DailyTrigger)
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -626,7 +626,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
- if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.WeeklyTrigger)
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -641,7 +641,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
}
- if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.IntervalTrigger)
{
if (!info.IntervalTicks.HasValue)
{
@@ -651,7 +651,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
- if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.StartupTrigger)
{
return new StartupTrigger(options);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index eb6afe05d..031d14776 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -156,7 +156,7 @@ public partial class AudioNormalizationTask : IScheduledTask
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index cb3f5b836..2c7d06ed4 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerDaily,
+ Type = TaskTriggerInfoType.DailyTrigger,
TimeOfDayTicks = TimeSpan.FromHours(2).Ticks,
MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 25e7ebe79..316e4a8f0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -135,6 +135,6 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return [new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup }];
+ return [new TaskTriggerInfo() { Type = TaskTriggerInfoType.StartupTrigger }];
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 0325cb9af..ff295d9b7 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return
[
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
];
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index 9babe8cf9..a091c2bd9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
return
[
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
];
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 315c245cc..d0896cc81 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -69,11 +69,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerStartup
+ Type = TaskTriggerInfoType.StartupTrigger
},
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
index d6fad7526..de1e60d30 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
@@ -111,7 +111,7 @@ public class MediaSegmentExtractionTask : IScheduledTask
{
yield return new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(12).Ticks
};
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 3e4925f74..7d4e2377d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return
[
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
];
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index c63bad474..2907f18b5 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromDays(7).Ticks
}
};
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index ad72a4c87..c597103dd 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -60,10 +60,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
// At startup
- yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerStartup };
+ yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.StartupTrigger };
// Every so often
- yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks };
+ yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks };
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index a59f0f366..172448dde 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
yield return new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(12).Ticks
};
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 54e0527c9..a641ec209 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
{
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
- var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
+ // Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
+ // Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
+ var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
+ var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
if (EncodingHelper.IsCopyCodec(codec)
- && ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
+ && (videoIsDoVi && clientSupportsDoVi))
{
if (isActualOutputVideoCodecHevc)
{
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index d7a8c37c4..7effe61e4 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <param name="regenerateTrickplay">(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <response code="204">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@@ -62,7 +63,8 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
- [FromQuery] bool replaceAllImages = false)
+ [FromQuery] bool replaceAllImages = false,
+ [FromQuery] bool regenerateTrickplay = false)
{
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
@@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| replaceAllImages
|| replaceAllMetadata,
IsAutomated = false,
- RemoveOldMetadata = replaceAllMetadata
+ RemoveOldMetadata = replaceAllMetadata,
+ RegenerateTrickplay = regenerateTrickplay
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 0ae8baa67..421f23fa1 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -962,9 +962,9 @@ public class LiveTvController : BaseJellyfinApiController
}
/// <summary>
- /// Get guid info.
+ /// Get guide info.
/// </summary>
- /// <response code="200">Guid info returned.</response>
+ /// <response code="200">Guide info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
[HttpGet("GuideInfo")]
[Authorize(Policy = Policies.LiveTvAccess)]
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index 3dc5167a2..2d1d4e2c8 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
- var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index e6f23b136..1ab36ccc6 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
+ await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
return NoContent();
}
@@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- var items = playlist.GetManageableItems().ToArray();
+ var user = _userManager.GetUserById(callingUserId);
+ var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
@@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var user = _userManager.GetUserById(callingUserId);
+
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
for (int index = 0; index < dtos.Count; index++)
{
- dtos[index].PlaylistItemId = items[index].Item1.Id;
+ dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
}
var result = new QueryResult<BaseItemDto>(
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index d641f521b..2d3a25357 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -61,7 +61,7 @@ public class MediaSegmentManager : IMediaSegmentManager
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.OrderBy(i =>
{
- var index = libraryOptions.MediaSegmentProvideOrder.IndexOf(i.Name);
+ var index = libraryOptions.MediaSegmentProviderOrder.IndexOf(i.Name);
return index == -1 ? int.MaxValue : index;
})
.ToList();
@@ -139,23 +139,53 @@ public class MediaSegmentManager : IMediaSegmentManager
}
/// <inheritdoc />
- public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
+ {
+ var baseItem = _libraryManager.GetItemById(itemId);
+
+ if (baseItem is null)
+ {
+ _logger.LogError("Tried to request segments for an invalid item");
+ return [];
+ }
+
+ return await GetSegmentsAsync(baseItem, typeFilter, filterByProvider).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
{
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var query = db.MediaSegments
- .Where(e => e.ItemId.Equals(itemId));
+ .Where(e => e.ItemId.Equals(item.Id));
if (typeFilter is not null)
{
query = query.Where(e => typeFilter.Contains(e.Type));
}
+ if (filterByProvider)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
+ var providerIds = _segmentProviders
+ .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
+ .Select(f => GetProviderId(f.Name))
+ .ToArray();
+ if (providerIds.Length == 0)
+ {
+ return [];
+ }
+
+ query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
+ }
+
return query
.OrderBy(e => e.StartTicks)
.AsNoTracking()
- .ToImmutableList()
- .Select(Map);
+ .AsEnumerable()
+ .Select(Map)
+ .ToArray();
}
private static MediaSegmentDto Map(MediaSegment segment)
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index cfe385106..af57bc134 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -238,7 +238,7 @@ public class TrickplayManager : ITrickplayManager
foreach (var tile in existingFiles)
{
var image = _imageEncoder.GetImageSize(tile);
- localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
+ localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, (int)Math.Ceiling((double)image.Height / localTrickplayInfo.TileHeight));
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
}
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
index 801026c54..901ed55be 100644
--- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -101,7 +101,7 @@ namespace Jellyfin.Server.Infrastructure
count: null);
}
- private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
+ private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
{
var fileInfo = GetFileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
@@ -118,6 +118,9 @@ namespace Jellyfin.Server.Infrastructure
// Copied from SendFileFallback.SendFileAsync
const int BufferSize = 1024 * 16;
+ var useRequestAborted = !cancellationToken.CanBeCanceled;
+ var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
+
var fileStream = new FileStream(
filePath,
FileMode.Open,
@@ -127,10 +130,17 @@ namespace Jellyfin.Server.Infrastructure
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
- fileStream.Seek(offset, SeekOrigin.Begin);
- await StreamCopyOperation
- .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
- .ConfigureAwait(true);
+ try
+ {
+ localCancel.ThrowIfCancellationRequested();
+ fileStream.Seek(offset, SeekOrigin.Begin);
+ await StreamCopyOperation
+ .CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
+ .ConfigureAwait(true);
+ }
+ catch (OperationCanceledException) when (useRequestAborted)
+ {
+ }
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 9d4441ac3..2ab130eef 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -47,7 +47,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
- typeof(Routines.MoveTrickplayFiles)
+ typeof(Routines.MoveTrickplayFiles),
+ typeof(Routines.RemoveDuplicatePlaylistChildren)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index 3655a610d..192c170b2 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines;
/// </summary>
internal class FixPlaylistOwner : IMigrationRoutine
{
- private readonly ILogger<RemoveDuplicateExtras> _logger;
+ private readonly ILogger<FixPlaylistOwner> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IPlaylistManager _playlistManager;
public FixPlaylistOwner(
- ILogger<RemoveDuplicateExtras> logger,
+ ILogger<FixPlaylistOwner> logger,
ILibraryManager libraryManager,
IPlaylistManager playlistManager)
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 247e1d845..9c2184029 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
}
/// <inheritdoc/>
- public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
+ public Guid Id => Guid.Parse("{73DAB92A-178B-48CD-B05B-FE18733ACDC8}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
new file mode 100644
index 000000000..f84bccc25
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Linq;
+using System.Threading;
+
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Remove duplicate playlist entries.
+/// </summary>
+internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
+{
+ private readonly ILogger<RemoveDuplicatePlaylistChildren> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IPlaylistManager _playlistManager;
+
+ public RemoveDuplicatePlaylistChildren(
+ ILogger<RemoveDuplicatePlaylistChildren> logger,
+ ILibraryManager libraryManager,
+ IPlaylistManager playlistManager)
+ {
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _playlistManager = playlistManager;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
+
+ /// <inheritdoc/>
+ public string Name => "RemoveDuplicatePlaylistChildren";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var playlists = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Playlist]
+ })
+ .Cast<Playlist>()
+ .Where(p => !p.OpenAccess || !p.OwnerUserId.Equals(Guid.Empty))
+ .ToArray();
+
+ if (playlists.Length > 0)
+ {
+ foreach (var playlist in playlists)
+ {
+ var linkedChildren = playlist.LinkedChildren;
+ if (linkedChildren.Length > 0)
+ {
+ var nullItemChildren = linkedChildren.Where(c => c.ItemId is null);
+ var deduplicatedChildren = linkedChildren.DistinctBy(c => c.ItemId);
+ var newLinkedChildren = nullItemChildren.Concat(deduplicatedChildren);
+ playlist.LinkedChildren = linkedChildren;
+ playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+ _playlistManager.SavePlaylistFile(playlist);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
index fd5fef3dc..98e4f525f 100644
--- a/MediaBrowser.Controller/Entities/LinkedChild.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -4,7 +4,6 @@
using System;
using System.Globalization;
-using System.Text.Json.Serialization;
namespace MediaBrowser.Controller.Entities
{
@@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
{
public LinkedChild()
{
- Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public string Path { get; set; }
@@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
public string LibraryItemId { get; set; }
- [JsonIgnore]
- public string Id { get; set; }
-
/// <summary>
/// Gets or sets the linked item id.
/// </summary>
@@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities
public static LinkedChild Create(BaseItem item)
{
+ ArgumentNullException.ThrowIfNull(item);
+
var child = new LinkedChild
{
Path = item.Path,
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index f8049cd48..e4806109a 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -50,11 +50,6 @@ namespace MediaBrowser.Controller.Extensions
public const string FfmpegPathKey = "ffmpeg";
/// <summary>
- /// The key for a setting that indicates whether playlists should allow duplicate entries.
- /// </summary>
- public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
-
- /// <summary>
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
/// </summary>
public const string BindToUnixSocketKey = "kestrel:socket";
@@ -121,14 +116,6 @@ namespace MediaBrowser.Controller.Extensions
=> configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
/// <summary>
- /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
- /// </summary>
- /// <param name="configuration">The configuration to read the setting from.</param>
- /// <returns>True if playlists should allow duplicates, otherwise false.</returns>
- public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration)
- => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey);
-
- /// <summary>
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 28f0d1fff..9399679a4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2196,7 +2196,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var videoFrameRate = videoStream.ReferenceFrameRate;
- if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
+ // Add a little tolerance to the framerate check because some videos might record a framerate
+ // that is slightly higher than the intended framerate, but the device can still play it correctly.
+ // 0.05 fps tolerance should be safe enough.
+ if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f)
{
return false;
}
@@ -3318,24 +3321,25 @@ namespace MediaBrowser.Controller.MediaEncoding
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
- procampParams += $"=b={options.VppTonemappingBrightness}";
+ procampParams += "procamp_vaapi=b={0}";
doVaVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
- procampParams += doVaVppProcamp ? ":" : "=";
- procampParams += $"c={options.VppTonemappingContrast}";
+ procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}";
doVaVppProcamp = true;
}
- args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
+ args = procampParams + "{2}tonemap_vaapi=format={3}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
return string.Format(
CultureInfo.InvariantCulture,
args,
- doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
+ options.VppTonemappingBrightness,
+ options.VppTonemappingContrast,
+ doVaVppProcamp ? "," : string.Empty,
videoFormat ?? "nv12");
}
else
@@ -3523,20 +3527,29 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
-
- var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
+ var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}";
if (options.TonemappingParam != 0)
{
- tonemapArgs += $":param={options.TonemappingParam}";
+ tonemapArgString += ":param={4}";
}
var range = options.TonemappingRange;
if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
- tonemapArgs += $":range={options.TonemappingRange}";
+ tonemapArgString += ":range={5}";
}
+ var tonemapArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ tonemapArgString,
+ options.TonemappingAlgorithm,
+ options.TonemappingDesat,
+ options.TonemappingPeak,
+ tonemapFormat,
+ options.TonemappingParam,
+ options.TonemappingRange);
+
mainFilters.Add(tonemapArgs);
}
else
@@ -4128,31 +4141,46 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isD3d11vaDecoder || isQsvDecoder)
{
var isRext = IsVideoStreamHevcRext(state);
- var twoPassVppTonemap = isRext;
+ var twoPassVppTonemap = false;
var doVppFullRangeOut = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
var doVppScaleModeHq = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
var doVppProcamp = false;
var procampParams = string.Empty;
+ var procampParamsString = string.Empty;
if (doVppTonemap)
{
+ if (isRext)
+ {
+ // VPP tonemap requires p010 input
+ twoPassVppTonemap = true;
+ }
+
if (options.VppTonemappingBrightness != 0
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
- procampParams += $":brightness={options.VppTonemappingBrightness}";
+ procampParamsString += ":brightness={0}";
twoPassVppTonemap = doVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
- procampParams += $":contrast={options.VppTonemappingContrast}";
+ procampParamsString += ":contrast={1}";
twoPassVppTonemap = doVppProcamp = true;
}
- procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
+ if (doVppProcamp)
+ {
+ procampParamsString += ":procamp=1:async_depth=2";
+ procampParams = string.Format(
+ CultureInfo.InvariantCulture,
+ procampParamsString,
+ options.VppTonemappingBrightness,
+ options.VppTonemappingContrast);
+ }
}
var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
index 010d7edb4..672f27eca 100644
--- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
+++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
@@ -50,8 +50,18 @@ public interface IMediaSegmentManager
/// </summary>
/// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
/// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
- Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
+
+ /// <summary>
+ /// Obtains all segments accociated with the itemId.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
+ /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
/// <summary>
/// Gets information about any media segments stored for the given itemId.
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index 038cbd2d6..497c4a511 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryId">The entry identifier.</param>
/// <param name="newIndex">The new index.</param>
+ /// <param name="callingUserId">The calling user.</param>
/// <returns>Task.</returns>
- Task MoveItemAsync(string playlistId, string entryId, int newIndex);
+ Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId);
/// <summary>
/// Removed all playlists of a user.
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 826ffd0b7..a34238cd6 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1035,6 +1035,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1)
{
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+ // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
+ // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
+ try
+ {
+ Directory.Delete(targetDirectory, true);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory);
+ }
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
}
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 6054ba34e..590b74304 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Configuration
TypeOptions = Array.Empty<TypeOptions>();
DisabledSubtitleFetchers = Array.Empty<string>();
DisabledMediaSegmentProviders = Array.Empty<string>();
- MediaSegmentProvideOrder = Array.Empty<string>();
+ MediaSegmentProviderOrder = Array.Empty<string>();
SubtitleFetcherOrder = Array.Empty<string>();
DisabledLocalMetadataReaders = Array.Empty<string>();
DisabledLyricFetchers = Array.Empty<string>();
@@ -99,7 +99,7 @@ namespace MediaBrowser.Model.Configuration
public string[] DisabledMediaSegmentProviders { get; set; }
- public string[] MediaSegmentProvideOrder { get; set; }
+ public string[] MediaSegmentProviderOrder { get; set; }
public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 5ad588200..bc4e6ef73 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -244,11 +244,6 @@ public class ServerConfiguration : BaseApplicationConfiguration
public int LibraryMetadataRefreshConcurrency { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
- /// </summary>
- public bool RemoveOldPlugins { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
/// </summary>
public bool AllowClientLogUpload { get; set; } = true;
diff --git a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
index 4a814f22a..b088cfb53 100644
--- a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
+++ b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
@@ -18,7 +18,7 @@ public static class LibraryOptionsExtension
{
ArgumentNullException.ThrowIfNull(options);
- return options.CustomTagDelimiters.Select<string, char?>(x =>
+ var delimiterList = options.CustomTagDelimiters.Select<string, char?>(x =>
{
var isChar = char.TryParse(x, out var c);
if (isChar)
@@ -27,6 +27,8 @@ public static class LibraryOptionsExtension
}
return null;
- }).Where(x => x is not null).Select(x => x!.Value).ToArray();
+ }).Where(x => x is not null).Select(x => x!.Value).ToList();
+ delimiterList.Add('\0');
+ return delimiterList.ToArray();
}
}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 5d65b0f9b..e4c0239b8 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -125,6 +125,7 @@ namespace MediaBrowser.Model.Net
new("audio/vorbis", ".vorbis"),
new("audio/x-ape", ".ape"),
new("audio/xsp", ".xsp"),
+ new("audio/x-aac", ".aac"),
new("audio/x-wavpack", ".wv"),
// Type image
diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
index 63709557d..186c0aed3 100644
--- a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
+++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
@@ -9,30 +9,10 @@ namespace MediaBrowser.Model.Tasks
public class TaskTriggerInfo
{
/// <summary>
- /// The daily trigger.
- /// </summary>
- public const string TriggerDaily = "DailyTrigger";
-
- /// <summary>
- /// The weekly trigger.
- /// </summary>
- public const string TriggerWeekly = "WeeklyTrigger";
-
- /// <summary>
- /// The interval trigger.
- /// </summary>
- public const string TriggerInterval = "IntervalTrigger";
-
- /// <summary>
- /// The startup trigger.
- /// </summary>
- public const string TriggerStartup = "StartupTrigger";
-
- /// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
- public string Type { get; set; }
+ public TaskTriggerInfoType Type { get; set; }
/// <summary>
/// Gets or sets the time of day.
diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs
new file mode 100644
index 000000000..b596cf580
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs
@@ -0,0 +1,28 @@
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Enum TaskTriggerInfoType.
+ /// </summary>
+ public enum TaskTriggerInfoType
+ {
+ /// <summary>
+ /// The daily trigger.
+ /// </summary>
+ DailyTrigger,
+
+ /// <summary>
+ /// The weekly trigger.
+ /// </summary>
+ WeeklyTrigger,
+
+ /// <summary>
+ /// The interval trigger.
+ /// </summary>
+ IntervalTrigger,
+
+ /// <summary>
+ /// The startup trigger.
+ /// </summary>
+ StartupTrigger
+ }
+}
diff --git a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
index 89d71e172..73912b579 100644
--- a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
+++ b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
@@ -162,7 +162,7 @@ public class LyricScheduledTask : IScheduledTask
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 27f6d120f..7f1fdbcb8 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -347,7 +347,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
+ var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
}
}
@@ -357,7 +358,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
}
}
@@ -367,7 +369,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
}
}
@@ -377,7 +380,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
}
}
@@ -387,7 +391,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
&& !string.IsNullOrEmpty(trackMbId))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
+ var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
}
}
@@ -441,5 +446,18 @@ namespace MediaBrowser.Providers.MediaInfo
return items;
}
+
+ // MusicBrainz IDs are multi-value tags, so we need to split them
+ // However, our current provider can only have one single ID, which means we need to pick the first one
+ private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist)
+ {
+ var val = tag.Split(InternalValueSeparator).FirstOrDefault();
+ if (val is not null && useCustomTagDelimiters)
+ {
+ val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
+ }
+
+ return val;
+ }
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 6eb75891a..938f3cb32 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -217,7 +217,7 @@ namespace MediaBrowser.Providers.MediaInfo
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 31c0eeb31..4310f93d4 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -63,7 +63,7 @@ public class TrickplayImagesTask : IScheduledTask
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerDaily,
+ Type = TaskTriggerInfoType.DailyTrigger,
TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
}
};
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index cdeaf29b0..c53ef275b 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -70,24 +70,11 @@ namespace Jellyfin.Extensions.Json.Converters
writer.WriteStartArray();
if (value.Length > 0)
{
- var toWrite = value.Length - 1;
foreach (var it in value)
{
- var wrote = false;
if (it is not null)
{
writer.WriteStringValue(it.ToString());
- wrote = true;
- }
-
- if (toWrite > 0)
- {
- if (wrote)
- {
- writer.WriteStringValue(Delimiter.ToString());
- }
-
- toWrite--;
}
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
index 79c5873d5..71e46764a 100644
--- a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
@@ -79,7 +79,7 @@ namespace Jellyfin.LiveTv.Channels
// Every so often
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks
+ Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
}
diff --git a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
index a9fde0850..5164d695f 100644
--- a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
@@ -66,7 +66,7 @@ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledT
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
diff --git a/tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs
new file mode 100644
index 000000000..40934d9c6
--- /dev/null
+++ b/tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs
@@ -0,0 +1,50 @@
+using System;
+using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.Listings;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.LiveTv.Tests.Listings;
+
+public class ListingsManagerTests
+{
+ private readonly IConfigurationManager _config;
+ private readonly IListingsProvider[] _listingsProviders;
+ private readonly ILogger<ListingsManager> _logger;
+ private readonly ITaskManager _taskManager;
+ private readonly ITunerHostManager _tunerHostManager;
+
+ public ListingsManagerTests()
+ {
+ _logger = Mock.Of<ILogger<ListingsManager>>();
+ _config = Mock.Of<IConfigurationManager>();
+ _taskManager = Mock.Of<ITaskManager>();
+ _tunerHostManager = Mock.Of<ITunerHostManager>();
+ _listingsProviders = new[] { Mock.Of<IListingsProvider>() };
+ }
+
+ [Fact]
+ public void DeleteListingsProvider_DeletesProvider()
+ {
+ // Arrange
+ var id = "MockId";
+ var manager = new ListingsManager(_logger, _config, _taskManager, _tunerHostManager, _listingsProviders);
+
+ Mock.Get(_config)
+ .Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new LiveTvOptions { ListingProviders = [new ListingsProviderInfo { Id = id }] });
+
+ // Act
+ manager.DeleteListingsProvider(id);
+
+ // Assert
+ Assert.DoesNotContain(
+ _config.GetLiveTvConfiguration().ListingProviders,
+ p => p.Id.Equals(id, StringComparison.Ordinal));
+ }
+}