aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml7
-rw-r--r--Emby.Dlna/DlnaManager.cs10
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj4
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs6
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs13
-rw-r--r--Emby.Dlna/Profiles/SonyPs3Profile.cs4
-rw-r--r--Emby.Dlna/Profiles/SonyPs4Profile.cs4
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml4
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml4
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs64
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs35
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs26
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs10
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj11
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs2
-rw-r--r--Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs130
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs25
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs14
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs19
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs2
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs20
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs15
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs70
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs21
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs40
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs77
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json66
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json19
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json17
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs17
-rw-r--r--Emby.Server.Implementations/Properties/AssemblyInfo.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs42
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs8
-rw-r--r--Emby.Server.Implementations/Serialization/JsonSerializer.cs281
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs16
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs50
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs3
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs18
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs53
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs14
-rw-r--r--Jellyfin.Api/Constants/Policies.cs18
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs23
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs121
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs16
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs16
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs11
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs31
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs28
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs25
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs7
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs44
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs6
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj4
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs28
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs55
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs2
-rw-r--r--Jellyfin.Data/Entities/ActivityLog.cs5
-rw-r--r--Jellyfin.Data/Entities/ItemDisplayPreferences.cs1
-rw-r--r--Jellyfin.Data/Entities/User.cs60
-rw-r--r--Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs28
-rw-r--r--Jellyfin.Data/Enums/SyncPlayUserAccessType.cs (renamed from Jellyfin.Data/Enums/SyncPlayAccess.cs)4
-rw-r--r--Jellyfin.Data/Enums/ViewType.cs101
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs2
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs13
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs12
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs13
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj5
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs1
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs3
-rw-r--r--Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs29
-rw-r--r--Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs4
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs23
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs37
-rw-r--r--Jellyfin.Server/Filters/FileResponseFilter.cs3
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj5
-rw-r--r--Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs54
-rw-r--r--Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs47
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs22
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs4
-rw-r--r--Jellyfin.Server/Program.cs3
-rw-r--r--Jellyfin.Server/Startup.cs4
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs30
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs34
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs11
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs33
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs35
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStructConverter.cs35
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs77
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj2
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs8
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs8
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs9
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs15
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs3
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs63
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs11
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs7
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs17
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs6
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs10
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs2
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj1
-rw-r--r--MediaBrowser.Model/Serialization/IJsonSerializer.cs102
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs4
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs7
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs46
-rw-r--r--MediaBrowser.Providers/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs133
-rw-r--r--README.md8
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.linux.amd64-musl31
-rw-r--r--deployment/Dockerfile.linux.arm6431
-rw-r--r--deployment/Dockerfile.linux.armhf31
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rwxr-xr-xdeployment/build.linux.amd64-musl31
-rwxr-xr-xdeployment/build.linux.arm6431
-rwxr-xr-xdeployment/build.linux.armhf31
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj4
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj3
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs34
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs23
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs69
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs68
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj7
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs134
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/discover.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/lineup.json1
187 files changed, 2465 insertions, 1282 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 47477ba60..0a63b329b 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf
Linux.amd64:
BuildConfiguration: linux.amd64
+ Linux.amd64-musl:
+ BuildConfiguration: linux.amd64-musl
+ Linux.arm64:
+ BuildConfiguration: linux.arm64
+ Linux.armhf:
+ BuildConfiguration: linux.armhf
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
@@ -204,6 +210,7 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: 'custom'
projects: |
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index fedd20b68..21ba1c755 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -7,12 +7,14 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
+using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -32,9 +34,9 @@ namespace Emby.Dlna
private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILogger<DlnaManager> _logger;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@@ -43,14 +45,12 @@ namespace Emby.Dlna
IFileSystem fileSystem,
IApplicationPaths appPaths,
ILoggerFactory loggerFactory,
- IJsonSerializer jsonSerializer,
IServerApplicationHost appHost)
{
_xmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DlnaManager>();
- _jsonSerializer = jsonSerializer;
_appHost = appHost;
}
@@ -495,9 +495,9 @@ namespace Emby.Dlna
return profile;
}
- var json = _jsonSerializer.SerializeToString(profile);
+ var json = JsonSerializer.Serialize(profile, _jsonOptions);
- return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
+ return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
}
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index bd30cc1e1..8b057a095 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -78,9 +78,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
- <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup>
</Project>
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index ced56a718..3f7b558f6 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -136,6 +136,11 @@ namespace Emby.Dlna.Main
public static DlnaEntryPoint Current { get; private set; }
+ /// <summary>
+ /// Gets a value indicating whether the dlna server is enabled.
+ /// </summary>
+ public static bool Enabled { get; private set; }
+
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
@@ -168,6 +173,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
+ Enabled = options.EnableServer;
StartSsdpHandler();
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index b7cd91a5c..486109304 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -340,10 +340,19 @@ namespace Emby.Dlna.PlayTo
}
var playlist = new PlaylistItem[len];
- playlist[0] = CreatePlaylistItem(items[0], user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex);
+
+ // Not nullable enabled - so this is required.
+ playlist[0] = CreatePlaylistItem(
+ items[0],
+ user,
+ command.StartPositionTicks ?? 0,
+ command.MediaSourceId ?? string.Empty,
+ command.AudioStreamIndex,
+ command.SubtitleStreamIndex);
+
for (int i = 1; i < len; i++)
{
- playlist[i] = CreatePlaylistItem(items[i], user, 0, null, null, null);
+ playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
}
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs
index d56b1df50..e4a7a3a59 100644
--- a/Emby.Dlna/Profiles/SonyPs3Profile.cs
+++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
- AudioCodec = "ac3,mp2,mp3,aac"
+ AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
- AudioCodec = "ac3,aac,mp3",
+ AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile
diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs
index db56094e2..985df0c9a 100644
--- a/Emby.Dlna/Profiles/SonyPs4Profile.cs
+++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
- AudioCodec = "ac3,mp2,mp3,aac"
+ AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
- AudioCodec = "mp3",
+ AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
index bafa44b82..129b188e2 100644
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
+++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
@@ -38,7 +38,7 @@
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
+ <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@@ -46,7 +46,7 @@
</DirectPlayProfiles>
<TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
+ <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles>
<ContainerProfiles>
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
index eb8e645b3..592119305 100644
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
+++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
@@ -38,7 +38,7 @@
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
+ <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@@ -46,7 +46,7 @@
</DirectPlayProfiles>
<TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
+ <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles>
<ContainerProfiles>
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 198852ec1..8d2486fee 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -49,7 +49,7 @@ namespace Emby.Dlna.Service
{
ControlRequestInfo requestInfo = null;
- using (var streamReader = new StreamReader(request.InputXml))
+ using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
var readerSettings = new XmlReaderSettings()
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index d74ea0352..8fa712914 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -3,14 +3,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
-using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
@@ -49,6 +50,7 @@ using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -117,12 +119,12 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly IXmlSerializer _xmlSerializer;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
/// <summary>
/// Gets a value indicating whether this instance can self restart.
@@ -256,7 +258,6 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
- _jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection;
@@ -284,13 +285,6 @@ namespace Emby.Server.Implementations
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
- CertificateInfo = new CertificateInfo
- {
- Path = ServerConfigurationManager.Configuration.CertificatePath,
- Password = ServerConfigurationManager.Configuration.CertificatePassword
- };
- Certificate = GetCertificate(CertificateInfo);
-
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
@@ -456,6 +450,7 @@ namespace Emby.Server.Implementations
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
+ ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath();
@@ -505,6 +500,13 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
+ CertificateInfo = new CertificateInfo
+ {
+ Path = networkConfiguration.CertificatePath,
+ Password = networkConfiguration.CertificatePassword
+ };
+ Certificate = GetCertificate(CertificateInfo);
+
DiscoverTypes();
RegisterServices();
@@ -526,8 +528,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
-
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>();
@@ -714,7 +714,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
- var localCert = new X509Certificate2(certificateLocation, password);
+ var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey)
{
@@ -752,7 +752,6 @@ namespace Emby.Server.Implementations
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
- CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
}
@@ -912,11 +911,11 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
+ var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0)
{
- var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort)
@@ -936,10 +935,7 @@ namespace Emby.Server.Implementations
requiresRestart = true;
}
- var currentCertPath = CertificateInfo?.Path;
- var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
-
- if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
+ if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
}
@@ -953,6 +949,33 @@ namespace Emby.Server.Implementations
}
/// <summary>
+ /// Validates the SSL certificate.
+ /// </summary>
+ /// <param name="networkConfig">The new configuration.</param>
+ /// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
+ private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
+ {
+ var newPath = networkConfig.CertificatePath;
+
+ if (!string.IsNullOrWhiteSpace(newPath)
+ && !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
+ {
+ if (File.Exists(newPath))
+ {
+ return true;
+ }
+
+ throw new FileNotFoundException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Certificate file '{0}' does not exist.",
+ newPath));
+ }
+
+ return false;
+ }
+
+ /// <summary>
/// Notifies that the kernel that a change has been made that requires a restart.
/// </summary>
public void NotifyPendingRestart()
@@ -1025,7 +1048,8 @@ namespace Emby.Server.Implementations
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
- var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
+ var jsonString = File.ReadAllText(metafile, Encoding.UTF8);
+ var manifest = JsonSerializer.Deserialize<PluginManifest>(jsonString, _jsonOptions);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 57684a429..2d5b19fa6 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -21,7 +24,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -44,10 +46,10 @@ namespace Emby.Server.Implementations.Channels
private readonly ILogger<ChannelManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
/// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
@@ -59,7 +61,6 @@ namespace Emby.Server.Implementations.Channels
/// <param name="config">The server configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="userDataManager">The user data manager.</param>
- /// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
@@ -70,7 +71,6 @@ namespace Emby.Server.Implementations.Channels
IServerConfigurationManager config,
IFileSystem fileSystem,
IUserDataManager userDataManager,
- IJsonSerializer jsonSerializer,
IProviderManager providerManager,
IMemoryCache memoryCache)
{
@@ -81,7 +81,6 @@ namespace Emby.Server.Implementations.Channels
_config = config;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
- _jsonSerializer = jsonSerializer;
_providerManager = providerManager;
_memoryCache = memoryCache;
}
@@ -343,7 +342,9 @@ namespace Emby.Server.Implementations.Channels
try
{
- return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>();
+ var jsonString = File.ReadAllText(path, Encoding.UTF8);
+ return JsonSerializer.Deserialize<List<MediaSourceInfo>>(jsonString, _jsonOptions)
+ ?? new List<MediaSourceInfo>();
}
catch
{
@@ -351,7 +352,7 @@ namespace Emby.Server.Implementations.Channels
}
}
- private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
+ private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
{
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
@@ -370,7 +371,8 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(mediaSources, path);
+ await using FileStream createStream = File.Create(path);
+ await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -812,7 +814,8 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+ await using FileStream jsonStream = File.OpenRead(cachePath);
+ var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
return null;
@@ -834,7 +837,8 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+ await using FileStream jsonStream = File.OpenRead(cachePath);
+ var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
return null;
@@ -865,7 +869,7 @@ namespace Emby.Server.Implementations.Channels
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
}
- CacheResponse(result, cachePath);
+ await CacheResponse(result, cachePath);
return result;
}
@@ -875,13 +879,14 @@ namespace Emby.Server.Implementations.Channels
}
}
- private void CacheResponse(object result, string path)
+ private async Task CacheResponse(object result, string path)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(result, path);
+ await using FileStream createStream = File.Create(path);
+ await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -1176,11 +1181,11 @@ namespace Emby.Server.Implementations.Channels
{
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
{
- SaveMediaSources(item, new List<MediaSourceInfo>());
+ await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
}
else
{
- SaveMediaSources(item, info.MediaSources);
+ await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index f05a30a89..7a8ed8c29 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -88,7 +88,6 @@ namespace Emby.Server.Implementations.Configuration
var newConfig = (ServerConfiguration)newConfiguration;
ValidateMetadataPath(newConfig);
- ValidateSslCertificate(newConfig);
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
@@ -96,31 +95,6 @@ namespace Emby.Server.Implementations.Configuration
}
/// <summary>
- /// Validates the SSL certificate.
- /// </summary>
- /// <param name="newConfig">The new configuration.</param>
- /// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
- private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
- {
- var serverConfig = (ServerConfiguration)newConfig;
-
- var newPath = serverConfig.CertificatePath;
-
- if (!string.IsNullOrWhiteSpace(newPath)
- && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
- {
- if (!File.Exists(newPath))
- {
- throw new FileNotFoundException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Certificate file '{0}' does not exist.",
- newPath));
- }
- }
- }
-
- /// <summary>
/// Validates the metadata path.
/// </summary>
/// <param name="newConfig">The new configuration.</param>
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index f3e3a6397..686944a28 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1138,7 +1138,10 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
- AttachPrimaryImageAspectRatio(dto, episodeSeries);
+ if (!dto.ImageTags.ContainsKey(ImageType.Primary))
+ {
+ AttachPrimaryImageAspectRatio(dto, episodeSeries);
+ }
}
}
@@ -1185,7 +1188,10 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
- AttachPrimaryImageAspectRatio(dto, series);
+ if (!dto.ImageTags.ContainsKey(ImageType.Primary))
+ {
+ AttachPrimaryImageAspectRatio(dto, series);
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 91c4648c6..1e54c3b33 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -23,21 +23,12 @@
<ItemGroup>
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
- <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
- <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
- <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
- <PackageReference Include="ServiceStack.Text.Core" Version="5.10.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index d62e2eefe..024404ceb 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
updateToken = true;
}
- authInfo.IsApiKey = true;
+ authInfo.IsApiKey = false;
}
else
{
- authInfo.IsApiKey = false;
+ authInfo.IsApiKey = true;
}
if (updateToken)
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 86914dea2..040b6b9e4 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
- return GetUser((HttpContext)requestContext);
+ return GetUser(((HttpRequest)requestContext).HttpContext);
}
}
}
diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs
deleted file mode 100644
index d4e790c9a..000000000
--- a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Library
-{
- /// <summary>
- /// A library post scan/refresh task for pre-fetching remote images.
- /// </summary>
- public class ImageFetcherPostScanTask : ILibraryPostScanTask
- {
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly ILogger<ImageFetcherPostScanTask> _logger;
- private readonly SemaphoreSlim _imageFetcherLock;
-
- private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
- /// </summary>
- /// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
- /// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
- /// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
- public ImageFetcherPostScanTask(
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- ILogger<ImageFetcherPostScanTask> logger)
- {
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _logger = logger;
- _queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
- _imageFetcherLock = new SemaphoreSlim(1, 1);
- _libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
- _libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
- _providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
- }
-
- /// <inheritdoc />
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- // Sometimes a library scan will cause this to run twice if there's an item refresh going on.
- await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
- {
- var now = DateTime.UtcNow;
- var itemGuids = _queuedItems.Keys.ToList();
-
- for (var i = 0; i < itemGuids.Count; i++)
- {
- if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
- {
- continue;
- }
-
- var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
- var itemType = queuedItem.item.GetType();
- _logger.LogDebug(
- "Updating remote images for item {ItemId} with media type {ItemMediaType}",
- itemId,
- itemType);
- try
- {
- await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
- }
-
- _queuedItems.TryRemove(queuedItem.item.Id, out _);
- }
-
- if (itemGuids.Count > 0)
- {
- _logger.LogInformation(
- "Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
- itemGuids.Count.ToString(CultureInfo.InvariantCulture),
- (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
- }
- else
- {
- _logger.LogDebug("No images were updated.");
- }
- }
- finally
- {
- _imageFetcherLock.Release();
- }
- }
-
- private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
- {
- if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
- {
- _queuedItems.AddOrUpdate(
- itemChangeEventArgs.Item.Id,
- (itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
- (key, existingValue) => existingValue);
- }
- }
-
- private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
- {
- if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
- {
- _queuedItems.AddOrUpdate(
- e.Argument.Id,
- (e.Argument, ItemUpdateType.None),
- (key, existingValue) => existingValue);
- }
-
- // The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
- // the item that was refreshed regardless of children refreshes. So we take it as a signal
- // that the refresh is entirely completed.
- Run(null, CancellationToken.None).GetAwaiter().GetResult();
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 5b926b0f4..db27862ce 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -42,7 +42,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
@@ -1955,9 +1954,12 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
- RunMetadataSavers(items, updateReason);
+ foreach (var item in items)
+ {
+ await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
+ }
_itemRepository.SaveItems(items, cancellationToken);
@@ -1988,25 +1990,22 @@ namespace Emby.Server.Implementations.Library
}
}
}
-
- return Task.CompletedTask;
}
/// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
- public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason)
+ public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
- foreach (var item in items)
+ if (item.IsFileProtocol)
{
- if (item.IsFileProtocol)
- {
- ProviderManager.SaveMetadata(item, updateReason);
- }
-
- item.DateLastSaved = DateTime.UtcNow;
+ ProviderManager.SaveMetadata(item, updateReason);
}
+
+ item.DateLastSaved = DateTime.UtcNow;
+
+ return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 041619d1e..2070df31e 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -5,16 +5,17 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
@@ -23,14 +24,13 @@ namespace Emby.Server.Implementations.Library
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
- private readonly IJsonSerializer _json;
private readonly IApplicationPaths _appPaths;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
+ public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
{
_mediaEncoder = mediaEncoder;
_logger = logger;
- _json = json;
_appPaths = appPaths;
}
@@ -47,7 +47,8 @@ namespace Emby.Server.Implementations.Library
{
try
{
- mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath);
+ await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
@@ -83,7 +84,8 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- _json.SerializeToFile(mediaInfo, cacheFilePath);
+ await using FileStream createStream = File.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 928f5f88e..660ec106b 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -6,12 +6,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -23,7 +25,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
@@ -36,7 +37,6 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILogger<MediaSourceManager> _logger;
private readonly IUserDataManager _userDataManager;
@@ -46,6 +46,7 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private IMediaSourceProvider[] _providers;
@@ -56,7 +57,6 @@ namespace Emby.Server.Implementations.Library
IUserManager userManager,
ILibraryManager libraryManager,
ILogger<MediaSourceManager> logger,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder)
@@ -65,7 +65,6 @@ namespace Emby.Server.Implementations.Library
_userManager = userManager;
_libraryManager = libraryManager;
_logger = logger;
- _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
_mediaEncoder = mediaEncoder;
@@ -504,7 +503,7 @@ namespace Emby.Server.Implementations.Library
// hack - these two values were taken from LiveTVMediaSourceProvider
string cacheKey = request.OpenToken;
- await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths)
+ await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
.ConfigureAwait(false);
}
@@ -516,9 +515,9 @@ namespace Emby.Server.Implementations.Library
}
// TODO: @bond Fix
- var json = _jsonSerializer.SerializeToString(mediaSource);
+ var json = JsonSerializer.Serialize(mediaSource, _jsonOptions);
_logger.LogInformation("Live stream opened: " + json);
- var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+ var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
{
@@ -643,7 +642,8 @@ namespace Emby.Server.Implementations.Library
{
try
{
- mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath);
+ await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
@@ -679,7 +679,8 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath);
+ await using FileStream createStream = File.Create(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/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 59af7ce8a..86242d137 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{
- private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
+ private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
{
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 1d9529dff..94602582b 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
if (query.Limit.HasValue)
{
- results = results.GetRange(0, query.Limit.Value);
+ results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
}
return new QueryResult<SearchHintInfo>
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index f9e5e6bbc..d16275b19 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Book = MediaBrowser.Controller.Entities.Book;
+using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
namespace Emby.Server.Implementations.Library
{
@@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
- if (positionTicks > 0 && hasRuntime)
+ if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -245,6 +246,23 @@ namespace Emby.Server.Implementations.Library
}
}
}
+ else if (positionTicks > 0 && hasRuntime && item is AudioBook)
+ {
+ var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
+ var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
+
+ if (minIn > _config.Configuration.MinAudiobookResume)
+ {
+ // ignore progress during the beginning
+ positionTicks = 0;
+ }
+ else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
+ {
+ // mark as completed close to the end
+ positionTicks = 0;
+ data.Played = playedToCompletion = true;
+ }
+ }
else if (!hasRuntime)
{
// If we don't know the runtime we'll just have to assume it was fully played
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index f51657c63..b6b7ea949 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -129,23 +129,23 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
- list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
+ list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
- var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
+ var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list
.OrderBy(i =>
{
- var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture));
+ var index = Array.IndexOf(orders, i.Id);
if (index == -1
&& i is UserView view
&& view.DisplayParentId != Guid.Empty)
{
- index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture));
+ index = Array.IndexOf(orders, view.DisplayParentId);
}
return index == -1 ? int.MaxValue : index;
@@ -280,8 +280,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
- .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
+ .Contains(i.Id))
.ToList();
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 0dc045ee6..2c0de661d 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -36,7 +36,6 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -51,7 +50,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger<EmbyTV> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
- private readonly IJsonSerializer _jsonSerializer;
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
@@ -81,7 +79,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
ILogger<EmbyTV> logger,
- IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
ILiveTvManager liveTvManager,
@@ -103,12 +100,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_providerManager = providerManager;
_mediaEncoder = mediaEncoder;
_liveTvManager = (LiveTvManager)liveTvManager;
- _jsonSerializer = jsonSerializer;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
- _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
- _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"));
+ _seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
+ _timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
_timerProvider.TimerFired += OnTimerProviderTimerFired;
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@@ -1052,7 +1048,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IgnoreIndex = true
};
- await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _config.CommonApplicationPaths)
+ await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return new List<MediaSourceInfo>
@@ -1635,7 +1631,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
- return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
+ return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _config);
}
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index e6ee9819e..78a82118e 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -6,16 +6,17 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -25,10 +26,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
- private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _serverConfigurationManager;
-
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
@@ -38,13 +38,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ILogger logger,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
- IJsonSerializer json,
IServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
- _json = json;
_serverConfigurationManager = serverConfigurationManager;
}
@@ -66,7 +64,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
- private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+ private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
@@ -95,8 +93,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// 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);
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
+ 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);
_process = new Process
{
@@ -115,8 +113,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
-
- return _taskCompletionSource.Task;
}
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration)
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index fc543dc55..c80ecd6b3 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -4,7 +4,10 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using MediaBrowser.Model.Serialization;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -12,18 +15,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class ItemDataProvider<T>
where T : class
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private T[] _items;
public ItemDataProvider(
- IJsonSerializer jsonSerializer,
ILogger logger,
string dataPath,
Func<T, T, bool> equalityComparer)
{
- _jsonSerializer = jsonSerializer;
Logger = logger;
_dataPath = dataPath;
EqualityComparer = equalityComparer;
@@ -46,7 +47,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- _items = _jsonSerializer.DeserializeFromFile<T[]>(_dataPath);
+ var jsonString = File.ReadAllText(_dataPath, Encoding.UTF8);
+ _items = JsonSerializer.Deserialize<T[]>(jsonString, _jsonOptions);
return;
}
catch (Exception ex)
@@ -61,7 +63,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
- _jsonSerializer.SerializeToFile(_items, _dataPath);
+ var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
+ File.WriteAllText(_dataPath, jsonString);
}
public IReadOnlyList<T> GetAll()
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
index 194e4606d..da707fec6 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -9,8 +9,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
{
- public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
- : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ public SeriesTimerManager(ILogger logger, string dataPath)
+ : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index dd479b7d1..1efa90e25 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -8,7 +8,6 @@ using System.Threading;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -17,8 +16,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
- public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
- : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ public TimerManager(ILogger logger, string dataPath)
+ : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 1084ddf74..7567ea312 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -9,16 +9,17 @@ using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.Listings
@@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
private readonly ILogger<SchedulesDirect> _logger;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
@@ -36,16 +36,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private DateTime _lastErrorResponse;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
- IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_cryptoProvider = cryptoProvider;
@@ -104,7 +103,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
};
- var requestString = _jsonSerializer.SerializeToString(requestList);
+ var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
@@ -112,7 +111,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -123,7 +122,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
var programIdsWithImages =
@@ -479,7 +478,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(response).ConfigureAwait(false);
+ return await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ShowImages>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -508,7 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Headends>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root != null)
{
@@ -611,25 +610,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
- try
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
+ if (response.IsSuccessStatusCode)
{
- return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
+ return response;
}
- catch (HttpRequestException ex)
- {
- _tokens.Clear();
- if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
- {
- enableRetry = false;
- }
-
- if (!enableRetry)
- {
- throw;
- }
+ // Response is automatically disposed in the calling function,
+ // so dispose manually if not returning.
+ response.Dispose();
+ if (!enableRetry || (int)response.StatusCode >= 500)
+ {
+ throw new HttpRequestException(
+ string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
+ null,
+ response.StatusCode);
}
+ _tokens.Clear();
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
}
@@ -647,8 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Token>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
@@ -701,15 +701,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+ httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
catch (HttpRequestException ex)
{
- // Apparently we're supposed to swallow this
+ // SchedulesDirect returns 400 if no lineups are configured.
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
{
return false;
@@ -775,25 +776,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
var allStations = root.stations ?? new List<ScheduleDirect.Station>();
var map = root.map;
- int len = map.Count;
- var array = new List<ChannelInfo>(len);
- for (int i = 0; i < len; i++)
+ var list = new List<ChannelInfo>(map.Count);
+ foreach (var channel in map)
{
- var channelNumber = GetChannelNumber(map[i]);
+ var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase));
+ var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
if (station == null)
{
station = new ScheduleDirect.Station
{
- stationID = map[i].stationID
+ stationID = channel.stationID
};
}
@@ -810,10 +810,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
channelInfo.ImageUrl = station.logo.URL;
}
- array[i] = channelInfo;
+ list.Add(channelInfo);
}
- return array;
+ return list;
}
private static string NormalizeName(string value)
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 8c9bb6ba0..7842be716 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var programDto in currentProgramDtos)
{
- if (currentChannelsDict.TryGetValue(programDto.ChannelId, out BaseItemDto channelDto))
+ if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto))
{
channelDto.CurrentProgram = programDto;
}
@@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
info.Name = program.Name;
- info.ChannelId = programDto.ChannelId;
+ info.ChannelId = programDto.ChannelId ?? Guid.Empty;
info.ChannelName = programDto.ChannelName;
info.StartDate = program.StartDate;
info.Name = program.Name;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
new file mode 100644
index 000000000..740cbb66e
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
@@ -0,0 +1,21 @@
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ internal class Channels
+ {
+ public string GuideNumber { get; set; }
+
+ public string GuideName { get; set; }
+
+ public string VideoCodec { get; set; }
+
+ public string AudioCodec { get; set; }
+
+ public string URL { get; set; }
+
+ public bool Favorite { get; set; }
+
+ public bool DRM { get; set; }
+
+ public bool HD { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
new file mode 100644
index 000000000..09d77f838
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ internal class DiscoverResponse
+ {
+ public string FriendlyName { get; set; }
+
+ public string ModelNumber { get; set; }
+
+ public string FirmwareName { get; set; }
+
+ public string FirmwareVersion { get; set; }
+
+ public string DeviceID { get; set; }
+
+ public string DeviceAuth { get; set; }
+
+ public string BaseURL { get; set; }
+
+ public string LineupURL { get; set; }
+
+ public int TunerCount { get; set; }
+
+ public bool SupportsTranscoding
+ {
+ get
+ {
+ var model = ModelNumber ?? string.Empty;
+
+ if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index b6444b172..5ef83f274 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -8,10 +8,12 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
+ private readonly JsonSerializerOptions _jsonOptions;
+
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
public HdHomerunHost(
@@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_socketFactory = socketFactory;
_networkManager = networkManager;
_streamHelper = streamHelper;
+
+ _jsonOptions = JsonDefaults.GetOptions();
}
public string Name => "HD Homerun";
@@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private string GetChannelId(TunerHostInfo info, Channels i)
=> ChannelIdPrefix + i.GuideNumber;
- private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
+ internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
+ var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly)
@@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Id = GetChannelId(info, i),
IsFavorite = i.Favorite,
TunerHostId = info.Id,
- IsHD = i.HD == 1,
+ IsHD = i.HD,
AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec,
ChannelType = ChannelType.TV,
@@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}).Cast<ChannelInfo>().ToList();
}
- private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
+ internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{
var cacheKey = info.Id;
@@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
+ var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
@@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/');
}
- private class Channels
- {
- public string GuideNumber { get; set; }
-
- public string GuideName { get; set; }
-
- public string VideoCodec { get; set; }
-
- public string AudioCodec { get; set; }
-
- public string URL { get; set; }
-
- public bool Favorite { get; set; }
-
- public bool DRM { get; set; }
-
- public int HD { get; set; }
- }
-
protected EncodingOptions GetEncodingOptions()
{
return Config.GetConfiguration<EncodingOptions>("encoding");
@@ -674,42 +662,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- public class DiscoverResponse
- {
- public string FriendlyName { get; set; }
-
- public string ModelNumber { get; set; }
-
- public string FirmwareName { get; set; }
-
- public string FirmwareVersion { get; set; }
-
- public string DeviceID { get; set; }
-
- public string DeviceAuth { get; set; }
-
- public string BaseURL { get; set; }
-
- public string LineupURL { get; set; }
-
- public int TunerCount { get; set; }
-
- public bool SupportsTranscoding
- {
- get
- {
- var model = ModelNumber ?? string.Empty;
-
- if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return true;
- }
-
- return false;
- }
- }
- }
-
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
{
lock (_modelCache)
@@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return list;
}
- private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
+ internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
{
var hostInfo = new TunerHostInfo
{
@@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
hostInfo.DeviceId = modelInfo.DeviceID;
hostInfo.FriendlyName = modelInfo.FriendlyName;
+ hostInfo.TunerCount = modelInfo.TunerCount;
return hostInfo;
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 5667bf337..a23037af8 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -14,8 +14,8 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderAlbumArtists": "এলবাম শিল্পী",
- "Genres": "জেনার",
+ "HeaderAlbumArtists": "এলবাম শিল্পীবৃন্দ",
+ "Genres": "শৈলী",
"Folders": "ফোল্ডারগুলো",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
@@ -112,5 +112,10 @@
"TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
"TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
- "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।"
+ "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
+ "Undefined": "অসঙ্গায়িত",
+ "Forced": "জোরকরে",
+ "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন",
+ "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
+ "Default": "প্রাথমিক"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index cd64cdde4..7667612b9 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "Library",
"TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
- "TaskCleanActivityLog": "Clean Activity Log"
+ "TaskCleanActivityLog": "Clean Activity Log",
+ "Undefined": "Undefined",
+ "Forced": "Forced",
+ "Default": "Default"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index dcd30694f..03c6d5f5d 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -112,5 +112,9 @@
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación",
- "AppDeviceValues": "App: {0}, Dispositivo: {1}"
+ "AppDeviceValues": "App: {0}, Dispositivo: {1}",
+ "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
+ "TaskCleanActivityLog": "Limpiar Registro de Actividades",
+ "Undefined": "Sin definir",
+ "Forced": "Forzado"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 1986decf0..7eb8e36e7 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -113,5 +113,7 @@
"TasksChannelsCategory": "کانال‌های داخلی",
"TasksApplicationCategory": "برنامه",
"TasksLibraryCategory": "کتابخانه",
- "TasksMaintenanceCategory": "تعمیر"
+ "TasksMaintenanceCategory": "تعمیر",
+ "Forced": "اجباری",
+ "Default": "پیشفرض"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 5aa65a525..3c51d64e0 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -1,9 +1,9 @@
{
"Albums": "Albums",
- "AppDeviceValues": "Application : {0}, Appareil : {1}",
+ "AppDeviceValues": "App : {0}, Appareil : {1}",
"Application": "Application",
"Artists": "Artistes",
- "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
+ "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
"Channels": "Chaînes",
@@ -11,12 +11,12 @@
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
- "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
+ "FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderContinueWatching": "Continuer à regarder",
+ "HeaderContinueWatching": "Reprendre le visionnement",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris",
"HeaderFavoriteEpisodes": "Épisodes favoris",
@@ -26,12 +26,12 @@
"HeaderNextUp": "À Suivre",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
- "Inherit": "Hériter",
+ "Inherit": "Hérite",
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
- "Latest": "Derniers",
+ "Latest": "Plus récent",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
@@ -40,15 +40,15 @@
"Movies": "Films",
"Music": "Musique",
"MusicVideos": "Vidéos musicales",
- "NameInstallFailed": "{0} échec d'installation",
+ "NameInstallFailed": "échec d'installation de {0}",
"NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue",
- "NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.",
+ "NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible.",
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
- "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
+ "NotificationOptionCameraImageUploaded": "Image d'appareil photo transférée",
"NotificationOptionInstallationFailed": "Échec d'installation",
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
"NotificationOptionPluginError": "Erreur d'extension",
@@ -70,9 +70,9 @@
"ScheduledTaskFailedWithName": "{0} a échoué",
"ScheduledTaskStartedWithName": "{0} a commencé",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
- "Shows": "Émissions",
+ "Shows": "Séries",
"Songs": "Chansons",
- "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
+ "StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser",
@@ -80,39 +80,43 @@
"TvShows": "Séries Télé",
"User": "Utilisateur",
"UserCreatedWithName": "L'utilisateur {0} a été créé",
- "UserDeletedWithName": "L'utilisateur {0} a été supprimé",
- "UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
+ "UserDeletedWithName": "L'utilisateur {0} supprimé",
+ "UserDownloadingItemWithValues": "{0} télécharge {1}",
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
- "UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
- "UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
- "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
+ "UserOfflineFromDevice": "{0} s'est déconnecté de {1}",
+ "UserOnlineFromDevice": "{0} s'est connecté de {1}",
+ "UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié",
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
- "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
- "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
+ "UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}",
+ "UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}",
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
- "TasksLibraryCategory": "Bibliothèque",
+ "TasksLibraryCategory": "Médiathèque",
"TasksMaintenanceCategory": "Entretien",
- "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
+ "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquant sur l'internet selon la configuration des métadonnées.",
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
- "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
- "TaskRefreshChannels": "Rafraîchir des chaines",
- "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
+ "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines internet.",
+ "TaskRefreshChannels": "Rafraîchir les chaines",
+ "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage datant de plus d'un jour.",
"TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
- "TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.",
+ "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour les m.à.j. automatiques.",
"TaskUpdatePlugins": "Mise à jour des extensions",
- "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.",
- "TaskRefreshPeople": "Rafraîchir les acteurs",
- "TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
+ "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
+ "TaskRefreshPeople": "Rafraîchir les personnes",
+ "TaskCleanLogsDescription": "Supprime les journaux plus vieux que {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshLibraryDescription": "Analyse votre médiathèque pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres.",
- "TaskRefreshLibrary": "Analyser la bibliothèque de médias",
+ "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
"TasksApplicationCategory": "Application",
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
- "TasksChannelsCategory": "Canaux Internet",
- "Default": "Par défaut"
+ "TasksChannelsCategory": "Chaines Internet",
+ "Default": "Par défaut",
+ "TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
+ "TaskCleanActivityLog": "Nettoyer le journal d'activité",
+ "Undefined": "Indéfini",
+ "Forced": "Forcé"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index df68d3bbd..4cc2b378b 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -1,3 +1,30 @@
{
- "Albums": "आल्बुम्"
+ "Albums": "संग्रह",
+ "HeaderRecordingGroups": "रिकॉर्डिंग समूह",
+ "HeaderNextUp": "इसके बाद",
+ "HeaderLiveTV": "लाइव टीवी",
+ "HeaderFavoriteSongs": "पसंदीदा गीत",
+ "HeaderFavoriteShows": "पसंदीदा शोज",
+ "HeaderFavoriteEpisodes": "पसंदीदा एपिसोड्स",
+ "HeaderFavoriteArtists": "पसंदीदा कलाकारसमूह",
+ "HeaderFavoriteAlbums": "पसंदीदा एलबम्स",
+ "HeaderContinueWatching": "देखते रहिए",
+ "HeaderAlbumArtists": "एल्बम कलकरसमुह",
+ "Genres": "शैली",
+ "Forced": "बलपूर्वक",
+ "Folders": "फोल्डेरें",
+ "Favorites": "पसंदीदा",
+ "FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ है",
+ "DeviceOnlineWithName": "{0} से संयोग हो गया है",
+ "DeviceOfflineWithName": "{0} से संयोग विच्छिन्न हो गया है",
+ "Default": "प्राथमिक",
+ "Collections": "संग्रह",
+ "ChapterNameValue": "अध्याय",
+ "Channels": "चैनल",
+ "CameraImageUploadedFrom": "कैमरा से एक नया चित्र अपलोड किया गया है",
+ "Books": "किताब",
+ "AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
+ "Artists": "कलाकारों",
+ "Application": "एप्लिकेशन",
+ "AppDeviceValues": "एप: {0}, मशीन: {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 9be91b724..9eb80b83b 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -115,5 +115,8 @@
"TasksChannelsCategory": "Internet kanali",
"TasksLibraryCategory": "Biblioteka",
"TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
- "TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
+ "TaskCleanActivityLog": "Očisti dnevnik aktivnosti",
+ "Undefined": "Nedefinirano",
+ "Forced": "Forsirani",
+ "Default": "Zadano"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 02bf8496f..fa0ab8b92 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -4,7 +4,7 @@
"Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
- "Books": "ブック",
+ "Books": "ブックス",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
@@ -114,5 +114,8 @@
"TaskRefreshChapterImages": "チャプター画像を抽出する",
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
- "TaskCleanActivityLog": "アクティビティの履歴を消去"
+ "TaskCleanActivityLog": "アクティビティの履歴を消去",
+ "Undefined": "未定義",
+ "Forced": "強制",
+ "Default": "デフォルト"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 91c1fb15b..7ce9822b6 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -91,5 +91,32 @@
"UserStoppedPlayingItemWithValues": "{0} - {1} oınatýyn {2} toqtatty",
"ValueHasBeenAddedToLibrary": "{0} (tasyǵyshhanaǵa ústelindi)",
"ValueSpecialEpisodeName": "Arnaıy - {0}",
- "VersionNumber": "Nusqasy {0}"
+ "VersionNumber": "Nusqasy {0}",
+ "Default": "Ádepki",
+ "TaskDownloadMissingSubtitles": "Joq sýbtıtrlerdi júktep alý",
+ "TaskRefreshChannels": "Arnalardy jańartý",
+ "TaskCleanTranscode": "Qaıta kodtaý katalogyn tazalaý",
+ "TaskUpdatePlugins": "Plagınderdi jańartý",
+ "TaskRefreshPeople": "Adamdardy jańartý",
+ "TaskCleanLogs": "Jurnal katalogyn tazalaý",
+ "TaskRefreshLibrary": "Tasyǵyshhanany skanerleý",
+ "TaskRefreshChapterImages": "Sahna keskinderin shyǵaryp alý",
+ "TaskCleanCache": "Kesh katalogyn tazalaý",
+ "TaskCleanActivityLog": "Áreket jurnalyn tazalaý",
+ "TasksChannelsCategory": "Internet-arnalar",
+ "TasksApplicationCategory": "Qoldanba",
+ "TasksLibraryCategory": "Tasyǵyshhana",
+ "TasksMaintenanceCategory": "Qyzmet kórsetý",
+ "Undefined": "Anyqtalmady",
+ "Forced": "Májbúrli",
+ "TaskDownloadMissingSubtitlesDescription": "Metaderekter teńshelimi negіzіnde joq sýbtıtrlerdі Internetten іzdeıdі.",
+ "TaskRefreshChannelsDescription": "Internet-arnalar málimetterin jańartady.",
+ "TaskCleanTranscodeDescription": "Bіr kúnnen asqan qaıta kodtaý faıldaryn joıady.",
+ "TaskUpdatePluginsDescription": "Avtomatty túrde jańartýǵa teńshelgen plagınder úshin jańartýlardy júktep alady jáne ornatady.",
+ "TaskRefreshPeopleDescription": "Tasyǵyshhanadaǵy aktórler men rejısórler metaderekterіn jańartady.",
+ "TaskCleanLogsDescription": "{0} kúnnen asqan jurnal faıldaryn joıady.",
+ "TaskRefreshLibraryDescription": "Tasyǵyshhanadaǵy jańa faıldardy skanerleıdі jáne metaderekterdі jańartady.",
+ "TaskRefreshChapterImagesDescription": "Sahnalarǵa bólіngen beıneler úshіn nobaılar jasaıdy.",
+ "TaskCleanCacheDescription": "Júıede qajet emes keshtelgen faıldardy joıady.",
+ "TaskCleanActivityLogDescription": "Áreketter jurnalyndaǵy teńshelgen jasynan asqan jazbalaly joıady."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 5e3acfbe9..a46bdc3de 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -15,9 +15,9 @@
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
"LabelRunningTimeValue": "Garums: {0}",
"Inherit": "Mantot",
- "AppDeviceValues": "Lietotne:{0}, Ierīce:{1}",
+ "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
- "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots tavai multvides bibliotēkai",
+ "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
"UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
@@ -95,7 +95,7 @@
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
- "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus pēc metadatu uzstādījumiem.",
+ "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
"TaskRefreshChannels": "Atjaunot Kanālus",
@@ -103,14 +103,19 @@
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
"TaskUpdatePlugins": "Atjaunot Paplašinājumus",
- "TaskRefreshPeopleDescription": "Atjauno metadatus priekš aktieriem un direktoriem tavā mediju bibliotēkā.",
+ "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
"TaskRefreshPeople": "Atjaunot Cilvēkus",
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
- "TaskRefreshLibraryDescription": "Skenē tavas mediju bibliotēkas priekš jaunām datnēm un atjauno metadatus.",
- "TaskRefreshLibrary": "Skanēt Mediju Bibliotēku",
+ "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
+ "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
"TasksChannelsCategory": "Interneta Kanāli",
- "TasksMaintenanceCategory": "Apkope"
+ "TasksMaintenanceCategory": "Apkope",
+ "Forced": "Piespiests",
+ "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
+ "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
+ "Undefined": "Nenoteikts",
+ "Default": "Noklusējums"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index b6672a554..1e80d0b5f 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,7 +1,7 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Programma",
+ "Application": "Applicatie",
"Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
"Books": "Boeken",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index ca6172fce..03d30247a 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -96,7 +96,7 @@
"TaskRefreshChannels": "Обновление каналов",
"TaskCleanTranscode": "Очистка каталога перекодировки",
"TaskUpdatePlugins": "Обновление плагинов",
- "TaskRefreshPeople": "Обновление метаданных людей",
+ "TaskRefreshPeople": "Подновить людей",
"TaskCleanLogs": "Очистка каталога журналов",
"TaskRefreshLibrary": "Сканирование медиатеки",
"TaskRefreshChapterImages": "Извлечение изображений сцен",
@@ -109,7 +109,7 @@
"TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.",
"TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.",
"TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.",
- "TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.",
+ "TaskRefreshPeopleDescription": "Обновляются метаданные для актёров и режиссёров в медиатеке.",
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 66681f025..343e067b7 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -113,5 +113,10 @@
"TasksApplicationCategory": "Aplikacija",
"TasksLibraryCategory": "Knjižnica",
"TasksMaintenanceCategory": "Vzdrževanje",
- "TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu."
+ "TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu.",
+ "TaskCleanActivityLogDescription": "Počisti zapise v dnevniku aktivnosti starejše od nastavljenega časa.",
+ "TaskCleanActivityLog": "Počisti dnevnik aktivnosti",
+ "Undefined": "Nedoločen",
+ "Forced": "Prisilno",
+ "Default": "Privzeto"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 8da92f309..d785bcb90 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -66,7 +66,7 @@
"Inherit": "Наследи",
"HomeVideos": "Кућни видео",
"HeaderRecordingGroups": "Групе снимања",
- "HeaderNextUp": "Следеће горе",
+ "HeaderNextUp": "Следи",
"HeaderLiveTV": "ТВ уживо",
"HeaderFavoriteSongs": "Омиљене песме",
"HeaderFavoriteShows": "Омиљене серије",
@@ -79,17 +79,17 @@
"Folders": "Фасцикле",
"Favorites": "Омиљено",
"FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
- "DeviceOnlineWithName": "{0} се повезао",
+ "DeviceOnlineWithName": "{0} је повезан",
"DeviceOfflineWithName": "{0} је прекинуо везу",
"Collections": "Колекције",
"ChapterNameValue": "Поглавље {0}",
"Channels": "Канали",
- "CameraImageUploadedFrom": "Нова фотографија је послата са {0}",
+ "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге",
"AuthenticationSucceededWithUserName": "{0} успешно проверено",
- "Artists": "Извођач",
+ "Artists": "Извођачи",
"Application": "Апликација",
- "AppDeviceValues": "Апл: {0}, уређај: {1}",
+ "AppDeviceValues": "Апликација: {0}, Уређај: {1}",
"Albums": "Албуми",
"TaskDownloadMissingSubtitlesDescription": "Претражује интернет за недостајуће титлове на основу конфигурације метаподатака.",
"TaskDownloadMissingSubtitles": "Преузмите недостајуће титлове",
@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Брише логове старије од {0} дана.",
"TaskCleanLogs": "Очистите директоријум логова",
"TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.",
- "TaskRefreshLibrary": "Скенирај Библиотеку Медија",
+ "TaskRefreshLibrary": "Скенирај библиотеку медија",
"TaskRefreshChapterImagesDescription": "Ствара сличице за видео записе који имају поглавља.",
"TaskRefreshChapterImages": "Издвоји слике из поглавља",
"TaskCleanCacheDescription": "Брише Кеш фајлове који више нису потребни систему.",
@@ -114,5 +114,8 @@
"TasksLibraryCategory": "Библиотека",
"TasksMaintenanceCategory": "Одржавање",
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
- "TaskCleanActivityLog": "Очисти историју активности"
+ "TaskCleanActivityLog": "Очисти историју активности",
+ "Undefined": "Недефинисано",
+ "Forced": "Форсирано",
+ "Default": "Подразумевано"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 30aaf3a05..3f9e22106 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -5,7 +5,9 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Text.Json;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -24,7 +26,6 @@ namespace Emby.Server.Implementations.Localization
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
private readonly IServerConfigurationManager _configurationManager;
- private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<LocalizationManager> _logger;
private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
@@ -35,19 +36,18 @@ namespace Emby.Server.Implementations.Localization
private List<CultureDto> _cultures;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
public LocalizationManager(
IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
ILogger<LocalizationManager> logger)
{
_configurationManager = configurationManager;
- _jsonSerializer = jsonSerializer;
_logger = logger;
}
@@ -179,8 +179,11 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
- => _jsonSerializer.DeserializeFromStream<IEnumerable<CountryInfo>>(
- _assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+ {
+ StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+
+ return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
+ }
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
@@ -344,7 +347,7 @@ namespace Emby.Server.Implementations.Localization
// If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
if (stream != null)
{
- var dict = await _jsonSerializer.DeserializeFromStreamAsync<Dictionary<string, string>>(stream).ConfigureAwait(false);
+ var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
foreach (var key in dict.Keys)
{
diff --git a/Emby.Server.Implementations/Properties/AssemblyInfo.cs b/Emby.Server.Implementations/Properties/AssemblyInfo.cs
index a1933f66e..cb7972173 100644
--- a/Emby.Server.Implementations/Properties/AssemblyInfo.cs
+++ b/Emby.Server.Implementations/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 3a9e28458..29440b64a 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -4,13 +4,15 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -21,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class ScheduledTaskWorker : IScheduledTaskWorker
{
- /// <summary>
- /// Gets or sets the json serializer.
- /// </summary>
- /// <value>The json serializer.</value>
- private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Gets or sets the application paths.
@@ -70,12 +67,16 @@ namespace Emby.Server.Implementations.ScheduledTasks
private string _id;
/// <summary>
+ /// The options for the json Serializer.
+ /// </summary>
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+
+ /// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
/// </summary>
/// <param name="scheduledTask">The scheduled task.</param>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="taskManager">The task manager.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
/// <exception cref="ArgumentNullException">
/// scheduledTask
@@ -88,7 +89,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// or
/// logger.
/// </exception>
- public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
+ public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger)
{
if (scheduledTask == null)
{
@@ -105,11 +106,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentNullException(nameof(taskManager));
}
- if (jsonSerializer == null)
- {
- throw new ArgumentNullException(nameof(jsonSerializer));
- }
-
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
@@ -118,7 +114,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTask = scheduledTask;
_applicationPaths = applicationPaths;
_taskManager = taskManager;
- _jsonSerializer = jsonSerializer;
_logger = logger;
InitTriggerEvents();
@@ -150,7 +145,15 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- _lastExecutionResult = _jsonSerializer.DeserializeFromFile<TaskResult>(path);
+ var jsonString = File.ReadAllText(path, Encoding.UTF8);
+ if (!string.IsNullOrWhiteSpace(jsonString))
+ {
+ _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(jsonString, _jsonOptions);
+ }
+ else
+ {
+ _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);
+ }
}
catch (Exception ex)
{
@@ -174,7 +177,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
- _jsonSerializer.SerializeToFile(value, path);
+ using FileStream createStream = File.OpenWrite(path);
+ JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
}
}
}
@@ -537,7 +541,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
TaskTriggerInfo[] list = null;
if (File.Exists(path))
{
- list = _jsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path);
+ var jsonString = File.ReadAllText(path, Encoding.UTF8);
+ list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(jsonString, _jsonOptions);
}
// Return defaults if file doesn't exist.
@@ -573,7 +578,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(triggers, path);
+ var json = JsonSerializer.Serialize(triggers, _jsonOptions);
+ File.WriteAllText(path, json, Encoding.UTF8);
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index cfbf03ddc..af316e108 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -19,6 +18,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class TaskManager : ITaskManager
{
public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+
public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
/// <summary>
@@ -33,7 +33,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
new ConcurrentQueue<Tuple<Type, TaskOptions>>();
- private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger<TaskManager> _logger;
@@ -41,15 +40,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Initializes a new instance of the <see cref="TaskManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
public TaskManager(
IApplicationPaths applicationPaths,
- IJsonSerializer jsonSerializer,
ILogger<TaskManager> logger)
{
_applicationPaths = applicationPaths;
- _jsonSerializer = jsonSerializer;
_logger = logger;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
@@ -196,7 +192,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="tasks">The tasks.</param>
public void AddTasks(IEnumerable<IScheduledTask> tasks)
{
- var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger));
+ var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger));
ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
}
diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs
deleted file mode 100644
index 5ec3a735a..000000000
--- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs
+++ /dev/null
@@ -1,281 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Serialization;
-
-namespace Emby.Server.Implementations.Serialization
-{
- /// <summary>
- /// Provides a wrapper around third party json serialization.
- /// </summary>
- public class JsonSerializer : IJsonSerializer
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="JsonSerializer" /> class.
- /// </summary>
- public JsonSerializer()
- {
- ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
- ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
- ServiceStack.Text.JsConfig.IncludeNullValues = false;
- ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
- ServiceStack.Text.JsConfig.AssumeUtc = true;
-
- ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid;
- }
-
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- public void SerializeToStream(object obj, Stream stream)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- ServiceStack.Text.JsonSerializer.SerializeToStream(obj, obj.GetType(), stream);
- }
-
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- public void SerializeToStream<T>(T obj, Stream stream)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- ServiceStack.Text.JsonSerializer.SerializeToStream<T>(obj, stream);
- }
-
- /// <summary>
- /// Serializes to file.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="file">The file.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- public void SerializeToFile(object obj, string file)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- if (string.IsNullOrEmpty(file))
- {
- throw new ArgumentNullException(nameof(file));
- }
-
- using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
- {
- SerializeToStream(obj, stream);
- }
- }
-
- private static Stream OpenFile(string path)
- {
- return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072);
- }
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <param name="file">The file.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">type</exception>
- public object DeserializeFromFile(Type type, string file)
- {
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- if (string.IsNullOrEmpty(file))
- {
- throw new ArgumentNullException(nameof(file));
- }
-
- using (var stream = OpenFile(file))
- {
- return DeserializeFromStream(stream, type);
- }
- }
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="file">The file.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">file</exception>
- public T DeserializeFromFile<T>(string file)
- where T : class
- {
- if (string.IsNullOrEmpty(file))
- {
- throw new ArgumentNullException(nameof(file));
- }
-
- using (var stream = OpenFile(file))
- {
- return DeserializeFromStream<T>(stream);
- }
- }
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="stream">The stream.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- public T DeserializeFromStream<T>(Stream stream)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
- }
-
- public Task<T> DeserializeFromStreamAsync<T>(Stream stream)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromStreamAsync<T>(stream);
- }
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="text">The text.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">text</exception>
- public T DeserializeFromString<T>(string text)
- {
- if (string.IsNullOrEmpty(text))
- {
- throw new ArgumentNullException(nameof(text));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(text);
- }
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <param name="stream">The stream.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- public object DeserializeFromStream(Stream stream, Type type)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
- }
-
- public async Task<object> DeserializeFromStreamAsync(Stream stream, Type type)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- using (var reader = new StreamReader(stream))
- {
- var json = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type);
- }
- }
-
- private static string SerializeGuid(Guid guid)
- {
- if (guid.Equals(Guid.Empty))
- {
- return null;
- }
-
- return guid.ToString("N", CultureInfo.InvariantCulture);
- }
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <param name="json">The json.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">json</exception>
- public object DeserializeFromString(string json, Type type)
- {
- if (string.IsNullOrEmpty(json))
- {
- throw new ArgumentNullException(nameof(json));
- }
-
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type);
- }
-
- /// <summary>
- /// Serializes to string.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">obj</exception>
- public string SerializeToString(object obj)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- return ServiceStack.Text.JsonSerializer.SerializeToString(obj, obj.GetType());
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index b3965fcca..885f65c64 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionActivity;
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> SessionControllerConnected;
+
/// <summary>
/// Gets all connections.
/// </summary>
@@ -313,6 +316,19 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
+ public void OnSessionControllerConnected(SessionInfo info)
+ {
+ EventHelper.QueueEventIfNotNull(
+ SessionControllerConnected,
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = info
+ },
+ _logger);
+ }
+
+ /// <inheritdoc />
public void CloseIfNeeded(SessionInfo session)
{
if (!session.SessionControllers.Any(i => i.IsSessionActive))
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 169eaefd8..39c369a01 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection);
+
+ _sessionManager.OnSessionControllerConnected(session);
}
/// <summary>
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 348213ee1..aee959c53 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -42,6 +42,12 @@ namespace Emby.Server.Implementations.SyncPlay
private readonly ILibraryManager _libraryManager;
/// <summary>
+ /// The map between users and counter of active sessions.
+ /// </summary>
+ private readonly ConcurrentDictionary<Guid, int> _activeUsers =
+ new ConcurrentDictionary<Guid, int>();
+
+ /// <summary>
/// The map between sessions and groups.
/// </summary>
private readonly ConcurrentDictionary<string, Group> _sessionToGroupMap =
@@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
- _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
+ _sessionManager.SessionControllerConnected += OnSessionControllerConnected;
}
/// <inheritdoc />
@@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!");
}
+ UpdateSessionsCounter(session.UserId, 1);
group.CreateGroup(session, request, cancellationToken);
}
}
@@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (existingGroup.GroupId.Equals(request.GroupId))
{
// Restore session.
+ UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken);
return;
}
@@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!");
}
+ UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken);
}
}
@@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not remove session from group!");
}
+ UpdateSessionsCounter(session.UserId, -1);
group.SessionLeave(session, request, cancellationToken);
if (group.IsGroupEmpty())
@@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay
}
}
+ /// <inheritdoc />
+ public bool IsUserActive(Guid userId)
+ {
+ if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
+ {
+ return sessionsCounter > 0;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
@@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
- _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
_disposed = true;
}
- private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
+ private void OnSessionControllerConnected(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
JoinGroup(session, request, CancellationToken.None);
}
}
+
+ private void UpdateSessionsCounter(Guid userId, int toAdd)
+ {
+ // Update sessions counter.
+ var newSessionsCounter = _activeUsers.AddOrUpdate(
+ userId,
+ 1,
+ (key, sessionsCounter) => sessionsCounter + toAdd);
+
+ // Should never happen.
+ if (newSessionsCounter < 0)
+ {
+ throw new InvalidOperationException("Sessions counter is negative!");
+ }
+
+ // Clean record if user has no more active sessions.
+ if (newSessionsCounter == 0)
+ {
+ _activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 447c587f9..f0734340b 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -75,8 +75,7 @@ namespace Emby.Server.Implementations.TV
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
- .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes).Contains(i.Id))
.ToArray();
}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index ef346dd5d..ae2fa3ce1 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -12,7 +12,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
-using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@@ -190,6 +189,22 @@ namespace Emby.Server.Implementations.Updates
continue;
}
+ for (var i = package.versions.Count - 1; i >= 0; i--)
+ {
+ // Remove versions with a target abi that is greater then the current application version.
+ if (Version.TryParse(package.versions[i].targetAbi, out var targetAbi)
+ && _applicationHost.ApplicationVersion < targetAbi)
+ {
+ package.versions.RemoveAt(i);
+ }
+ }
+
+ // Don't add a package that doesn't have any compatible versions.
+ if (package.versions.Count == 0)
+ {
+ continue;
+ }
+
var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
if (existing != null)
{
@@ -407,6 +422,7 @@ namespace Emby.Server.Implementations.Updates
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
index b5932ea6b..b898ac76c 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
@@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary>
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
{
+ private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
/// </summary>
+ /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler(
+ ISyncPlayManager syncPlayManager,
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
+ _syncPlayManager = syncPlayManager;
_userManager = userManager;
}
@@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
var userId = ClaimHelpers.GetUserId(context.User);
var user = _userManager.GetUserById(userId!.Value);
- if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess)
- || user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
+ if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{
- context.Succeed(requirement);
+ if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
+ || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
+ || _syncPlayManager.IsUserActive(userId!.Value))
+ {
+ context.Succeed(requirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+ }
+ else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
+ {
+ if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups)
+ {
+ context.Succeed(requirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+ }
+ else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
+ {
+ if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
+ || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups)
+ {
+ context.Succeed(requirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+ }
+ else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
+ {
+ if (_syncPlayManager.IsUserActive(userId!.Value))
+ {
+ context.Succeed(requirement);
+ }
+ else
+ {
+ context.Fail();
+ }
}
else
{
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
index 7fcaf69f6..6fab4c0ad 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
@@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary>
- /// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param>
- public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess)
+ /// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
+ public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
{
RequiredAccess = requiredAccess;
}
/// <summary>
- /// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
- /// </summary>
- public SyncPlayAccessRequirement()
- {
- RequiredAccess = null;
- }
-
- /// <summary>
/// Gets the required SyncPlay access.
/// </summary>
- public SyncPlayAccess? RequiredAccess { get; }
+ public SyncPlayAccessRequirementType RequiredAccess { get; }
}
}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index b35ceea1a..632dedb3c 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
/// <summary>
- /// Policy name for requiring access to SyncPlay.
+ /// Policy name for accessing SyncPlay.
/// </summary>
- public const string SyncPlayAccess = "SyncPlayAccess";
+ public const string SyncPlayHasAccess = "SyncPlayHasAccess";
/// <summary>
- /// Policy name for requiring group creation access to SyncPlay.
+ /// Policy name for creating a SyncPlay group.
/// </summary>
- public const string SyncPlayCreateGroupAccess = "SyncPlayCreateGroupAccess";
+ public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
+
+ /// <summary>
+ /// Policy name for joining a SyncPlay group.
+ /// </summary>
+ public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
+
+ /// <summary>
+ /// Policy name for accessing a SyncPlay group.
+ /// </summary>
+ public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
}
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 8b8f63015..87b4577b6 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
@@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers
public class DisplayPreferencesController : BaseJellyfinApiController
{
private readonly IDisplayPreferencesManager _displayPreferencesManager;
+ private readonly ILogger<DisplayPreferencesController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
/// </summary>
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
- public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
+ /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
+ public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
{
_displayPreferencesManager = displayPreferencesManager;
+ _logger = logger;
}
/// <summary>
@@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers
{
Client = displayPreferences.Client,
Id = displayPreferences.ItemId.ToString(),
- ViewType = itemPreferences.ViewType.ToString(),
SortBy = itemPreferences.SortBy,
SortOrder = itemPreferences.SortOrder,
IndexBy = displayPreferences.IndexBy?.ToString(),
@@ -77,16 +80,12 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
}
- foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
- {
- dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
- }
-
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+ dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
// Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
@@ -189,10 +188,9 @@ namespace Jellyfin.Api.Controllers
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
- if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId))
+ if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
{
- var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client);
- itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
+ _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
}
}
@@ -204,11 +202,6 @@ namespace Jellyfin.Api.Controllers
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
itemPrefs.ItemId = itemId;
- if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
- {
- itemPrefs.ViewType = viewType;
- }
-
// Set all remaining custom preferences.
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges();
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 4fd9c2fbf..694d16ad9 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Description xml returned.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{
- var url = GetAbsoluteUri();
- var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
- var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
- return Ok(xml);
+ if (DlnaEntryPoint.Enabled)
+ {
+ var url = GetAbsoluteUri();
+ var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
+ var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
+ return Ok(xml);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna content directory returned.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{
- return Ok(_contentDirectory.GetServiceXml());
+ if (DlnaEntryPoint.Enabled)
+ {
+ return Ok(_contentDirectory.GetServiceXml());
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{
- return Ok(_mediaReceiverRegistrar.GetServiceXml());
+ if (DlnaEntryPoint.Enabled)
+ {
+ return Ok(_mediaReceiverRegistrar.GetServiceXml());
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{
- return Ok(_connectionManager.GetServiceXml());
+ if (DlnaEntryPoint.Enabled)
+ {
+ return Ok(_connectionManager.GetServiceXml());
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{
- return ProcessEventRequest(_mediaReceiverRegistrar);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return ProcessEventRequest(_mediaReceiverRegistrar);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{
- return ProcessEventRequest(_contentDirectory);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return ProcessEventRequest(_contentDirectory);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{
- return ProcessEventRequest(_connectionManager);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return ProcessEventRequest(_connectionManager);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <param name="fileName">The icon filename.</param>
+ /// <response code="200">Request processed.</response>
+ /// <response code="404">Not Found.</response>
+ /// <response code="503">DLNA is disabled.</response>
/// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile]
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{
- return GetIconInternal(fileName);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return GetIconInternal(fileName);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns>
+ /// <response code="200">Request processed.</response>
+ /// <response code="404">Not Found.</response>
+ /// <response code="503">DLNA is disabled.</response>
[HttpGet("icons/{fileName}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile]
public ActionResult GetIcon([FromRoute, Required] string fileName)
{
- return GetIconInternal(fileName);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return GetIconInternal(fileName);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
private ActionResult GetIconInternal(string fileName)
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 65de81d7a..c606d327c 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -98,7 +98,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
@@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to delete the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to delete the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
@@ -325,9 +325,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
+ await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -358,9 +360,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
+ await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 7e9035f80..b84136ac6 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -254,18 +254,18 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { "Playlist" };
}
- bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
+ var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
+
+ bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled
- || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
+ || Array.IndexOf(enabledChannels, item.Id) != -1
// Assume all items inside an EnabledChannel are enabled
- || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
+ || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
- if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
- collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
- StringComparer.OrdinalIgnoreCase))
+ if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
{
isInEnabledFolder = true;
}
@@ -786,12 +786,12 @@ namespace Jellyfin.Api.Controllers
var ancestorIds = Array.Empty<Guid>();
- var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
+ var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !excludeFolderIds.Contains(i.Id))
.Select(i => i.Id)
.ToArray();
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 184843b39..28d359ac3 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
}
// TODO determine non-ASCII validity.
- return PhysicalFile(path, MimeTypes.GetMimeType(path));
+ return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
}
/// <summary>
@@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers
{
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
- IsMovie = isMovie,
- IsSeries = isSeries,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 56d4b3933..6f2d43227 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Set channel mappings.
/// </summary>
- /// <param name="providerId">Provider id.</param>
- /// <param name="tunerChannelId">Tuner channel id.</param>
- /// <param name="providerChannelId">Provider channel id.</param>
+ /// <param name="setChannelMappingDto">The set channel mapping dto.</param>
/// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping(
- [FromQuery] string? providerId,
- [FromQuery] string? tunerChannelId,
- [FromQuery] string? providerChannelId)
+ public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
{
- return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false);
+ return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index a76dc057a..baa2e0636 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
@@ -119,7 +120,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableTranscoding,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
- [FromBody] PlaybackInfoDto? playbackInfoDto)
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
@@ -258,24 +259,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels,
[FromQuery] Guid? itemId,
- [FromBody] OpenLiveStreamDto openLiveStreamDto,
- [FromQuery] bool enableDirectPlay = true,
- [FromQuery] bool enableDirectStream = true)
+ [FromBody] OpenLiveStreamDto? openLiveStreamDto,
+ [FromQuery] bool? enableDirectPlay,
+ [FromQuery] bool? enableDirectStream)
{
var request = new LiveStreamRequest
{
- OpenToken = openToken,
- UserId = userId ?? Guid.Empty,
- PlaySessionId = playSessionId,
- MaxStreamingBitrate = maxStreamingBitrate,
- StartTimeTicks = startTimeTicks,
- AudioStreamIndex = audioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex,
- MaxAudioChannels = maxAudioChannels,
- ItemId = itemId ?? Guid.Empty,
+ OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
+ UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
+ PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
+ MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
+ StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
+ AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
+ MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
+ ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
DeviceProfile = openLiveStreamDto?.DeviceProfile,
- EnableDirectPlay = enableDirectPlay,
- EnableDirectStream = enableDirectStream,
+ EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
+ EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
};
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 3e55434c0..fcdad4bc7 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@@ -17,6 +18,7 @@ using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.Controllers
{
@@ -53,6 +55,13 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Creates a new playlist.
/// </summary>
+ /// <remarks>
+ /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+ /// </remarks>
+ /// <param name="name">The playlist name.</param>
+ /// <param name="ids">The item ids.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="mediaType">The media type.</param>
/// <param name="createPlaylistRequest">The create playlist payload.</param>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
@@ -61,14 +70,23 @@ namespace Jellyfin.Api.Controllers
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
- [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
+ [FromQuery] string? name,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? mediaType,
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{
+ if (ids.Count == 0)
+ {
+ ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
+ }
+
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
- Name = createPlaylistRequest.Name,
- ItemIdList = createPlaylistRequest.Ids,
- UserId = createPlaylistRequest.UserId,
- MediaType = createPlaylistRequest.MediaType
+ Name = name ?? createPlaylistRequest?.Name,
+ ItemIdList = ids,
+ UserId = userId ?? createPlaylistRequest?.UserId ?? default,
+ MediaType = mediaType ?? createPlaylistRequest?.MediaType
}).ConfigureAwait(false);
return result;
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 73da2f906..4ac849181 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
{
if (_quickConnect.State == QuickConnectState.Unavailable)
{
- return Forbid("Quick connect is unavailable");
+ return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
}
_quickConnect.Activate();
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue)
{
- return Forbid("Unknown user id");
+ return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
}
return _quickConnect.AuthorizeRequest(userId.Value, code);
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 471c9180d..82cbe58df 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// The sync play controller.
/// </summary>
- [Authorize(Policy = Policies.SyncPlayAccess)]
+ [Authorize(Policy = Policies.SyncPlayHasAccess)]
public class SyncPlayController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
+ [Authorize(Policy = Policies.SyncPlayCreateGroup)]
public ActionResult SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData)
{
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayAccess)]
+ [Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData)
{
@@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Leave")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayLeaveGroup()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.SyncPlayAccess)]
+ [Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData)
{
@@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData)
{
@@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
@@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData)
{
@@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData)
{
@@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayUnpause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayStop()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData)
{
@@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData)
{
@@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData)
{
@@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData)
{
@@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData)
{
@@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData)
{
@@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData)
{
@@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData)
{
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 7784e8a11..e67a27ae3 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
+using System.Net;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
@@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo()
{
- return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
+ return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
/// <summary>
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{
- return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
+ return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
/// <summary>
@@ -202,7 +203,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);
- return File(stream, "text/plain");
+ return File(stream, "text/plain; charset=utf-8");
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 9805b84b1..0f0bee4bc 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -133,11 +133,11 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteUser([FromRoute, Required] Guid userId)
+ public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
_sessionManager.RevokeUserTokens(user.Id, null);
- _userManager.DeleteUser(userId);
+ await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
return NoContent();
}
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
{
- return Forbid("Only sha1 password is not allowed.");
+ return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
}
// Password should always be null
@@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword(
[FromRoute, Required] Guid userId,
- [FromBody] UpdateUserPassword request)
+ [FromBody, Required] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the password.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
var user = _userManager.GetUserById(userId);
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
if (success == null)
{
- return Forbid("Invalid user or password entered.");
+ return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
}
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
- [FromBody] UpdateUserEasyPassword request)
+ [FromBody, Required] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the easy password.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
var user = _userManager.GetUserById(userId);
@@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser(
[FromRoute, Required] Guid userId,
- [FromBody] UserDto updateUser)
+ [FromBody, Required] UserDto updateUser)
{
- if (updateUser == null)
- {
- return BadRequest();
- }
-
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{
- return Forbid("User update not allowed.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
var user = _userManager.GetUserById(userId);
@@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId,
- [FromBody] UserPolicy newPolicy)
+ [FromBody, Required] UserPolicy newPolicy)
{
- if (newPolicy == null)
- {
- return BadRequest();
- }
-
var user = _userManager.GetUserById(userId);
// If removing admin access
@@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
{
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
- return Forbid("There must be at least one user in the system with administrative access.");
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
}
}
// If disabling
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{
- return Forbid("Administrators cannot be disabled.");
+ return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
}
// If disabling
@@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
{
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{
- return Forbid("There must be at least one enabled user in the system.");
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
@@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId,
- [FromBody] UserConfiguration userConfig)
+ [FromBody, Required] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{
- return Forbid("User configuration update not allowed");
+ return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
@@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("New")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
+ public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
{
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index d8bc9df1f..44dc63952 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Merges videos into a single record.
/// </summary>
- /// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param>
+ /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
/// <response code="204">Videos merged.</response>
/// <response code="400">Supply at least 2 video ids.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
@@ -204,9 +204,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
+ public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- var items = itemIds
+ var items = ids
.Select(i => _libraryManager.GetItemById(i))
.OfType<Video>()
.OrderBy(i => i.Id)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index da6e5fa2d..f01f50cea 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -15,9 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
diff --git a/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
new file mode 100644
index 000000000..2ddaa89e8
--- /dev/null
+++ b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.LiveTvDtos
+{
+ /// <summary>
+ /// Set channel mapping dto.
+ /// </summary>
+ public class SetChannelMappingDto
+ {
+ /// <summary>
+ /// Gets or sets the provider id.
+ /// </summary>
+ [Required]
+ public string ProviderId { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the tuner channel id.
+ /// </summary>
+ [Required]
+ public string TunerChannelId { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the provider channel id.
+ /// </summary>
+ [Required]
+ public string ProviderChannelId { get; set; } = string.Empty;
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
index b0b3de855..704542326 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -11,6 +11,61 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
public class OpenLiveStreamDto
{
/// <summary>
+ /// Gets or sets the open token.
+ /// </summary>
+ public string? OpenToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ public Guid? UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play session id.
+ /// </summary>
+ public string? PlaySessionId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max streaming bitrate.
+ /// </summary>
+ public int? MaxStreamingBitrate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start time in ticks.
+ /// </summary>
+ public long? StartTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio stream index.
+ /// </summary>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle stream index.
+ /// </summary>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max audio channels.
+ /// </summary>
+ public int? MaxAudioChannels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ public Guid? ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable direct play.
+ /// </summary>
+ public bool? EnableDirectPlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enale direct stream.
+ /// </summary>
+ public bool? EnableDirectStream { get; set; }
+
+ /// <summary>
/// Gets or sets the device profile.
/// </summary>
public DeviceProfile? DeviceProfile { get; set; }
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index d0d6889fc..65d4b644e 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -24,7 +24,7 @@ namespace Jellyfin.Api.Models.PlaylistDtos
/// <summary>
/// Gets or sets the user id.
/// </summary>
- public Guid UserId { get; set; }
+ public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the media type.
diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs
index 620e82830..e2d5c7187 100644
--- a/Jellyfin.Data/Entities/ActivityLog.cs
+++ b/Jellyfin.Data/Entities/ActivityLog.cs
@@ -18,7 +18,8 @@ namespace Jellyfin.Data.Entities
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
/// <param name="userId">The user id.</param>
- public ActivityLog(string name, string type, Guid userId)
+ /// <param name="logLevel">The log level.</param>
+ public ActivityLog(string name, string type, Guid userId, LogLevel logLevel = LogLevel.Information)
{
if (string.IsNullOrEmpty(name))
{
@@ -34,7 +35,7 @@ namespace Jellyfin.Data.Entities
Type = type;
UserId = userId;
DateCreated = DateTime.UtcNow;
- LogSeverity = LogLevel.Trace;
+ LogSeverity = logLevel;
}
/// <summary>
diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
index d81e4a31c..2b25bb25f 100644
--- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
@@ -23,7 +23,6 @@ namespace Jellyfin.Data.Entities
Client = client;
SortBy = "SortName";
- ViewType = ViewType.Poster;
SortOrder = SortOrder.Ascending;
RememberSorting = false;
RememberIndexing = false;
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 6d4681914..362f3b4eb 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
EnableAutoLogin = false;
PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default;
- SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
+ SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
AddDefaultPermissions();
AddDefaultPreferences();
@@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
/// <summary>
/// Gets or sets the level of sync play permissions this user has.
/// </summary>
- public SyncPlayAccess SyncPlayAccess { get; set; }
+ public SyncPlayUserAccessType SyncPlayAccess { get; set; }
/// <summary>
/// Gets or sets the row version.
@@ -414,6 +414,44 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
+ /// Gets the user's preferences for the given preference kind.
+ /// </summary>
+ /// <param name="preference">The preference kind.</param>
+ /// <typeparam name="T">Type of preference.</typeparam>
+ /// <returns>A {T} array containing the user's preference.</returns>
+ public T[] GetPreferenceValues<T>(PreferenceKind preference)
+ {
+ var val = Preferences.First(p => p.Kind == preference).Value;
+ if (string.IsNullOrEmpty(val))
+ {
+ return Array.Empty<T>();
+ }
+
+ // Convert array of {string} to array of {T}
+ var converter = TypeDescriptor.GetConverter(typeof(T));
+ var stringValues = val.Split(Delimiter);
+ var convertedCount = 0;
+ var parsedValues = new T[stringValues.Length];
+ for (var i = 0; i < stringValues.Length; i++)
+ {
+ try
+ {
+ var parsedValue = converter.ConvertFromString(stringValues[i].Trim());
+ if (parsedValue != null)
+ {
+ parsedValues[convertedCount++] = (T)parsedValue;
+ }
+ }
+ catch (FormatException)
+ {
+ // Unable to convert value
+ }
+ }
+
+ return parsedValues[..convertedCount];
+ }
+
+ /// <summary>
/// Sets the specified preference to the given value.
/// </summary>
/// <param name="preference">The preference kind.</param>
@@ -421,7 +459,19 @@ namespace Jellyfin.Data.Entities
public void SetPreference(PreferenceKind preference, string[] values)
{
Preferences.First(p => p.Kind == preference).Value
- = string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values);
+ = string.Join(Delimiter, values);
+ }
+
+ /// <summary>
+ /// Sets the specified preference to the given value.
+ /// </summary>
+ /// <param name="preference">The preference kind.</param>
+ /// <param name="values">The values.</param>
+ /// <typeparam name="T">The type of value.</typeparam>
+ public void SetPreference<T>(PreferenceKind preference, T[] values)
+ {
+ Preferences.First(p => p.Kind == preference).Value
+ = string.Join(Delimiter, values);
}
/// <summary>
@@ -441,7 +491,7 @@ namespace Jellyfin.Data.Entities
/// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
public bool IsFolderGrouped(Guid id)
{
- return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id);
+ return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1;
}
private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
diff --git a/Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs b/Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs
new file mode 100644
index 000000000..8c3e6cb17
--- /dev/null
+++ b/Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs
@@ -0,0 +1,28 @@
+namespace Jellyfin.Data.Enums
+{
+ /// <summary>
+ /// Enum SyncPlayAccessRequirementType.
+ /// </summary>
+ public enum SyncPlayAccessRequirementType
+ {
+ /// <summary>
+ /// User must have access to SyncPlay, in some form.
+ /// </summary>
+ HasAccess = 0,
+
+ /// <summary>
+ /// User must be able to create groups.
+ /// </summary>
+ CreateGroup = 1,
+
+ /// <summary>
+ /// User must be able to join groups.
+ /// </summary>
+ JoinGroup = 2,
+
+ /// <summary>
+ /// User must be in a group.
+ /// </summary>
+ IsInGroup = 3
+ }
+}
diff --git a/Jellyfin.Data/Enums/SyncPlayAccess.cs b/Jellyfin.Data/Enums/SyncPlayUserAccessType.cs
index 8c13b37a1..030d16fb9 100644
--- a/Jellyfin.Data/Enums/SyncPlayAccess.cs
+++ b/Jellyfin.Data/Enums/SyncPlayUserAccessType.cs
@@ -1,9 +1,9 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
- /// Enum SyncPlayAccess.
+ /// Enum SyncPlayUserAccessType.
/// </summary>
- public enum SyncPlayAccess
+ public enum SyncPlayUserAccessType
{
/// <summary>
/// User can create groups and join them.
diff --git a/Jellyfin.Data/Enums/ViewType.cs b/Jellyfin.Data/Enums/ViewType.cs
index 595429ab1..c0fd7d448 100644
--- a/Jellyfin.Data/Enums/ViewType.cs
+++ b/Jellyfin.Data/Enums/ViewType.cs
@@ -1,4 +1,4 @@
-namespace Jellyfin.Data.Enums
+namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the type of view for a library or collection.
@@ -6,33 +6,108 @@
public enum ViewType
{
/// <summary>
- /// Shows banners.
+ /// Shows albums.
/// </summary>
- Banner = 0,
+ Albums = 0,
/// <summary>
- /// Shows a list of content.
+ /// Shows album artists.
/// </summary>
- List = 1,
+ AlbumArtists = 1,
/// <summary>
- /// Shows poster artwork.
+ /// Shows artists.
/// </summary>
- Poster = 2,
+ Artists = 2,
/// <summary>
- /// Shows poster artwork with a card containing the name and year.
+ /// Shows channels.
/// </summary>
- PosterCard = 3,
+ Channels = 3,
/// <summary>
- /// Shows a thumbnail.
+ /// Shows collections.
/// </summary>
- Thumb = 4,
+ Collections = 4,
/// <summary>
- /// Shows a thumbnail with a card containing the name and year.
+ /// Shows episodes.
/// </summary>
- ThumbCard = 5
+ Episodes = 5,
+
+ /// <summary>
+ /// Shows favorites.
+ /// </summary>
+ Favorites = 6,
+
+ /// <summary>
+ /// Shows genres.
+ /// </summary>
+ Genres = 7,
+
+ /// <summary>
+ /// Shows guide.
+ /// </summary>
+ Guide = 8,
+
+ /// <summary>
+ /// Shows movies.
+ /// </summary>
+ Movies = 9,
+
+ /// <summary>
+ /// Shows networks.
+ /// </summary>
+ Networks = 10,
+
+ /// <summary>
+ /// Shows playlists.
+ /// </summary>
+ Playlists = 11,
+
+ /// <summary>
+ /// Shows programs.
+ /// </summary>
+ Programs = 12,
+
+ /// <summary>
+ /// Shows recordings.
+ /// </summary>
+ Recordings = 13,
+
+ /// <summary>
+ /// Shows schedule.
+ /// </summary>
+ Schedule = 14,
+
+ /// <summary>
+ /// Shows series.
+ /// </summary>
+ Series = 15,
+
+ /// <summary>
+ /// Shows shows.
+ /// </summary>
+ Shows = 16,
+
+ /// <summary>
+ /// Shows songs.
+ /// </summary>
+ Songs = 17,
+
+ /// <summary>
+ /// Shows songs.
+ /// </summary>
+ Suggestions = 18,
+
+ /// <summary>
+ /// Shows trailers.
+ /// </summary>
+ Trailers = 19,
+
+ /// <summary>
+ /// Shows upcoming.
+ /// </summary>
+ Upcoming = 20
}
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 89d6f4d9b..572038d00 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -41,8 +41,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index ee60748c7..eab5777d5 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -435,7 +435,7 @@ namespace Jellyfin.Drawing.Skia
0f,
kernelOffset,
SKShaderTileMode.Clamp,
- false);
+ true);
canvas.DrawBitmap(
source,
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 0e94f87f6..e9f9aad57 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Text.RegularExpressions;
using SkiaSharp;
namespace Jellyfin.Drawing.Skia
@@ -118,6 +119,16 @@ namespace Jellyfin.Drawing.Skia
};
canvas.DrawRect(0, 0, width, height, paintColor);
+ var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
+
+ // use the system fallback to find a typeface for the given CJK character
+ var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
+ var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
+ if (!string.IsNullOrEmpty(filteredName))
+ {
+ typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
+ }
+
// draw library name
var textPaint = new SKPaint
{
@@ -125,7 +136,7 @@ namespace Jellyfin.Drawing.Skia
Style = SKPaintStyle.Fill,
TextSize = 112,
TextAlign = SKTextAlign.Center,
- Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright),
+ Typeface = typeFace,
IsAntialias = true
};
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index df420f48a..792e57f6a 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -28,6 +28,16 @@ namespace Jellyfin.Networking.Configuration
public bool RequireHttps { get; set; }
/// <summary>
+ /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
+ /// </summary>
+ public string CertificatePath { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
+ /// </summary>
+ public string CertificatePassword { get; set; } = string.Empty;
+
+ /// <summary>
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
/// </summary>
public string BaseUrl
@@ -83,7 +93,7 @@ namespace Jellyfin.Networking.Configuration
/// </summary>
/// <remarks>
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
- /// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>.
+ /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
/// </remarks>
public bool EnableHttps { get; set; }
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index a76ba49b6..60b899519 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -913,15 +913,6 @@ namespace Jellyfin.Networking.Manager
{
string[] lanAddresses = config.LocalNetworkAddresses;
- // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334
-
- if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1)
- {
- lanAddresses = lanAddresses[0].Split(',');
- }
-
- // TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334
-
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces)
{
@@ -1314,9 +1305,7 @@ namespace Jellyfin.Networking.Manager
return true;
}
- // Have to return something, so return an internal address
-
- _logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source);
+ _logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source);
return false;
}
}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 7bde4f35b..27360afb0 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Activity
}
/// <inheritdoc/>
- public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+ public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
/// <inheritdoc/>
public async Task CreateAsync(ActivityLog entry)
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index ec4a76e7f..0340248bb 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -86,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string GetPlaybackNotificationType(string mediaType)
+ private static string? GetPlaybackNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index a0bad29e9..1648b1b47 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -94,7 +94,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string GetPlaybackStoppedNotificationType(string mediaType)
+ private static string? GetPlaybackStoppedNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index e663798da..9e4a2065f 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -5,6 +5,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -25,11 +26,11 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 7f3f83749..39f842354 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index 662b4bf65..6a78e7ee6 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Linq;
using System.Text;
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 334f27f85..9cc1c3e5e 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
index 1fb89c4a6..dbba80c21 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
@@ -1,5 +1,4 @@
-#nullable enable
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
index 5f32479e1..c4e4c460a 100644
--- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Authentication;
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index f684d151d..d1de5408c 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -1,5 +1,4 @@
-#nullable enable
-#pragma warning disable CA1307
+#pragma warning disable CA1307
using System;
using System.Collections.Concurrent;
@@ -220,7 +219,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void DeleteUser(Guid userId)
+ public async Task DeleteUserAsync(Guid userId)
{
if (!_users.TryGetValue(userId, out var user))
{
@@ -246,7 +245,7 @@ namespace Jellyfin.Server.Implementations.Users
nameof(userId));
}
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
// Clear all entities related to the user from the database.
if (user.ProfileImage != null)
@@ -258,10 +257,10 @@ namespace Jellyfin.Server.Implementations.Users
dbContext.RemoveRange(user.Preferences);
dbContext.RemoveRange(user.AccessSchedules);
dbContext.Users.Remove(user);
- dbContext.SaveChanges();
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
_users.Remove(userId);
- _eventManager.Publish(new UserDeletedEventArgs(user));
+ await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -376,14 +375,14 @@ namespace Jellyfin.Server.Implementations.Users
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
- EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(),
+ EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
- EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(),
+ EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
SyncPlayAccess = user.SyncPlayAccess,
- BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(),
- BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(),
- BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
+ BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
+ BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
+ BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
}
};
}
@@ -704,13 +703,11 @@ namespace Jellyfin.Server.Implementations.Users
}
// TODO: fix this at some point
- user.SetPreference(
- PreferenceKind.BlockUnratedItems,
- policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
+ user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
diff --git a/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs b/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs
index 8a510898b..a9a18c823 100644
--- a/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs
+++ b/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs
@@ -13,9 +13,9 @@ namespace Jellyfin.Server.Implementations.ValueConverters
/// </summary>
/// <param name="kind">The kind to specify.</param>
/// <param name="mappingHints">The mapping hints.</param>
- public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null)
+ public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints? mappingHints = null)
: base(v => v.ToUniversalTime(), v => DateTime.SpecifyKind(v, kind), mappingHints)
{
}
}
-} \ No newline at end of file
+}
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 6bf6f383f..88e2b4152 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -107,5 +107,28 @@ namespace Jellyfin.Server.Extensions
{
return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
}
+
+ /// <summary>
+ /// Adds robots.txt redirection to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseRobotsRedirection(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds /emby and /mediabrowser route trimming to the application pipeline.
+ /// </summary>
+ /// <remarks>
+ /// This must be injected before any path related middleware.
+ /// </remarks>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>();
+ }
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 74e7bb4b1..cd594b5c5 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -24,6 +24,7 @@ using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -127,18 +128,32 @@ namespace Jellyfin.Server.Extensions
policy.AddRequirements(new RequiresElevationRequirement());
});
options.AddPolicy(
- Policies.SyncPlayAccess,
+ Policies.SyncPlayHasAccess,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups));
+ policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
});
options.AddPolicy(
- Policies.SyncPlayCreateGroupAccess,
+ Policies.SyncPlayCreateGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.CreateAndJoinGroups));
+ policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
+ });
+ options.AddPolicy(
+ Policies.SyncPlayJoinGroup,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
+ });
+ options.AddPolicy(
+ Policies.SyncPlayIsInGroup,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
});
});
}
@@ -169,11 +184,19 @@ namespace Jellyfin.Server.Extensions
.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
- for (var i = 0; i < knownProxies.Count; i++)
+ if (knownProxies.Count == 0)
{
- if (IPAddress.TryParse(knownProxies[i], out var address))
+ options.KnownNetworks.Clear();
+ options.KnownProxies.Clear();
+ }
+ else
+ {
+ for (var i = 0; i < knownProxies.Count; i++)
{
- options.KnownProxies.Add(address);
+ if (IPHost.TryParse(knownProxies[i], out var host))
+ {
+ options.KnownProxies.Add(host.Address);
+ }
}
}
})
diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs
index 7ad9466c1..eae9a8004 100644
--- a/Jellyfin.Server/Filters/FileResponseFilter.cs
+++ b/Jellyfin.Server/Filters/FileResponseFilter.cs
@@ -14,7 +14,8 @@ namespace Jellyfin.Server.Filters
{
Schema = new OpenApiSchema
{
- Type = "file"
+ Type = "string",
+ Format = "binary"
}
};
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 03d06fdff..bc000fd45 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -40,8 +40,8 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.1" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
@@ -52,7 +52,6 @@
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
- <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
new file mode 100644
index 000000000..fdd8974d2
--- /dev/null
+++ b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Removes /emby and /mediabrowser from requested route.
+ /// </summary>
+ public class LegacyEmbyRouteRewriteMiddleware
+ {
+ private const string EmbyPath = "/emby";
+ private const string MediabrowserPath = "/mediabrowser";
+
+ private readonly RequestDelegate _next;
+ private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ /// <param name="logger">The logger.</param>
+ public LegacyEmbyRouteRewriteMiddleware(
+ RequestDelegate next,
+ ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
+ {
+ httpContext.Request.Path = localPath[EmbyPath.Length..];
+ _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
+ }
+ else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
+ {
+ httpContext.Request.Path = localPath[MediabrowserPath.Length..];
+ _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
new file mode 100644
index 000000000..9d40d74fe
--- /dev/null
+++ b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Redirect requests to robots.txt to web/robots.txt.
+ /// </summary>
+ public class RobotsRedirectionMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger<RobotsRedirectionMiddleware> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ /// <param name="logger">The logger.</param>
+ public RobotsRedirectionMiddleware(
+ RequestDelegate next,
+ ILogger<RobotsRedirectionMiddleware> logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
+ httpContext.Response.Redirect("web/robots.txt");
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index af4be5a26..f4040748d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -8,7 +8,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -81,6 +80,8 @@ namespace Jellyfin.Server.Migrations.Routines
{ "unstable", ChromecastVersion.Unstable }
};
+ var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
{
@@ -97,6 +98,15 @@ namespace Jellyfin.Server.Migrations.Routines
var itemId = new Guid(result[1].ToBlob());
var dtoUserId = new Guid(result[1].ToBlob());
+ var client = result[2].ToString();
+ var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
+ if (displayPrefs.Contains(displayPreferencesKey))
+ {
+ // Duplicate display preference.
+ continue;
+ }
+
+ displayPrefs.Add(displayPreferencesKey);
var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null)
{
@@ -109,7 +119,7 @@ namespace Jellyfin.Server.Migrations.Routines
: ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion");
- var displayPreferences = new DisplayPreferences(dtoUserId, itemId, result[2].ToString())
+ var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop,
@@ -185,7 +195,13 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var (key, value) in dto.CustomPrefs)
{
- dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
+ // Custom display preferences can have a key collision.
+ var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}";
+ if (!customDisplayPrefs.Contains(indexKey))
+ {
+ dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
+ customDisplayPrefs.Add(indexKey);
+ }
}
dbContext.Add(displayPreferences);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 74c550331..33f039c39 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -168,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
}
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index a1a7a3053..f05cdfe9b 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -598,7 +598,8 @@ namespace Jellyfin.Server
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}"))
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
+ encoding: Encoding.UTF8))
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.CreateLogger();
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index aa3ef5350..3395d2413 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -128,6 +128,8 @@ namespace Jellyfin.Server
mainApp.UseHttpsRedirection();
}
+ // This must be injected before any path related middleware.
+ mainApp.UsePathTrim();
mainApp.UseStaticFiles();
if (appConfig.HostWebClient())
{
@@ -142,6 +144,8 @@ namespace Jellyfin.Server
RequestPath = "/web",
ContentTypeProvider = extensionProvider
});
+
+ mainApp.UseRobotsRedirection();
}
mainApp.UseAuthentication();
diff --git a/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs b/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs
new file mode 100644
index 000000000..b29e6a71a
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a number to a boolean.
+ /// This is needed for HDHomerun.
+ /// </summary>
+ public class JsonBoolNumberConverter : JsonConverter<bool>
+ {
+ /// <inheritdoc />
+ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ return Convert.ToBoolean(reader.GetInt32());
+ }
+
+ return reader.GetBoolean();
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
+ {
+ writer.WriteBooleanValue(value);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs
new file mode 100644
index 000000000..73e3a0493
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Legacy DateTime converter.
+ /// Milliseconds aren't output if zero by default.
+ /// </summary>
+ public class JsonDateTimeConverter : JsonConverter<DateTime>
+ {
+ /// <inheritdoc />
+ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.GetDateTime();
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
+ {
+ if (value.Millisecond == 0)
+ {
+ // Remaining ticks value will be 0, manually format.
+ writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ writer.WriteStringValue(value);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs
index 52e08d071..bd9600110 100644
--- a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -13,21 +14,13 @@ namespace MediaBrowser.Common.Json.Converters
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var guidStr = reader.GetString();
-
return guidStr == null ? Guid.Empty : new Guid(guidStr);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
{
- if (value == Guid.Empty)
- {
- writer.WriteNullValue();
- }
- else
- {
- writer.WriteStringValue(value);
- }
+ writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs
new file mode 100644
index 000000000..6d96d5496
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a GUID object or value to/from JSON.
+ /// </summary>
+ public class JsonNullableGuidConverter : JsonConverter<Guid?>
+ {
+ /// <inheritdoc />
+ public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var guidStr = reader.GetString();
+ return guidStr == null ? null : new Guid(guidStr);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
+ {
+ if (value == null || value == Guid.Empty)
+ {
+ writer.WriteNullValue();
+ }
+ else
+ {
+ writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
new file mode 100644
index 000000000..4fec2ea3f
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a string <c>N/A</c> to <c>string.Empty</c>.
+ /// </summary>
+ public class JsonOmdbNotAvailableStringConverter : JsonConverter<string>
+ {
+ /// <inheritdoc />
+ public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var str = reader.GetString();
+ if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return str;
+ }
+
+ return JsonSerializer.Deserialize<string>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(value, options);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStructConverter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStructConverter.cs
new file mode 100644
index 000000000..b9e67ce2d
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStructConverter.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a string <c>N/A</c> to <c>string.Empty</c>.
+ /// </summary>
+ /// <typeparam name="T">The resulting type.</typeparam>
+ public class JsonOmdbNotAvailableStructConverter<T> : JsonConverter<T?>
+ where T : struct
+ {
+ /// <inheritdoc />
+ public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var str = reader.GetString();
+ if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ }
+
+ return JsonSerializer.Deserialize<T>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(value, options);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index c5050a21d..2ef24a884 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -20,53 +20,70 @@ namespace MediaBrowser.Common.Json
public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
/// <summary>
+ /// When changing these options, update
+ /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+ /// -> AddJellyfinApi
+ /// -> AddJsonOptions.
+ /// </summary>
+ private static readonly JsonSerializerOptions _jsonSerializerOptions = new ()
+ {
+ ReadCommentHandling = JsonCommentHandling.Disallow,
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ Converters =
+ {
+ new JsonGuidConverter(),
+ new JsonNullableGuidConverter(),
+ new JsonVersionConverter(),
+ new JsonStringEnumConverter(),
+ new JsonNullableStructConverterFactory(),
+ new JsonBoolNumberConverter(),
+ new JsonDateTimeConverter()
+ }
+ };
+
+ private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions)
+ {
+ PropertyNamingPolicy = null
+ };
+
+ private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions)
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ /// <summary>
/// Gets the default <see cref="JsonSerializerOptions" /> options.
/// </summary>
/// <remarks>
- /// When changing these options, update
- /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
- /// -> AddJellyfinApi
- /// -> AddJsonOptions.
+ /// The return value must not be modified.
+ /// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetOptions()
- {
- var options = new JsonSerializerOptions
- {
- ReadCommentHandling = JsonCommentHandling.Disallow,
- WriteIndented = false,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- NumberHandling = JsonNumberHandling.AllowReadingFromString
- };
-
- options.Converters.Add(new JsonGuidConverter());
- options.Converters.Add(new JsonVersionConverter());
- options.Converters.Add(new JsonStringEnumConverter());
- options.Converters.Add(new JsonNullableStructConverterFactory());
-
- return options;
- }
+ => _jsonSerializerOptions;
/// <summary>
/// Gets camelCase json options.
/// </summary>
+ /// <remarks>
+ /// The return value must not be modified.
+ /// If the defaults must be modified the author must use the copy constructor.
+ /// </remarks>
/// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetCamelCaseOptions()
- {
- var options = GetOptions();
- options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
- return options;
- }
+ => _camelCaseJsonSerializerOptions;
/// <summary>
/// Gets PascalCase json options.
/// </summary>
+ /// <remarks>
+ /// The return value must not be modified.
+ /// If the defaults must be modified the author must use the copy constructor.
+ /// </remarks>
/// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetPascalCaseOptions()
- {
- var options = GetOptions();
- options.PropertyNamingPolicy = null;
- return options;
- }
+ => _pascalCaseJsonSerializerOptions;
}
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index be5e7f5b4..320e60dc6 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -14,6 +14,7 @@
</PropertyGroup>
<ItemGroup>
+ <FrameworkReference Include="Microsoft.AspNetCore.App"/>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup>
@@ -21,7 +22,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
- <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index 085f769d0..31dd95402 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -48,10 +48,10 @@ namespace MediaBrowser.Controller.BaseItemManager
return !baseItem.EnableMediaSourceDisplay;
}
- var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+ var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.BaseItemManager
return !baseItem.EnableMediaSourceDisplay;
}
- var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+ var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index 129cdb519..b2315bda4 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -17,9 +17,10 @@ namespace MediaBrowser.Controller.Channels
{
public override bool IsVisible(User user)
{
- if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
+ var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
+ if (blockedChannelsPreference.Length != 0)
{
- if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (blockedChannelsPreference.Contains(Id))
{
return false;
}
@@ -27,8 +28,7 @@ namespace MediaBrowser.Controller.Channels
else
{
if (!user.HasPermission(PermissionKind.EnableAllChannels)
- && !user.GetPreference(PreferenceKind.EnabledChannels)
- .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 48cd9371a..9a33ad9d7 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index c5e50cf45..8a9bb12c7 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index d8fad3bfb..cbb02aabd 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -480,11 +480,11 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders);
+ var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
if (SourceType == SourceType.Channel)
{
- return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase);
+ return allowed.Contains(ChannelId);
}
else
{
@@ -492,7 +492,7 @@ namespace MediaBrowser.Controller.Entities
foreach (var folder in collectionFolders)
{
- if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (allowed.Contains(folder.Id))
{
return true;
}
@@ -1385,6 +1385,7 @@ namespace MediaBrowser.Controller.Entities
new List<FileSystemMetadata>();
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
+ await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
if (ownedItemsChanged)
{
@@ -1908,7 +1909,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType());
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index d25545a2f..c3b6af76e 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -4,9 +4,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -24,10 +26,9 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class CollectionFolder : Folder, ICollectionFolder
{
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public static IXmlSerializer XmlSerializer { get; set; }
- public static IJsonSerializer JsonSerializer { get; set; }
-
public static IServerApplicationHost ApplicationHost { get; set; }
public CollectionFolder()
@@ -122,7 +123,7 @@ namespace MediaBrowser.Controller.Entities
{
LibraryOptions[path] = options;
- var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options));
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.Serialize(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 23f4c00c1..cac5026f7 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -186,13 +186,10 @@ namespace MediaBrowser.Controller.Entities
{
if (this is ICollectionFolder && !(this is BasePluginFolder))
{
- var blockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders);
+ var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
if (blockedMediaFolders.Length > 0)
{
- if (blockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) ||
-
- // Backwards compatibility
- blockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+ if (blockedMediaFolders.Contains(Id))
{
return false;
}
@@ -200,8 +197,7 @@ namespace MediaBrowser.Controller.Entities
else
{
if (!user.HasPermission(PermissionKind.EnableAllFolders)
- && !user.GetPreference(PreferenceKind.EnabledFolders)
- .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
{
return false;
}
@@ -354,6 +350,11 @@ namespace MediaBrowser.Controller.Entities
{
await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
+ else
+ {
+ // metadata is up-to-date; make sure DB has correct images dimensions and hash
+ await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
+ }
continue;
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 8de88cc1b..05e4229ca 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Controller.Entities.Movies
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
}
public override double GetDefaultPrimaryImageAspectRatio()
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index e8afa9a49..1a379074d 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities.TV
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 24b101694..6700761fc 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -571,7 +571,7 @@ namespace MediaBrowser.Controller.Library
string videoPath,
string[] files);
- void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason);
+ Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
BaseItem GetParentItem(string parentId, Guid? userId);
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 8fd3b8c34..6e267834b 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -93,7 +93,8 @@ namespace MediaBrowser.Controller.Library
/// Deletes the specified user.
/// </summary>
/// <param name="userId">The id of the user to be deleted.</param>
- void DeleteUser(Guid userId);
+ /// <returns>A task representing the deletion of the user.</returns>
+ Task DeleteUserAsync(Guid userId);
/// <summary>
/// Resets the password.
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 91a03e647..efab87a38 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1127,13 +1127,25 @@ namespace MediaBrowser.Controller.MediaEncoding
targetVideoCodec = "hevc";
}
- var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
- profile = Regex.Replace(profile, @"\s+", String.Empty);
+ var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
+ profile = Regex.Replace(profile, @"\s+", string.Empty);
+
+ // We only transcode to HEVC 8-bit for now, force Main Profile.
+ if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
+ || profile.Contains("mainstill", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
+
+ // Extended Profile is not supported by any known h264 encoders, force Main Profile.
+ if (profile.Contains("extended", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
// Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("high10", StringComparison.OrdinalIgnoreCase))
{
profile = "high";
}
@@ -1141,8 +1153,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_baseline";
}
@@ -1151,16 +1162,36 @@ namespace MediaBrowser.Controller.MediaEncoding
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
- && profile != null
- && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "baseline";
}
+ // libx264, h264_qsv, h264_nvenc and h264_vaapi does not support Constrained High profile, force High in this case.
+ if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ && profile.Contains("high", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "high";
+ }
+
+ if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "constrained_baseline";
+ }
+
+ if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "constrained_high";
+ }
+
// Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
- if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1)
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("main10", StringComparison.OrdinalIgnoreCase))
{
profile = "main";
}
@@ -1691,6 +1722,16 @@ namespace MediaBrowser.Controller.MediaEncoding
: transcoderChannelLimit.Value;
}
+ // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout).
+ // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices
+ if (isTranscodingAudio
+ && state.TranscodingType != TranscodingJobType.Progressive
+ && resultChannels.HasValue
+ && (resultChannels.Value > 2 && resultChannels.Value < 6 || resultChannels.Value == 7))
+ {
+ resultChannels = 2;
+ }
+
return resultChannels;
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 9ad8557ce..6c06dcad5 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -47,6 +47,11 @@ namespace MediaBrowser.Controller.Session
event EventHandler<SessionEventArgs> SessionActivity;
/// <summary>
+ /// Occurs when [session controller connected].
+ /// </summary>
+ event EventHandler<SessionEventArgs> SessionControllerConnected;
+
+ /// <summary>
/// Occurs when [capabilities changed].
/// </summary>
event EventHandler<SessionEventArgs> CapabilitiesChanged;
@@ -78,6 +83,12 @@ namespace MediaBrowser.Controller.Session
/// <param name="user">The user.</param>
SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
+ /// <summary>
+ /// Used to report that a session controller has connected.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ void OnSessionControllerConnected(SessionInfo session);
+
void UpdateDeviceName(string sessionId, string reportedDeviceName);
/// <summary>
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
index d0244563a..1c954828c 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
@@ -51,5 +51,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Checks whether a user has an active session using SyncPlay.
+ /// </summary>
+ /// <param name="userId">The user identifier to check.</param>
+ /// <returns><c>true</c> if the user is using SyncPlay; <c>false</c> otherwise.</returns>
+ bool IsUserActive(Guid userId);
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index b1da9c712..fbd08a97c 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -603,16 +603,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
+ // mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will lower the processing speed.
var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
if (enableThumbnail)
{
+ var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
+ var batchSize = useLargerBatchSize ? "50" : "24";
if (string.IsNullOrEmpty(vf))
{
- vf = "-vf thumbnail=24";
+ vf = "-vf thumbnail=" + batchSize;
}
else
{
- vf += ",thumbnail=24";
+ vf += ",thumbnail=" + batchSize;
}
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 7bb2a7d03..f8af499e4 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -24,7 +24,7 @@
<ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.1" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
+ <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.3.0" />
</ItemGroup>
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 100756c24..38b333510 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -88,11 +88,11 @@ namespace MediaBrowser.Model.Configuration
// The left side of the dot is the platform number, and the right side is the device number on the platform.
OpenclDevice = "0.0";
EnableTonemapping = false;
- TonemappingAlgorithm = "reinhard";
+ TonemappingAlgorithm = "hable";
TonemappingRange = "auto";
TonemappingDesat = 0;
TonemappingThreshold = 0.8;
- TonemappingPeak = 0;
+ TonemappingPeak = 100;
TonemappingParam = 0;
H264Crf = 23;
H265Crf = 28;
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 0dbd51bdc..9fb978e9b 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -49,8 +49,6 @@ namespace MediaBrowser.Model.Configuration
new MetadataOptions
{
ItemType = "Series",
- DisabledMetadataFetchers = new[] { "TheMovieDb" },
- DisabledImageFetchers = new[] { "TheMovieDb" }
},
new MetadataOptions
{
@@ -69,13 +67,10 @@ namespace MediaBrowser.Model.Configuration
new MetadataOptions
{
ItemType = "Season",
- DisabledMetadataFetchers = new[] { "TheMovieDb" },
},
new MetadataOptions
{
ItemType = "Episode",
- DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" },
- DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" }
}
};
}
@@ -304,6 +299,18 @@ namespace MediaBrowser.Model.Configuration
public int MinResumeDurationSeconds { get; set; } = 300;
/// <summary>
+ /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume in minutes.</value>
+ public int MinAudiobookResume { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The remaining time in minutes.</value>
+ public int MaxAudiobookResume { get; set; } = 5;
+
+ /// <summary>
/// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
/// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
/// different directories and files.
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 59c981000..431cf0baf 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1033,9 +1033,9 @@ namespace MediaBrowser.Model.Dlna
{
_logger.LogInformation(
"Profile: {0}, No video direct play profiles found for {1} with codec {2}",
- profile.Name ?? "Unknown Profile",
- mediaSource.Path ?? "Unknown path",
- videoStream.Codec ?? "Unknown codec");
+ profile?.Name ?? "Unknown Profile",
+ mediaSource?.Path ?? "Unknown path",
+ videoStream?.Codec ?? "Unknown codec");
return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
}
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index fac754177..3f7aac9cd 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -152,7 +152,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the channel identifier.
/// </summary>
/// <value>The channel identifier.</value>
- public Guid ChannelId { get; set; }
+ public Guid? ChannelId { get; set; }
public string ChannelName { get; set; }
@@ -270,7 +270,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the parent id.
/// </summary>
/// <value>The parent id.</value>
- public Guid ParentId { get; set; }
+ public Guid? ParentId { get; set; }
/// <summary>
/// Gets or sets the type.
@@ -344,13 +344,13 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the series id.
/// </summary>
/// <value>The series id.</value>
- public Guid SeriesId { get; set; }
+ public Guid? SeriesId { get; set; }
/// <summary>
/// Gets or sets the season identifier.
/// </summary>
/// <value>The season identifier.</value>
- public Guid SeasonId { get; set; }
+ public Guid? SeasonId { get; set; }
/// <summary>
/// Gets or sets the special feature count.
@@ -428,7 +428,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the album id.
/// </summary>
/// <value>The album id.</value>
- public Guid AlbumId { get; set; }
+ public Guid? AlbumId { get; set; }
/// <summary>
/// Gets or sets the album image tag.
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index 1782b42e2..98097477c 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Entities
}
instance.ProviderIds.TryGetValue(name, out string? id);
- return id;
+ return string.IsNullOrEmpty(id) ? null : id;
}
/// <summary>
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index b86187f9b..334fe8209 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -33,7 +33,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
- <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="5.0.0" />
diff --git a/MediaBrowser.Model/Serialization/IJsonSerializer.cs b/MediaBrowser.Model/Serialization/IJsonSerializer.cs
deleted file mode 100644
index 09b6ff9b5..000000000
--- a/MediaBrowser.Model/Serialization/IJsonSerializer.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Model.Serialization
-{
- public interface IJsonSerializer
- {
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- void SerializeToStream(object obj, Stream stream);
-
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- void SerializeToStream<T>(T obj, Stream stream);
-
- /// <summary>
- /// Serializes to file.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="file">The file.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- void SerializeToFile(object obj, string file);
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <param name="file">The file.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">type</exception>
- object DeserializeFromFile(Type type, string file);
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="file">The file.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">file</exception>
- T DeserializeFromFile<T>(string file)
- where T : class;
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="stream">The stream.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- T DeserializeFromStream<T>(Stream stream);
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="text">The text.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">text</exception>
- T DeserializeFromString<T>(string text);
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <param name="stream">The stream.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- object DeserializeFromStream(Stream stream, Type type);
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <param name="json">The json.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">json</exception>
- object DeserializeFromString(string json, Type type);
-
- /// <summary>
- /// Serializes to string.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">obj</exception>
- string SerializeToString(object obj);
-
- Task<object> DeserializeFromStreamAsync(Stream stream, Type type);
- Task<T> DeserializeFromStreamAsync<T>(Stream stream);
- }
-}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 363b2633f..37da04adf 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -111,7 +111,7 @@ namespace MediaBrowser.Model.Users
/// Gets or sets a value indicating what SyncPlay features the user can access.
/// </summary>
/// <value>Access level to SyncPlay features.</value>
- public SyncPlayAccess SyncPlayAccess { get; set; }
+ public SyncPlayUserAccessType SyncPlayAccess { get; set; }
public UserPolicy()
{
@@ -160,7 +160,7 @@ namespace MediaBrowser.Model.Users
EnableContentDownloading = true;
EnablePublicSharing = true;
EnableRemoteAccess = true;
- SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
+ SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
}
}
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 6dbce3067..8b3ca17ca 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -229,7 +229,7 @@ namespace MediaBrowser.Providers.Manager
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
}
- private Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
+ private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
{
var personsToSave = new List<BaseItem>();
@@ -239,6 +239,7 @@ namespace MediaBrowser.Providers.Manager
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
{
+ var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
var personEntity = LibraryManager.GetPerson(person.Name);
foreach (var id in person.ProviderIds)
@@ -261,18 +262,18 @@ namespace MediaBrowser.Providers.Manager
0);
saveEntity = true;
+ itemUpdateType = ItemUpdateType.ImageUpdate;
}
if (saveEntity)
{
personsToSave.Add(personEntity);
+ await LibraryManager.RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
}
}
}
- LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload);
LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
- return Task.CompletedTask;
}
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index accdea36e..071a149db 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -19,10 +19,10 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
<PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
- <PackageReference Include="TvDbSharper" Version="3.2.2" />
</ItemGroup>
<PropertyGroup>
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
index 293087da7..cd9e47743 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
@@ -1,9 +1,12 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -19,13 +22,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IJsonSerializer json)
+ public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory)
{
_config = config;
_httpClientFactory = httpClientFactory;
- _json = json;
}
/// <inheritdoc />
@@ -56,7 +58,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<AudioDbAlbumProvider.RootObject>(path);
+ await using FileStream jsonStream = File.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/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
index 97bba10ba..f463a3566 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
@@ -6,10 +6,12 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -27,16 +29,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public static AudioDbAlbumProvider Current;
- public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
+ public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
_config = config;
_fileSystem = fileSystem;
_httpClientFactory = httpClientFactory;
- _json = json;
Current = this;
}
@@ -64,7 +65,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<RootObject>(path);
+ await using FileStream jsonStream = File.OpenRead(path);
+ var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
index d250acfa8..36700d191 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
@@ -1,9 +1,12 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -19,12 +22,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClientFactory httpClientFactory)
+ public AudioDbArtistImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory)
{
_config = config;
- _json = json;
_httpClientFactory = httpClientFactory;
}
@@ -58,7 +60,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<AudioDbArtistProvider.RootObject>(path);
+ await using FileStream jsonStream = File.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/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
index a2a03e1f9..7a15adb8e 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -5,10 +5,12 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -29,14 +31,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
+ public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
_config = config;
_fileSystem = fileSystem;
_httpClientFactory = httpClientFactory;
- _json = json;
Current = this;
}
@@ -65,7 +66,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<RootObject>(path);
+ await using FileStream jsonStream = File.OpenRead(path);
+ var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index bfc840ea5..24ef80a35 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -12,13 +12,11 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly OmdbItemProvider _itemProvider;
private readonly IFileSystem _fileSystem;
@@ -26,19 +24,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IApplicationHost _appHost;
public OmdbEpisodeProvider(
- IJsonSerializer jsonSerializer,
IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
- _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
+ _itemProvider = new OmdbItemProvider(_appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
}
// After TheTvDb
@@ -69,7 +65,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
{
- result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager)
+ result.HasMetadata = await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager)
.FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index 8f4240dc1..df67aff31 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System.Collections.Generic;
-using System.Net.Http;
using System.Globalization;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -15,21 +15,18 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
- public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbImageProvider(IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
@@ -56,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var list = new List<RemoteImageInfo>();
- var provider = new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager);
+ var provider = new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager);
if (!string.IsNullOrWhiteSpace(imdbId))
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 43d8af75f..71d551063 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -6,9 +6,12 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -19,34 +22,35 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbItemProvider : IRemoteMetadataProvider<Series, SeriesInfo>,
IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
+ private readonly JsonSerializerOptions _jsonOptions;
public OmdbItemProvider(
- IJsonSerializer jsonSerializer,
IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
+
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStructConverter<int>());
}
public string Name => "The Open Movie Database";
@@ -138,7 +142,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (isSearch)
{
- var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false);
+ var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (searchResultList != null && searchResultList.Search != null)
{
resultList.AddRange(searchResultList.Search);
@@ -146,7 +150,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
else
{
- var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
+ var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
{
resultList.Add(result);
@@ -221,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
@@ -253,7 +257,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index e540e4471..2372e3183 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -7,35 +7,41 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbProvider
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IApplicationHost _appHost;
+ private readonly JsonSerializerOptions _jsonOptions;
- public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+ public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
+
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStructConverter<int>());
}
public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, CancellationToken cancellationToken)
@@ -220,7 +226,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- var result = _jsonSerializer.DeserializeFromString<RootObject>(resultString);
+ var result = JsonSerializer.Deserialize<RootObject>(resultString, _jsonOptions);
return result;
}
@@ -239,7 +245,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- var result = _jsonSerializer.DeserializeFromString<SeasonRootObject>(resultString);
+ var result = JsonSerializer.Deserialize<SeasonRootObject>(resultString, _jsonOptions);
return result;
}
@@ -297,11 +303,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb
"i={0}&plot=short&tomatoes=true&r=json",
imdbParam));
- using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
+ var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(rootObject, path);
+ await using FileStream jsonFileStream = File.OpenWrite(path);
+ await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
}
@@ -335,15 +340,22 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam,
seasonId));
- using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
+ var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(rootObject, path);
+ await using FileStream jsonFileStream = File.OpenWrite(path);
+ await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
}
+ public async Task<T> GetDeserializedOmdbResponse<T>(HttpClient httpClient, string url, CancellationToken cancellationToken)
+ {
+ using var response = await GetOmdbResponse(httpClient, url, cancellationToken).ConfigureAwait(false);
+ await using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+
+ return await JsonSerializer.DeserializeAsync<T>(content, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
+
public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken)
{
return httpClient.GetAsync(url, cancellationToken);
@@ -425,7 +437,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Director.Trim(),
+ Name = result.Writer.Trim(),
Type = PersonType.Writer
};
@@ -465,7 +477,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public string seriesID { get; set; }
- public int Season { get; set; }
+ public int? Season { get; set; }
public int? totalSeasons { get; set; }
@@ -526,7 +538,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public string Response { get; set; }
- public int Episode { get; set; }
+ public int? Episode { get; set; }
public float? GetRottenTomatoScore()
{
diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
index f1c46899c..fe4749c79 100644
--- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Common.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index c8fc568a2..967908197 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,10 +1,16 @@
#pragma warning disable CS1591
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -13,14 +19,27 @@ namespace MediaBrowser.Providers.TV
{
public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
+ private readonly ILocalizationManager _localizationManager;
+
public SeriesMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<SeriesMetadataService> logger,
IProviderManager providerManager,
IFileSystem fileSystem,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ ILocalizationManager localizationManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
+ _localizationManager = localizationManager;
+ }
+
+ /// <inheritdoc />
+ protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+ {
+ await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+
+ RemoveObsoleteSeasons(item);
+ await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -62,5 +81,117 @@ namespace MediaBrowser.Providers.TV
targetItem.AirDays = sourceItem.AirDays;
}
}
+
+ private void RemoveObsoleteSeasons(Series series)
+ {
+ // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync.
+ var physicalSeasonNumbers = new HashSet<int>();
+ var virtualSeasons = new List<Season>();
+ foreach (var existingSeason in series.Children.OfType<Season>())
+ {
+ if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
+ {
+ physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
+ }
+ else if (existingSeason.LocationType == LocationType.Virtual)
+ {
+ virtualSeasons.Add(existingSeason);
+ }
+ }
+
+ foreach (var virtualSeason in virtualSeasons)
+ {
+ var seasonNumber = virtualSeason.IndexNumber;
+ // If there's a physical season with the same number or no episodes in the season, delete it
+ if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
+ || !virtualSeason.GetEpisodes().Any())
+ {
+ Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
+
+ LibraryManager.DeleteItem(
+ virtualSeason,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ false);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates seasons for all episodes that aren't in a season folder.
+ /// If no season number can be determined, a dummy season will be created.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
+ {
+ var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
+ .Cast<Episode>()
+ .Where(i => !i.IsInSeasonFolder);
+
+ List<Season> seasons = series.Children.OfType<Season>().ToList();
+
+ // Loop through the unique season numbers
+ foreach (var episode in episodesInSeriesFolder)
+ {
+ // Null season numbers will have a 'dummy' season created because seasons are always required.
+ var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null;
+ var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
+
+ if (existingSeason == null)
+ {
+ var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false);
+ seasons.Add(season);
+ }
+ else if (existingSeason.IsVirtualItem)
+ {
+ existingSeason.IsVirtualItem = false;
+ await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The newly created season.</returns>
+ private async Task<Season> CreateSeasonAsync(
+ Series series,
+ int? seasonNumber,
+ CancellationToken cancellationToken)
+ {
+ string seasonName = seasonNumber switch
+ {
+ null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
+ 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
+ _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
+ };
+
+ Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
+
+ var season = new Season
+ {
+ Name = seasonName,
+ IndexNumber = seasonNumber,
+ Id = LibraryManager.GetNewItemId(
+ series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
+ typeof(Season)),
+ IsVirtualItem = false,
+ SeriesId = series.Id,
+ SeriesName = series.Name
+ };
+
+ series.AddChild(season, cancellationToken);
+
+ await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
+
+ return season;
+ }
}
}
diff --git a/README.md b/README.md
index 1ab246f84..29f992349 100644
--- a/README.md
+++ b/README.md
@@ -105,12 +105,6 @@ There are three options to get the files for the web client.
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
-Once you have a copy of the built web client files, you need to copy them into a specific directory.
-
-> `<repository root>/Mediabrowser.WebDashboard/jellyfin-web`
-
-As part of the build process, this folder will be copied to the build output directory, where it can be accessed by the server.
-
### Running The Server
The following instructions will help you get the project up and running via the command line, or your preferred IDE.
@@ -133,7 +127,7 @@ To run the server from the command line you can use the `dotnet run` command. Th
```bash
cd jellyfin # Move into the repository directory
-dotnet run --project Jellyfin.Server # Run the server startup project
+dotnet run --project Jellyfin.Server --webdir /absolute/path/to/jellyfin-web/dist # Run the server startup project
```
A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options.
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index f0d9188c1..d2f98ca82 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index 8132ee887..ffc94e088 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 31f534838..b25f59329 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 2bedafcc5..2e993c25d 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
new file mode 100644
index 000000000..08b4ffa52
--- /dev/null
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -0,0 +1,31 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=5.0
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64-musl /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
new file mode 100644
index 000000000..b8499c917
--- /dev/null
+++ b/deployment/Dockerfile.linux.arm64
@@ -0,0 +1,31 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=5.0
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=arm64
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.arm64 /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
new file mode 100644
index 000000000..80c4d1469
--- /dev/null
+++ b/deployment/Dockerfile.linux.armhf
@@ -0,0 +1,31 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=5.0
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=armhf
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.armhf /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index d470f9b74..f2bbe7f24 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index d2007c075..603becedf 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 084159d45..a6c7cc5d4 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index c2caf4cf8..3a8005816 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 719b3a85b..22b9e7ea8 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index e6905c906..b1ca61053 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/build.linux.amd64-musl b/deployment/build.linux.amd64-musl
new file mode 100755
index 000000000..72444c05e
--- /dev/null
+++ b/deployment/build.linux.amd64-musl
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux amd64-musl .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-amd64-musl.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64
new file mode 100755
index 000000000..e362607a7
--- /dev/null
+++ b/deployment/build.linux.arm64
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux arm64 .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-arm64.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf
new file mode 100755
index 000000000..c0d0607fc
--- /dev/null
+++ b/deployment/build.linux.armhf
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux armhf .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-armhf.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 7c552ec06..b5e8e521c 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -16,9 +16,9 @@
<PackageReference Include="AutoFixture" Version="4.14.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index e8eca6760..19c5612c0 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
@@ -29,6 +29,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
new file mode 100644
index 000000000..9ded01f2b
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public static class JsonBoolNumberTests
+ {
+ [Theory]
+ [InlineData("1", true)]
+ [InlineData("0", false)]
+ [InlineData("2", true)]
+ [InlineData("true", true)]
+ [InlineData("false", false)]
+ public static void Deserialize_Number_Valid_Success(string input, bool? output)
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonBoolNumberConverter());
+ var value = JsonSerializer.Deserialize<bool>(input, options);
+ Assert.Equal(value, output);
+ }
+
+ [Theory]
+ [InlineData(true, "true")]
+ [InlineData(false, "false")]
+ public static void Serialize_Bool_Success(bool input, string output)
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonBoolNumberConverter());
+ var value = JsonSerializer.Serialize(input, options);
+ Assert.Equal(value, output);
+ }
+ }
+} \ No newline at end of file
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
index 3c94db491..1e1cde957 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
@@ -1,9 +1,10 @@
using System;
+using System.Globalization;
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;
-namespace Jellyfin.Common.Tests.Extensions
+namespace Jellyfin.Common.Tests.Json
{
public class JsonGuidConverterTests
{
@@ -44,9 +45,25 @@ namespace Jellyfin.Common.Tests.Extensions
}
[Fact]
- public void Serialize_EmptyGuid_Null()
+ public void Serialize_EmptyGuid_EmptyGuid()
{
- Assert.Equal("null", JsonSerializer.Serialize(Guid.Empty, _options));
+ Assert.Equal($"\"{Guid.Empty:N}\"", JsonSerializer.Serialize(Guid.Empty, _options));
+ }
+
+ [Fact]
+ public void Serialize_Valid_NoDash_Success()
+ {
+ var guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
+ var str = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal($"\"{guid:N}\"", str);
+ }
+
+ [Fact]
+ public void Serialize_Nullable_Success()
+ {
+ Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
+ var str = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal($"\"{guid:N}\"", str);
}
}
}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
new file mode 100644
index 000000000..efc0c4af9
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public class JsonNullableGuidConverterTests
+ {
+ private readonly JsonSerializerOptions _options;
+
+ public JsonNullableGuidConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new JsonNullableGuidConverter());
+ }
+
+ [Fact]
+ public void Deserialize_Valid_Success()
+ {
+ Guid? value = JsonSerializer.Deserialize<Guid?>(@"""a852a27afe324084ae66db579ee3ee18""", _options);
+ Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value);
+ }
+
+ [Fact]
+ public void Deserialize_ValidDashed_Success()
+ {
+ Guid? value = JsonSerializer.Deserialize<Guid?>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options);
+ Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
+ }
+
+ [Fact]
+ public void Roundtrip_Valid_Success()
+ {
+ Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18");
+ string value = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal(guid, JsonSerializer.Deserialize<Guid?>(value, _options));
+ }
+
+ [Fact]
+ public void Deserialize_Null_EmptyGuid()
+ {
+ Assert.Null(JsonSerializer.Deserialize<Guid?>("null", _options));
+ }
+
+ [Fact]
+ public void Serialize_EmptyGuid_EmptyGuid()
+ {
+ Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, _options));
+ }
+
+ [Fact]
+ public void Serialize_Valid_NoDash_Success()
+ {
+ var guid = (Guid?)new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
+ var str = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal($"\"{guid:N}\"", str);
+ }
+
+ [Fact]
+ public void Serialize_Nullable_Success()
+ {
+ Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
+ var str = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal($"\"{guid:N}\"", str);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
new file mode 100644
index 000000000..6f85fe092
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
@@ -0,0 +1,68 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Providers.Plugins.Omdb;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public class JsonOmdbConverterTests
+ {
+ private readonly JsonSerializerOptions _options;
+
+ public JsonOmdbConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new JsonOmdbNotAvailableStringConverter());
+ _options.Converters.Add(new JsonOmdbNotAvailableStructConverter<int>());
+ _options.NumberHandling = JsonNumberHandling.AllowReadingFromString;
+ }
+
+ [Fact]
+ public void Deserialize_Omdb_Response_Not_Available_Success()
+ {
+ const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}";
+ var seasonRootObject = JsonSerializer.Deserialize<OmdbProvider.RootObject>(Input, _options);
+ Assert.NotNull(seasonRootObject);
+ Assert.Null(seasonRootObject?.Awards);
+ Assert.Null(seasonRootObject?.Episode);
+ Assert.Null(seasonRootObject?.Metascore);
+ }
+
+ [Theory]
+ [InlineData("\"N/A\"")]
+ [InlineData("null")]
+ public void Deserialization_To_Nullable_Int_Shoud_Be_Null(string input)
+ {
+ var result = JsonSerializer.Deserialize<int?>(input, _options);
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("\"N/A\"")]
+ [InlineData("null")]
+ public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input)
+ {
+ var result = JsonSerializer.Deserialize<string?>(input, _options);
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("\"8\"", 8)]
+ [InlineData("8", 8)]
+ public void Deserialize_Int_Success(string input, int expected)
+ {
+ var result = JsonSerializer.Deserialize<int>(input, _options);
+ Assert.Equal(result, expected);
+ }
+
+ [Fact]
+ public void Deserialize_Normal_String_Success()
+ {
+ const string Input = "\"Jellyfin\"";
+ const string Expected = "Jellyfin";
+ var result = JsonSerializer.Deserialize<string>(Input, _options);
+ Assert.Equal(Expected, result);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 6e3fac43d..1ec88dada 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index f91db6744..8c9dc4820 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index e88de3811..c934ea1c2 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 567cf34ef..6118581e1 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
index 48b0b4c7d..90782f6bb 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index fffbc6212..bcd12deaf 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.14.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Moq" Version="4.15.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
@@ -35,6 +35,11 @@
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
</ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="LiveTv\discover.json" />
+ <EmbeddedResource Include="LiveTv\lineup.json" />
+ </ItemGroup>
+
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
new file mode 100644
index 000000000..fb7cf6a47
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun;
+using MediaBrowser.Model.LiveTv;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.LiveTv
+{
+ public class HdHomerunHostTests
+ {
+ private const string TestIp = "http://192.168.1.182";
+
+ private readonly Fixture _fixture;
+ private readonly HdHomerunHost _hdHomerunHost;
+
+ public HdHomerunHostTests()
+ {
+ const string BaseResourcePath = "Jellyfin.Server.Implementations.Tests.LiveTv.";
+
+ var messageHandler = new Mock<HttpMessageHandler>();
+ messageHandler.Protected()
+ .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
+ .Returns<HttpRequestMessage, CancellationToken>(
+ (m, _) =>
+ {
+ var resource = BaseResourcePath + m.RequestUri?.Segments[^1];
+ var stream = typeof(HdHomerunHostTests).Assembly.GetManifestResourceStream(resource);
+ if (stream == null)
+ {
+ throw new NullReferenceException("Resource doesn't exist: " + resource);
+ }
+
+ return Task.FromResult(new HttpResponseMessage()
+ {
+ Content = new StreamContent(stream)
+ });
+ });
+
+ var http = new Mock<IHttpClientFactory>();
+ http.Setup(x => x.CreateClient(It.IsAny<string>()))
+ .Returns(new HttpClient(messageHandler.Object));
+ _fixture = new Fixture();
+ _fixture.Customize(new AutoMoqCustomization
+ {
+ ConfigureMembers = true
+ }).Inject(http);
+ _hdHomerunHost = _fixture.Create<HdHomerunHost>();
+ }
+
+ [Fact]
+ public async Task GetModelInfo_Valid_Success()
+ {
+ var host = new TunerHostInfo()
+ {
+ Url = TestIp
+ };
+
+ var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal("HDHomeRun PRIME", modelInfo.FriendlyName);
+ Assert.Equal("HDHR3-CC", modelInfo.ModelNumber);
+ Assert.Equal("hdhomerun3_cablecard", modelInfo.FirmwareName);
+ Assert.Equal("20160630atest2", modelInfo.FirmwareVersion);
+ Assert.Equal("FFFFFFFF", modelInfo.DeviceID);
+ Assert.Equal("FFFFFFFF", modelInfo.DeviceAuth);
+ Assert.Equal(3, modelInfo.TunerCount);
+ Assert.Equal("http://192.168.1.182:80", modelInfo.BaseURL);
+ Assert.Equal("http://192.168.1.182:80/lineup.json", modelInfo.LineupURL);
+ }
+
+ [Fact]
+ public async Task GetModelInfo_EmptyUrl_ArgumentException()
+ {
+ var host = new TunerHostInfo()
+ {
+ Url = string.Empty
+ };
+
+ await Assert.ThrowsAsync<ArgumentException>(() => _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task GetLineup_Valid_Success()
+ {
+ var host = new TunerHostInfo()
+ {
+ Url = TestIp
+ };
+
+ var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(6, channels.Count);
+ Assert.Equal("4.1", channels[0].GuideNumber);
+ Assert.Equal("WCMH-DT", channels[0].GuideName);
+ Assert.True(channels[0].HD);
+ Assert.True(channels[0].Favorite);
+ Assert.Equal("http://192.168.1.111:5004/auto/v4.1", channels[0].URL);
+ }
+
+ [Fact]
+ public async Task GetLineup_ImportFavoritesOnly_Success()
+ {
+ var host = new TunerHostInfo()
+ {
+ Url = TestIp,
+ ImportFavoritesOnly = true
+ };
+
+ var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false);
+ Assert.Single(channels);
+ Assert.Equal("4.1", channels[0].GuideNumber);
+ Assert.Equal("WCMH-DT", channels[0].GuideName);
+ Assert.True(channels[0].HD);
+ Assert.True(channels[0].Favorite);
+ Assert.Equal("http://192.168.1.111:5004/auto/v4.1", channels[0].URL);
+ }
+
+ [Fact]
+ public async Task TryGetTunerHostInfo_Valid_Success()
+ {
+ var host = await _hdHomerunHost.TryGetTunerHostInfo(TestIp, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(_hdHomerunHost.Type, host.Type);
+ Assert.Equal(TestIp, host.Url);
+ Assert.Equal("HDHomeRun PRIME", host.FriendlyName);
+ Assert.Equal("FFFFFFFF", host.DeviceId);
+ Assert.Equal(3, host.TunerCount);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/discover.json b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/discover.json
new file mode 100644
index 000000000..851f17bb2
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/discover.json
@@ -0,0 +1 @@
+{"FriendlyName":"HDHomeRun PRIME","ModelNumber":"HDHR3-CC","FirmwareName":"hdhomerun3_cablecard","FirmwareVersion":"20160630atest2","DeviceID":"FFFFFFFF","DeviceAuth":"FFFFFFFF","TunerCount":3,"ConditionalAccess":1,"BaseURL":"http://192.168.1.182:80","LineupURL":"http://192.168.1.182:80/lineup.json"}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/lineup.json b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/lineup.json
new file mode 100644
index 000000000..4cb5ebc8e
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/lineup.json
@@ -0,0 +1 @@
+[ { "GuideNumber": "4.1", "GuideName": "WCMH-DT", "HD": 1, "Favorite": 1, "URL": "http://192.168.1.111:5004/auto/v4.1" }, { "GuideNumber": "4.2", "GuideName": "MeTV", "URL": "http://192.168.1.111:5004/auto/v4.2" }, { "GuideNumber": "4.3", "GuideName": "ION TV", "URL": "http://192.168.1.111:5004/auto/v4.3" }, { "GuideNumber": "6.1", "GuideName": "WSYX DT", "HD": 1, "URL": "http://192.168.1.111:5004/auto/v6.1" }, { "GuideNumber": "6.2", "GuideName": "MYTV", "URL": "http://192.168.1.111:5004/auto/v6.2" }, { "GuideNumber": "6.3", "GuideName": "ANTENNA", "URL": "http://192.168.1.111:5004/auto/v6.3" } ]