aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.devcontainer/devcontainer.json24
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--Directory.Packages.props44
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs53
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs28
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs5
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs24
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json51
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json1
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs42
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs22
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs20
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs12
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs15
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs4
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs10
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs23
-rw-r--r--Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs17
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs8
-rw-r--r--Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs22
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs15
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs70
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs5
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs16
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs1
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs11
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs120
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs100
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs35
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs94
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs329
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs12
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs19
77 files changed, 1017 insertions, 344 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 1d65527d9..302ac67b6 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "10.0.2",
+ "version": "10.0.3",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 8b6b12c31..c67c29237 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,17 +1,31 @@
{
"name": "Development Jellyfin Server",
- "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"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",
+ // The previous way of installing extensions via the vs command dont work on selfhosted devcontainers
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-dotnettools.csharp",
+ "editorconfig.editorconfig",
+ "github.vscode-github-actions",
+ "ms-dotnettools.vscode-dotnet-runtime",
+ "ms-dotnettools.csdevkit",
+ "alexcvzz.vscode-sqlite",
+ "streetsidesoftware.code-spell-checker",
+ "eamodio.gitlens",
+ "redhat.vscode-xml"
+ ]
+ }
+ },
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
- "dotnetRuntimeVersions": "9.0",
- "aspNetCoreRuntimeVersions": "9.0"
+ "dotnetRuntimeVersions": "10.0",
+ "aspNetCoreRuntimeVersions": "10.0"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false,
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index c7a7785e9..10fb775fe 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -28,13 +28,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
+ uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
+ uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
+ uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 1bf01bf75..08eedd54f 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -133,7 +133,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@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
+ uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -194,7 +194,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@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
+ uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 242cc77e6..2f4c5302a 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="8.0.0" />
+ <PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -25,28 +25,28 @@
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
- <PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
+ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -74,12 +74,12 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="3.2.1" />
+ <PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" />
- <PackageVersion Include="System.Text.Json" Version="10.0.2" />
+ <PackageVersion Include="System.Text.Json" Version="10.0.3" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="7.10.0" />
+ <PackageVersion Include="z440.atl.core" Version="7.11.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index c5dc3b054..b392340f7 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1051,16 +1051,16 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
- dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
- .Where(e => e.Value.Length > 0)
- .Select(i =>
- {
- return new NameGuidPair
- {
- Name = i.Key,
- Id = i.Value.First().Id
- };
- }).Where(i => i is not null).ToArray();
+ var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+
+ dto.ArtistItems = hasArtist.Artists
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .Distinct()
+ .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
+ ? new NameGuidPair { Name = name, Id = artists[0].Id }
+ : null)
+ .Where(item => item is not null)
+ .ToArray();
}
if (item is IHasAlbumArtist hasAlbumArtist)
@@ -1085,31 +1085,16 @@ namespace Emby.Server.Implementations.Dto
// })
// .ToList();
- dto.AlbumArtists = hasAlbumArtist.AlbumArtists
- // .Except(foundArtists, new DistinctNameComparer())
- .Select(i =>
- {
- // This should not be necessary but we're seeing some cases of it
- if (string.IsNullOrEmpty(i))
- {
- return null;
- }
-
- var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
- {
- EnableImages = false
- });
- if (artist is not null)
- {
- return new NameGuidPair
- {
- Name = artist.Name,
- Id = artist.Id
- };
- }
+ var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
- return null;
- }).Where(i => i is not null).ToArray();
+ dto.AlbumArtists = hasAlbumArtist.AlbumArtists
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .Distinct()
+ .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
+ ? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
+ : null)
+ .Where(item => item is not null)
+ .ToArray();
}
// Add video info
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 4874eca8e..996cd1b3c 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images
{
var image = item.GetImageInfo(type, 0);
- if (image is not null)
+ if (image is null)
{
- if (!image.IsLocalFile)
- {
- return false;
- }
+ return GetItemsWithImages(item).Count is not 0;
+ }
- if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
- {
- return false;
- }
+ if (!image.IsLocalFile)
+ {
+ return false;
+ }
- if (!HasChangedByDate(item, image))
- {
- return false;
- }
+ if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
+ {
+ return false;
+ }
+
+ if (!HasChangedByDate(item, image))
+ {
+ return false;
}
return true;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 5fac2f6b0..59ccb9e2c 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library
"**/lost+found",
"**/subs/**",
"**/subs",
+ "**/.snapshots/**",
+ "**/.snapshots",
+ "**/.snapshot/**",
+ "**/.snapshot",
// Trickplay files
"**/*.trickplay",
@@ -83,7 +87,6 @@ namespace Emby.Server.Implementations.Library
// Unix hidden files
"**/.*",
- "**/.*/**",
// Mac - if you ever remove the above.
// "**/._*",
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 21e7079d8..fc63251ad 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -37,15 +37,25 @@ namespace Emby.Server.Implementations.Library
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;
- if (attributeIndex > 0
- && str[attributeIndex - 1] == '['
- && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
+ if (attributeIndex > 0)
{
- var closingIndex = str[attributeEnd..].IndexOf(']');
- // Must be at least 1 character before the closing bracket.
- if (closingIndex > 1)
+ var attributeOpener = str[attributeIndex - 1];
+ var attributeCloser = attributeOpener switch
{
- return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+ '[' => ']',
+ '(' => ')',
+ '{' => '}',
+ _ => '\0'
+ };
+ if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
+ {
+ var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
+
+ // Must be at least 1 character before the closing bracket.
+ if (closingIndex > 1)
+ {
+ return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+ }
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index d09a7884e..7ce8baef5 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -73,7 +73,6 @@
"Shows": "العروض",
"Songs": "الأغاني",
"StartupEmbyServerIsLoading": "يتم تحميل خادم Jellyfin . الرجاء المحاولة بعد قليل.",
- "SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}",
"SubtitleDownloadFailureFromForItem": "فشل تحميل الترجمات من {0} ل {1}",
"Sync": "مزامنة",
"System": "النظام",
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 3d598c491..cb11cc089 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -3,7 +3,7 @@
"Playlists": "Плэй-лісты",
"Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}",
- "ItemAddedWithName": "{0} даданы ў бібліятэку",
+ "ItemAddedWithName": "{0} дададзены ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
@@ -14,7 +14,7 @@
"Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
- "Default": "Па змаўчанні",
+ "Default": "Прадвызначана",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі",
"Favorites": "Абранае",
@@ -81,8 +81,8 @@
"NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
- "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
- "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
+ "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена",
+ "NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index fd3666ef1..92b8e5d56 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -73,7 +73,6 @@
"Shows": "Сериали",
"Songs": "Песни",
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
- "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
"Sync": "Синхронизиране",
"System": "Система",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 596df6348..1e7279be8 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -73,7 +73,6 @@
"Shows": "Sèries",
"Songs": "Cançons",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitza",
"System": "Sistema",
@@ -105,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
- "TaskRefreshLibrary": "Escaneig de les mediateques",
+ "TaskRefreshLibrary": "Escaneja la mediateca",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index e14edcffa..4d2477044 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -73,7 +73,6 @@
"Shows": "Seriály",
"Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
- "SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
"Sync": "Synchronizace",
"System": "Systém",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index bbee38ba5..8b0d8745d 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -73,7 +73,6 @@
"Shows": "Serier",
"Songs": "Sange",
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
- "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"Sync": "Synkroniser",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 0b042c8fe..e9a1630d9 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -73,7 +73,6 @@
"Shows": "Serien",
"Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
- "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
"Sync": "Synchronisation",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 2ba2085da..87362ff8e 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -73,7 +73,6 @@
"Shows": "Σειρές",
"Songs": "Τραγούδια",
"StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.",
- "SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
"Sync": "Συγχρονισμός",
"System": "Σύστημα",
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 720f550b3..bd5be0b1f 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -73,7 +73,6 @@
"Shows": "Shows",
"Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"Sync": "Sync",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 1f8af4c8a..2bbf0d514 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -20,7 +20,7 @@
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Capítulos favoritos",
- "HeaderFavoriteShows": "Programas favoritos",
+ "HeaderFavoriteShows": "Series favoritas",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "Siguiente",
@@ -73,7 +73,6 @@
"Shows": "Series",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 2830c657b..6748fff4c 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -73,7 +73,6 @@
"Shows": "Programas",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
- "SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 1ec5eaa2a..b9c57afe6 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -73,7 +73,6 @@
"Shows": "Series",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
- "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
"SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index ff14c1367..90cd3a58e 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -73,7 +73,6 @@
"Shows": "سریال‌ها",
"Songs": "موسیقی‌ها",
"StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.",
- "SubtitleDownloadFailureForItem": "دانلود زیرنویس برای {0} ناموفق بود",
"SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد",
"Sync": "همگام‌سازی",
"System": "سیستم",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 6d079d2f5..a8964e8b6 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -73,7 +73,6 @@
"Shows": "Séries",
"Songs": "Chansons",
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser",
"System": "Système",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 8bf41c02a..b2a2e502a 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -73,7 +73,6 @@
"Shows": "Séries",
"Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
- "SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser",
"System": "Système",
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index e1ee8cf7c..9be6f05ee 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -73,7 +73,6 @@
"Shows": "Serie",
"Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
"Sync": "Synchronisation",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 90c921898..ef95a639f 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -73,7 +73,6 @@
"Shows": "סדרות",
"Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
"Sync": "סנכרון",
"System": "מערכת",
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 67263d3b2..94db43571 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
- "Collections": "Kolekcije",
+ "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
@@ -70,10 +70,9 @@
"ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
- "Shows": "Serije",
+ "Shows": "Emisije",
"Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
- "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
"Sync": "Sinkronizacija",
"System": "Sustav",
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 81a996330..7d72c1f30 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -55,7 +55,7 @@
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
"NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
- "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
+ "NotificationOptionServerRestartRequired": "A szerver újraindítása szükséges",
"NotificationOptionTaskFailed": "Hiba az ütemezett feladatban",
"NotificationOptionUserLockedOut": "Felhasználó tiltva",
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
@@ -73,7 +73,6 @@
"Shows": "Sorozatok",
"Songs": "Számok",
"StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
"Sync": "Szinkronizálás",
"System": "Rendszer",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 421c4ee30..f0c4b5027 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"Application": "Applicazione",
"Artists": "Artisti",
- "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
+ "AuthenticationSucceededWithUserName": "{0} autenticato correttamente",
"Books": "Libri",
"CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
"Channels": "Canali",
@@ -11,36 +11,36 @@
"Collections": "Collezioni",
"DeviceOfflineWithName": "{0} si è disconnesso",
"DeviceOnlineWithName": "{0} è connesso",
- "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
+ "FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}",
"Favorites": "Preferiti",
"Folders": "Cartelle",
"Genres": "Generi",
"HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare",
- "HeaderFavoriteAlbums": "Album Preferiti",
- "HeaderFavoriteArtists": "Artisti Preferiti",
- "HeaderFavoriteEpisodes": "Episodi Preferiti",
- "HeaderFavoriteShows": "Serie TV Preferite",
- "HeaderFavoriteSongs": "Brani Preferiti",
+ "HeaderFavoriteAlbums": "Album preferiti",
+ "HeaderFavoriteArtists": "Artisti preferiti",
+ "HeaderFavoriteEpisodes": "Episodi preferiti",
+ "HeaderFavoriteShows": "Serie TV preferite",
+ "HeaderFavoriteSongs": "Brani preferiti",
"HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo",
- "HeaderRecordingGroups": "Gruppi di Registrazione",
- "HomeVideos": "Video Personali",
+ "HeaderRecordingGroups": "Gruppi di registrazione",
+ "HomeVideos": "Video personali",
"Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
"LabelIpAddressValue": "Indirizzo IP: {0}",
"LabelRunningTimeValue": "Durata: {0}",
"Latest": "Novità",
- "MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato",
+ "MessageApplicationUpdated": "Jellyfin Server è stato aggiornato",
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto",
"Movies": "Film",
"Music": "Musica",
- "MusicVideos": "Video Musicali",
- "NameInstallFailed": "{0} installazione fallita",
+ "MusicVideos": "Video musicali",
+ "NameInstallFailed": "{0} installazione non riuscita",
"NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta",
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
@@ -49,38 +49,37 @@
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
"NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta",
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
- "NotificationOptionInstallationFailed": "Installazione fallita",
+ "NotificationOptionInstallationFailed": "Installazione non riuscita",
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
"NotificationOptionPluginError": "Errore del plugin",
"NotificationOptionPluginInstalled": "Plugin installato",
"NotificationOptionPluginUninstalled": "Plugin disinstallato",
"NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
- "NotificationOptionTaskFailed": "Operazione pianificata fallita",
+ "NotificationOptionTaskFailed": "Operazione pianificata non riuscita",
"NotificationOptionUserLockedOut": "Utente bloccato",
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto",
- "Playlists": "Playlist",
+ "Playlists": "Scalette",
"Plugin": "Plugin",
- "PluginInstalledWithName": "{0} è stato Installato",
+ "PluginInstalledWithName": "{0} è stato installato",
"PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato",
"ProviderValue": "Provider: {0}",
- "ScheduledTaskFailedWithName": "{0} fallito",
+ "ScheduledTaskFailedWithName": "{0} non riuscito",
"ScheduledTaskStartedWithName": "{0} avviato",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV",
"Songs": "Brani",
- "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
- "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
+ "StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
"Sync": "Sincronizza",
"System": "Sistema",
"TvShows": "Serie TV",
"User": "Utente",
"UserCreatedWithName": "L'utente {0} è stato creato",
- "UserDeletedWithName": "L'utente {0} è stato rimosso",
+ "UserDeletedWithName": "L'utente {0} è stato eliminato",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
@@ -115,20 +114,20 @@
"TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
- "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.",
- "Undefined": "Non Definito",
+ "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.",
+ "Undefined": "Non specificato",
"Forced": "Forzato",
"Default": "Predefinito",
"TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
"TaskOptimizeDatabase": "Ottimizza database",
"TaskKeyframeExtractor": "Estrattore di Keyframe",
- "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
+ "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori scalette HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno",
- "HearingImpaired": "Non Udenti",
+ "HearingImpaired": "Non udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
- "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
- "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
+ "TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index e050196bc..fc5fcf3c4 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -73,7 +73,6 @@
"Shows": "Körsetımder",
"Songs": "Äuender",
"StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.",
- "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
"SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
"Sync": "Ündestıru",
"System": "Jüie",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 3d1b1ed27..2b24ea2c8 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -73,7 +73,6 @@
"Shows": "시리즈",
"Songs": "노래",
"StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다",
"Sync": "동기화",
"System": "시스템",
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 3918ab81c..bdf63b4ca 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -73,7 +73,6 @@
"Shows": "Laidos",
"Songs": "Kūriniai",
"StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
"Sync": "Sinchronizuoti",
"System": "Sistema",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 971f79c2c..2be04be80 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -73,7 +73,6 @@
"Shows": "Tayangan",
"Songs": "Lagu-lagu",
"StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
"Sync": "Segerak",
"System": "Sistem",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index e73c56cb9..cd0315720 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -73,7 +73,6 @@
"Shows": "Serier",
"Songs": "Sanger",
"StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
- "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}",
"Sync": "Synkroniser",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 09246bd11..534c64e93 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -73,7 +73,6 @@
"Shows": "Series",
"Songs": "Nummers",
"StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.",
- "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt",
"SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}",
"Sync": "Synchronisatie",
"System": "Systeem",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index 8ca22ac04..f1c19ac1d 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -73,7 +73,6 @@
"Shows": "Seriale",
"Songs": "Utwory",
"StartupEmbyServerIsLoading": "Trwa wczytywanie serwera Jellyfin. Spróbuj ponownie za chwilę.",
- "SubtitleDownloadFailureForItem": "Pobieranie napisów dla {0} zakończone niepowodzeniem",
"SubtitleDownloadFailureFromForItem": "Nieudane pobieranie napisów z {0} dla {1}",
"Sync": "Synchronizacja",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index dc5bff161..8e76c6c63 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -73,7 +73,6 @@
"Shows": "Séries",
"Songs": "Músicas",
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor, tente novamente mais tarde.",
- "SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}",
"SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 17284854f..c2ce2ba40 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -73,7 +73,6 @@
"Shows": "Séries",
"Songs": "Músicas",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
"Sync": "Sincronização",
"System": "Sistema",
@@ -125,8 +124,8 @@
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo",
"HearingImpaired": "Surdo",
- "TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.",
+ "TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 74bb1c63a..9ae346e25 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -124,8 +124,8 @@
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
- "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
- "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
+ "TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 1470a538c..03bce0ebd 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -73,7 +73,6 @@
"Shows": "Сериалы",
"Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
- "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
"Sync": "Синхронизация",
"System": "Система",
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 1de78eeae..7c8d86047 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -73,7 +73,6 @@
"Shows": "Seriály",
"Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.",
- "SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo",
"SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo",
"Sync": "Synchronizácia",
"System": "Systém",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index ff92db2f2..7c7c88e28 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -73,7 +73,6 @@
"Shows": "Serije",
"Songs": "Pesmi",
"StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
"Sync": "Sinhroniziraj",
"System": "Sistem",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 1ee1a5366..23acd3c53 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -73,7 +73,6 @@
"Shows": "Serier",
"Songs": "Låtar",
"StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.",
- "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades",
"SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}",
"Sync": "Synk",
"System": "System",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index a07e6864e..d13f662e4 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -73,7 +73,6 @@
"Shows": "Diziler",
"Songs": "Şarkılar",
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} için altyazılar {0} sağlayıcısından indirilemedi",
"Sync": "Eşzamanlama",
"System": "Sistem",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index b9635105a..0a0795d41 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -73,7 +73,6 @@
"Shows": "节目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 服务器正在启动,请稍后再试。",
- "SubtitleDownloadFailureForItem": "为 {0} 下载字幕失败",
"SubtitleDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的字幕",
"Sync": "同步",
"System": "系统",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index c8800e256..e57a0c5b0 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -73,7 +73,6 @@
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "正在載入 Jellyfin,請稍後再試。",
- "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"Sync": "同步",
"System": "系統",
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 1577c5c9c..409414139 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -198,17 +198,22 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(items, user, options);
}
- public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
+ public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId)
{
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
- return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
- {
- EnableImages = true
- });
+ return AddToPlaylistInternal(
+ playlistId,
+ itemIds,
+ user,
+ new DtoOptions(false)
+ {
+ EnableImages = true
+ },
+ position);
}
- private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
+ private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options, int? position = null)
{
// Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@@ -243,7 +248,30 @@ namespace Emby.Server.Implementations.Playlists
}
// Update the playlist in the repository
- playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
+ if (position.HasValue)
+ {
+ if (position.Value <= 0)
+ {
+ playlist.LinkedChildren = [.. childrenToAdd, .. playlist.LinkedChildren];
+ }
+ else if (position.Value >= playlist.LinkedChildren.Length)
+ {
+ playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
+ }
+ else
+ {
+ playlist.LinkedChildren = [
+ .. playlist.LinkedChildren[0..position.Value],
+ .. childrenToAdd,
+ .. playlist.LinkedChildren[position.Value..playlist.LinkedChildren.Length]
+ ];
+ }
+ }
+ else
+ {
+ playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
+ }
+
playlist.DateLastMediaAdded = DateTime.UtcNow;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index cf2ca047c..8e14f5bdf 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -793,6 +793,16 @@ namespace Emby.Server.Implementations.Session
PlaySessionId = info.PlaySessionId
};
+ if (info.Item is not null)
+ {
+ _logger.LogInformation(
+ "User {0} started playback of '{1}' ({2} {3})",
+ session.UserName,
+ info.Item.Name,
+ session.Client,
+ session.ApplicationVersion);
+ }
+
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
// Nothing to save here
@@ -1060,11 +1070,12 @@ namespace Emby.Server.Implementations.Session
var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown";
_logger.LogInformation(
- "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
- session.Client,
- session.ApplicationVersion,
+ "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
+ session.UserName,
info.Item.Name,
- msString);
+ msString,
+ session.Client,
+ session.ApplicationVersion);
}
if (info.NowPlayingQueue is not null)
@@ -1175,7 +1186,8 @@ namespace Emby.Server.Implementations.Session
return session;
}
- private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
+ /// <inheritdoc />
+ public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
{
return new SessionInfoDto
{
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 8dcaebf6d..9e03fbeb0 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -3,8 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text.Json;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
-using Jellyfin.Api.Models.ConfigurationDtos;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
@@ -143,22 +141,4 @@ public class ConfigurationController : BaseJellyfinApiController
return NoContent();
}
-
- /// <summary>
- /// Updates the path to the media encoder.
- /// </summary>
- /// <param name="mediaEncoderPath">Media encoder path form body.</param>
- /// <response code="204">Media encoder path updated.</response>
- /// <returns>Status.</returns>
- [Obsolete("This endpoint is obsolete.")]
- [ApiExplorerSettings(IgnoreApi = true)]
- [HttpPost("MediaEncoder/Path")]
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
- {
- // API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES
- // _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
- return NoContent();
- }
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 585318d24..ef54e9db5 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -191,9 +191,17 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
- if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
+ var viewType = displayPreferences.CustomPrefs[key];
+
+ if (string.IsNullOrEmpty(viewType))
+ {
+ displayPreferences.CustomPrefs.Remove(key);
+ continue;
+ }
+
+ if (!Enum.TryParse<ViewType>(viewType, true, out _))
{
- _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
+ _logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType);
displayPreferences.CustomPrefs.Remove(key);
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 15b04051f..f80b36c39 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1400,10 +1400,20 @@ public class DynamicHlsController : BaseJellyfinApiController
cancellationTokenSource.Token)
.ConfigureAwait(false);
var mediaSourceId = state.BaseRequest.MediaSourceId;
+ double fps = state.TargetFramerate ?? 0.0f;
+ int segmentLength = state.SegmentLength * 1000;
+
+ // If framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
+ if (Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
+ {
+ double nearestIntFramerate = Math.Ceiling(fps);
+ segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps));
+ }
+
var request = new CreateMainPlaylistRequest(
mediaSourceId is null ? null : Guid.Parse(mediaSourceId),
state.MediaPath,
- state.SegmentLength * 1000,
+ segmentLength,
state.RunTimeTicks ?? 0,
state.Request.SegmentContainer ?? string.Empty,
"hls1/main/",
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 284a97621..794ca9693 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.EnvironmentDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Extensions;
@@ -129,20 +128,6 @@ public class EnvironmentController : BaseJellyfinApiController
}
/// <summary>
- /// Gets network paths.
- /// </summary>
- /// <response code="200">Empty array returned.</response>
- /// <returns>List of entries.</returns>
- [Obsolete("This endpoint is obsolete.")]
- [HttpGet("NetworkShares")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
- {
- _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
- return Array.Empty<FileSystemEntryInfo>();
- }
-
- /// <summary>
/// Gets available drives from the server's file system.
/// </summary>
/// <response code="200">List of entries returned.</response>
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 59e6fd779..967918093 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -359,6 +359,7 @@ public class PlaylistsController : BaseJellyfinApiController
/// </summary>
/// <param name="playlistId">The playlist id.</param>
/// <param name="ids">Item id, comma delimited.</param>
+ /// <param name="position">Optional. 0-based index where to place the items or at the end if <c>null</c>.</param>
/// <param name="userId">The userId.</param>
/// <response code="204">Items added to playlist.</response>
/// <response code="403">Access forbidden.</response>
@@ -371,6 +372,7 @@ public class PlaylistsController : BaseJellyfinApiController
public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
+ [FromQuery] int? position,
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -388,7 +390,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
+ await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 14f5265aa..2a15ff767 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -66,16 +66,6 @@ public class QuickConnectController : BaseJellyfinApiController
}
/// <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>
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 2817e3cbc..c86c9b8f6 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -69,7 +68,6 @@ public class TvShowsController : BaseJellyfinApiController
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
- /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
/// <param name="enableResumable">Whether to include resumable episodes in next up results.</param>
/// <param name="enableRewatching">Whether to include watched episodes in next up results.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
@@ -88,7 +86,6 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
- [FromQuery][ParameterObsolete] bool disableFirstEpisode = false,
[FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false)
{
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index d0ced277a..536b95dbb 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -338,29 +338,6 @@ public class UserController : BaseJellyfinApiController
=> UpdateUserPassword(userId, request);
/// <summary>
- /// Updates a user's easy password.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
- /// <response code="204">Password successfully reset.</response>
- /// <response code="403">User is not allowed to update the password.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
- [HttpPost("{userId}/EasyPassword")]
- [Obsolete("Use Quick Connect instead")]
- [ApiExplorerSettings(IgnoreApi = true)]
- [Authorize]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateUserEasyPassword(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UpdateUserEasyPassword request)
- {
- return Forbid();
- }
-
- /// <summary>
/// Updates a user.
/// </summary>
/// <param name="userId">The user id.</param>
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
deleted file mode 100644
index 5a48345eb..000000000
--- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Jellyfin.Api.Models.ConfigurationDtos;
-
-/// <summary>
-/// Media Encoder Path Dto.
-/// </summary>
-public class MediaEncoderPathDto
-{
- /// <summary>
- /// Gets or sets media encoder path.
- /// </summary>
- public string Path { get; set; } = null!;
-
- /// <summary>
- /// Gets or sets media encoder path type.
- /// </summary>
- public string PathType { get; set; } = null!;
-}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
index 9c29e372c..2a1a312d5 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.StartupDtos;
@@ -13,11 +12,4 @@ public class StartupRemoteAccessDto
/// </summary>
[Required]
public bool EnableRemoteAccess { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether enable automatic port mapping.
- /// </summary>
- [Required]
- [Obsolete("No longer supported")]
- public bool EnableAutomaticPortMapping { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
deleted file mode 100644
index f19d0b57a..000000000
--- a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace Jellyfin.Api.Models.UserDtos;
-
-/// <summary>
-/// The update user easy password request body.
-/// </summary>
-public class UpdateUserEasyPassword
-{
- /// <summary>
- /// Gets or sets the new sha1-hashed password.
- /// </summary>
- public string? NewPassword { get; set; }
-
- /// <summary>
- /// Gets or sets the new password.
- /// </summary>
- public string? NewPw { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to reset the password.
- /// </summary>
- public bool ResetPassword { get; set; }
-}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 143d82bac..db24c9746 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@@ -15,7 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners;
/// <summary>
/// Class SessionInfoWebSocketListener.
/// </summary>
-public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
+public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfoDto>, WebSocketListenerState>
{
private readonly ISessionManager _sessionManager;
private bool _disposed;
@@ -52,24 +53,26 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// Gets the data to send.
/// </summary>
/// <returns>Task{SystemInfo}.</returns>
- protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
+ protected override Task<IEnumerable<SessionInfoDto>> GetDataToSend()
{
- return Task.FromResult(_sessionManager.Sessions);
+ return Task.FromResult(_sessionManager.Sessions.Select(_sessionManager.ToSessionInfoDto));
}
/// <inheritdoc />
- protected override Task<IEnumerable<SessionInfo>> GetDataToSendForConnection(IWebSocketConnection connection)
+ protected override Task<IEnumerable<SessionInfoDto>> GetDataToSendForConnection(IWebSocketConnection connection)
{
+ var sessions = _sessionManager.Sessions;
+
// For non-admin users, filter the sessions to only include their own sessions
if (connection.AuthorizationInfo?.User is not null &&
!connection.AuthorizationInfo.IsApiKey &&
!connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
var userId = connection.AuthorizationInfo.User.Id;
- return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId)));
+ sessions = sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId));
}
- return Task.FromResult(_sessionManager.Sessions);
+ return Task.FromResult(sessions.Select(_sessionManager.ToSessionInfoDto));
}
/// <inheritdoc />
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 90aa3a22e..5bb4494dd 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -295,6 +295,25 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
+
+ var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
+ if (hasRandomSort)
+ {
+ var orderedIds = dbQuery.Select(e => e.Id).ToList();
+ if (orderedIds.Count == 0)
+ {
+ return Array.Empty<BaseItemDto>();
+ }
+
+ var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
+ .AsEnumerable()
+ .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
+ .Where(dto => dto is not null)
+ .ToDictionary(i => i!.Id);
+
+ return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
+ }
+
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
@@ -762,16 +781,30 @@ public sealed class BaseItemRepository
await using (dbContext.ConfigureAwait(false))
{
- var userKeys = item.GetUserDataKeys().ToArray();
- var retentionDate = (DateTime?)null;
- await dbContext.UserData
- .Where(e => e.ItemId == PlaceholderId)
- .Where(e => userKeys.Contains(e.CustomDataKey))
- .ExecuteUpdateAsync(
- e => e
- .SetProperty(f => f.ItemId, item.Id)
- .SetProperty(f => f.RetentionDate, retentionDate),
- cancellationToken).ConfigureAwait(false);
+ var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using (transaction.ConfigureAwait(false))
+ {
+ var userKeys = item.GetUserDataKeys().ToArray();
+ var retentionDate = (DateTime?)null;
+
+ await dbContext.UserData
+ .Where(e => e.ItemId == PlaceholderId)
+ .Where(e => userKeys.Contains(e.CustomDataKey))
+ .ExecuteUpdateAsync(
+ e => e
+ .SetProperty(f => f.ItemId, item.Id)
+ .SetProperty(f => f.RetentionDate, retentionDate),
+ cancellationToken).ConfigureAwait(false);
+
+ // Rehydrate the cached userdata
+ item.UserData = await dbContext.UserData
+ .AsNoTracking()
+ .Where(e => e.ItemId == item.Id)
+ .ToArrayAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ }
}
}
@@ -2682,6 +2715,21 @@ public sealed class BaseItemRepository
.Where(e => artistNames.Contains(e.Name))
.ToArray();
- return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
+ var lookup = artists
+ .GroupBy(e => e.Name!)
+ .ToDictionary(
+ g => g.Key,
+ g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
+
+ var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
+ foreach (var name in artistNames)
+ {
+ if (lookup.TryGetValue(name, out var artistArray))
+ {
+ result[name] = artistArray;
+ }
+ }
+
+ return result;
}
}
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index 355ed6479..e2569241d 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -74,9 +74,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc />
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
{
- foreach (var item in people.Where(e => e.Role is null))
+ foreach (var person in people)
{
- item.Role = string.Empty;
+ person.Name = person.Name.Trim();
+ person.Role = person.Role?.Trim() ?? string.Empty;
}
// multiple metadata providers can provide the _same_ person
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 4b1e53a35..70761fa7d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -1163,7 +1163,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Item = null!,
ProviderId = e[0],
ProviderValue = string.Join('|', e.Skip(1))
- }).ToArray();
+ })
+ .DistinctBy(e => e.ProviderId)
+ .ToArray();
}
if (reader.TryGetString(index++, out var imageInfos))
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index d2a3290c4..2ecb6cbdf 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities
// That's all the new and changed ones - now see if any have been removed and need cleanup
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
var shouldRemove = !IsRoot || allowRemoveRoot;
+ var actuallyRemoved = new List<BaseItem>();
// If it's an AggregateFolder, don't remove
if (shouldRemove && itemsRemoved.Count > 0)
{
@@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities
{
Logger.LogDebug("Removed item: {Path}", item.Path);
+ actuallyRemoved.Add(item);
item.SetParent(null);
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
}
@@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities
{
LibraryManager.CreateItems(newItems, this, cancellationToken);
}
+
+ // After removing items, reattach any detached user data to remaining children
+ // that share the same user data keys (eg. same episode replaced with a new file).
+ if (actuallyRemoved.Count > 0)
+ {
+ var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
+ foreach (var child in validChildren)
+ {
+ if (child.GetUserDataKeys().Any(removedKeys.Contains))
+ {
+ await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
}
else
{
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index 497c4a511..92aa92396 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -61,9 +61,10 @@ namespace MediaBrowser.Controller.Playlists
/// </summary>
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="itemIds">The item ids.</param>
+ /// <param name="position">Optional. 0-based index where to place the items or at the end if null.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
+ Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId);
/// <summary>
/// Removes from playlist.
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 2b3afa117..c11c65c33 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -350,5 +350,12 @@ namespace MediaBrowser.Controller.Session
/// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
/// <returns>Task.</returns>
Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
+
+ /// <summary>
+ /// Gets the dto for session info.
+ /// </summary>
+ /// <param name="sessionInfo">The session info.</param>
+ /// <returns><see cref="SessionInfoDto"/> of the session.</returns>
+ SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo);
}
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 50f7716d8..dbe532289 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -83,6 +83,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"Smith/Kotzen",
"We;Na",
"LSR/CITY",
+ "Kairon; IRSE!",
};
/// <summary>
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index fc1f24ae1..597845fc1 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
@@ -31,15 +30,5 @@ namespace MediaBrowser.Model.Session
public string AppStoreUrl { get; set; }
public string IconUrl { get; set; }
-
- // TODO: Remove after 10.9
- [Obsolete("Unused")]
- [DefaultValue(false)]
- public bool? SupportsContentUploading { get; set; } = false;
-
- // TODO: Remove after 10.9
- [Obsolete("Unused")]
- [DefaultValue(false)]
- public bool? SupportsSync { get; set; } = false;
}
}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs
new file mode 100644
index 000000000..69cae7762
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Provides the primary image for EPUB items that have embedded covers.
+ /// </summary>
+ public class EpubImageProvider : IDynamicImageProvider
+ {
+ private readonly ILogger<EpubImageProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpubImageProvider"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{EpubImageProvider}"/> interface.</param>
+ public EpubImageProvider(ILogger<EpubImageProvider> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "EPUB Metadata";
+
+ /// <inheritdoc />
+ public bool Supports(BaseItem item)
+ {
+ return item is Book;
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ yield return ImageType.Primary;
+ }
+
+ /// <inheritdoc />
+ public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
+ {
+ if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetFromZip(item, cancellationToken);
+ }
+
+ return Task.FromResult(new DynamicImageResponse { HasImage = false });
+ }
+
+ private async Task<DynamicImageResponse> LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory, CancellationToken cancellationToken)
+ {
+ var utilities = new OpfReader<EpubImageProvider>(opf, _logger);
+ var coverReference = utilities.ReadCoverPath(opfRootDirectory);
+ if (coverReference == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var cover = coverReference.Value;
+ var coverFile = epub.GetEntry(cover.Path);
+
+ if (coverFile == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var memoryStream = new MemoryStream();
+
+ var coverStream = await coverFile.OpenAsync(cancellationToken).ConfigureAwait(false);
+ await using (coverStream.ConfigureAwait(false))
+ {
+ await coverStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
+ }
+
+ memoryStream.Position = 0;
+
+ var response = new DynamicImageResponse { HasImage = true, Stream = memoryStream };
+ response.SetFormatFromMimeType(cover.MimeType);
+
+ return response;
+ }
+
+ private async Task<DynamicImageResponse> GetFromZip(BaseItem item, CancellationToken cancellationToken)
+ {
+ using var epub = await ZipFile.OpenReadAsync(item.Path, cancellationToken).ConfigureAwait(false);
+
+ var opfFilePath = EpubUtils.ReadContentFilePath(epub);
+ if (opfFilePath == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var opfRootDirectory = Path.GetDirectoryName(opfFilePath);
+ if (opfRootDirectory == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var opfFile = epub.GetEntry(opfFilePath);
+ if (opfFile == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ using var opfStream = await opfFile.OpenAsync(cancellationToken).ConfigureAwait(false);
+
+ var opfDocument = new XmlDocument();
+ opfDocument.Load(opfStream);
+
+ return await LoadCover(epub, opfDocument, opfRootDirectory, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs
new file mode 100644
index 000000000..bc77e5928
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Provides book metadata from OPF content in an EPUB item.
+ /// </summary>
+ public class EpubProvider : ILocalMetadataProvider<Book>
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<EpubProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpubProvider"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{EpubProvider}"/> interface.</param>
+ public EpubProvider(IFileSystem fileSystem, ILogger<EpubProvider> logger)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "EPUB Metadata";
+
+ /// <inheritdoc />
+ public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
+ {
+ var path = GetEpubFile(info.Path)?.FullName;
+
+ if (path is null)
+ {
+ return Task.FromResult(new MetadataResult<Book> { HasMetadata = false });
+ }
+
+ var result = ReadEpubAsZip(path, cancellationToken);
+
+ if (result is null)
+ {
+ return Task.FromResult(new MetadataResult<Book> { HasMetadata = false });
+ }
+ else
+ {
+ return Task.FromResult(result);
+ }
+ }
+
+ private FileSystemMetadata? GetEpubFile(string path)
+ {
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.IsDirectory)
+ {
+ return null;
+ }
+
+ if (!string.Equals(Path.GetExtension(fileInfo.FullName), ".epub", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return fileInfo;
+ }
+
+ private MetadataResult<Book>? ReadEpubAsZip(string path, CancellationToken cancellationToken)
+ {
+ using var epub = ZipFile.OpenRead(path);
+
+ var opfFilePath = EpubUtils.ReadContentFilePath(epub);
+ if (opfFilePath == null)
+ {
+ return null;
+ }
+
+ var opf = epub.GetEntry(opfFilePath);
+ if (opf == null)
+ {
+ return null;
+ }
+
+ using var opfStream = opf.Open();
+
+ var opfDocument = new XmlDocument();
+ opfDocument.Load(opfStream);
+
+ var utilities = new OpfReader<EpubProvider>(opfDocument, _logger);
+ return utilities.ReadOpfData(cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs
new file mode 100644
index 000000000..e5d298731
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs
@@ -0,0 +1,35 @@
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Utilities for EPUB files.
+ /// </summary>
+ public static class EpubUtils
+ {
+ /// <summary>
+ /// Attempt to read content from ZIP archive.
+ /// </summary>
+ /// <param name="epub">The ZIP archive.</param>
+ /// <returns>The content file path.</returns>
+ public static string? ReadContentFilePath(ZipArchive epub)
+ {
+ var container = epub.GetEntry(Path.Combine("META-INF", "container.xml"));
+ if (container == null)
+ {
+ return null;
+ }
+
+ using var containerStream = container.Open();
+
+ XNamespace containerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container";
+ var containerDocument = XDocument.Load(containerStream);
+ var element = containerDocument.Descendants(containerNamespace + "rootfile").FirstOrDefault();
+
+ return element?.Attribute("full-path")?.Value;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
new file mode 100644
index 000000000..6e678802c
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
@@ -0,0 +1,94 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Provides metadata for book items that have an OPF file in the same directory. Supports the standard
+ /// content.opf filename, bespoke metadata.opf name from Calibre libraries, and OPF files that have the
+ /// same name as their respective books for directories with several books.
+ /// </summary>
+ public class OpfProvider : ILocalMetadataProvider<Book>, IHasItemChangeMonitor
+ {
+ private const string StandardOpfFile = "content.opf";
+ private const string CalibreOpfFile = "metadata.opf";
+
+ private readonly IFileSystem _fileSystem;
+
+ private readonly ILogger<OpfProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpfProvider"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{OpfProvider}"/> interface.</param>
+ public OpfProvider(IFileSystem fileSystem, ILogger<OpfProvider> logger)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Open Packaging Format";
+
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ var file = GetXmlFile(item.Path);
+
+ return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
+ }
+
+ /// <inheritdoc />
+ public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
+ {
+ var path = GetXmlFile(info.Path).FullName;
+
+ try
+ {
+ return Task.FromResult(ReadOpfData(path, cancellationToken));
+ }
+ catch (FileNotFoundException)
+ {
+ return Task.FromResult(new MetadataResult<Book> { HasMetadata = false });
+ }
+ }
+
+ private FileSystemMetadata GetXmlFile(string path)
+ {
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path)!);
+
+ // check for OPF with matching name first since it's the most specific filename
+ var specificFile = Path.Combine(directoryInfo.FullName, Path.GetFileNameWithoutExtension(path) + ".opf");
+ var file = _fileSystem.GetFileInfo(specificFile);
+
+ if (file.Exists)
+ {
+ return file;
+ }
+
+ file = _fileSystem.GetFileInfo(Path.Combine(directoryInfo.FullName, StandardOpfFile));
+
+ // check metadata.opf last since it's really only used by Calibre
+ return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryInfo.FullName, CalibreOpfFile));
+ }
+
+ private MetadataResult<Book> ReadOpfData(string file, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var doc = new XmlDocument();
+ doc.Load(file);
+
+ var utilities = new OpfReader<OpfProvider>(doc, _logger);
+ return utilities.ReadOpfData(cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs
new file mode 100644
index 000000000..5d202c59e
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs
@@ -0,0 +1,329 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Xml;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Methods used to pull metadata and other information from Open Packaging Format in XML objects.
+ /// </summary>
+ /// <typeparam name="TCategoryName">The type of category.</typeparam>
+ public class OpfReader<TCategoryName>
+ {
+ private const string DcNamespace = @"http://purl.org/dc/elements/1.1/";
+ private const string OpfNamespace = @"http://www.idpf.org/2007/opf";
+
+ private readonly XmlNamespaceManager _namespaceManager;
+ private readonly XmlDocument _document;
+
+ private readonly ILogger<TCategoryName> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpfReader{TCategoryName}"/> class.
+ /// </summary>
+ /// <param name="document">The XML document to parse.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TCategoryName}"/> interface.</param>
+ public OpfReader(XmlDocument document, ILogger<TCategoryName> logger)
+ {
+ _document = document;
+ _logger = logger;
+ _namespaceManager = new XmlNamespaceManager(_document.NameTable);
+
+ _namespaceManager.AddNamespace("dc", DcNamespace);
+ _namespaceManager.AddNamespace("opf", OpfNamespace);
+ }
+
+ /// <summary>
+ /// Checks for the existence of a cover image.
+ /// </summary>
+ /// <param name="opfRootDirectory">The root directory in which the OPF file is located.</param>
+ /// <returns>Returns the found cover and its type or null.</returns>
+ public (string MimeType, string Path)? ReadCoverPath(string opfRootDirectory)
+ {
+ var coverImage = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@properties='cover-image']");
+ if (coverImage is not null)
+ {
+ return coverImage;
+ }
+
+ var coverId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='cover' and @media-type='image/*']");
+ if (coverId is not null)
+ {
+ return coverId;
+ }
+
+ var coverImageId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='*cover-image']");
+ if (coverImageId is not null)
+ {
+ return coverImageId;
+ }
+
+ var metaCoverImage = _document.SelectSingleNode("//opf:meta[@name='cover']", _namespaceManager);
+ var content = metaCoverImage?.Attributes?["content"]?.Value;
+ if (string.IsNullOrEmpty(content) || metaCoverImage is null)
+ {
+ return null;
+ }
+
+ var coverPath = Path.Combine("Images", content);
+ var coverFileManifest = _document.SelectSingleNode($"//opf:item[@href='{coverPath}']", _namespaceManager);
+ var mediaType = coverFileManifest?.Attributes?["media-type"]?.Value;
+ if (coverFileManifest?.Attributes is not null && !string.IsNullOrEmpty(mediaType) && IsValidImage(mediaType))
+ {
+ return (mediaType, Path.Combine(opfRootDirectory, coverPath));
+ }
+
+ var coverFileIdManifest = _document.SelectSingleNode($"//opf:item[@id='{content}']", _namespaceManager);
+ if (coverFileIdManifest is not null)
+ {
+ return ReadManifestItem(coverFileIdManifest, opfRootDirectory);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Read all supported OPF data from the file.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The metadata result to update.</returns>
+ public MetadataResult<Book> ReadOpfData(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var book = CreateBookFromOpf();
+ var result = new MetadataResult<Book> { Item = book, HasMetadata = true };
+
+ FindAuthors(result);
+ ReadStringInto("//dc:language", language => result.ResultLanguage = language);
+
+ return result;
+ }
+
+ private Book CreateBookFromOpf()
+ {
+ var book = new Book
+ {
+ Name = FindMainTitle(),
+ ForcedSortName = FindSortTitle(),
+ };
+
+ ReadStringInto("//dc:description", summary => book.Overview = summary);
+ ReadStringInto("//dc:publisher", publisher => book.AddStudio(publisher));
+ ReadStringInto("//dc:identifier[@opf:scheme='AMAZON']", amazon => book.SetProviderId("Amazon", amazon));
+ ReadStringInto("//dc:identifier[@opf:scheme='GOOGLE']", google => book.SetProviderId("GoogleBooks", google));
+ ReadStringInto("//dc:identifier[@opf:scheme='ISBN']", isbn => book.SetProviderId("ISBN", isbn));
+
+ ReadStringInto("//dc:date", date =>
+ {
+ if (DateTime.TryParse(date, out var dateValue))
+ {
+ book.PremiereDate = dateValue.Date;
+ book.ProductionYear = dateValue.Date.Year;
+ }
+ });
+
+ var genreNodes = _document.SelectNodes("//dc:subject", _namespaceManager);
+
+ if (genreNodes?.Count > 0)
+ {
+ foreach (var node in genreNodes.Cast<XmlNode>().Where(node => !string.IsNullOrEmpty(node.InnerText) && !book.Genres.Contains(node.InnerText)))
+ {
+ // specification has no rules about content and some books combine every genre into a single element
+ foreach (var item in node.InnerText.Split(["/", "&", ",", ";", " - "], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ book.AddGenre(item);
+ }
+ }
+ }
+
+ ReadInt32AttributeInto("//opf:meta[@name='calibre:series_index']", index => book.IndexNumber = index);
+ ReadInt32AttributeInto("//opf:meta[@name='calibre:rating']", rating => book.CommunityRating = rating);
+
+ var seriesNameNode = _document.SelectSingleNode("//opf:meta[@name='calibre:series']", _namespaceManager);
+
+ if (!string.IsNullOrEmpty(seriesNameNode?.Attributes?["content"]?.Value))
+ {
+ try
+ {
+ book.SeriesName = seriesNameNode.Attributes["content"]?.Value;
+ }
+ catch (Exception)
+ {
+ _logger.LogError("error parsing Calibre series name");
+ }
+ }
+
+ return book;
+ }
+
+ private string FindMainTitle()
+ {
+ var title = string.Empty;
+ var titleTypes = _document.SelectNodes("//opf:meta[@property='title-type']", _namespaceManager);
+
+ if (titleTypes is not null && titleTypes.Count > 0)
+ {
+ foreach (XmlElement titleNode in titleTypes)
+ {
+ string refines = titleNode.GetAttribute("refines").TrimStart('#');
+ string titleType = titleNode.InnerText;
+
+ var titleElement = _document.SelectSingleNode($"//dc:title[@id='{refines}']", _namespaceManager);
+ if (titleElement is not null && string.Equals(titleType, "main", StringComparison.OrdinalIgnoreCase))
+ {
+ title = titleElement.InnerText;
+ }
+ }
+ }
+
+ // fallback in case there is no main title definition
+ if (string.IsNullOrEmpty(title))
+ {
+ ReadStringInto("//dc:title", titleString => title = titleString);
+ }
+
+ return title;
+ }
+
+ private string? FindSortTitle()
+ {
+ var titleTypes = _document.SelectNodes("//opf:meta[@property='file-as']", _namespaceManager);
+
+ if (titleTypes is not null && titleTypes.Count > 0)
+ {
+ foreach (XmlElement titleNode in titleTypes)
+ {
+ string refines = titleNode.GetAttribute("refines").TrimStart('#');
+ string sortTitle = titleNode.InnerText;
+
+ var titleElement = _document.SelectSingleNode($"//dc:title[@id='{refines}']", _namespaceManager);
+ if (titleElement is not null)
+ {
+ return sortTitle;
+ }
+ }
+ }
+
+ // search for OPF 2.0 style title_sort node
+ var resultElement = _document.SelectSingleNode("//opf:meta[@name='calibre:title_sort']", _namespaceManager);
+ var titleSort = resultElement?.Attributes?["content"]?.Value;
+
+ return titleSort;
+ }
+
+ private void FindAuthors(MetadataResult<Book> book)
+ {
+ var resultElement = _document.SelectNodes("//dc:creator", _namespaceManager);
+
+ if (resultElement != null && resultElement.Count > 0)
+ {
+ foreach (XmlElement creator in resultElement)
+ {
+ var creatorName = creator.InnerText;
+ var role = creator.GetAttribute("opf:role");
+ var person = new PersonInfo { Name = creatorName, Type = GetRole(role) };
+
+ book.AddPerson(person);
+ }
+ }
+ }
+
+ private PersonKind GetRole(string? role)
+ {
+ switch (role)
+ {
+ case "arr":
+ return PersonKind.Arranger;
+ case "art":
+ return PersonKind.Artist;
+ case "aut":
+ case "aqt":
+ case "aft":
+ case "aui":
+ default:
+ return PersonKind.Author;
+ case "edt":
+ return PersonKind.Editor;
+ case "ill":
+ return PersonKind.Illustrator;
+ case "lyr":
+ return PersonKind.Lyricist;
+ case "mus":
+ return PersonKind.AlbumArtist;
+ case "oth":
+ return PersonKind.Unknown;
+ case "trl":
+ return PersonKind.Translator;
+ }
+ }
+
+ private void ReadStringInto(string xmlPath, Action<string> commitResult)
+ {
+ var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager);
+ if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.InnerText))
+ {
+ commitResult(resultElement.InnerText);
+ }
+ }
+
+ private void ReadInt32AttributeInto(string xmlPath, Action<int> commitResult)
+ {
+ var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager);
+ var resultValue = resultElement?.Attributes?["content"]?.Value;
+
+ if (!string.IsNullOrEmpty(resultValue))
+ {
+ try
+ {
+ commitResult(Convert.ToInt32(Convert.ToDouble(resultValue, CultureInfo.InvariantCulture)));
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "error converting to Int32");
+ }
+ }
+ }
+
+ private (string MimeType, string Path)? ReadEpubCoverInto(string opfRootDirectory, string xmlPath)
+ {
+ var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager);
+
+ if (resultElement is not null)
+ {
+ return ReadManifestItem(resultElement, opfRootDirectory);
+ }
+
+ return null;
+ }
+
+ private (string MimeType, string Path)? ReadManifestItem(XmlNode manifestNode, string opfRootDirectory)
+ {
+ var href = manifestNode.Attributes?["href"]?.Value;
+ var mediaType = manifestNode.Attributes?["media-type"]?.Value;
+
+ if (string.IsNullOrEmpty(href) || string.IsNullOrEmpty(mediaType) || !IsValidImage(mediaType))
+ {
+ return null;
+ }
+
+ var coverPath = Path.Combine(opfRootDirectory, href);
+
+ return (MimeType: mediaType, Path: coverPath);
+ }
+
+ private static bool IsValidImage(string? mimeType)
+ {
+ return !string.IsNullOrEmpty(mimeType) && !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType));
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index fedf34598..abaca65ff 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -518,7 +518,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
return null;
}
- return _tmDbClient.GetImageUrl(size, path, true).ToString();
+ // Use "original" as default size if size is null or empty to prevent malformed URLs
+ var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
+
+ return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
}
/// <summary>
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index d6e66a0e6..0944b557e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The Jellyfin person type.</returns>
public static PersonKind MapCrewToPersonType(Crew crew)
{
- if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
+ if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Director;
@@ -82,7 +82,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase))
+ && (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase)))
{
return PersonKind.Writer;
}
@@ -117,14 +117,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
languages.Add(preferredLanguage);
-
- if (preferredLanguage.Length == 5) // Like en-US
- {
- // Currently, TMDb supports 2-letter language codes only.
- // They are planning to change this in the future, thus we're
- // supplying both codes if we're having a 5-letter code.
- languages.Add(preferredLanguage.Substring(0, 2));
- }
}
languages.Add("null");
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index 4cb6cb960..07061cfc7 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/movies/#recycle", true)]
[InlineData("thumbs.db", true)]
[InlineData(@"C:\media\movies\movie.avi", false)]
- [InlineData("/media/.hiddendir/file.mp4", true)]
+ [InlineData("/media/.hiddendir/file.mp4", false)]
[InlineData("/media/dir/.hiddenfile.mp4", true)]
[InlineData("/media/dir/._macjunk.mp4", true)]
[InlineData("/volume1/video/Series/@eaDir", true)]
@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
- public void PathIgnored(string path, bool expected)
+ public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index 940e3c2b1..650d67b19 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -11,21 +11,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
- [InlineData("Superman: Red Son", "something", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
- [InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
- [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")]
- [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
+ [InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
+ [InlineData("{tmdbid=618355}", "tmdbid", "618355")]
+ [InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
+ [InlineData("{tmdbid-618355)", "tmdbid", null)]
+ [InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)]
@@ -36,6 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
+ [InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
+ [InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
+ [InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));