aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--Directory.Packages.props25
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs4
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs3
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/enm.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/km.json132
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json63
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json6
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs30
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs2
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs8
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs16
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs28
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs2
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs10
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs61
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs1
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs8
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs5
-rw-r--r--Jellyfin.Data/Entities/MediaSegment.cs42
-rw-r--r--Jellyfin.Data/Enums/MediaSegmentType.cs39
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs157
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs20
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj1
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs5
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs106
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs708
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs38
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs30
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs15
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs4
-rw-r--r--Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs2
-rw-r--r--Jellyfin.Server/Startup.cs7
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs12
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs32
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs1
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs161
-rw-r--r--MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs53
-rw-r--r--MediaBrowser.Controller/Providers/SeasonInfo.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs12
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs6
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs6
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs18
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs2
-rw-r--r--MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs35
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs4
-rw-r--r--MediaBrowser.Providers/Lyric/LyricScheduledTask.cs170
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs12
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs10
-rw-r--r--README.md12
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs3
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs12
68 files changed, 1802 insertions, 408 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index af5264279..a3847dcdf 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.7",
+ "version": "8.0.8",
"commands": [
"dotnet-ef"
]
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index 85590c0b0..b71b365f7 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.8+
+ - 10.9.9+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index ba66526e0..ed18a6a39 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
+ uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
+ uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
+ uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index d5dc1a860..93bfc83a2 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -27,7 +27,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: openapi-head
retention-days: 14
@@ -61,7 +61,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: openapi-base
retention-days: 14
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 683f2eee3..12fcc26a4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AsyncKeyedLock" Version="7.0.0" />
+ <PackageVersion Include="AsyncKeyedLock" Version="7.0.1" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -16,7 +16,6 @@
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
@@ -25,14 +24,14 @@
<PackageVersion Include="libse" Version="4.0.7" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
@@ -41,14 +40,14 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -81,7 +80,7 @@
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="5.25.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.1.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5bf9c4fc2..ceda0bd64 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.MediaSegments;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -552,6 +553,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddScoped<DynamicHlsHelper>();
serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
+
+ serviceCollection.AddSingleton<IMediaSegmentManager, MediaSegmentManager>();
}
/// <summary>
@@ -635,6 +638,7 @@ namespace Emby.Server.Implementations
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+ BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this;
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index c06cd8510..f0c267627 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
- { SqliteCacheSizeKey, "20000" },
- { SqliteDisableSecondLevelCacheKey, bool.FalseString }
+ { SqliteCacheSizeKey, "20000" }
};
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 5291999dc..8ed72c208 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
- /// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
+ /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 1a69627fa..d1410ef5e 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -122,6 +122,8 @@
"TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.",
"TaskRefreshChannels": "Refresh Channels",
"TaskRefreshChannelsDescription": "Refreshes internet channel information.",
+ "TaskDownloadMissingLyrics": "Download missing lyrics",
+ "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
"TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
"TaskOptimizeDatabase": "Optimize database",
diff --git a/Emby.Server.Implementations/Localization/Core/enm.json b/Emby.Server.Implementations/Localization/Core/enm.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/enm.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index a13ee48d5..e8c0f7fc3 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
+ "HeaderAlbumArtists": "Artistes d'albums",
"HeaderContinueWatching": "Continuer de regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json
index 02f9d4443..5d10975f3 100644
--- a/Emby.Server.Implementations/Localization/Core/km.json
+++ b/Emby.Server.Implementations/Localization/Core/km.json
@@ -1,3 +1,133 @@
{
- "Albums": "Albums"
+ "Albums": "អាលប៊ុម",
+ "MessageApplicationUpdatedTo": "ម៉ាស៊ីនមេនៃJellyfinត្រូវបានអាប់ដេតទៅកាន់ {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ការកំណត់ម៉ាស៊ីនមេ ផ្នែក {0} ត្រូវបានអាប់ដេត",
+ "MessageServerConfigurationUpdated": "ការកំណត់ម៉ាស៊ីនមេត្រូវបានអាប់ដេត",
+ "AppDeviceValues": "កម្មវិធី: {0}, ឧបករណ៍: {1}",
+ "MixedContent": "មាតិកាចម្រុះ",
+ "UserLockedOutWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវ​បាន​ផ្អាក",
+ "Application": "កម្មវិធី",
+ "Artists": "សិល្បករ",
+ "AuthenticationSucceededWithUserName": "{0} បានផ្ទៀងផ្ទាត់ដោយជោគជ័យ",
+ "Books": "សៀវភៅ",
+ "NameSeasonNumber": "រដូវកាលទី {0}",
+ "NotificationOptionPluginInstalled": "Plugin បានដំឡើងរួច",
+ "CameraImageUploadedFrom": "រូបភាពកាមេរ៉ាថ្មីត្រូវបានបង្ហោះពី {0}",
+ "Channels": "ប៉ុស្ត៍",
+ "ChapterNameValue": "ជំពូក {0}",
+ "Collections": "បណ្តុំ",
+ "External": "ខាងក្រៅ",
+ "Default": "លំនាំដើម",
+ "NotificationOptionInstallationFailed": "ការដំឡើងមិនបានសម្រេច",
+ "DeviceOfflineWithName": "{0} បានផ្តាច់",
+ "Folders": "ថតឯកសារ",
+ "DeviceOnlineWithName": "{0} បានភ្ចាប់",
+ "HearingImpaired": "ខ្សោយការស្តាប់",
+ "HomeVideos": "វីឌីអូថតខ្លួនឯង",
+ "Favorites": "ចំណូលចិត្ត",
+ "HeaderFavoriteEpisodes": "ភាគដែលចូលចិត្ត",
+ "Forced": "បង្ខំ",
+ "Genres": "ប្រភេទ",
+ "HeaderFavoriteArtists": "សិល្បករដែលចូលចិត្ត",
+ "NotificationOptionApplicationUpdateAvailable": "កម្មវិធី យើងអាចអាប់ដេតបាន",
+ "NotificationOptionApplicationUpdateInstalled": "កម្មវិធី ដែលបានដំឡើងរួច",
+ "NotificationOptionAudioPlaybackStopped": "ការ​ចាក់សម្លេងបានផ្អាក",
+ "HeaderContinueWatching": "បន្តមើល",
+ "HeaderFavoriteAlbums": "អាល់ប៊ុមដែលចូលចិត្ត",
+ "HeaderFavoriteShows": "រឿងភាគដែលចូលចិត្ត",
+ "NewVersionIsAvailable": "មានជំនាន់ថ្មី ម៉ាស៊ីនមេJellyfin អាចទាញយកបាន.",
+ "HeaderAlbumArtists": "សិល្បករអាល់ប៊ុម",
+ "NotificationOptionCameraImageUploaded": "រូបភាពពីកាំមេរ៉ាបានអាប់ឡូតរួច",
+ "HeaderFavoriteSongs": "ចម្រៀងដែលចូលចិត្ត",
+ "HeaderNextUp": "បន្ទាប់",
+ "HeaderLiveTV": "ទូរទស្សន៍ផ្សាយផ្ទាល់",
+ "Movies": "រឿង",
+ "HeaderRecordingGroups": "ក្រុមនៃការថត",
+ "Music": "តន្ត្រី",
+ "Inherit": "មរតក",
+ "MusicVideos": "វីដេអូតន្ត្រី",
+ "NameInstallFailed": "{0} ការដំឡើងបានបរាជ័យ",
+ "NotificationOptionNewLibraryContent": "មាតិកាថ្មីៗត្រូវបានបន្ថែម",
+ "ItemAddedWithName": "{0} ត្រូវបានបន្ថែមទៅបណ្ណាល័យ",
+ "NameSeasonUnknown": "រដូវកាលមិនច្បាស់លាស់",
+ "ItemRemovedWithName": "{0} ត្រូវបានដកចេញពីបណ្ណាល័យ",
+ "LabelIpAddressValue": "លេខ IP: {0}",
+ "LabelRunningTimeValue": "ពេលវេលាកំពុងដំណើរការ: {0}",
+ "Latest": "ចុងក្រោយ",
+ "NotificationOptionAudioPlayback": "ការ​ចាក់​សំឡេង​បាន​ចាប់ផ្ដើម",
+ "NotificationOptionPluginError": "Plugin មិនដំណើរការ",
+ "NotificationOptionPluginUninstalled": "Plugin បានលុបចេញរួច",
+ "MessageApplicationUpdated": "ម៉ាស៊ីនមេនៃJellyfinត្រូវបានអាប់ដេត",
+ "NotificationOptionPluginUpdateInstalled": "Plugin អាប់ដេតបានដំឡើងរួច",
+ "NotificationOptionUserLockedOut": "អ្នកប្រើប្រាស់ត្រូវបានជាប់គាំង",
+ "NotificationOptionServerRestartRequired": "តម្រូវឱ្យចាប់ផ្ដើមម៉ាស៊ីនមេឡើងវិញ",
+ "Photos": "រូបថត",
+ "Playlists": "បញ្ជីចាក់",
+ "Plugin": "Plugin",
+ "PluginInstalledWithName": "{0} ត្រូវបានដំឡើង",
+ "NotificationOptionTaskFailed": "កិច្ចការដែលបានគ្រោងទុកបានបរាជ័យ",
+ "PluginUpdatedWithName": "{0} ត្រូវបានអាប់ដេត",
+ "NotificationOptionVideoPlayback": "ការចាក់វីដេអូបានចាប់ផ្តើម",
+ "Songs": "ចម្រៀង",
+ "ScheduledTaskStartedWithName": "{0} បានចាប់ផ្តើម",
+ "NotificationOptionVideoPlaybackStopped": "ការ​ចាក់​វីដេអូ​បាន​បញ្ឈប់",
+ "PluginUninstalledWithName": "{0} ត្រូវបានលុបចេញ",
+ "Shows": "រឿងភាគ",
+ "ProviderValue": "អ្នកផ្តល់សេវា: {0}",
+ "SubtitleDownloadFailureFromForItem": "សាប់ថាយថលបានបរាជ័យក្នុងការទាញយកពី {0} នៃ {1}",
+ "Sync": "ធ្វើអោយដំណាលគ្នា",
+ "System": "ប្រព័ន្ធ",
+ "TvShows": "កម្មវិធីទូរទស្សន៍",
+ "ScheduledTaskFailedWithName": "{0} បានបរាជ័យ",
+ "Undefined": "មិនបានកំណត់",
+ "User": "អ្នកប្រើប្រាស់",
+ "UserCreatedWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវបានបង្កើតឡើង",
+ "ServerNameNeedsToBeRestarted": "{0} ចាំបាច់ត្រូវចាប់ផ្តើមឡើងវិញ",
+ "StartupEmbyServerIsLoading": "ម៉ាស៊ីនមេJellyfin កំពុងដំណើរការ. សូមព្យាយាមម្តងទៀតក្នុងពេលឆាប់ៗនេះ.",
+ "UserDeletedWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវបានលុបចេញ",
+ "UserOnlineFromDevice": "{0} បានឃើញអនឡានពី {1}",
+ "UserDownloadingItemWithValues": "{0} កំពុងទាញយក {1}",
+ "UserOfflineFromDevice": "{0} បានផ្តាច់ចេញពី {1}",
+ "UserStartedPlayingItemWithValues": "{0} កំពុងចាក់ {1} នៅលើ {2}",
+ "TaskRefreshChapterImagesDescription": "បង្កើតរូបភាពតូចៗសម្រាប់វីដេអូដែលមានតាមជំពូក.",
+ "VersionNumber": "កំណែ {0}",
+ "TasksMaintenanceCategory": "តំហែរទាំ",
+ "TasksLibraryCategory": "បណ្ណាល័យ",
+ "TasksApplicationCategory": "កម្មវិធី",
+ "TaskCleanActivityLog": "សម្អាតកំណត់ហេតុសកម្មភាព",
+ "UserPasswordChangedWithName": "ពាក្យសម្ងាត់ត្រូវបានផ្លាស់ប្តូរសម្រាប់អ្នកប្រើប្រាស់ {0}",
+ "TaskCleanCache": "សម្អាតបញ្ជីឃ្លាំងសម្ងាត់",
+ "TaskRefreshChapterImages": "ដកស្រង់រូបភាពតាមជំពូក",
+ "UserPolicyUpdatedWithName": "គោលការណ៍អ្នកប្រើប្រាស់ត្រូវបានធ្វើបច្ចុប្បន្នភាពសម្រាប់ {0}",
+ "UserStoppedPlayingItemWithValues": "{0} បានបញ្ចប់ការចាក់ {1} នៅលើ {2}",
+ "ValueHasBeenAddedToLibrary": "{0} ត្រូវបានបញ្ចូលទៅក្នុងបណ្ណាល័យរឿងរបស់អ្នក",
+ "ValueSpecialEpisodeName": "ពិសេស - {0}",
+ "TasksChannelsCategory": "ប៉ុស្តតាមអ៊ីនធឺណិត",
+ "TaskAudioNormalization": "ធ្វើឱ្យមានតន្ត្រីមានសម្លេងស្មើគ្នា",
+ "TaskCleanActivityLogDescription": "លុបកំណត់ហេតុសកម្មភាពចាស់ជាងអាយុដែលបានកំណត់រចនាសម្ព័ន្ធ.",
+ "TaskCleanCacheDescription": "លុបឯកសារឃ្លាំងសម្ងាត់លែងត្រូវការដោយប្រព័ន្ធ.",
+ "TaskRefreshLibraryDescription": "ស្កេនបណ្ណាល័យរឿងរបស់អ្នក សម្រាប់ឯកសារថ្មីៗ និងmetadata ឡើងវិញ.",
+ "TaskCleanLogsDescription": "លុបឯកសារកំណត់ហេតុដែលមានអាយុកាលលើសពី {0} ថ្ងៃ.",
+ "TaskRefreshPeopleDescription": "ធ្វើបច្ចុប្បន្នភាព metadata សម្រាប់តួសម្តែង និងអ្នកដឹកនាំនៅក្នុងបណ្ណាល័យរឿងរបស់អ្នក.",
+ "TaskOptimizeDatabaseDescription": "បង្រួម Database និង Truncate free space. ដំណើរការកិច្ចការនេះ បន្ទាប់ពីការស្កេនបណ្ណាល័យ ឬធ្វើការផ្លាស់ប្តូរផ្សេងទៀត ដែលបញ្ជាក់ថា ការកែប្រែ Database អាចធ្វើឱ្យដំណើរការប្រសើរឡើង.",
+ "TaskRefreshTrickplayImages": "បង្កើតបណ្តុំរូបភាពតាម Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "បង្កើត​ trickplay previews សម្រាប់​វីដេអូ​ក្នុង​បណ្ណាល័យ​ដែល​បានបង្ហាញ.",
+ "TaskKeyframeExtractorDescription": "ស្រង់យកFrame គន្លឹះៗពីវីដេអូ ដើម្បីបង្កើតបញ្ជីចាក់ HLS ច្បាស់លាស់ជាងមុន. កិច្ចការនេះអាចនឹងដំណើរការយូរ.",
+ "FailedLoginAttemptWithUserName": "បរាជ័យក្នុងការព្យាយាមចូលពី {0}",
+ "TaskCleanTranscode": "សម្អាតថតឯកសារ ​Transcode",
+ "TaskRefreshChannelsDescription": "Refreshes ព័ត៌មានបណ្តាញអ៊ីនធឺណិត.",
+ "TaskDownloadMissingSubtitles": "ទាញយកសាប់ថាយថលដែលបាត់",
+ "TaskRefreshChannels": "Refresh ឆានែល",
+ "TaskKeyframeExtractor": "ការញែក Keyframe",
+ "TaskAudioNormalizationDescription": "ស្កែនឯកសារសម្រាប់ធ្វើឱ្យមានតន្ត្រីមានសម្លេងស្មើគ្នា.",
+ "TaskRefreshLibrary": "ស្កេនបណ្ណាល័យរឿង",
+ "TaskCleanLogs": "សម្អាត Log Directory",
+ "TaskRefreshPeople": "Refresh អ្នកប្រើប្រាស់",
+ "TaskUpdatePlugins": "ធ្វើបច្ចុប្បន្នភាព Plugins",
+ "TaskUpdatePluginsDescription": "ទាញយក និងដំឡើងបច្ចុប្បន្នភាពសម្រាប់Plugins ដែលត្រូវបាន Config ដើម្បីធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិ.",
+ "TaskCleanTranscodeDescription": "លុបឯកសារ Transcode ដែលលើសពីមួយថ្ងៃ.",
+ "TaskDownloadMissingSubtitlesDescription": "ស្វែងរកតាមអ៊ីនធឺណិត សម្រាប់សាប់ថាយថល ដែលបាត់ដោយផ្អែកលើ metadata.",
+ "TaskOptimizeDatabase": "ធ្វើឱ្យ Database ប្រសើរឡើង",
+ "TaskCleanCollectionsAndPlaylistsDescription": "លុបរបស់របរចេញពីបណ្តុំ និងបញ្ជីចាក់ដែលលែងមាន.",
+ "TaskCleanCollectionsAndPlaylists": "សម្អាតបណ្តុំ និងបញ្ជីចាក់"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
new file mode 100644
index 000000000..d6ff58785
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -0,0 +1,63 @@
+{
+ "Collections": "Kuntellow",
+ "DeviceOfflineWithName": "{0} re anjunyas",
+ "External": "A-ves",
+ "Folders": "Plegellow",
+ "HeaderFavoriteAlbums": "Albomow Drudh",
+ "HeaderFavoriteArtists": "Artydhyon Drudh",
+ "HeaderFavoriteEpisodes": "Towlennow Drudh",
+ "HeaderFavoriteSongs": "Kanow Drudh",
+ "HeaderRecordingGroups": "Bagasow Rekordya",
+ "HearingImpaired": "Klewans Aperys",
+ "HomeVideos": "Gwydhyow Tre",
+ "Inherit": "Herya",
+ "LabelRunningTimeValue": "Prys ow ponya: {0}",
+ "Latest": "Diwettha",
+ "MessageApplicationUpdated": "Servell Jellyfin re beu nowedhys",
+ "MessageApplicationUpdatedTo": "Servell Jellyfin re beu nowedhys dhe {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Rann dewisyans servell {0} re beu nowedhys",
+ "MixedContent": "Dalgh kemmyskys",
+ "Movies": "Fylmow",
+ "MusicVideos": "Gwydhyow Ilow",
+ "NameSeasonUnknown": "Seson Anwodhvedhys",
+ "NotificationOptionAudioPlayback": "Seneans dallethys",
+ "NotificationOptionAudioPlaybackStopped": "Seneans hedhys",
+ "NotificationOptionPluginError": "Defowt ystynnans",
+ "NotificationOptionPluginUninstalled": "Ystynnans anynstallys",
+ "NotificationOptionPluginUpdateInstalled": "Nowedheans ystynnans ynstallys",
+ "Application": "Gweythres",
+ "Favorites": "Drudh",
+ "Forced": "Konstrynys",
+ "Albums": "Albomow",
+ "Books": "Lyvrow",
+ "Channels": "Gothi",
+ "AppDeviceValues": "App: {0}, Devis: {1}",
+ "Artists": "Artydhyon",
+ "HeaderAlbumArtists": "Albom artydhyon",
+ "HeaderNextUp": "Nessa",
+ "CameraImageUploadedFrom": "Skeusen kamera nowydh re beu ughkargys a-dhyworth {0}",
+ "ChapterNameValue": "Chaptra {0}",
+ "FailedLoginAttemptWithUserName": "Assay omgelm fyllys a-dhyworth {0}",
+ "AuthenticationSucceededWithUserName": "{0} omgelmys yn sewen",
+ "Default": "Defowt",
+ "DeviceOnlineWithName": "{0} yw junys",
+ "ItemRemovedWithName": "{0} a veu dileys a-dhyworth an lyverva",
+ "LabelIpAddressValue": "Trigva PK: {)}",
+ "Music": "Ilow",
+ "HeaderContinueWatching": "Pesya ow kweles",
+ "NameSeasonNumber": "Seson {0}",
+ "NotificationOptionApplicationUpdateInstalled": "Nowedheans gweythres ynstallys",
+ "NotificationOptionCameraImageUploaded": "Skeusen kamera ughkargys",
+ "HeaderFavoriteShows": "Diskwedhyansow Drudh",
+ "HeaderLiveTV": "PW Yn Fyw",
+ "MessageServerConfigurationUpdated": "Dewisyans servell re beu nowedhys",
+ "ItemAddedWithName": "{0} a veu keworrys dhe'n lyverva",
+ "NameInstallFailed": "{0} ynstallyans fyllys",
+ "NotificationOptionNewLibraryContent": "Dalgh nowydh keworrys",
+ "NewVersionIsAvailable": "Yma versyon nowydh a Servell Jellyfin neb yw kavadow rag iskarga.",
+ "NotificationOptionApplicationUpdateAvailable": "Nowedheans gweythres kavadow",
+ "NotificationOptionInstallationFailed": "Defowt ynstallyans",
+ "Genres": "Eghennow",
+ "NotificationOptionPluginInstalled": "Ystynnans ynstallys",
+ "NotificationOptionServerRestartRequired": "Dastalleth servell yw res"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 4f076b680..39e7cd546 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
- "HomeVideos": "Thuis video's",
+ "HomeVideos": "Homevideo's",
"Inherit": "Erven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
"TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
"TaskAudioNormalization": "Geluidsnormalisatie",
- "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie."
+ "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.",
+ "TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden",
+ "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index f36385be2..a24a837ab 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
"TaskAudioNormalization": "Normalizacja dźwięku",
- "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku."
+ "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.",
+ "TaskDownloadMissingLyrics": "Pobierz brakujące słowa",
+ "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index cd0120fc7..2f52aafa3 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Extractor de cadre cheie",
"HearingImpaired": "Ascultare Impară",
"TaskRefreshTrickplayImages": "Generează imagini Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate."
+ "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate.",
+ "TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.",
+ "TaskAudioNormalization": "Normalizare sunet",
+ "TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare."
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 3dda5fdee..681c252b6 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(deviceId);
var activityDate = DateTime.UtcNow;
- var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate;
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- private async Task<SessionInfo> GetSessionInfo(
+ private SessionInfo GetSessionInfo(
string appName,
string appVersion,
string deviceId,
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
{
- sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
_activeConnections[key] = sessionInfo;
}
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
- private async Task<SessionInfo> CreateSession(
+ private SessionInfo CreateSession(
string key,
string appName,
string appVersion,
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
- var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
+ var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
if (string.IsNullOrEmpty(deviceOptions.CustomName))
{
sessionInfo.DeviceName = deviceName;
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
return new[] { item };
}
- private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
+ private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
return new List<BaseItem>();
}
- return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
+ return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
}
/// <inheritdoc />
@@ -1520,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
// This should be validated above, but if it isn't don't delete all tokens.
ArgumentException.ThrowIfNullOrEmpty(deviceId);
- var existing = (await _deviceManager.GetDevices(
+ var existing = _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId,
UserId = user.Id
- }).ConfigureAwait(false)).Items;
+ }).Items;
foreach (var auth in existing)
{
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(accessToken);
- var existing = (await _deviceManager.GetDevices(
+ var existing = _deviceManager.GetDevices(
new DeviceQuery
{
Limit = 1,
AccessToken = accessToken
- }).ConfigureAwait(false)).Items;
+ }).Items;
if (existing.Count > 0)
{
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- var existing = await _deviceManager.GetDevices(new DeviceQuery
+ var existing = _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
- }).ConfigureAwait(false);
+ });
foreach (var info in existing.Items)
{
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
- var items = (await _deviceManager.GetDevices(new DeviceQuery
+ var items = _deviceManager.GetDevices(new DeviceQuery
{
AccessToken = token,
Limit = 1
- }).ConfigureAwait(false)).Items;
+ }).Items;
if (items.Count == 0)
{
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index c1a615666..34c9e86f2 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
}
string? presentationUniqueKey = null;
- int? limit = request.Limit;
+ int? limit = null;
if (!request.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 8954c8ef5..a47c60473 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -46,7 +46,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -76,7 +76,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -213,7 +213,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -243,7 +243,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 6d9ec343e..2a2ab4ad1 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
+ public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
+ return _deviceManager.GetDevicesForUser(userId);
}
/// <summary>
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
+ public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
{
- var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo is null)
{
return NotFound();
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
+ public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
{
- var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
+ var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo is null)
{
return NotFound();
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
{
- var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ var existingDevice = _deviceManager.GetDevice(id);
if (existingDevice is null)
{
return NotFound();
}
- var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
+ var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
foreach (var session in sessions.Items)
{
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 130c1192f..016c5b163 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -116,7 +116,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -146,7 +146,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -355,7 +355,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -387,7 +387,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -531,7 +531,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -562,7 +562,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -700,7 +700,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -732,7 +732,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -871,7 +871,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -902,7 +902,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -1043,7 +1043,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -1075,7 +1075,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -1227,7 +1227,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -1258,7 +1258,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 8e8accab3..6a169eae3 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -2089,6 +2089,8 @@ public class ImageController : BaseJellyfinApiController
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
+ Response.Headers.ContentDisposition = "attachment";
+
if (disableCaching)
{
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 62cb59335..afc93c3a8 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -857,6 +857,16 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
+ result.LyricFetchers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LyricFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
var typeOptions = new List<LibraryTypeOptionsDto>();
foreach (var type in types)
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
new file mode 100644
index 000000000..e97704d48
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.MediaSegments;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Media Segments api.
+/// </summary>
+[Authorize]
+public class MediaSegmentsController : BaseJellyfinApiController
+{
+ private readonly IMediaSegmentManager _mediaSegmentManager;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaSegmentsController"/> class.
+ /// </summary>
+ /// <param name="mediaSegmentManager">MediaSegments Manager.</param>
+ /// <param name="libraryManager">The Library manager.</param>
+ public MediaSegmentsController(IMediaSegmentManager mediaSegmentManager, ILibraryManager libraryManager)
+ {
+ _mediaSegmentManager = mediaSegmentManager;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets all media segments based on an itemId.
+ /// </summary>
+ /// <param name="itemId">The ItemId.</param>
+ /// <param name="includeSegmentTypes">Optional filter of requested segment types.</param>
+ /// <returns>A list of media segment objects related to the requested itemId.</returns>
+ [HttpGet("{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<QueryResult<MediaSegmentDto>>> GetSegmentsAsync(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] IEnumerable<MediaSegmentType>? includeSegmentTypes = null)
+ {
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
+ return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
+ }
+}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 6abd7a23e..53b7349e7 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
return NotFound();
}
+ Response.Headers.ContentDisposition = "attachment";
+
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
}
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 0afe053da..60d49af9e 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
if (System.IO.File.Exists(path))
{
+ Response.Headers.ContentDisposition = "attachment";
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 7f9608378..effe7b021 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -267,7 +267,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -299,7 +299,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -508,7 +508,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -540,7 +540,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
index 78efacd94..53b5e3b7c 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -24,6 +24,11 @@ public class LibraryOptionsResultDto
public IReadOnlyList<LibraryOptionInfoDto> SubtitleFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
/// <summary>
+ /// Gets or sets the list of lyric fetchers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> LyricFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+
+ /// <summary>
/// Gets or sets the type options.
/// </summary>
public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs
new file mode 100644
index 000000000..90120d772
--- /dev/null
+++ b/Jellyfin.Data/Entities/MediaSegment.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class MediaSegment
+{
+ /// <summary>
+ /// Gets or sets the id of the media segment.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Type of content this segment defines.
+ /// </summary>
+ public MediaSegmentType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end of the segment.
+ /// </summary>
+ public long EndTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start of the segment.
+ /// </summary>
+ public long StartTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets Id of the media segment provider this entry originates from.
+ /// </summary>
+ public required string SegmentProviderId { get; set; }
+}
diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Data/Enums/MediaSegmentType.cs
new file mode 100644
index 000000000..458635450
--- /dev/null
+++ b/Jellyfin.Data/Enums/MediaSegmentType.cs
@@ -0,0 +1,39 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// Defines the types of content an individual <see cref="MediaSegment"/> represents.
+/// </summary>
+public enum MediaSegmentType
+{
+ /// <summary>
+ /// Default media type or custom one.
+ /// </summary>
+ Unknown = 0,
+
+ /// <summary>
+ /// Commercial.
+ /// </summary>
+ Commercial = 1,
+
+ /// <summary>
+ /// Preview.
+ /// </summary>
+ Preview = 2,
+
+ /// <summary>
+ /// Recap.
+ /// </summary>
+ Recap = 3,
+
+ /// <summary>
+ /// Outro.
+ /// </summary>
+ Outro = 4,
+
+ /// <summary>
+ /// Intro.
+ /// </summary>
+ Intro = 5
+}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index d8d1b6fa8..d7a46e2d5 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IUserManager _userManager;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
+ private readonly ConcurrentDictionary<int, Device> _devices;
+ private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
@@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
{
_dbProvider = dbProvider;
_userManager = userManager;
+ _devices = new ConcurrentDictionary<int, Device>();
+ _deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
+
+ using var dbContext = _dbProvider.CreateDbContext();
+ foreach (var device in dbContext.Devices
+ .OrderBy(d => d.Id)
+ .AsEnumerable())
+ {
+ _devices.TryAdd(device.Id, device);
+ }
+
+ foreach (var deviceOption in dbContext.DeviceOptions
+ .OrderBy(d => d.Id)
+ .AsEnumerable())
+ {
+ _deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption);
+ }
}
/// <inheritdoc />
@@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
+ _deviceOptions[deviceId] = deviceOptions;
+
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
}
@@ -76,25 +97,17 @@ namespace Jellyfin.Server.Implementations.Devices
await using (dbContext.ConfigureAwait(false))
{
dbContext.Devices.Add(device);
-
await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ _devices.TryAdd(device.Id, device);
}
return device;
}
/// <inheritdoc />
- public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
+ public DeviceOptions GetDeviceOptions(string deviceId)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- DeviceOptions? deviceOptions;
- await using (dbContext.ConfigureAwait(false))
- {
- deviceOptions = await dbContext.DeviceOptions
- .AsNoTracking()
- .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
- .ConfigureAwait(false);
- }
+ _deviceOptions.TryGetValue(deviceId, out var deviceOptions);
return deviceOptions ?? new DeviceOptions(deviceId);
}
@@ -108,57 +121,43 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public async Task<DeviceInfo?> GetDevice(string id)
+ public DeviceInfo? GetDevice(string id)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
- {
- var device = await dbContext.Devices
- .Where(d => d.DeviceId == id)
- .OrderByDescending(d => d.DateLastActivity)
- .Include(d => d.User)
- .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
- .FirstOrDefaultAsync()
- .ConfigureAwait(false);
+ var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
+ _deviceOptions.TryGetValue(id, out var deviceOption);
- var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
-
- return deviceInfo;
- }
+ var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
+ return deviceInfo;
}
/// <inheritdoc />
- public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
+ public QueryResult<Device> GetDevices(DeviceQuery query)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ IEnumerable<Device> devices = _devices.Values
+ .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
+ .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
+ .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
+ .OrderBy(d => d.Id)
+ .ToList();
+ var count = devices.Count();
+
+ if (query.Skip.HasValue)
{
- var devices = dbContext.Devices
- .OrderBy(d => d.Id)
- .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
- .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
- .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
-
- var count = await devices.CountAsync().ConfigureAwait(false);
-
- if (query.Skip.HasValue)
- {
- devices = devices.Skip(query.Skip.Value);
- }
-
- if (query.Limit.HasValue)
- {
- devices = devices.Take(query.Limit.Value);
- }
+ devices = devices.Skip(query.Skip.Value);
+ }
- return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
+ if (query.Limit.HasValue)
+ {
+ devices = devices.Take(query.Limit.Value);
}
+
+ return new QueryResult<Device>(query.Skip, count, devices.ToList());
}
/// <inheritdoc />
- public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
+ public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
{
- var devices = await GetDevices(query).ConfigureAwait(false);
+ var devices = GetDevices(query);
return new QueryResult<DeviceInfo>(
devices.StartIndex,
@@ -167,38 +166,36 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
+ public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ IEnumerable<Device> devices = _devices.Values
+ .OrderByDescending(d => d.DateLastActivity)
+ .ThenBy(d => d.DeviceId);
+
+ if (!userId.IsNullOrEmpty())
{
- var sessions = dbContext.Devices
- .Include(d => d.User)
- .OrderByDescending(d => d.DateLastActivity)
- .ThenBy(d => d.DeviceId)
- .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
- .AsAsyncEnumerable();
-
- if (!userId.IsNullOrEmpty())
+ var user = _userManager.GetUserById(userId.Value);
+ if (user is null)
{
- var user = _userManager.GetUserById(userId.Value);
- if (user is null)
- {
- throw new ResourceNotFoundException();
- }
-
- sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
+ throw new ResourceNotFoundException();
}
- var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
-
- return new QueryResult<DeviceInfo>(array);
+ devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
}
+
+ var array = devices.Select(device =>
+ {
+ _deviceOptions.TryGetValue(device.DeviceId, out var option);
+ return ToDeviceInfo(device, option);
+ }).ToArray();
+
+ return new QueryResult<DeviceInfo>(array);
}
/// <inheritdoc />
public async Task DeleteDevice(Device device)
{
+ _devices.TryRemove(device.Id, out _);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
@@ -208,6 +205,19 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
+ public async Task UpdateDevice(Device device)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.Devices.Update(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ _devices[device.Id] = device;
+ }
+
+ /// <inheritdoc />
public bool CanAccessDevice(User user, string deviceId)
{
ArgumentNullException.ThrowIfNull(user);
@@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
{
var caps = GetCapabilities(authInfo.DeviceId);
+ var user = _userManager.GetUserById(authInfo.UserId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
+ }
return new DeviceInfo
{
@@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId,
LastUserId = authInfo.UserId,
- LastUserName = authInfo.User.Username,
+ LastUserName = user.Username,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps.IconUrl,
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index a88989840..ff29d69b4 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -16,28 +15,13 @@ public static class ServiceCollectionExtensions
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary>
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
- /// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
/// <returns>The updated service collection.</returns>
- public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
+ public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
{
- if (!disableSecondLevelCache)
- {
- serviceCollection.AddEFSecondLevelCache(options =>
- options.UseMemoryCacheProvider()
- .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
- .UseCacheKeyPrefix("EF_")
- // Don't cache null values. Remove this optional setting if it's not necessary.
- .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
- }
-
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
- if (!disableSecondLevelCache)
- {
- dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
- }
+ opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
});
return serviceCollection;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 7c4155bfc..20944ee4b 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -27,7 +27,6 @@
<ItemGroup>
<PackageReference Include="AsyncKeyedLock" />
- <PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index ea99af004..150bc8bb4 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -83,6 +83,11 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
+ /// </summary>
+ public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
new file mode 100644
index 000000000..7916d15c9
--- /dev/null
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.MediaSegments;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.MediaSegments;
+
+/// <summary>
+/// Manages media segments retrival and storage.
+/// </summary>
+public class MediaSegmentManager : IMediaSegmentManager
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaSegmentManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">EFCore Database factory.</param>
+ public MediaSegmentManager(IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task<MediaSegmentDto> CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks);
+
+ using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
+ await db.SaveChangesAsync().ConfigureAwait(false);
+ return mediaSegment;
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteSegmentAsync(Guid segmentId)
+ {
+ using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
+ {
+ using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.MediaSegments
+ .Where(e => e.ItemId.Equals(itemId));
+
+ if (typeFilter is not null)
+ {
+ query = query.Where(e => typeFilter.Contains(e.Type));
+ }
+
+ return query
+ .OrderBy(e => e.StartTicks)
+ .AsNoTracking()
+ .ToImmutableList()
+ .Select(Map);
+ }
+
+ private static MediaSegmentDto Map(MediaSegment segment)
+ {
+ return new MediaSegmentDto()
+ {
+ Id = segment.Id,
+ EndTicks = segment.EndTicks,
+ ItemId = segment.ItemId,
+ StartTicks = segment.StartTicks,
+ Type = segment.Type
+ };
+ }
+
+ private static MediaSegment Map(MediaSegmentDto segment, string segmentProviderId)
+ {
+ return new MediaSegment()
+ {
+ Id = segment.Id,
+ EndTicks = segment.EndTicks,
+ ItemId = segment.ItemId,
+ StartTicks = segment.StartTicks,
+ Type = segment.Type,
+ SegmentProviderId = segmentProviderId
+ };
+ }
+
+ /// <inheritdoc />
+ public bool HasSegments(Guid itemId)
+ {
+ using var db = _dbProvider.CreateDbContext();
+ return db.MediaSegments.Any(e => e.ItemId.Equals(itemId));
+ }
+
+ /// <inheritdoc/>
+ public bool IsTypeSupported(BaseItem baseItem)
+ {
+ return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio;
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs
new file mode 100644
index 000000000..c03cb4760
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs
@@ -0,0 +1,708 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20240729140605_AddMediaSegments")]
+ partial class AddMediaSegments
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs
new file mode 100644
index 000000000..24a8ffc42
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs
@@ -0,0 +1,38 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddMediaSegments : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "MediaSegments",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "TEXT", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Type = table.Column<int>(type: "INTEGER", nullable: false),
+ EndTicks = table.Column<long>(type: "INTEGER", nullable: false),
+ StartTicks = table.Column<long>(type: "INTEGER", nullable: false),
+ SegmentProviderId = table.Column<string>(type: "TEXT", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MediaSegments", x => x.Id);
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "MediaSegments");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index f725ababe..cdeeb6d87 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,4 +1,4 @@
-// <auto-generated />
+// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -270,6 +270,32 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ItemDisplayPreferences");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SegmentProviderId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 6bda12c5b..2ae722982 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -4,7 +4,10 @@ using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
+using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
{
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
private readonly IUserManager _userManager;
+ private readonly IDeviceManager _deviceManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(
IDbContextFactory<JellyfinDbContext> jellyfinDb,
IUserManager userManager,
+ IDeviceManager deviceManager,
IServerApplicationHost serverApplicationHost)
{
_jellyfinDbProvider = jellyfinDb;
_userManager = userManager;
+ _deviceManager = deviceManager;
_serverApplicationHost = serverApplicationHost;
}
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
+ var device = _deviceManager.GetDevices(
+ new DeviceQuery
+ {
+ AccessToken = token
+ }).Items.FirstOrDefault();
if (device is not null)
{
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
if (updateToken)
{
- dbContext.Devices.Update(device);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
}
}
else
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
index e40b541a3..634aea9f0 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
private async Task UpdateDeviceAccess(User user)
{
- var existing = (await _deviceManager.GetDevices(new DeviceQuery
+ var existing = _deviceManager.GetDevices(new DeviceQuery
{
UserId = user.Id
- }).ConfigureAwait(false)).Items;
+ }).Items;
foreach (var device in existing)
{
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
index 858df6728..6b95770ed 100644
--- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
}
})
- .UseStartup(_ => new Startup(appHost, startupConfig));
+ .UseStartup(_ => new Startup(appHost));
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 2ff377403..e9fb3e4c2 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -40,18 +40,15 @@ namespace Jellyfin.Server
{
private readonly CoreAppHost _serverApplicationHost;
private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IConfiguration _startupConfig;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="appHost">The server application host.</param>
- /// <param name="startupConfig">The server startupConfig.</param>
- public Startup(CoreAppHost appHost, IConfiguration startupConfig)
+ public Startup(CoreAppHost appHost)
{
_serverApplicationHost = appHost;
_serverConfigurationManager = appHost.ConfigurationManager;
- _startupConfig = startupConfig;
}
/// <summary>
@@ -70,7 +67,7 @@ namespace Jellyfin.Server
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
- services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
+ services.AddJellyfinDbContext();
services.AddJellyfinApiSwagger();
// configure custom legacy authentication
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index eb181dcc4..5566421cb 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>DeviceInfo.</returns>
- Task<DeviceInfo> GetDevice(string id);
+ DeviceInfo GetDevice(string id);
/// <summary>
/// Gets devices based on the provided query.
/// </summary>
/// <param name="query">The device query.</param>
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
- Task<QueryResult<Device>> GetDevices(DeviceQuery query);
+ QueryResult<Device> GetDevices(DeviceQuery query);
- Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
+ QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
/// <summary>
/// Gets the devices.
/// </summary>
/// <param name="userId">The user's id, or <c>null</c>.</param>
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
- Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
+ QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
Task DeleteDevice(Device device);
+ Task UpdateDevice(Device device);
+
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
/// </summary>
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
Task UpdateDeviceOptions(string deviceId, string deviceName);
- Task<DeviceOptions> GetDeviceOptions(string deviceId);
+ DeviceOptions GetDeviceOptions(string deviceId);
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 7b6f364f7..634f34681 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
- = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
+ = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg" };
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{
@@ -487,6 +487,8 @@ namespace MediaBrowser.Controller.Entities
public static IMediaSourceManager MediaSourceManager { get; set; }
+ public static IMediaSegmentManager MediaSegmentManager { get; set; }
+
/// <summary>
/// Gets or sets the name of the forced sort.
/// </summary>
@@ -1116,7 +1118,10 @@ namespace MediaBrowser.Controller.Entities
RunTimeTicks = item.RunTimeTicks,
Container = item.Container,
Size = item.Size,
- Type = type
+ Type = type,
+ HasSegments = MediaSegmentManager.IsTypeSupported(item)
+ && (protocol is null or MediaProtocol.File)
+ && MediaSegmentManager.HasSegments(item.Id)
};
if (string.IsNullOrEmpty(info.Path))
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index d7ccfd8ae..a07187d2f 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -112,37 +112,31 @@ namespace MediaBrowser.Controller.Entities.Movies
return true;
}
- public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ private IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user)
{
- var children = base.GetChildren(user, includeLinkedChildren, query);
-
- if (string.Equals(DisplayOrder, "SortName", StringComparison.OrdinalIgnoreCase))
+ if (!Enum.TryParse<ItemSortBy>(DisplayOrder, out var sortBy))
{
- // Sort by name
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ sortBy = ItemSortBy.PremiereDate;
}
- if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
+ if (sortBy == ItemSortBy.Default)
{
- // Sort by release date
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ return items;
}
- // Default sorting
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ var children = base.GetChildren(user, includeLinkedChildren, query);
+ return Sort(children, user).ToList();
}
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
var children = base.GetRecursiveChildren(user, query);
-
- if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
- {
- // Sort by release date
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
- }
-
- return children;
+ return Sort(children, user).ToList();
}
public BoxSetInfo GetLookupInfo()
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 083f12746..181b9be2b 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -238,6 +238,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (series is not null)
{
id.SeriesProviderIds = series.ProviderIds;
+ id.SeriesDisplayOrder = series.DisplayOrder;
}
return id;
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 7dfda73bf..6c58064ce 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -65,11 +65,6 @@ namespace MediaBrowser.Controller.Extensions
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
- /// Disable second level cache of sqlite.
- /// </summary>
- public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
-
- /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -133,15 +128,5 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
-
- /// <summary>
- /// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
- /// </summary>
- /// <param name="configuration">The configuration to read the setting from.</param>
- /// <returns>Whether second level cache disabled.</returns>
- public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
- {
- return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
- }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index c068cf055..64864bad0 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -108,7 +108,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels
private static readonly Dictionary<string, int> _audioTranscodeChannelLookup = new(StringComparer.OrdinalIgnoreCase)
{
- { "wmav2", 2 },
{ "libmp3lame", 2 },
{ "libfdk_aac", 6 },
{ "ac3", 6 },
@@ -410,27 +409,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
- if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
- {
- return "libvpx";
- }
-
- if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
- {
- return "libvpx-vp9";
- }
-
- if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
- {
- return "wmv2";
- }
-
- if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
- {
- return "libtheora";
- }
-
if (_validationRegex.IsMatch(codec))
{
return codec.ToLowerInvariant();
@@ -750,11 +728,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libvorbis";
}
- if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
- {
- return "wmav2";
- }
-
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
{
return "libopus";
@@ -1342,7 +1315,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
- && (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{
@@ -1381,20 +1354,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// Currently use the same buffer size for all encoders
int bufsize = bitrate * 2;
- if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase))
- {
- // When crf is used with vpx, b:v becomes a max rate
- // https://trac.ffmpeg.org/wiki/Encode/VP8
- // https://trac.ffmpeg.org/wiki/Encode/VP9
- return FormattableString.Invariant($" -maxrate:v {bitrate} -bufsize:v {bufsize} -b:v {bitrate}");
- }
-
- if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- return FormattableString.Invariant($" -b:v {bitrate}");
- }
-
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}");
@@ -1930,93 +1889,6 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
}
}
- else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
- {
- // Values 0-3, 0 being highest quality but slower
- var profileScore = 0;
-
- var qmin = "0";
- var qmax = "50";
- var crf = "10";
-
- if (isVc1)
- {
- profileScore++;
- }
-
- // Max of 2
- profileScore = Math.Min(profileScore, 2);
-
- // http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format(
- CultureInfo.InvariantCulture,
- " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
- profileScore.ToString(CultureInfo.InvariantCulture),
- crf,
- qmin,
- qmax);
- }
- else if (string.Equals(videoEncoder, "libvpx-vp9", StringComparison.OrdinalIgnoreCase)) // vp9
- {
- // When `-deadline` is set to `good` or `best`, `-cpu-used` ranges from 0-5.
- // When `-deadline` is set to `realtime`, `-cpu-used` ranges from 0-15.
- // Resources:
- // * https://trac.ffmpeg.org/wiki/Encode/VP9
- // * https://superuser.com/questions/1586934
- // * https://developers.google.com/media/vp9
- param += encodingOptions.EncoderPreset switch
- {
- "veryslow" => " -deadline best -cpu-used 0",
- "slower" => " -deadline best -cpu-used 2",
- "slow" => " -deadline best -cpu-used 3",
- "medium" => " -deadline good -cpu-used 0",
- "fast" => " -deadline good -cpu-used 1",
- "faster" => " -deadline good -cpu-used 2",
- "veryfast" => " -deadline good -cpu-used 3",
- "superfast" => " -deadline good -cpu-used 4",
- "ultrafast" => " -deadline good -cpu-used 5",
- _ => " -deadline good -cpu-used 1"
- };
-
- // TODO: until VP9 gets its own CRF setting, base CRF on H.265.
- int h265Crf = encodingOptions.H265Crf;
- int defaultVp9Crf = 31;
- if (h265Crf >= 0 && h265Crf <= 51)
- {
- // This conversion factor is chosen to match the default CRF for H.265 to the
- // recommended 1080p CRF from Google. The factor also maps the logarithmic CRF
- // scale of x265 [0, 51] to that of VP9 [0, 63] relatively well.
-
- // Resources:
- // * https://developers.google.com/media/vp9/settings/vod
- const float H265ToVp9CrfConversionFactor = 1.12F;
-
- var vp9Crf = Convert.ToInt32(h265Crf * H265ToVp9CrfConversionFactor);
-
- // Encoder allows for CRF values in the range [0, 63].
- vp9Crf = Math.Clamp(vp9Crf, 0, 63);
-
- param += FormattableString.Invariant($" -crf {vp9Crf}");
- }
- else
- {
- param += FormattableString.Invariant($" -crf {defaultVp9Crf}");
- }
-
- param += " -row-mt 1 -profile 1";
- }
- else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
- {
- param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
- }
- else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
- {
- param += " -qmin 2";
- }
- else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- param += " -mbd 2";
- }
param += GetVideoBitrateParam(state, videoEncoder);
@@ -3151,7 +3023,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Format(
CultureInfo.InvariantCulture,
- @"scale=-1:{1}:fast_bilinear,scale,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
+ @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
outWidth.Value,
outHeight.Value);
}
@@ -3950,7 +3822,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw
if (doOclTonemap)
{
- mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=16");
+ mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=24");
mainFilters.Add("format=d3d11");
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
}
@@ -5423,8 +5295,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// INPUT videotoolbox/memory surface(vram/uma)
// this will pass-through automatically if in/out format matches.
- mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
mainFilters.Insert(0, "hwupload");
+ mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
}
return (mainFilters, subFilters, overlayFilters);
@@ -6457,12 +6329,6 @@ namespace MediaBrowser.Controller.MediaEncoding
if (is8bitSwFormatsVt)
{
- if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
- || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
- }
-
if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return GetHwaccelType(state, options, "vp8", bitDepth, useHwSurface);
@@ -6471,6 +6337,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (is8_10bitSwFormatsVt)
{
+ if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
+ }
+
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
@@ -6592,24 +6464,15 @@ namespace MediaBrowser.Controller.MediaEncoding
#nullable enable
public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{
- // VP8 and VP9 encoders must have their thread counts set.
- bool mustSetThreadCount = string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)
- || string.Equals(outputVideoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase);
-
var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
if (threads <= 0)
{
// Automatically set thread count
- return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0;
- }
-
- if (threads >= Environment.ProcessorCount)
- {
- return Environment.ProcessorCount;
+ return 0;
}
- return threads;
+ return Math.Min(threads, Environment.ProcessorCount);
}
#nullable disable
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
new file mode 100644
index 000000000..4fcf084e1
--- /dev/null
+++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.MediaSegments;
+
+namespace MediaBrowser.Controller;
+
+/// <summary>
+/// Defines methods for interacting with media segments.
+/// </summary>
+public interface IMediaSegmentManager
+{
+ /// <summary>
+ /// Returns if this item supports media segments.
+ /// </summary>
+ /// <param name="baseItem">The base Item to check.</param>
+ /// <returns>True if supported otherwise false.</returns>
+ bool IsTypeSupported(BaseItem baseItem);
+
+ /// <summary>
+ /// Creates a new Media Segment associated with an Item.
+ /// </summary>
+ /// <param name="mediaSegment">The segment to create.</param>
+ /// <param name="segmentProviderId">The id of the Provider who created this segment.</param>
+ /// <returns>The created Segment entity.</returns>
+ Task<MediaSegmentDto> CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId);
+
+ /// <summary>
+ /// Deletes a single media segment.
+ /// </summary>
+ /// <param name="segmentId">The <see cref="MediaSegment.Id"/> to delete.</param>
+ /// <returns>a task.</returns>
+ Task DeleteSegmentAsync(Guid segmentId);
+
+ /// <summary>
+ /// Obtains all segments accociated with the itemId.
+ /// </summary>
+ /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
+ /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
+
+ /// <summary>
+ /// Gets information about any media segments stored for the given itemId.
+ /// </summary>
+ /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
+ /// <returns>True if there are any segments stored for the item, otherwise false.</returns>
+ /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson.
+ bool HasSegments(Guid itemId);
+}
diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs
index 1edceb0e4..3af5ec2a3 100644
--- a/MediaBrowser.Controller/Providers/SeasonInfo.cs
+++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs
@@ -1,4 +1,5 @@
#pragma warning disable CA2227, CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -13,5 +14,7 @@ namespace MediaBrowser.Controller.Providers
}
public Dictionary<string, string> SeriesProviderIds { get; set; }
+
+ public string SeriesDisplayOrder { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 914990558..7e307286a 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -89,7 +89,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
+ var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
+ && (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
if (shouldExtractOneByOne)
{
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index eb5d88de6..6c43be3ab 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -62,10 +62,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libx264",
"libx265",
"libsvtav1",
- "mpeg4",
- "msmpeg4",
- "libvpx",
- "libvpx-vp9",
"aac",
"aac_at",
"libfdk_aac",
@@ -178,6 +174,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly string _encoderPath;
+ private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
+
public EncoderValidator(ILogger logger, string encoderPath)
{
_logger = logger;
@@ -487,7 +485,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- public bool CheckSupportedRuntimeKey(string keyDesc)
+ public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
{
if (string.IsNullOrEmpty(keyDesc))
{
@@ -497,7 +495,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
+ // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
+ var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
+ output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
}
catch (Exception ex)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 5cfead502..e8461e77f 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -194,7 +194,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
- _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
+ _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
_isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
// Check the Vaapi device vendor
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 67a2dddb8..42f355b05 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
o.PoolInitialFill = 1;
});
+ private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
+
/// <summary>
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
/// </summary>
@@ -555,7 +557,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
{
- if (EnableThrottling(state))
+ if (EnableThrottling(state)
+ && (_mediaEncoder.IsPkeyPauseSupported
+ || _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
{
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
transcodingJob.TranscodingThrottler.Start();
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index c956bee47..b0f5c2a11 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -13,6 +13,8 @@ namespace MediaBrowser.Model.Configuration
DisabledSubtitleFetchers = Array.Empty<string>();
SubtitleFetcherOrder = Array.Empty<string>();
DisabledLocalMetadataReaders = Array.Empty<string>();
+ DisabledLyricFetchers = Array.Empty<string>();
+ LyricFetcherOrder = Array.Empty<string>();
SkipSubtitlesIfAudioTrackMatches = true;
RequirePerfectSubtitleMatch = true;
@@ -97,6 +99,10 @@ namespace MediaBrowser.Model.Configuration
[DefaultValue(false)]
public bool SaveLyricsWithMedia { get; set; }
+ public string[] DisabledLyricFetchers { get; set; }
+
+ public string[] LyricFetcherOrder { get; set; }
+
public bool AutomaticallyAddToCollection { get; set; }
public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index d2715e2ac..1101c76ea 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -965,8 +965,10 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(videoStream?.Codec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
- var isFirstAppliedCodecProfile = true;
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
+ // Reverse codec profiles for backward compatibility - first codec profile has higher priority
+ .Reverse();
+
foreach (var i in appliedVideoConditions)
{
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
@@ -974,8 +976,7 @@ namespace MediaBrowser.Model.Dlna
{
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
{
- ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
- isFirstAppliedCodecProfile = false;
+ ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
continue;
}
}
@@ -997,8 +998,10 @@ namespace MediaBrowser.Model.Dlna
var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
i.ContainsAnyCodec(audioStream?.Codec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
- isFirstAppliedCodecProfile = true;
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
+ // Reverse codec profiles for backward compatibility - first codec profile has higher priority
+ .Reverse();
+
foreach (var codecProfile in appliedAudioConditions)
{
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
@@ -1006,8 +1009,7 @@ namespace MediaBrowser.Model.Dlna
{
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
{
- ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
- isFirstAppliedCodecProfile = false;
+ ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
break;
}
}
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index b7236b1e8..1c6037325 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -117,6 +117,8 @@ namespace MediaBrowser.Model.Dto
public int? DefaultSubtitleStreamIndex { get; set; }
+ public bool HasSegments { get; set; }
+
[JsonIgnore]
public MediaStream VideoStream
{
diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
new file mode 100644
index 000000000..a0433fee1
--- /dev/null
+++ b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
@@ -0,0 +1,35 @@
+using System;
+using Jellyfin.Data.Enums;
+
+namespace MediaBrowser.Model.MediaSegments;
+
+/// <summary>
+/// Api model for MediaSegment's.
+/// </summary>
+public class MediaSegmentDto
+{
+ /// <summary>
+ /// Gets or sets the id of the media segment.
+ /// </summary>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of content this segment defines.
+ /// </summary>
+ public MediaSegmentType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start of the segment.
+ /// </summary>
+ public long StartTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end of the segment.
+ /// </summary>
+ public long EndTicks { get; set; }
+}
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
index f4d83c28b..2ab93ea05 100644
--- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -6,12 +6,12 @@ namespace MediaBrowser.Model.Plugins
public class PluginPageInfo
{
/// <summary>
- /// Gets or sets the name.
+ /// Gets or sets the name of the plugin.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
- /// Gets or sets the display name.
+ /// Gets or sets the display name of the plugin.
/// </summary>
public string? DisplayName { get; set; }
diff --git a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
new file mode 100644
index 000000000..1a4136504
--- /dev/null
+++ b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Lyrics;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Lyric;
+
+/// <summary>
+/// Task to download lyrics.
+/// </summary>
+public class LyricScheduledTask : IScheduledTask
+{
+ private const int QueryPageLimit = 100;
+
+ private static readonly BaseItemKind[] _itemKinds = [BaseItemKind.Audio];
+ private static readonly MediaType[] _mediaTypes = [MediaType.Audio];
+ private static readonly SourceType[] _sourceTypes = [SourceType.Library];
+ private static readonly DtoOptions _dtoOptions = new(false);
+
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILyricManager _lyricManager;
+ private readonly ILogger<LyricScheduledTask> _logger;
+ private readonly ILocalizationManager _localizationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LyricScheduledTask"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{DownloaderScheduledTask}"/> interface.</param>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public LyricScheduledTask(
+ ILibraryManager libraryManager,
+ ILyricManager lyricManager,
+ ILogger<LyricScheduledTask> logger,
+ ILocalizationManager localizationManager)
+ {
+ _libraryManager = libraryManager;
+ _lyricManager = lyricManager;
+ _logger = logger;
+ _localizationManager = localizationManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localizationManager.GetLocalizedString("TaskDownloadMissingLyrics");
+
+ /// <inheritdoc />
+ public string Key => "DownloadLyrics";
+
+ /// <inheritdoc />
+ public string Description => _localizationManager.GetLocalizedString("TaskDownloadMissingLyricsDescription");
+
+ /// <inheritdoc />
+ public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var totalCount = _libraryManager.GetCount(new InternalItemsQuery
+ {
+ Recursive = true,
+ IsVirtualItem = false,
+ IncludeItemTypes = _itemKinds,
+ DtoOptions = _dtoOptions,
+ MediaTypes = _mediaTypes,
+ SourceTypes = _sourceTypes
+ });
+
+ var completed = 0;
+
+ foreach (var library in _libraryManager.RootFolder.Children.ToList())
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(library);
+ var itemQuery = new InternalItemsQuery
+ {
+ Recursive = true,
+ IsVirtualItem = false,
+ IncludeItemTypes = _itemKinds,
+ DtoOptions = _dtoOptions,
+ MediaTypes = _mediaTypes,
+ SourceTypes = _sourceTypes,
+ Limit = QueryPageLimit,
+ Parent = library
+ };
+
+ int previousCount;
+ var startIndex = 0;
+ do
+ {
+ itemQuery.StartIndex = startIndex;
+ var audioItems = _libraryManager.GetItemList(itemQuery);
+
+ foreach (var audioItem in audioItems.OfType<Audio>())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ if (audioItem.GetMediaStreams().All(s => s.Type != MediaStreamType.Lyric))
+ {
+ _logger.LogDebug("Searching for lyrics for {Path}", audioItem.Path);
+ var lyricResults = await _lyricManager.SearchLyricsAsync(
+ new LyricSearchRequest
+ {
+ MediaPath = audioItem.Path,
+ SongName = audioItem.Name,
+ AlbumName = audioItem.Album,
+ ArtistNames = audioItem.GetAllArtists().ToList(),
+ Duration = audioItem.RunTimeTicks,
+ IsAutomated = true,
+ DisabledLyricFetchers = libraryOptions.DisabledLyricFetchers,
+ LyricFetcherOrder = libraryOptions.LyricFetcherOrder
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ if (lyricResults.Count != 0)
+ {
+ _logger.LogDebug("Saving lyrics for {Path}", audioItem.Path);
+ await _lyricManager.DownloadLyricsAsync(
+ audioItem,
+ libraryOptions,
+ lyricResults[0].Id,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error downloading lyrics for {Path}", audioItem.Path);
+ }
+
+ completed++;
+ progress.Report(100d * completed / totalCount);
+ }
+
+ startIndex += QueryPageLimit;
+ previousCount = audioItems.Count;
+ } while (previousCount > 0);
+ }
+
+ progress.Report(100);
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return
+ [
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerInterval,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ }
+ ];
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
index 78150153a..28f8c0c61 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <summary>
/// Artwork repository URL.
/// </summary>
- public const string DefaultServer = "https://raw.github.com/jellyfin/emby-artwork/master/studios";
+ public const string DefaultServer = "https://raw.githubusercontent.com/jellyfin/emby-artwork/master/studios";
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index a8461e991..5ca9f6f9a 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -53,10 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new ImageType[]
- {
- ImageType.Thumb
- };
+ return [ImageType.Thumb];
}
/// <inheritdoc />
@@ -72,13 +69,10 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
if (imageInfo is null)
{
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
- return new RemoteImageInfo[]
- {
- imageInfo
- };
+ return [imageInfo];
}
private RemoteImageInfo GetImage(BaseItem item, string filename, ImageType type, string remoteFilename)
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index b03d6ffb5..80c56351c 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -61,8 +61,8 @@ namespace MediaBrowser.Providers.TV
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
RemoveObsoleteEpisodes(item);
- await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
RemoveObsoleteSeasons(item);
+ await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -211,8 +211,12 @@ namespace MediaBrowser.Providers.TV
}
else if (existingSeason.IsVirtualItem)
{
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
+ if (episodeCount > 0)
+ {
+ existingSeason.IsVirtualItem = false;
+ await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
}
}
}
diff --git a/README.md b/README.md
index e5e16c716..7da0cb30d 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ Instructions to run this project from the command line are included here, but yo
### Cloning the Repository
-After dependencies are installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS.
+After dependencies have been installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS.
```bash
git clone https://github.com/jellyfin/jellyfin.git
@@ -116,7 +116,7 @@ Second, you need to [install the recommended extensions for the workspace](https
After the required extensions are installed, you can run the server by pressing `F5`.
-#### Running From The Command Line
+#### Running From the Command Line
To run the server from the command line you can use the `dotnet run` command. The example below shows how to do this if you have cloned the repository into a directory named `jellyfin` (the default directory name) and should work on all operating systems.
@@ -143,9 +143,9 @@ If the Server is configured to host the Web Client, and the Server is running, t
API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index.html`
-### Running from GH-Codespaces
+### Running from GitHub Codespaces
-As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
+As Jellyfin will run on a container on a GitHub hosted server, JF needs to handle some things differently.
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 seconds to load all extensions and prepare the environment while VS Code is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
@@ -182,12 +182,12 @@ The following sections describe some more advanced scenarios for running the ser
It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
-To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line
+To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can be specified using the command line
switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
-**NOTE:** The setup wizard can not be run if the web client is hosted separately.
+**NOTE:** The setup wizard cannot be run if the web client is hosted separately.
---
<p align="center">
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 213328a39..5d4732234 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -102,7 +102,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
"astc",
"ktx",
"pkm",
- "wbmp"
+ "wbmp",
+ "avif"
};
/// <inheritdoc />
diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
index cecc363f0..7dc30f727 100644
--- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
@@ -167,7 +167,7 @@ namespace Jellyfin.LiveTv.Listings
Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year,
SeasonNumber = program.Episode.Series,
- IsSeries = program.Episode.Series is not null,
+ IsSeries = program.Episode.Episode is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null,
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index e0a7fa3aa..988073074 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV701Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
@@ -33,6 +34,7 @@ namespace Jellyfin.MediaEncoding.Tests
{
public GetFFmpegVersionTestData()
{
+ Add(EncoderValidatorTestsData.FFmpegV701Output, new Version(7, 0, 1));
Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 30df94950..1f2d618aa 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV701Output = @"ffmpeg version 7.0.1-Jellyfin Copyright (c) 2000-2024 the FFmpeg developers
+built with clang version 18.1.8
+configuration: --cc=clang --pkg-config-flags=--static --extra-cflags=-I/clang64/ffbuild/include --extra-ldflags=-L/clang64/ffbuild/lib --prefix=/clang64/ffbuild/jellyfin-ffmpeg --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --enable-lto=thin --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libharfbuzz --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libopenmpt --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libvpl --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
+libavutil 59. 8.100 / 59. 8.100
+libavcodec 61. 3.100 / 61. 3.100
+libavformat 61. 1.100 / 61. 1.100
+libavdevice 61. 1.100 / 61. 1.100
+libavfilter 10. 1.100 / 10. 1.100
+libswscale 8. 1.100 / 8. 1.100
+libswresample 5. 1.100 / 5. 1.100
+libpostproc 58. 1.100 / 58. 1.100";
+
public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317