aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md5
-rw-r--r--.github/workflows/commands.yml2
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Dockerfile12
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Dlna/DlnaManager.cs2
-rw-r--r--Emby.Drawing/ImageProcessor.cs2
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs1
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs4
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs5
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs62
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json6
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs3
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs6
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs44
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs8
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs1
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs1
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs1
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs2
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs18
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs3
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs4
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs3
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs4
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs2
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj4
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs14
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs4
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs94
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs14
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs28
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs25
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs24
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs21
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs56
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs57
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs5
-rw-r--r--Jellyfin.Server/CoreAppHost.cs4
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj2
-rw-r--r--Jellyfin.Server/Program.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs6
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs8
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs1
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs162
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs1
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationManager.cs1
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs10
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupMember.cs23
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs11
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs6
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs18
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs2
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs8
-rw-r--r--MediaBrowser.Model/Configuration/MetadataOptions.cs2
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs1
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs1
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs2
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs1
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs16
-rw-r--r--MediaBrowser.Model/IO/AsyncFile.cs34
-rw-r--r--MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs10
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs7
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs31
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj4
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs1
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs1
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs79
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs22
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html52
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs58
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs6
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs1
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs1
-rwxr-xr-xdebian/bin/restart.sh40
-rw-r--r--debian/conf/jellyfin5
-rw-r--r--debian/conf/jellyfin-sudoers6
-rw-r--r--debian/control2
-rw-r--r--debian/jellyfin.service2
-rw-r--r--fedora/jellyfin.spec2
-rw-r--r--fedora/jellyfin.sudoers7
-rwxr-xr-xfedora/restart.sh40
-rw-r--r--jellyfin.ruleset10
-rw-r--r--src/Jellyfin.Extensions/DictionaryExtensions.cs1
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs7
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs7
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs57
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs7
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj2
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs1
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs3
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs1
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj2
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs3
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs1
147 files changed, 1037 insertions, 549 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 5a525267a..c1d49778e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -14,8 +14,9 @@ assignees: ''
- OS: [e.g. Debian, Windows]
- Virtualization: [e.g. Docker, KVM, LXC]
- Clients: [Browser, Android, Fire Stick, etc.]
- - Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- - Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
+ - Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
+ - Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
+ - FFmpeg Version: [e.g. 4.3.2-Jellyfin]
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index e0b91ecee..af4d8beb9 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@1.4
+ uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 1fe255385..cb52cafed 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -46,6 +46,7 @@
- [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername)
- [gnattu](https://github.com/gnattu)
+ - [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
diff --git a/Dockerfile b/Dockerfile
index 3190fec5c..791a6113e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,7 @@
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
+# Requires binfm_misc registration
+# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=5.0
FROM node:lts-alpine as web-builder
@@ -18,10 +22,10 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
# https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=20.3.2
-ARG IGC_VERSION=1.0.5435
-ARG NEO_VERSION=20.46.18421
-ARG LEVEL_ZERO_VERSION=1.0.18421
+ARG GMMLIB_VERSION=21.2.1
+ARG IGC_VERSION=1.0.8517
+ARG NEO_VERSION=21.35.20826
+ARG LEVEL_ZERO_VERSION=1.2.20826
# Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
diff --git a/Dockerfile.arm b/Dockerfile.arm
index dcd006ff8..8d4b548bc 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -1,4 +1,4 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 7311c6b9f..835aa36a1 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -1,4 +1,4 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index af70793cc..68fc80c0a 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -366,7 +366,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(systemProfilesPath);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 7d952aa23..0ad8bca31 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -102,7 +102,7 @@ namespace Emby.Drawing
{
var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+ using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 41d1f9b39..6faa5d363 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -815,7 +815,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- await using FileStream jsonStream = File.OpenRead(cachePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -838,7 +838,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- await using FileStream jsonStream = File.OpenRead(cachePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 30f88c796..88fc5018d 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -16,7 +16,6 @@ using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -25,7 +24,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index fa24e9dd1..e48dbcd19 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -31,8 +31,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
- <PackageReference Include="sharpcompress" Version="0.28.3" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.1" />
+ <PackageReference Include="sharpcompress" Version="0.29.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 0a4efd73c..640754af4 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -9,7 +9,6 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 2c722ff25..1bc229b0c 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -5,11 +5,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
@@ -248,7 +246,7 @@ namespace Emby.Server.Implementations.IO
{
try
{
- using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ using (Stream thisFileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
{
result.Length = thisFileStream.Length;
}
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 806269182..16b45161f 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -17,6 +17,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = File.OpenWrite(cacheFilePath);
+ await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 91c9e61cf..6f83973ba 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -521,7 +521,7 @@ namespace Emby.Server.Implementations.Library
// TODO: @bond Fix
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
- _logger.LogInformation("Live stream opened: " + json);
+ _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
@@ -638,7 +638,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index a1562abd3..4d8a6494c 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -8,7 +8,6 @@ using System.IO;
using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index bb3d635d1..c5a9a92ec 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
{
onStarted();
@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
+ await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO);
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index e10bc7647..d806a0295 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index ebad4eddf..8202fab86 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
+ await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO))
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 9bff0861b..096b7f045 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
- await using var writeStream = File.OpenWrite(channelCacheFile);
+ await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
@@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- await using var readStream = File.OpenRead(channelCacheFile);
+ await using var readStream = AsyncFile.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 96a678c1d..2c21a4a89 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -155,15 +155,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
cancellationToken = linkedCancellationTokenSource.Token;
- // use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
- var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
-
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
var nextFileInfo = GetNextFile(null);
var nextFile = nextFileInfo.file;
var isLastFile = nextFileInfo.isLastFile;
+ var allowAsync = AsyncFile.UseAsyncIO;
while (!string.IsNullOrEmpty(nextFile))
{
var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index d28c39e21..23071a430 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -14,6 +14,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
@@ -50,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- return File.OpenRead(info.Url);
+ return AsyncFile.OpenRead(info.Url);
}
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index f572151b8..862993877 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -129,37 +129,39 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
- return Task.Run(async () =>
- {
- try
- {
- Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
- using var message = response;
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
- await StreamHelper.CopyToAsync(
- stream,
- fileStream,
- IODefaults.CopyToBufferSize,
- () => Resolve(openTaskCompletionSource),
- cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException ex)
- {
- Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
- openTaskCompletionSource.TrySetException(ex);
- }
- catch (Exception ex)
+ return Task.Run(
+ async () =>
{
- Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
- openTaskCompletionSource.TrySetException(ex);
- }
-
- openTaskCompletionSource.TrySetResult(false);
-
- EnableStreamSharing = false;
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
- }, CancellationToken.None);
+ try
+ {
+ Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
+ using var message = response;
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ IODefaults.CopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+
+ openTaskCompletionSource.TrySetResult(false);
+
+ EnableStreamSharing = false;
+ await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
+ },
+ CancellationToken.None);
}
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 4f1d231a4..1ea378321 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Televize",
"HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek",
- "HomeVideos": "Domáci videa",
+ "HomeVideos": "Domácí videa",
"Inherit": "Zdědit",
"ItemAddedWithName": "{0} byl přidán do knihovny",
"ItemRemovedWithName": "{0} byl odstraněn z knihovny",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3c51d64e0..2a56d0745 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Indéfini",
- "Forced": "Forcé"
+ "Forced": "Forcé",
+ "TaskOptimizeDatabaseDescription": "Compacte la base de données et tronque l'espace libre. Lancer cette tâche après avoir scanné la bibliothèque ou faire d'autres changements impliquant des modifications de la base peuvent ameliorer les performances.",
+ "TaskOptimizeDatabase": "Optimiser la base de données"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 3d69e418b..33aa0eea0 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -10,11 +10,11 @@
"Photos": "Ảnh",
"Playlists": "Danh sách phát",
"Shows": "Chương Trình TV",
- "Songs": "Các Bài Hát",
+ "Songs": "Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
"Albums": "Tuyển Tập",
- "Artists": "Các Nghệ Sĩ",
+ "Artists": "Ca Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
@@ -32,7 +32,7 @@
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
- "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+ "TaskCleanCache": "Làm Sạch Thư Mục Bộ Nhớ Đệm",
"TasksChannelsCategory": "Kênh Internet",
"TasksApplicationCategory": "Ứng Dụng",
"TasksLibraryCategory": "Thư Viện",
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index fc0920edf..b8e1dc2c0 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -15,6 +15,7 @@ using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
@@ -371,7 +372,7 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
- await using var fileStream = File.OpenWrite(imagePath);
+ await using var fileStream = AsyncFile.OpenWrite(imagePath);
try
{
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 5ff73de81..059211a0b 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="file">The file.</param>
public void SerializeToFile(object obj, string file)
{
- using (var stream = new FileStream(file, FileMode.Create))
+ using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write))
{
SerializeToStream(obj, stream);
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 4111590c8..334ce5c9d 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1199,16 +1199,18 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public async Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
+ var session = GetSession(sessionId);
await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
- public async Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken)
{
CheckDisposed();
+ var session = GetSession(sessionId);
await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index 12efff261..75cf890e5 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -164,26 +164,26 @@ namespace Emby.Server.Implementations.SyncPlay
/// <summary>
/// Filters sessions of this group.
/// </summary>
- /// <param name="from">The current session.</param>
+ /// <param name="fromId">The current session identifier.</param>
/// <param name="type">The filtering type.</param>
/// <returns>The list of sessions matching the filter.</returns>
- private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, SyncPlayBroadcastType type)
+ private IEnumerable<string> FilterSessions(string fromId, SyncPlayBroadcastType type)
{
return type switch
{
- SyncPlayBroadcastType.CurrentSession => new SessionInfo[] { from },
+ SyncPlayBroadcastType.CurrentSession => new string[] { fromId },
SyncPlayBroadcastType.AllGroup => _participants
.Values
- .Select(session => session.Session),
+ .Select(member => member.SessionId),
SyncPlayBroadcastType.AllExceptCurrentSession => _participants
.Values
- .Select(session => session.Session)
- .Where(session => !session.Id.Equals(from.Id, StringComparison.OrdinalIgnoreCase)),
+ .Select(member => member.SessionId)
+ .Where(sessionId => !sessionId.Equals(fromId, StringComparison.OrdinalIgnoreCase)),
SyncPlayBroadcastType.AllReady => _participants
.Values
- .Where(session => !session.IsBuffering)
- .Select(session => session.Session),
- _ => Enumerable.Empty<SessionInfo>()
+ .Where(member => !member.IsBuffering)
+ .Select(member => member.SessionId),
+ _ => Enumerable.Empty<string>()
};
}
@@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.SyncPlay
// Get list of users.
var users = _participants
.Values
- .Select(participant => _userManager.GetUserById(participant.Session.UserId));
+ .Select(participant => _userManager.GetUserById(participant.UserId));
// Find problematic users.
var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue));
@@ -353,7 +353,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <returns>The group info for the clients.</returns>
public GroupInfoDto GetInfo()
{
- var participants = _participants.Values.Select(session => session.Session.UserName).Distinct().ToList();
+ var participants = _participants.Values.Select(session => session.UserName).Distinct().ToList();
return new GroupInfoDto(GroupId, GroupName, _state.Type, participants, DateTime.UtcNow);
}
@@ -389,9 +389,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- foreach (var session in FilterSessions(from, type))
+ foreach (var sessionId in FilterSessions(from.Id, type))
{
- yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayGroupUpdate(sessionId, message, cancellationToken);
}
}
@@ -403,9 +403,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- foreach (var session in FilterSessions(from, type))
+ foreach (var sessionId in FilterSessions(from.Id, type))
{
- yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayCommand(sessionId, message, cancellationToken);
}
}
@@ -537,6 +537,16 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
+ public void ClearPlayQueue(bool clearPlayingItem)
+ {
+ PlayQueue.ClearPlaylist(clearPlayingItem);
+ if (clearPlayingItem)
+ {
+ RestartCurrentItem();
+ }
+ }
+
+ /// <inheritdoc />
public bool RemoveFromPlayQueue(IReadOnlyList<Guid> playlistItemIds)
{
var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds);
@@ -649,8 +659,9 @@ namespace Emby.Server.Implementations.SyncPlay
public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason)
{
var startPositionTicks = PositionTicks;
+ var isPlaying = _state.Type.Equals(GroupStateType.Playing);
- if (_state.Type.Equals(GroupStateType.Playing))
+ if (isPlaying)
{
var currentTime = DateTime.UtcNow;
var elapsedTime = currentTime - LastActivity;
@@ -669,6 +680,7 @@ namespace Emby.Server.Implementations.SyncPlay
PlayQueue.GetPlaylist(),
PlayQueue.PlayingItemIndex,
startPositionTicks,
+ isPlaying,
PlayQueue.ShuffleMode,
PlayQueue.RepeatMode);
}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 993456196..2ebeea717 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -160,7 +160,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId);
var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -172,7 +172,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue.", session.Id, group.GroupId.ToString());
var error = new GroupUpdate<string>(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -249,7 +249,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
}
@@ -329,7 +329,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
}
}
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 720b22b1d..8e0332d3e 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index a6e70e72d..54ac06276 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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>
@@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 35435b007..a54003357 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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>
@@ -316,7 +315,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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>
@@ -482,7 +481,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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>
@@ -813,7 +812,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index e1b808098..99ab7f232 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
}
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return File(AsyncFile.OpenRead(path), contentType);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 9fa307858..448510c06 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -17,7 +14,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index b20eae750..93dc76729 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 010a3b19a..99c90d19e 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -18,7 +18,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index ec836f43e..7a2c23991 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -4,10 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -16,7 +14,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -208,7 +205,7 @@ namespace Jellyfin.Api.Controllers
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+ await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index a811a29c3..97eec4bd2 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 1b3248c0c..c6b70f3d2 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -162,7 +162,7 @@ namespace Jellyfin.Api.Controllers
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
- var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index bbbe5fb8d..e6584f0fe 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
- FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
+ FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
return File(stream, "text/plain; charset=utf-8");
}
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 5c941b276..ef25db8c9 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -20,12 +19,10 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
@@ -140,7 +137,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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>
@@ -267,6 +264,9 @@ namespace Jellyfin.Api.Controllers
// CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
+ // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
+ // since it gets disposed when ffmpeg exits
+ var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
Request,
@@ -281,7 +281,7 @@ namespace Jellyfin.Api.Controllers
_deviceManager,
_transcodingJobHelper,
TranscodingJobType,
- cancellationTokenSource.Token)
+ cancellationToken)
.ConfigureAwait(false);
TranscodingJobDto? job = null;
@@ -290,7 +290,7 @@ namespace Jellyfin.Api.Controllers
if (!System.IO.File.Exists(playlistPath))
{
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!System.IO.File.Exists(playlistPath))
@@ -317,7 +317,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments;
if (minSegments > 0)
{
- await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 29a25fa6a..bc6fc904a 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -25,14 +25,12 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers
{
@@ -310,7 +308,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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>
@@ -456,9 +454,9 @@ namespace Jellyfin.Api.Controllers
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ }.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
@@ -495,9 +493,9 @@ namespace Jellyfin.Api.Controllers
if (state.MediaSource.IsInfiniteStream)
{
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ }.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
return File(Response.Body, contentType);
@@ -570,7 +568,7 @@ namespace Jellyfin.Api.Controllers
/// <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. 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="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/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index 264131905..ddcde1cf6 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -11,12 +11,10 @@ using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Helpers
{
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index dc5d6715b..4abe4c5d5 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -18,11 +18,9 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index d1cdaf867..f36769dc2 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
@@ -39,7 +38,7 @@ namespace Jellyfin.Api.Helpers
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- FileOptions.SequentialScan);
+ (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
using var reader = new StreamReader(fileStream);
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index 963e17724..81970b041 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -1,7 +1,6 @@
using System;
using System.Buffers;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -85,8 +84,7 @@ namespace Jellyfin.Api.Helpers
var fileOptions = FileOptions.SequentialScan;
var allowAsyncFileRead = false;
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (AsyncFile.UseAsyncIO)
{
fileOptions |= FileOptions.Asynchronous;
allowAsyncFileRead = true;
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index 499dbe84d..d4cc0172d 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -1,7 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -40,7 +39,7 @@ namespace Jellyfin.Api.Helpers
_allowAsyncFileRead = false;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (AsyncFile.UseAsyncIO)
{
fileOptions |= FileOptions.Asynchronous;
_allowAsyncFileRead = true;
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 1c2b301cd..0041251e3 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -433,7 +433,9 @@ namespace Jellyfin.Api.Helpers
return ".ogv";
}
- if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
return ".webm";
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 5d3724c11..4e1e98df0 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -557,7 +557,7 @@ namespace Jellyfin.Api.Helpers
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
// FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 669925198..2fca88f24 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -16,8 +16,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.5" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.5" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.1" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
index e9b2b2cb3..02ce5a048 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
@@ -17,9 +17,21 @@ namespace Jellyfin.Api.Models.SyncPlayDtos
}
/// <summary>
- /// Gets or sets the playlist identifiers ot the items.
+ /// Gets or sets the playlist identifiers ot the items. Ignored when clearing the playlist.
/// </summary>
/// <value>The playlist identifiers ot the items.</value>
public IReadOnlyList<Guid> PlaylistItemIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the entire playlist should be cleared.
+ /// </summary>
+ /// <value>Whether the entire playlist should be cleared.</value>
+ public bool ClearPlaylist { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playing item should be removed as well. Used only when clearing the playlist.
+ /// </summary>
+ /// <value>Whether the playing item should be removed as well.</value>
+ public bool ClearPlayingItem { get; set; }
}
}
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 09a370238..d1cc2255d 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -112,7 +112,7 @@ namespace Jellyfin.Drawing.Skia
canvas.DrawImage(residedBackdrop, 0, 0);
// draw shadow rectangle
- var paintColor = new SKPaint
+ using var paintColor = new SKPaint
{
Color = SKColors.Black.WithAlpha(0x78),
Style = SKPaintStyle.Fill
@@ -130,7 +130,7 @@ namespace Jellyfin.Drawing.Skia
}
// draw library name
- var textPaint = new SKPaint
+ using var textPaint = new SKPaint
{
Color = SKColors.White,
Style = SKPaintStyle.Fill,
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 6f35a2c1c..dc4f53913 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -153,100 +153,10 @@ namespace Jellyfin.Server.Implementations
{
modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
base.OnModelCreating(modelBuilder);
-
modelBuilder.HasDefaultSchema("jellyfin");
- // Collations
-
- modelBuilder.Entity<User>()
- .Property(user => user.Username)
- .UseCollation("NOCASE");
-
- // Delete behavior
-
- modelBuilder.Entity<User>()
- .HasOne(u => u.ProfileImage)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.Permissions)
- .WithOne()
- .HasForeignKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.Preferences)
- .WithOne()
- .HasForeignKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.AccessSchedules)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.DisplayPreferences)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.ItemDisplayPreferences)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<DisplayPreferences>()
- .HasMany(d => d.HomeSections)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- // Indexes
-
- modelBuilder.Entity<ApiKey>()
- .HasIndex(entity => entity.AccessToken)
- .IsUnique();
-
- modelBuilder.Entity<User>()
- .HasIndex(entity => entity.Username)
- .IsUnique();
-
- modelBuilder.Entity<Device>()
- .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
-
- modelBuilder.Entity<Device>()
- .HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
-
- modelBuilder.Entity<Device>()
- .HasIndex(entity => new { entity.UserId, entity.DeviceId });
-
- modelBuilder.Entity<Device>()
- .HasIndex(entity => entity.DeviceId);
-
- modelBuilder.Entity<DeviceOptions>()
- .HasIndex(entity => entity.DeviceId)
- .IsUnique();
-
- modelBuilder.Entity<DisplayPreferences>()
- .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
- .IsUnique();
-
- modelBuilder.Entity<CustomItemDisplayPreferences>()
- .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
- .IsUnique();
-
- // Used to get a user's permissions or a specific permission for a user.
- // Also prevents multiple values being created for a user.
- // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
- modelBuilder.Entity<Permission>()
- .HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
- .IsUnique();
-
- modelBuilder.Entity<Preference>()
- .HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
- .IsUnique();
+ // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly);
}
}
}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
index 486be6053..c2c5198d1 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -1,8 +1,10 @@
using System;
using System.IO;
+using System.Linq;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations
{
@@ -13,19 +15,27 @@ namespace Jellyfin.Server.Implementations
{
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationPaths _appPaths;
+ private readonly ILogger<JellyfinDbProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <param name="serviceProvider">The application's service provider.</param>
/// <param name="appPaths">The application paths.</param>
- public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
+ /// <param name="logger">The logger.</param>
+ public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> logger)
{
_serviceProvider = serviceProvider;
_appPaths = appPaths;
+ _logger = logger;
using var jellyfinDb = CreateContext();
- jellyfinDb.Database.Migrate();
+ if (jellyfinDb.Database.GetPendingMigrations().Any())
+ {
+ _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
+ jellyfinDb.Database.Migrate();
+ _logger.LogInformation("EFCore migrations applied successfully");
+ }
}
/// <summary>
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
new file mode 100644
index 000000000..3f19b6986
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the ApiKey entity.
+ /// </summary>
+ public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ApiKey> builder)
+ {
+ builder
+ .HasIndex(entity => entity.AccessToken)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..779aec986
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the CustomItemDisplayPreferences entity.
+ /// </summary>
+ public class CustomItemDisplayPreferencesConfiguration : IEntityTypeConfiguration<CustomItemDisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<CustomItemDisplayPreferences> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs
new file mode 100644
index 000000000..a750b65c0
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs
@@ -0,0 +1,28 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Device entity.
+ /// </summary>
+ public class DeviceConfiguration : IEntityTypeConfiguration<Device>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Device> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.DeviceId });
+
+ builder
+ .HasIndex(entity => entity.DeviceId);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
new file mode 100644
index 000000000..038afd752
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DeviceOptions entity.
+ /// </summary>
+ public class DeviceOptionsConfiguration : IEntityTypeConfiguration<DeviceOptions>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DeviceOptions> builder)
+ {
+ builder
+ .HasIndex(entity => entity.DeviceId)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..9b437861b
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
@@ -0,0 +1,25 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DisplayPreferencesConfiguration entity.
+ /// </summary>
+ public class DisplayPreferencesConfiguration : IEntityTypeConfiguration<DisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DisplayPreferences> builder)
+ {
+ builder
+ .HasMany(d => d.HomeSections)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs
new file mode 100644
index 000000000..240e284c0
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs
@@ -0,0 +1,24 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PermissionConfiguration : IEntityTypeConfiguration<Permission>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Permission> builder)
+ {
+ // Used to get a user's permissions or a specific permission for a user.
+ // Also prevents multiple values being created for a user.
+ // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs
new file mode 100644
index 000000000..49c869c6a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs
@@ -0,0 +1,21 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PreferenceConfiguration : IEntityTypeConfiguration<Preference>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Preference> builder)
+ {
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs
new file mode 100644
index 000000000..a369cf656
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs
@@ -0,0 +1,56 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the User entity.
+ /// </summary>
+ public class UserConfiguration : IEntityTypeConfiguration<User>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<User> builder)
+ {
+ builder
+ .Property(user => user.Username)
+ .UseCollation("NOCASE");
+
+ builder
+ .HasOne(u => u.ProfileImage)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Permissions)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Preferences)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.AccessSchedules)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.DisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.ItemDisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => entity.Username)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 659932aca..244abf469 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -253,29 +252,57 @@ namespace Jellyfin.Server.Implementations.Security
return null;
}
+ // Remove up until the first space
authorizationHeader = authorizationHeader[(firstSpace + 1)..];
+ return GetParts(authorizationHeader);
+ }
- var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ /// <summary>
+ /// Get the authorization header components.
+ /// </summary>
+ /// <param name="authorizationHeader">The authorization header.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ public static Dictionary<string, string> GetParts(ReadOnlySpan<char> authorizationHeader)
+ {
+ var result = new Dictionary<string, string>();
+ var escaped = false;
+ int start = 0;
+ string key = string.Empty;
- foreach (var item in authorizationHeader.Split(','))
+ int i;
+ for (i = 0; i < authorizationHeader.Length; i++)
{
- var trimmedItem = item.Trim();
- var firstEqualsSign = trimmedItem.IndexOf('=');
-
- if (firstEqualsSign > 0)
+ var token = authorizationHeader[i];
+ if (token == '"' || token == ',')
{
- var key = trimmedItem[..firstEqualsSign].ToString();
- var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
- result[key] = value;
+ // Applying a XOR logic to evaluate whether it is opening or closing a value
+ escaped = (!escaped) == (token == '"');
+ if (token == ',' && !escaped)
+ {
+ // Meeting a comma after a closing escape char means the value is complete
+ if (start < i)
+ {
+ result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
+ key = string.Empty;
+ }
+
+ start = i + 1;
+ }
+ }
+ else if (!escaped && token == '=')
+ {
+ key = authorizationHeader[start.. i].Trim().ToString();
+ start = i + 1;
}
}
- return result;
- }
+ // Add last value
+ if (start < i)
+ {
+ result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
+ }
- private static string NormalizeValue(string value)
- {
- return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
+ return result;
}
}
}
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index c99c5e4ef..6e98ad863 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@@ -53,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Users
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
- await using (var str = File.OpenRead(resetFile))
+ await using (var str = AsyncFile.OpenRead(resetFile))
{
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
@@ -110,7 +111,7 @@ namespace Jellyfin.Server.Implementations.Users
UserName = user.Username
};
- await using (FileStream fileStream = File.OpenWrite(filePath))
+ await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
await fileStream.FlushAsync().ConfigureAwait(false);
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index d41b5f74e..21bd9ba01 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -78,7 +78,9 @@ namespace Jellyfin.Server
}
ServiceCollection.AddDbContextPool<JellyfinDb>(
- options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
+ options => options
+ .UseLoggerFactory(LoggerFactory)
+ .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
ServiceCollection.AddEventServices();
ServiceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index a57666cd6..1fdad73b7 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -44,7 +44,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.5" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.6" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 7018d537f..3c0ee069d 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -15,6 +15,7 @@ using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -223,7 +224,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
- using var context = new JellyfinDbProvider(appHost.ServiceProvider, appPaths).CreateContext();
+ using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext();
if (context.Database.IsSqlite())
{
context.Database.ExecuteSqlRaw("PRAGMA optimize");
@@ -546,7 +547,7 @@ namespace Jellyfin.Server
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
// Copy the resource contents to the expected file path for the config file
- await using Stream dst = File.Open(configPath, FileMode.CreateNew);
+ await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 3b182f7c9..f4c91973b 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -19,6 +19,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
@@ -2439,6 +2440,17 @@ namespace MediaBrowser.Controller.Entities
};
}
+ // Music albums usually don't have dedicated backdrops, so return one from the artist instead
+ if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
+ {
+ var artist = FindParent<MusicArtist>();
+
+ if (artist != null)
+ {
+ return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
+ }
+ }
+
return GetImages(imageType)
.ElementAtOrDefault(imageIndex);
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index dd08c31ed..18b4ec3c6 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1015,17 +1015,17 @@ namespace MediaBrowser.Controller.Entities
if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
{
- items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1);
+ items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1);
}
if (!string.IsNullOrEmpty(query.NameStartsWith))
{
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.CurrentCultureIgnoreCase));
+ items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase));
}
if (!string.IsNullOrEmpty(query.NameLessThan))
{
- items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1);
+ items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1);
}
// This must be the last filter
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index f3bf4749d..e547db523 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -24,6 +24,14 @@ namespace MediaBrowser.Controller.Entities
private readonly object _childIdsLock = new object();
private List<Guid> _childrenIds = null;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserRootFolder"/> class.
+ /// </summary>
+ public UserRootFolder()
+ {
+ IsRoot = true;
+ }
+
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index b0abca367..3da0a5875 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -2,7 +2,6 @@
#pragma warning disable CS1591
-using System;
using System.Collections.Generic;
using System.Net;
using MediaBrowser.Common;
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index a49dcacc1..d2ed3465a 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Diacritics.Extensions;
-using MediaBrowser.Controller.Extensions;
namespace MediaBrowser.Controller.Library
{
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 1be3a1ca4..bdb379332 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -37,7 +37,7 @@ namespace MediaBrowser.Controller.MediaEncoding
"ConstrainedHigh"
};
- private static readonly Version minVersionForCudaOverlay = new Version(4, 4);
+ private static readonly Version _minVersionForCudaOverlay = new Version(4, 4);
public EncodingHelper(
IMediaEncoder mediaEncoder,
@@ -206,11 +206,17 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetH264Encoder(state, encodingOptions);
}
- if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
+ 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";
@@ -442,7 +448,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
{
- return "vpx";
+ // TODO: this may not always mean VP8, as the codec ages
+ return "vp8";
}
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
@@ -560,12 +567,12 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isOpenclTonemappingSupported && !isVppTonemappingSupported)
{
- arg.Append("-init_hw_device vaapi=va:")
- .Append(options.VaapiDevice)
- .Append(" -init_hw_device opencl=ocl@va ")
- .Append("-hwaccel_device va ")
- .Append("-hwaccel_output_format vaapi ")
- .Append("-filter_hw_device ocl ");
+ arg.Append("-init_hw_device vaapi=va:")
+ .Append(options.VaapiDevice)
+ .Append(" -init_hw_device opencl=ocl@va ")
+ .Append("-hwaccel_device va ")
+ .Append("-hwaccel_output_format vaapi ")
+ .Append("-filter_hw_device ocl ");
}
else
{
@@ -640,8 +647,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (state.IsVideoRequest
- && ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
- && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder))))
+ && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+ && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder))
{
if (!isCudaTonemappingSupported && isOpenclTonemappingSupported)
{
@@ -774,49 +781,37 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
- var bitrate = state.OutputVideoBitrate;
-
- if (bitrate.HasValue)
+ if (state.OutputVideoBitrate == null)
{
- if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
- {
- // When crf is used with vpx, b:v becomes a max rate
- // https://trac.ffmpeg.org/wiki/Encode/VP9
- return string.Format(
- CultureInfo.InvariantCulture,
- " -maxrate:v {0} -bufsize:v {1} -b:v {0}",
- bitrate.Value,
- bitrate.Value * 2);
- }
+ return string.Empty;
+ }
- if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- " -b:v {0}",
- bitrate.Value);
- }
+ int bitrate = state.OutputVideoBitrate.Value;
- if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
- {
- // h264
- return string.Format(
- CultureInfo.InvariantCulture,
- " -maxrate {0} -bufsize {1}",
- bitrate.Value,
- bitrate.Value * 2);
- }
+ // Currently use the same buffer size for all encoders
+ int bufsize = bitrate * 2;
- // h264
- return string.Format(
- CultureInfo.InvariantCulture,
- " -b:v {0} -maxrate {0} -bufsize {1}",
- bitrate.Value,
- bitrate.Value * 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}");
}
- return string.Empty;
+ if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return FormattableString.Invariant($" -b:v {bitrate}");
+ }
+
+ if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
+ {
+ return FormattableString.Invariant($" -maxrate {bitrate} -bufsize {bufsize}");
+ }
+
+ return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
@@ -1199,7 +1194,7 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -header_insertion_mode gop -gops_per_idr 1";
}
}
- else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
+ else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
{
// Values 0-3, 0 being highest quality but slower
var profileScore = 0;
@@ -1227,6 +1222,55 @@ namespace MediaBrowser.Controller.MediaEncoding
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";
@@ -2055,7 +2099,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
- var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay;
+ var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay;
var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
// Tonemapping and burn-in graphical subtitles requires overlay_vaapi.
@@ -2336,7 +2380,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase);
var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
- var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay;
+ var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay;
var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@@ -2639,7 +2683,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
- var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay;
+ var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay;
var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@@ -3151,20 +3195,16 @@ namespace MediaBrowser.Controller.MediaEncoding
#nullable enable
public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{
- if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
- {
- // per docs:
- // -threads number of threads to use for encoding, can't be 0 [auto] with VP8
- // (recommended value : number of real cores - 1)
- return Math.Max(Environment.ProcessorCount - 1, 1);
- }
+ // 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;
- // Automatic
if (threads <= 0)
{
- return 0;
+ // Automatically set thread count
+ return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0;
}
else if (threads >= Environment.ProcessorCount)
{
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index b23c95112..aa5e2c403 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -6,7 +6,6 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
index 29621b73e..e3d18c8c0 100644
--- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs
+++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
@@ -1,6 +1,5 @@
#nullable enable
-using System;
using System.Collections.Generic;
using System.Threading.Tasks;
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index cc12cb102..c86556095 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -10,8 +10,6 @@ using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@@ -159,21 +157,21 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends a SyncPlayCommand to a session.
/// </summary>
- /// <param name="session">The session.</param>
+ /// <param name="sessionId">The identifier of the session.</param>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken);
+ Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken);
/// <summary>
/// Sends a SyncPlayGroupUpdate to a session.
/// </summary>
- /// <param name="session">The session.</param>
+ /// <param name="sessionId">The identifier of the session.</param>
/// <param name="command">The group update.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <typeparam name="T">Type of group.</typeparam>
/// <returns>Task.</returns>
- Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken);
+ Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken);
/// <summary>
/// Sends the browse command.
diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
index 7e7e759a5..b973672c4 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
@@ -1,5 +1,6 @@
#nullable disable
+using System;
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.SyncPlay
@@ -15,14 +16,28 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public GroupMember(SessionInfo session)
{
- Session = session;
+ SessionId = session.Id;
+ UserId = session.UserId;
+ UserName = session.UserName;
}
/// <summary>
- /// Gets the session.
+ /// Gets the identifier of the session.
/// </summary>
- /// <value>The session.</value>
- public SessionInfo Session { get; }
+ /// <value>The session identifier.</value>
+ public string SessionId { get; }
+
+ /// <summary>
+ /// Gets the identifier of the user.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; }
+
+ /// <summary>
+ /// Gets the username.
+ /// </summary>
+ /// <value>The username.</value>
+ public string UserName { get; }
/// <summary>
/// Gets or sets the ping, in milliseconds.
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
index 91a13fb28..51c95a1bb 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
@@ -68,7 +68,16 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
/// <inheritdoc />
public virtual void HandleRequest(RemoveFromPlaylistGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
{
- var playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds);
+ bool playingItemRemoved;
+ if (request.ClearPlaylist)
+ {
+ context.ClearPlayQueue(request.ClearPlayingItem);
+ playingItemRemoved = request.ClearPlayingItem;
+ }
+ else
+ {
+ playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds);
+ }
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
index de26c7d9e..d2de22450 100644
--- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
+++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
@@ -163,6 +163,12 @@ namespace MediaBrowser.Controller.SyncPlay
bool SetPlayingItem(Guid playlistItemId);
/// <summary>
+ /// Clears the play queue.
+ /// </summary>
+ /// <param name="clearPlayingItem">Whether to remove the playing item as well.</param>
+ void ClearPlayQueue(bool clearPlayingItem);
+
+ /// <summary>
/// Removes items from the play queue.
/// </summary>
/// <param name="playlistItemIds">The items to remove.</param>
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
index 689145293..2f38d6adc 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
@@ -17,9 +17,13 @@ namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
/// Initializes a new instance of the <see cref="RemoveFromPlaylistGroupRequest"/> class.
/// </summary>
/// <param name="items">The playlist ids of the items to remove.</param>
- public RemoveFromPlaylistGroupRequest(IReadOnlyList<Guid> items)
+ /// <param name="clearPlaylist">Whether to clear the entire playlist. The items list will be ignored.</param>
+ /// <param name="clearPlayingItem">Whether to remove the playing item as well. Used only when clearing the playlist.</param>
+ public RemoveFromPlaylistGroupRequest(IReadOnlyList<Guid> items, bool clearPlaylist = false, bool clearPlayingItem = false)
{
PlaylistItemIds = items ?? Array.Empty<Guid>();
+ ClearPlaylist = clearPlaylist;
+ ClearPlayingItem = clearPlayingItem;
}
/// <summary>
@@ -28,6 +32,18 @@ namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
/// <value>The playlist identifiers ot the items.</value>
public IReadOnlyList<Guid> PlaylistItemIds { get; }
+ /// <summary>
+ /// Gets a value indicating whether the entire playlist should be cleared.
+ /// </summary>
+ /// <value>Whether the entire playlist should be cleared.</value>
+ public bool ClearPlaylist { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the playing item should be removed as well.
+ /// </summary>
+ /// <value>Whether the playing item should be removed as well.</value>
+ public bool ClearPlayingItem { get; }
+
/// <inheritdoc />
public override PlaybackRequestType Action { get; } = PlaybackRequestType.RemoveFromPlaylist;
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index a0ec3bd90..a524aeaa9 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -89,7 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
CancellationToken cancellationToken)
{
var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
- return File.OpenRead(attachmentPath);
+ return AsyncFile.OpenRead(attachmentPath);
}
private async Task<string> GetReadableFile(
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index 41143c259..d55688e3d 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public bool IsDir => _impl.IsDirectory;
- public System.IO.Stream OpenRead()
+ public Stream OpenRead()
{
return new FileStream(
FullName,
@@ -33,9 +33,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo
FileShare.Read);
}
- public System.IO.StreamReader OpenText()
+ public StreamReader OpenText()
{
- return new System.IO.StreamReader(OpenRead());
+ return new StreamReader(OpenRead());
}
}
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 6da9886a4..5deaecc95 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -23,7 +23,7 @@
<ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.1" />
- <PackageReference Include="libse" Version="3.6.0" />
+ <PackageReference Include="libse" Version="3.6.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.4.0" />
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 04f0be307..2516aad1c 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1552,7 +1552,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var packetBuffer = new byte[197];
- using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
{
fs.Read(packetBuffer);
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 24ceb1b57..3d864e29c 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -5,7 +5,7 @@ using System.Threading;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core;
+using Nikse.SubtitleEdit.Core.Common;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 608ebf443..6f6178af2 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -192,7 +192,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- return File.OpenRead(fileInfo.Path);
+ return AsyncFile.OpenRead(fileInfo.Path);
}
private async Task<SubtitleInfo> GetReadableFile(
@@ -671,7 +671,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string text;
Encoding encoding;
- using (var fileStream = File.OpenRead(file))
+ using (var fileStream = AsyncFile.OpenRead(file))
using (var reader = new StreamReader(fileStream, true))
{
encoding = reader.CurrentEncoding;
@@ -684,7 +684,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal))
{
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
using (var writer = new StreamWriter(fileStream, encoding))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
@@ -750,7 +750,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
case MediaProtocol.File:
- return File.OpenRead(path);
+ return AsyncFile.OpenRead(path);
default:
throw new ArgumentOutOfRangeException(nameof(protocol));
}
diff --git a/MediaBrowser.Model/Configuration/MetadataOptions.cs b/MediaBrowser.Model/Configuration/MetadataOptions.cs
index 76b72bd08..384a7997f 100644
--- a/MediaBrowser.Model/Configuration/MetadataOptions.cs
+++ b/MediaBrowser.Model/Configuration/MetadataOptions.cs
@@ -1,5 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, CA1819
using System;
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index fa3ad098f..03c3a7265 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
index 7ce248509..93a9ae615 100644
--- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index 806877ff0..94071b419 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -5,7 +5,7 @@ using System;
namespace MediaBrowser.Model.Dlna
{
- public class ResolutionNormalizer
+ public static class ResolutionNormalizer
{
private static readonly ResolutionConfiguration[] Configurations =
new[]
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 214578a85..709bdad31 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
index 712fa381e..a5a6b18aa 100644
--- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -18,6 +18,12 @@ namespace MediaBrowser.Model.Extensions
/// <returns>The ordered remote image infos.</returns>
public static IEnumerable<RemoteImageInfo> OrderByLanguageDescending(this IEnumerable<RemoteImageInfo> remoteImageInfos, string requestedLanguage)
{
+ if (string.IsNullOrWhiteSpace(requestedLanguage))
+ {
+ // Default to English if no requested language is specified.
+ requestedLanguage = "en";
+ }
+
var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
return remoteImageInfos.OrderByDescending(i =>
@@ -27,14 +33,16 @@ namespace MediaBrowser.Model.Extensions
return 3;
}
- if (!isRequestedLanguageEn && string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(i.Language))
{
- return 2;
+ // Assume empty image language is likely to be English.
+ return isRequestedLanguageEn ? 3 : 2;
}
- if (string.IsNullOrEmpty(i.Language))
+ if (!isRequestedLanguageEn && string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
{
- return isRequestedLanguageEn ? 3 : 2;
+ // Prioritize English over non-requested languages.
+ return 2;
}
return 0;
diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs
new file mode 100644
index 000000000..b888a4163
--- /dev/null
+++ b/MediaBrowser.Model/IO/AsyncFile.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Helper class to create async <see cref="FileStream" />s.
+ /// </summary>
+ public static class AsyncFile
+ {
+ /// <summary>
+ /// Gets a value indicating whether we should use async IO on this platform.
+ /// <see href="https://github.com/dotnet/runtime/issues/16354" />.
+ /// </summary>
+ /// <returns>Returns <c>false</c> on Windows; otherwise <c>true</c>.</returns>
+ public static bool UseAsyncIO => !OperatingSystem.IsWindows();
+
+ /// <summary>
+ /// Opens an existing file for reading.
+ /// </summary>
+ /// <param name="path">The file to be opened for reading.</param>
+ /// <returns>A read-only <see cref="FileStream" /> on the specified path.</returns>
+ public static FileStream OpenRead(string path)
+ => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, UseAsyncIO);
+
+ /// <summary>
+ /// Opens an existing file for writing.
+ /// </summary>
+ /// <param name="path">The file to be opened for writing.</param>
+ /// <returns>An unshared <see cref="FileStream" /> object on the specified path with Write access.</returns>
+ public static FileStream OpenWrite(string path)
+ => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, UseAsyncIO);
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
index a851229f7..cce99c77d 100644
--- a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
@@ -16,15 +16,17 @@ namespace MediaBrowser.Model.SyncPlay
/// <param name="playlist">The playlist.</param>
/// <param name="playingItemIndex">The playing item index in the playlist.</param>
/// <param name="startPositionTicks">The start position ticks.</param>
+ /// <param name="isPlaying">The playing item status.</param>
/// <param name="shuffleMode">The shuffle mode.</param>
/// <param name="repeatMode">The repeat mode.</param>
- public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
+ public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
{
Reason = reason;
LastUpdate = lastUpdate;
Playlist = playlist;
PlayingItemIndex = playingItemIndex;
StartPositionTicks = startPositionTicks;
+ IsPlaying = isPlaying;
ShuffleMode = shuffleMode;
RepeatMode = repeatMode;
}
@@ -60,6 +62,12 @@ namespace MediaBrowser.Model.SyncPlay
public long StartPositionTicks { get; }
/// <summary>
+ /// Gets a value indicating whether the current item is playing.
+ /// </summary>
+ /// <value>The playing item status.</value>
+ public bool IsPlaying { get; }
+
+ /// <summary>
/// Gets the shuffle mode.
/// </summary>
/// <value>The shuffle mode.</value>
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 111070d81..3634d0705 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -1,5 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, CA1819
using System;
using System.Xml.Serialization;
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 7d259a9d3..6c14c8de1 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -264,7 +264,7 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index cc4c75d7d..7fdef6b44 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
@@ -164,7 +163,7 @@ namespace MediaBrowser.Providers.Manager
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
@@ -276,7 +275,7 @@ namespace MediaBrowser.Providers.Manager
item,
new RemoteImageQuery(provider.Name)
{
- IncludeAllLanguages = false,
+ IncludeAllLanguages = true,
IncludeDisabledProviders = false,
},
cancellationToken).ConfigureAwait(false);
@@ -470,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
CancellationToken cancellationToken)
{
var eligibleImages = images
- .Where(i => i.Type == type && !(i.Width.HasValue && i.Width.Value < minWidth))
+ .Where(i => i.Type == type && i.Width >= minWidth)
.ToList();
if (EnableImageStub(item) && eligibleImages.Count > 0)
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 7e60eced0..b51a25417 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
@@ -209,7 +210,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(source));
}
- var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true);
+ var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
}
@@ -235,14 +236,7 @@ namespace MediaBrowser.Providers.Manager
var preferredLanguage = item.GetPreferredMetadataLanguage();
- var languages = new List<string>();
- if (!query.IncludeAllLanguages && !string.IsNullOrWhiteSpace(preferredLanguage))
- {
- languages.Add(preferredLanguage);
- }
-
- // TODO include [query.IncludeAllLanguages] as an argument to the providers
- var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
+ var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -254,17 +248,21 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
/// <param name="item">The item.</param>
/// <param name="provider">The provider.</param>
- /// <param name="preferredLanguages">The preferred languages.</param>
+ /// <param name="preferredLanguage">The preferred language.</param>
+ /// <param name="includeAllLanguages">Whether to include all languages in results.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="type">The type.</param>
/// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
private async Task<IEnumerable<RemoteImageInfo>> GetImages(
BaseItem item,
IRemoteImageProvider provider,
- IReadOnlyCollection<string> preferredLanguages,
+ string preferredLanguage,
+ bool includeAllLanguages,
CancellationToken cancellationToken,
ImageType? type = null)
{
+ bool hasPreferredLanguage = !string.IsNullOrWhiteSpace(preferredLanguage);
+
try
{
var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
@@ -274,14 +272,17 @@ namespace MediaBrowser.Providers.Manager
result = result.Where(i => i.Type == type.Value);
}
- if (preferredLanguages.Count > 0)
+ if (!includeAllLanguages && hasPreferredLanguage)
{
- result = result.Where(i => string.IsNullOrEmpty(i.Language) ||
- preferredLanguages.Contains(i.Language, StringComparer.OrdinalIgnoreCase) ||
+ // Filter out languages that do not match the preferred languages.
+ //
+ // TODO: should exception case of "en" (English) eventually be removed?
+ result = result.Where(i => string.IsNullOrWhiteSpace(i.Language) ||
+ string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase) ||
string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
}
- return result;
+ return result.OrderByLanguageDescending(preferredLanguage);
}
catch (OperationCanceledException)
{
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 3d866cdc2..3a6e16274 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -29,8 +29,6 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <AnalysisMode Condition=" '$(Configuration)' == 'Debug'">AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<Nullable>disable</Nullable>
</PropertyGroup>
@@ -49,5 +47,7 @@
<EmbeddedResource Include="Plugins\Omdb\Configuration\config.html" />
<None Remove="Plugins\MusicBrainz\Configuration\config.html" />
<EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" />
+ <None Remove="Plugins\Tmdb\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\Tmdb\Configuration\config.html" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index aa0743bd0..449f0d259 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index b3d065929..d7f6a5fac 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.IO;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index 453938be7..8b96205c2 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -21,37 +21,33 @@ namespace MediaBrowser.Providers.MediaInfo
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger<VideoImageProvider> _logger;
- private readonly IFileSystem _fileSystem;
- public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger, IFileSystem fileSystem)
+ public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
{
_mediaEncoder = mediaEncoder;
_logger = logger;
- _fileSystem = fileSystem;
}
+ /// <inheritdoc />
public string Name => "Screen Grabber";
+ /// <inheritdoc />
// Make sure this comes after internet image providers
public int Order => 100;
+ /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType> { ImageType.Primary };
+ return new[] { ImageType.Primary };
}
+ /// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
var video = (Video)item;
- // No support for this
- if (video.IsPlaceHolder)
- {
- return Task.FromResult(new DynamicImageResponse { HasImage = false });
- }
-
- // No support for this
- if (video.VideoType == VideoType.Dvd)
+ // No support for these
+ if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
@@ -59,18 +55,21 @@ namespace MediaBrowser.Providers.MediaInfo
// Can't extract if we didn't find a video stream in the file
if (!video.DefaultVideoStreamIndex.HasValue)
{
- _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {0}.", video.Path ?? string.Empty);
+ _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {Path}.", video.Path ?? string.Empty);
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
return GetVideoImage(video, cancellationToken);
}
- public async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
+ private async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
{
- var protocol = item.PathProtocol ?? MediaProtocol.File;
-
- var inputPath = item.Path;
+ MediaSourceInfo mediaSource = new MediaSourceInfo
+ {
+ VideoType = item.VideoType,
+ IsoType = item.IsoType,
+ Protocol = item.PathProtocol ?? MediaProtocol.File,
+ };
var mediaStreams =
item.GetMediaStreams();
@@ -80,41 +79,27 @@ namespace MediaBrowser.Providers.MediaInfo
.Where(i => i.Type == MediaStreamType.EmbeddedImage)
.ToList();
- var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("front", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault();
-
string extractedImagePath;
- if (imageStream != null)
- {
- MediaSourceInfo mediaSource = new MediaSourceInfo
- {
- VideoType = item.VideoType,
- IsoType = item.IsoType,
- Protocol = item.PathProtocol.Value,
- };
-
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false);
- }
- else
+ if (imageStreams.Count == 0)
{
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
// Always use 10 seconds for dvd because our duration could be out of whack
var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
item.RunTimeTicks.Value > 0
- ? TimeSpan.FromTicks(Convert.ToInt64(item.RunTimeTicks.Value * .1))
+ ? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
: TimeSpan.FromSeconds(10);
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
- var mediaSource = new MediaSourceInfo
- {
- VideoType = item.VideoType,
- IsoType = item.IsoType,
- Protocol = item.PathProtocol.Value,
- };
-
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+ extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ var imageStream = imageStreams.Find(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase))
+ ?? imageStreams.Find(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase))
+ ?? imageStreams[0];
+
+ extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false);
}
return new DynamicImageResponse
@@ -126,6 +111,7 @@ namespace MediaBrowser.Providers.MediaInfo
};
}
+ /// <inheritdoc />
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
@@ -138,12 +124,7 @@ namespace MediaBrowser.Providers.MediaInfo
return false;
}
- if (item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia)
- {
- return true;
- }
-
- return false;
+ return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
index 36d8eeb40..81bbc26b8 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -57,7 +58,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index 9f2f7fc11..c1226febf 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -66,7 +66,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
@@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index aa61a56f6..3ffdcdbeb 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -59,7 +60,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 2857c6c13..8572b3413 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
@@ -155,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index c97affdbf..93f8902de 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -12,7 +12,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 1ae712e9e..1dea3dece 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -236,31 +236,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
{
var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
- await using var stream = File.OpenRead(path);
+ await using var stream = AsyncFile.OpenRead(path);
return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
{
var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
- await using var stream = File.OpenRead(path);
+ await using var stream = AsyncFile.OpenRead(path);
return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
- internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
- {
- if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id))
- {
- // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
- if (!string.IsNullOrWhiteSpace(id))
- {
- return true;
- }
- }
-
- return false;
- }
-
/// <summary>Gets OMDB URL.</summary>
/// <param name="query">Appends query string to URL.</param>
/// <returns>OMDB URL with optional query string.</returns>
@@ -309,7 +295,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam));
var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
@@ -349,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
seasonId));
var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index 5ad61c567..a5287e749 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -101,7 +101,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
});
}
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
new file mode 100644
index 000000000..907f0160d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Plugin configuration class for TMDb library.
+ /// </summary>
+ public class PluginConfiguration : BasePluginConfiguration
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether include adult content when searching with TMDb.
+ /// </summary>
+ public bool IncludeAdult { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
new file mode 100644
index 000000000..6f42549d7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>TMDb</title>
+</head>
+<body>
+ <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div data-role="content">
+ <div class="content-primary">
+ <form class="configForm">
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="includeAdult" />
+ <span>Include adult content in search results.</span>
+ </label>
+ <br />
+ <div>
+ <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+ </div>
+ </form>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var PluginConfig = {
+ pluginId: "b8715ed1-6c47-4528-9ad3-f72deb539cd4"
+ };
+
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#includeAdult').checked = config.IncludeAdult;
+ Dashboard.hideLoadingMsg();
+ });
+ });
+
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.IncludeAdult = document.querySelector('#includeAdult').checked;
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
+ });
+ </script>
+ </div>
+</body>
+</html>
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index f34d689c1..d3cef49d8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find;
@@ -118,7 +117,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
});
}
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index e4c908a62..1fc5ccba5 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -10,7 +10,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
@@ -77,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
};
}
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
new file mode 100644
index 000000000..ea81eb96e
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Plugin class for the TMDb library.
+ /// </summary>
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Plugin"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">application paths.</param>
+ /// <param name="xmlSerializer">xml serializer.</param>
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ /// <summary>
+ /// Gets the instance of TMDb plugin.
+ /// </summary>
+ public static Plugin Instance { get; private set; }
+
+ /// <inheritdoc/>
+ public override Guid Id => new Guid("b8715ed1-6c47-4528-9ad3-f72deb539cd4");
+
+ /// <inheritdoc/>
+ public override string Name => "TMDb";
+
+ /// <inheritdoc/>
+ public override string Description => "Get metadata for movies and other video content from TheMovieDb.";
+
+ // TODO remove when plugin removed from server.
+
+ /// <inheritdoc/>
+ public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml";
+
+ /// <summary>
+ /// Return the plugin configuration page.
+ /// </summary>
+ /// <returns>PluginPageInfo.</returns>
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index ba18c542f..45e18c0ac 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -92,7 +91,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
};
}
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index 0d23c7872..1bda1a09b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -81,7 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
};
}
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 326c116b3..f3f340378 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -107,7 +106,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
};
}
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 4de4bf4db..5bd5dd2e8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -358,7 +358,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -386,7 +386,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchPersonAsync(name, cancellationToken: cancellationToken)
+ .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -428,7 +428,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
+ .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 63e78d15e..7a057c065 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Studios
Directory.CreateDirectory(Path.GetDirectoryName(file));
await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(file, FileMode.Create);
+ await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 0c791a2fe..d6c346ba1 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Subtitles
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true);
+ using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, AsyncFile.UseAsyncIO);
await stream.CopyToAsync(fs).ConfigureAwait(false);
return;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index ca3ec79b7..3a305024e 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.IO;
-using System.Text;
using System.Threading;
using System.Xml;
using MediaBrowser.Common.Configuration;
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index e49c0e77b..e0116c068 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
diff --git a/debian/bin/restart.sh b/debian/bin/restart.sh
index 34fce0670..4847b918b 100755
--- a/debian/bin/restart.sh
+++ b/debian/bin/restart.sh
@@ -11,23 +11,43 @@
#
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
-get_service_command() {
- for command in systemctl service; do
- if which $command &>/dev/null; then
- echo $command && return
+# This is the Right Way(tm) to check if we are booted with
+# systemd, according to sd_booted(3)
+if [ -d /run/systemd/system ]; then
+ cmd=systemctl
+else
+ # Everything else is really hard to figure out, so we just use
+ # service(8) if it's available - that works with most init
+ # systems/distributions I know of, including FreeBSD
+ if type service >/dev/null 2>&1; then
+ cmd=service
+ else
+ # If even service(8) isn't available, we just try /etc/init.d
+ # and hope for the best
+ if [ -d /etc/init.d ]; then
+ cmd=sysv
+ else
+ echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
+ echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
+ exit 1
fi
- done
- echo "sysv"
-}
+ fi
+fi
+
+if type sudo >/dev/null 2>&1; then
+ sudo_command=sudo
+else
+ sudo_command=
+fi
-cmd="$( get_service_command )"
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
case $cmd in
'systemctl')
- echo "sleep 0.5; /usr/bin/sudo $( which systemctl ) start jellyfin" | at now
+ # Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
+ $sudo_command systemd-run systemctl restart jellyfin
;;
'service')
- echo "sleep 0.5; /usr/bin/sudo $( which service ) jellyfin start" | at now
+ echo "sleep 0.5; $sudo_command service jellyfin start" | at now
;;
'sysv')
echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 9ebaf2bd8..ab8d5d1d4 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -33,6 +33,9 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# [OPTIONAL] run Jellyfin without the web app
#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
+# Space to add additional command line options to jellyfin (for help see ~$ jellyfin --help)
+JELLYFIN_ADDITIONAL_OPTS=""
+
# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
# 0 = Workstation
# 1 = Server
@@ -45,4 +48,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
diff --git a/debian/conf/jellyfin-sudoers b/debian/conf/jellyfin-sudoers
index b481ba4ad..f84e7454f 100644
--- a/debian/conf/jellyfin-sudoers
+++ b/debian/conf/jellyfin-sudoers
@@ -2,9 +2,9 @@
Cmnd_Alias RESTARTSERVER_SYSV = /sbin/service jellyfin restart, /usr/sbin/service jellyfin restart
Cmnd_Alias STARTSERVER_SYSV = /sbin/service jellyfin start, /usr/sbin/service jellyfin start
Cmnd_Alias STOPSERVER_SYSV = /sbin/service jellyfin stop, /usr/sbin/service jellyfin stop
-Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemctl restart jellyfin, /bin/systemctl restart jellyfin
-Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemctl start jellyfin, /bin/systemctl start jellyfin
-Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemctl stop jellyfin, /bin/systemctl stop jellyfin
+Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
+Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
+Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
Cmnd_Alias RESTARTSERVER_INITD = /etc/init.d/jellyfin restart
Cmnd_Alias STARTSERVER_INITD = /etc/init.d/jellyfin start
Cmnd_Alias STOPSERVER_INITD = /etc/init.d/jellyfin stop
diff --git a/debian/control b/debian/control
index 9675d36ca..51b20c670 100644
--- a/debian/control
+++ b/debian/control
@@ -23,6 +23,6 @@ Depends: at,
libfontconfig1,
libfreetype6,
libssl1.1
-Recommends: jellyfin-web
+Recommends: jellyfin-web, sudo
Description: Jellyfin is the Free Software Media System.
This package provides the Jellyfin server backend and API.
diff --git a/debian/jellyfin.service b/debian/jellyfin.service
index c9d1a4d13..b79cd47c7 100644
--- a/debian/jellyfin.service
+++ b/debian/jellyfin.service
@@ -6,7 +6,7 @@ After = network-online.target
Type = simple
EnvironmentFile = /etc/default/jellyfin
User = jellyfin
-ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
+ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS}
Restart = on-failure
TimeoutSec = 15
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 928fe590f..0d606f9f7 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -40,7 +40,7 @@ Jellyfin is a free software media system that puts you in control of managing an
Summary: The Free Software Media System Server backend
Requires(pre): shadow-utils
Requires: ffmpeg
-Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at
+Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at, sudo
%description server
The Jellyfin media server backend.
diff --git a/fedora/jellyfin.sudoers b/fedora/jellyfin.sudoers
index dd245af4b..57a9e7b67 100644
--- a/fedora/jellyfin.sudoers
+++ b/fedora/jellyfin.sudoers
@@ -1,8 +1,7 @@
# Allow jellyfin group to start, stop and restart itself
-Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemctl restart jellyfin, /bin/systemctl restart jellyfin
-Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemctl start jellyfin, /bin/systemctl start jellyfin
-Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemctl stop jellyfin, /bin/systemctl stop jellyfin
-
+Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
+Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
+Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
diff --git a/fedora/restart.sh b/fedora/restart.sh
index 34fce0670..4847b918b 100755
--- a/fedora/restart.sh
+++ b/fedora/restart.sh
@@ -11,23 +11,43 @@
#
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
-get_service_command() {
- for command in systemctl service; do
- if which $command &>/dev/null; then
- echo $command && return
+# This is the Right Way(tm) to check if we are booted with
+# systemd, according to sd_booted(3)
+if [ -d /run/systemd/system ]; then
+ cmd=systemctl
+else
+ # Everything else is really hard to figure out, so we just use
+ # service(8) if it's available - that works with most init
+ # systems/distributions I know of, including FreeBSD
+ if type service >/dev/null 2>&1; then
+ cmd=service
+ else
+ # If even service(8) isn't available, we just try /etc/init.d
+ # and hope for the best
+ if [ -d /etc/init.d ]; then
+ cmd=sysv
+ else
+ echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
+ echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
+ exit 1
fi
- done
- echo "sysv"
-}
+ fi
+fi
+
+if type sudo >/dev/null 2>&1; then
+ sudo_command=sudo
+else
+ sudo_command=
+fi
-cmd="$( get_service_command )"
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
case $cmd in
'systemctl')
- echo "sleep 0.5; /usr/bin/sudo $( which systemctl ) start jellyfin" | at now
+ # Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
+ $sudo_command systemd-run systemctl restart jellyfin
;;
'service')
- echo "sleep 0.5; /usr/bin/sudo $( which service ) jellyfin start" | at now
+ echo "sleep 0.5; $sudo_command service jellyfin start" | at now
;;
'sysv')
echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 68fb9064e..dfb991170 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
- <!-- disable warning CA1040: Avoid empty interfaces -->
- <Rule Id="CA1040" Action="Info" />
-
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
@@ -57,12 +54,19 @@
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
+ <!-- disable warning CA1040: Avoid empty interfaces -->
+ <Rule Id="CA1040" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
+ <!-- TODO: enable when false positives are fixed -->
+ <!-- disable warning CA1508: Avoid dead conditional code -->
+ <Rule Id="CA1508" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
+ <!-- disable warning CA1724: Type names should not match namespaces -->
+ <Rule Id="CA1724" Action="Info" />
<!-- disable warning CA1805: Do not initialize unnecessarily -->
<Rule Id="CA1805" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
diff --git a/src/Jellyfin.Extensions/DictionaryExtensions.cs b/src/Jellyfin.Extensions/DictionaryExtensions.cs
index 43ed41ab1..5bb828d01 100644
--- a/src/Jellyfin.Extensions/DictionaryExtensions.cs
+++ b/src/Jellyfin.Extensions/DictionaryExtensions.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
namespace Jellyfin.Extensions
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs
index 44980ec02..0d0cc2d06 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -1,9 +1,4 @@
-using System;
-using System.ComponentModel;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Jellyfin.Extensions.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert comma delimited string to array of type.
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs
index e3e492e24..6e59fe464 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs
@@ -1,9 +1,4 @@
-using System;
-using System.ComponentModel;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Jellyfin.Extensions.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert Pipe delimited string to array of type.
diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
index a62fd8d5a..23c51999f 100644
--- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -4,6 +4,7 @@ using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
+using Jellyfin.Server.Implementations.Security;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -49,5 +50,61 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
+
+ [Theory]
+ [MemberData(nameof(GetParts_ValidAuthHeader_Success_Data))]
+ public void GetParts_ValidAuthHeader_Success(string input, Dictionary<string, string> parts)
+ {
+ var dict = AuthorizationContext.GetParts(input);
+ foreach (var (key, value) in parts)
+ {
+ Assert.Equal(dict[key], value);
+ }
+ }
+
+ private static TheoryData<string, Dictionary<string, string>> GetParts_ValidAuthHeader_Success_Data()
+ {
+ var data = new TheoryData<string, Dictionary<string, string>>();
+
+ data.Add(
+ "x=\"123,123\",y=\"123\"",
+ new Dictionary<string, string>
+ {
+ { "x", "123,123" },
+ { "y", "123" }
+ });
+
+ data.Add(
+ "x=\"123,123\", y=\"123\",z=\"'hi'\"",
+ new Dictionary<string, string>
+ {
+ { "x", "123,123" },
+ { "y", "123" },
+ { "z", "'hi'" }
+ });
+
+ data.Add(
+ "x=\"ab\"",
+ new Dictionary<string, string>
+ {
+ { "x", "ab" }
+ });
+
+ data.Add(
+ "param=Hörbücher",
+ new Dictionary<string, string>
+ {
+ { "param", "Hörbücher" }
+ });
+
+ data.Add(
+ "param=%22%Hörbücher",
+ new Dictionary<string, string>
+ {
+ { "param", "\"%Hörbücher" }
+ });
+
+ return data;
+ }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
index 117083815..59a6b52d1 100644
--- a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
@@ -1,13 +1,6 @@
using System;
using System.Collections.Generic;
-using AutoFixture;
-using AutoFixture.AutoMoq;
using Jellyfin.Api.Controllers;
-using Jellyfin.Api.Helpers;
-using Jellyfin.Api.Models.StreamingDtos;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 8e6b07716..1619fa89c 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 72cd9aa45..20680157f 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -17,7 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
index d0e3e9456..125229ff9 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
@@ -1,4 +1,3 @@
-using System.Globalization;
using System.Text.Json;
using FsCheck;
using FsCheck.Xunit;
diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
index 2955104a2..97dbb3be0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
@@ -3,6 +3,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.IO;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests
@@ -14,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Tests
public async Task Test(string fileName)
{
var path = Path.Join("Test Data", fileName);
- await using (var stream = File.OpenRead(path))
+ await using (var stream = AsyncFile.OpenRead(path))
{
var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
Assert.NotNull(res);
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index fcb85a3ac..d002d5a34 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Text.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index e9b7b1850..09b8a7a94 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -11,7 +11,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index dd593c9e7..5fa2ecfe9 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index b92cb165c..a1bdfa31b 100644
--- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -1,9 +1,6 @@
using System;
-using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Net;
-using System.Text;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using Jellyfin.Server.Extensions;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index ef3ca15d5..7ea45d14d 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.System;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;