aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStepan <ste.martinek+git@gmail.com>2020-11-10 12:42:49 +0100
committerStepan <ste.martinek+git@gmail.com>2020-11-10 12:42:49 +0100
commitc0747512d6d3973ac102ea4990d2e1fa44e5a5d1 (patch)
tree2cf9ad33386422dfb00757237b68523e60284034
parent3d1076ae42433314835f4277ff42cd5ef1e6c016 (diff)
parentcdf979efef94dc29f4ef0b4ad3f918b4983560e5 (diff)
Merge remote-tracking branch 'jellyfin/master' into emby-namig-nullable
-rw-r--r--.ci/azure-pipelines-abi.yml3
-rw-r--r--.ci/azure-pipelines-package.yml5
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs36
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs2
-rw-r--r--Emby.Dlna/DlnaManager.cs6
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs2
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs4
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs4
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs1
-rw-r--r--Emby.Naming/Video/CleanDateTimeParser.cs5
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs5
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs88
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs19
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs97
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs7
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs145
-rw-r--r--Emby.Server.Implementations/Images/ArtistImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs9
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs4
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs28
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs2
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs2
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs26
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs8
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json120
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json42
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs8
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs78
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs4
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs76
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs7
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs15
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs8
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs8
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs6
-rw-r--r--Jellyfin.Api/Constants/InternalClaimTypes.cs5
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs21
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs12
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs26
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs143
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs19
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs11
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs3
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs22
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs140
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs190
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs136
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs125
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs9
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs32
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs27
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs3
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs3
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs13
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs5
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs4
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs166
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs56
-rw-r--r--Jellyfin.Api/Helpers/SimilarItemsHelper.cs4
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs2
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs29
-rw-r--r--Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs34
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs12
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs20
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj2
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs46
-rw-r--r--Jellyfin.Server/Program.cs30
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs4
-rw-r--r--MediaBrowser.Common/Plugins/LocalPlugin.cs113
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs23
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs8
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs5
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs10
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs30
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs6
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs19
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs5
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs18
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs4
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs9
-rw-r--r--MediaBrowser.Model/Activity/IActivityManager.cs7
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs5
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs2
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs6
-rw-r--r--MediaBrowser.Model/Dlna/AudioOptions.cs4
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs4
-rw-r--r--MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs2
-rw-r--r--MediaBrowser.Model/Subtitles/FontFile.cs34
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs32
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj2
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs69
-rw-r--r--debian/control1
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs6
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj6
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs2
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs26
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs92
-rw-r--r--tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs (renamed from tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs)4
-rw-r--r--tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs19
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
144 files changed, 1719 insertions, 1328 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 4d38a906e..b558d2a6f 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -62,6 +62,7 @@ jobs:
- task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact'
+ enabled: false
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@@ -73,6 +74,7 @@ jobs:
- task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact'
+ enabled: false
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll'
@@ -83,6 +85,7 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
+ enabled: false
inputs:
command: custom
custom: compat
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 67aac45c9..0dc604a79 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -63,6 +63,7 @@ jobs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
+ targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
@@ -166,7 +167,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
@@ -175,7 +176,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 4b108b89e..5f25b8cdc 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { typeof(Book).Name },
+ ExcludeItemTypes = new[] { nameof(Book) },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { typeof(Book).Name },
+ ExcludeItemTypes = new[] { nameof(Book) },
IsPlaceHolder = false,
DtoOptions = GetDtoOptions()
};
@@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
StartIndex = startIndex,
Limit = limit,
};
- query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
+ query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
SetSorting(query, sort, false);
@@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
@@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
@@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
// query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.IncludeItemTypes = new[] { nameof(BoxSet) };
var result = _libraryManager.GetItemsResult(query);
@@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
@@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Audio).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
@@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Audio).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
@@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
@@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
var result = _libraryManager.GetItemsResult(query);
@@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
@@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
@@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
{
UserId = user.Id,
Limit = 50,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
},
@@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
{
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
{
- stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
- id = id.Split(new[] { '_' }, 2)[1];
+ stubType = Enum.Parse<StubType>(name, true);
+ id = id.Split('_', 2)[1];
break;
}
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 5b8a89d8f..abaf522bc 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
{
foreach (var att in profile.XmlRootAttributes)
{
- var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+ var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 1807ac6a1..069400833 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -383,9 +383,9 @@ namespace Emby.Dlna
continue;
}
- var filename = Path.GetFileName(name).Substring(namespaceName.Length);
-
- var path = Path.Combine(systemProfilesPath, filename);
+ var path = Path.Join(
+ systemProfilesPath,
+ Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name))
{
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 7d8da86ef..770d56c30 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
builder.Append("</e:propertyset>");
- using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
+ using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 40c2cc0e0..f8a00efac 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints()
{
- var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
+ var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId);
+ var descriptorUri = "/dlna/" + udn + "/description.xml";
foreach (var address in addresses)
{
@@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
- var descriptorUri = "/dlna/" + udn + "/description.xml";
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
var device = new SsdpRootDevice
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index a5b8e2b3c..c07c8aefa 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{
- _logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
+ _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
@@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
var startIndex = command.StartIndex ?? 0;
if (startIndex > 0)
{
- items = items.Skip(startIndex).ToList();
+ items = items.GetRange(startIndex, items.Count - startIndex);
}
var playlist = new List<PlaylistItem>();
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 4b0bbba96..198852ec1 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -169,6 +169,7 @@ namespace Emby.Dlna.Service
var result = new ControlRequestInfo(localName, namespaceURI);
using var subReader = reader.ReadSubtree();
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
+ return result;
}
else
{
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index 579c9e91e..f05d540f8 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -15,6 +15,11 @@ namespace Emby.Naming.Video
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{
CleanDateTimeResult result = new CleanDateTimeResult(name);
+ if (string.IsNullOrEmpty(name))
+ {
+ return result;
+ }
+
var len = cleanDateTimeRegexes.Count;
for (int i = 0; i < len; i++)
{
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index ded22d26c..7116d52b1 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -209,7 +209,10 @@ namespace Emby.Notifications
_libraryUpdateTimer = null;
}
- items = items.Take(10).ToList();
+ if (items.Count > 10)
+ {
+ items = items.GetRange(0, 10);
+ }
foreach (var item in items)
{
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 2adc1d6c3..660bbb2de 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
}
/// <inheritdoc />
- public string VirtualDataPath { get; } = "%AppDataPath%";
+ public string VirtualDataPath => "%AppDataPath%";
/// <summary>
/// Gets the image cache path.
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 31ca73829..9d5b651d9 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
@@ -339,7 +337,7 @@ namespace Emby.Server.Implementations
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
- public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
+ public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
@@ -403,7 +401,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Resolves this instance.
/// </summary>
- /// <typeparam name="T">The type</typeparam>
+ /// <typeparam name="T">The type.</typeparam>
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
@@ -993,62 +991,36 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
- /// <summary>
- /// Comparison function used in <see cref="GetPlugins" />.
- /// </summary>
- /// <param name="a">Item to compare.</param>
- /// <param name="b">Item to compare with.</param>
- /// <returns>Boolean result of the operation.</returns>
- private static int VersionCompare(
- (Version PluginVersion, string Name, string Path) a,
- (Version PluginVersion, string Name, string Path) b)
- {
- int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
-
- if (compare == 0)
- {
- return a.PluginVersion.CompareTo(b.PluginVersion);
- }
-
- return compare;
- }
-
- /// <summary>
- /// Returns a list of plugins to install.
- /// </summary>
- /// <param name="path">Path to check.</param>
- /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
- /// <returns>Enumerable list of dlls to load.</returns>
- private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+ /// <inheritdoc/>
+ public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
- var dllList = new List<string>();
- var versions = new List<(Version PluginVersion, string Name, string Path)>();
+ var minimumVersion = new Version(0, 0, 0, 1);
+ var versions = new List<LocalPlugin>();
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
- string metafile;
foreach (var dir in directories)
{
try
{
- metafile = Path.Combine(dir, "meta.json");
+ var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
- targetAbi = new Version(0, 0, 0, 1);
+ targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
- version = new Version(0, 0, 0, 1);
+ version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
- versions.Add((version, manifest.Name, dir));
+ versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
@@ -1057,15 +1029,15 @@ namespace Emby.Server.Implementations
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
- if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+ if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
{
// Versioned folder.
- versions.Add((ver, metafile, dir));
+ versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
- versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+ versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
@@ -1076,14 +1048,14 @@ namespace Emby.Server.Implementations
}
string lastName = string.Empty;
- versions.Sort(VersionCompare);
+ versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
- dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+ versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
@@ -1091,6 +1063,7 @@ namespace Emby.Server.Implementations
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
+ versions.RemoveAt(x);
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
@@ -1103,7 +1076,7 @@ namespace Emby.Server.Implementations
}
}
- return dllList;
+ return versions;
}
/// <summary>
@@ -1114,21 +1087,24 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
- foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
+ foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
{
- Assembly plugAss;
- try
- {
- plugAss = Assembly.LoadFrom(file);
- }
- catch (FileLoadException ex)
+ foreach (var file in plugin.DllFiles)
{
- Logger.LogError(ex, "Failed to load assembly {Path}", file);
- continue;
- }
+ Assembly plugAss;
+ try
+ {
+ plugAss = Assembly.LoadFrom(file);
+ }
+ catch (FileLoadException ex)
+ {
+ Logger.LogError(ex, "Failed to load assembly {Path}", file);
+ continue;
+ }
- Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
- yield return plugAss;
+ Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
+ yield return plugAss;
+ }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index fb1bb65a0..19045b72b 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
var all = channels;
var totalCount = all.Count;
- if (query.StartIndex.HasValue)
+ if (query.StartIndex.HasValue || query.Limit.HasValue)
{
- all = all.Skip(query.StartIndex.Value).ToList();
+ int startIndex = query.StartIndex ?? 0;
+ int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
+ all = all.GetRange(startIndex, count);
}
- if (query.Limit.HasValue)
- {
- all = all.Take(query.Limit.Value).ToList();
- }
-
- var returnItems = all.ToArray();
-
if (query.RefreshLatestChannelItems)
{
- foreach (var item in returnItems)
+ foreach (var item in all)
{
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
}
@@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
return new QueryResult<Channel>
{
- Items = returnItems,
+ Items = all,
TotalRecordCount = totalCount
};
}
@@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Channel).Name },
+ IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
}
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index eeb49b8fe..2391eed42 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Channel).Name },
+ IncludeItemTypes = new[] { nameof(Channel) },
ExcludeItemIds = installedChannelIds.ToArray()
});
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 1c2aeda70..acb75e9b8 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -3914,7 +3914,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue)
{
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
{
if (query.IsPlayed.Value)
{
@@ -4755,29 +4755,29 @@ namespace Emby.Server.Implementations.Data
{
var list = new List<string>();
- if (IsTypeInQuery(typeof(Person).Name, query))
+ if (IsTypeInQuery(nameof(Person), query))
{
- list.Add(typeof(Person).Name);
+ list.Add(nameof(Person));
}
- if (IsTypeInQuery(typeof(Genre).Name, query))
+ if (IsTypeInQuery(nameof(Genre), query))
{
- list.Add(typeof(Genre).Name);
+ list.Add(nameof(Genre));
}
- if (IsTypeInQuery(typeof(MusicGenre).Name, query))
+ if (IsTypeInQuery(nameof(MusicGenre), query))
{
- list.Add(typeof(MusicGenre).Name);
+ list.Add(nameof(MusicGenre));
}
- if (IsTypeInQuery(typeof(MusicArtist).Name, query))
+ if (IsTypeInQuery(nameof(MusicArtist), query))
{
- list.Add(typeof(MusicArtist).Name);
+ list.Add(nameof(MusicArtist));
}
- if (IsTypeInQuery(typeof(Studio).Name, query))
+ if (IsTypeInQuery(nameof(Studio), query))
{
- list.Add(typeof(Studio).Name);
+ list.Add(nameof(Studio));
}
return list;
@@ -4832,12 +4832,12 @@ namespace Emby.Server.Implementations.Data
var types = new[]
{
- typeof(Episode).Name,
- typeof(Video).Name,
- typeof(Movie).Name,
- typeof(MusicVideo).Name,
- typeof(Series).Name,
- typeof(Season).Name
+ nameof(Episode),
+ nameof(Video),
+ nameof(Movie),
+ nameof(MusicVideo),
+ nameof(Series),
+ nameof(Season)
};
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@@ -5002,26 +5002,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- var commandText = "select Distinct Name from People";
+ var commandText = new StringBuilder("select Distinct p.Name from People p");
+
+ if (query.User != null && query.IsFavorite.HasValue)
+ {
+ commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
+ commandText.Append(typeof(Person).FullName);
+ commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
+ }
var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
}
- commandText += " order by ListOrder";
+ commandText.Append(" order by ListOrder");
if (query.Limit > 0)
{
- commandText += " LIMIT " + query.Limit;
+ commandText.Append(" LIMIT ").Append(query.Limit);
}
using (var connection = GetConnection(true))
{
var list = new List<string>();
- using (var statement = PrepareStatement(connection, commandText))
+ using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
@@ -5087,19 +5094,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (!query.ItemId.Equals(Guid.Empty))
{
whereClauses.Add("ItemId=@ItemId");
- if (statement != null)
- {
- statement.TryBind("@ItemId", query.ItemId.ToByteArray());
- }
+ statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
}
if (!query.AppearsInItemId.Equals(Guid.Empty))
{
- whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
- if (statement != null)
- {
- statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
- }
+ whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
+ statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@@ -5107,10 +5108,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryPersonTypes.Count == 1)
{
whereClauses.Add("PersonType=@PersonType");
- if (statement != null)
- {
- statement.TryBind("@PersonType", queryPersonTypes[0]);
- }
+ statement?.TryBind("@PersonType", queryPersonTypes[0]);
}
else if (queryPersonTypes.Count > 1)
{
@@ -5124,10 +5122,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryExcludePersonTypes.Count == 1)
{
whereClauses.Add("PersonType<>@PersonType");
- if (statement != null)
- {
- statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
- }
+ statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
else if (queryExcludePersonTypes.Count > 1)
{
@@ -5139,19 +5134,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (query.MaxListOrder.HasValue)
{
whereClauses.Add("ListOrder<=@MaxListOrder");
- if (statement != null)
- {
- statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
- }
+ statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
if (!string.IsNullOrWhiteSpace(query.NameContains))
{
- whereClauses.Add("Name like @NameContains");
- if (statement != null)
- {
- statement.TryBind("@NameContains", "%" + query.NameContains + "%");
- }
+ whereClauses.Add("p.Name like @NameContains");
+ statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
+ }
+
+ if (query.IsFavorite.HasValue)
+ {
+ whereClauses.Add("isFavorite=@IsFavorite");
+ statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
+ }
+
+ if (query.User != null)
+ {
+ statement?.TryBind("@UserId", query.User.InternalId);
}
return whereClauses;
@@ -5420,6 +5420,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
Tags = query.Tags,
OfficialRatings = query.OfficialRatings,
+ StudioIds = query.StudioIds,
GenreIds = query.GenreIds,
Genres = query.Genres,
Years = query.Years,
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index edb8753fd..73502c2c9 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
{
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Name = item.Album,
Limit = 1
});
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 68d981ad1..df7a034e8 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
- if (auth?.User == null)
+ if (!auth.IsAuthenticated)
{
- return null;
+ throw new AuthenticationException("Invalid token.");
}
- if (auth.User.HasPermission(PermissionKind.IsDisabled))
+ if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
{
throw new SecurityException("User account has been disabled.");
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 4b407dd9d..e733c9092 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
- var (authInfo, _) =
- GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+ var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
@@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
- var (authInfo, originalAuthInfo) =
- GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
-
- if (originalAuthInfo != null)
- {
- httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
- }
+ var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
- private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+ private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
@@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
Device = device,
DeviceId = deviceId,
Version = version,
- Token = token
+ Token = token,
+ IsAuthenticated = false
};
- AuthenticationInfo originalAuthenticationInfo = null;
- if (!string.IsNullOrWhiteSpace(token))
+ if (string.IsNullOrWhiteSpace(token))
{
- var result = _authRepo.Get(new AuthenticationInfoQuery
- {
- AccessToken = token
- });
+ // Request doesn't contain a token.
+ return authInfo;
+ }
- originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
+ var result = _authRepo.Get(new AuthenticationInfoQuery
+ {
+ AccessToken = token
+ });
- if (originalAuthenticationInfo != null)
- {
- var updateToken = false;
+ if (result.Items.Count > 0)
+ {
+ authInfo.IsAuthenticated = true;
+ }
- // TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(authInfo.Client))
- {
- authInfo.Client = originalAuthenticationInfo.AppName;
- }
+ var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
- if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
- {
- authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
- }
+ if (originalAuthenticationInfo != null)
+ {
+ var updateToken = false;
- // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+ // TODO: Remove these checks for IsNullOrWhiteSpace
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
+ {
+ authInfo.Client = originalAuthenticationInfo.AppName;
+ }
- if (string.IsNullOrWhiteSpace(authInfo.Device))
- {
- authInfo.Device = originalAuthenticationInfo.DeviceName;
- }
- else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
- {
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- originalAuthenticationInfo.DeviceName = authInfo.Device;
- }
- }
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+ {
+ authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
+ }
- if (string.IsNullOrWhiteSpace(authInfo.Version))
- {
- authInfo.Version = originalAuthenticationInfo.AppVersion;
- }
- else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
+ {
+ authInfo.Device = originalAuthenticationInfo.DeviceName;
+ }
+ else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
{
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- originalAuthenticationInfo.AppVersion = authInfo.Version;
- }
+ updateToken = true;
+ originalAuthenticationInfo.DeviceName = authInfo.Device;
}
+ }
- if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
+ {
+ authInfo.Version = originalAuthenticationInfo.AppVersion;
+ }
+ else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
{
- originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
+ originalAuthenticationInfo.AppVersion = authInfo.Version;
}
+ }
- if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
- {
- authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
+ if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+ {
+ originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
+ updateToken = true;
+ }
- if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
- {
- originalAuthenticationInfo.UserName = authInfo.User.Username;
- updateToken = true;
- }
- }
+ if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
+ {
+ authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
- if (updateToken)
+ if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
- _authRepo.Update(originalAuthenticationInfo);
+ originalAuthenticationInfo.UserName = authInfo.User.Username;
+ updateToken = true;
}
+
+ authInfo.IsApiKey = true;
+ }
+ else
+ {
+ authInfo.IsApiKey = false;
+ }
+
+ if (updateToken)
+ {
+ _authRepo.Update(originalAuthenticationInfo);
}
}
- return (authInfo, originalAuthenticationInfo);
+ return authInfo;
}
/// <summary>
@@ -267,7 +274,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (param.Length == 2)
{
var value = NormalizeValue(param[1].Trim(new[] { '"' }));
- result.Add(param[0], value);
+ result[param[0]] = value;
}
}
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
index bf57382ed..afa4ec7b1 100644
--- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
// return _libraryManager.GetItemList(new InternalItemsQuery
// {
// ArtistIds = new[] { item.Id },
- // IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ // IncludeItemTypes = new[] { nameof(MusicAlbum) },
// OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
// Limit = 4,
// Recursive = true,
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 1cd4cd66b..381788231 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name },
+ IncludeItemTypes = new[]
+ {
+ nameof(MusicAlbum),
+ nameof(MusicVideo),
+ nameof(Audio)
+ },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
@@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
- IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
+ IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 6f85a2408..04dd1e5eb 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
}
+ public BaseItem GetParentItem(string parentId, Guid? userId)
+ {
+ if (!string.IsNullOrEmpty(parentId))
+ {
+ return GetItemById(new Guid(parentId));
+ }
+
+ if (userId.HasValue && userId != Guid.Empty)
+ {
+ return GetUserRootFolder();
+ }
+
+ return RootFolder;
+ }
+
/// <inheritdoc />
public bool IsVideoFile(string path)
{
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 877fdec86..658c53f28 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
DtoOptions = dtoOptions
})
.Cast<Audio>()
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = genreIds.ToArray(),
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 9a69bce0e..c850e3a08 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = query.ExcludeItemTypes.ToList();
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
- excludeItemTypes.Add(typeof(Year).Name);
- excludeItemTypes.Add(typeof(Folder).Name);
+ excludeItemTypes.Add(nameof(Year));
+ excludeItemTypes.Add(nameof(Folder));
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(Genre).Name);
- AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
+ AddIfMissing(includeItemTypes, nameof(Genre));
+ AddIfMissing(includeItemTypes, nameof(MusicGenre));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(Genre).Name);
- AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
+ AddIfMissing(excludeItemTypes, nameof(Genre));
+ AddIfMissing(excludeItemTypes, nameof(MusicGenre));
}
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(Person).Name);
+ AddIfMissing(includeItemTypes, nameof(Person));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(Person).Name);
+ AddIfMissing(excludeItemTypes, nameof(Person));
}
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(Studio).Name);
+ AddIfMissing(includeItemTypes, nameof(Studio));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(Studio).Name);
+ AddIfMissing(excludeItemTypes, nameof(Studio));
}
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
+ AddIfMissing(includeItemTypes, nameof(MusicArtist));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
+ AddIfMissing(excludeItemTypes, nameof(MusicArtist));
}
- AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
- AddIfMissing(excludeItemTypes, typeof(Folder).Name);
+ AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
+ AddIfMissing(excludeItemTypes, nameof(Folder));
var mediaTypes = query.MediaTypes.ToList();
if (includeItemTypes.Count > 0)
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index d4c8c35e6..f9a3e2c64 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(MusicArtist).Name },
+ IncludeItemTypes = new[] { nameof(MusicArtist) },
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 8275c873a..8739a9e1b 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Person).Name },
+ IncludeItemTypes = new[] { nameof(Person) },
IsDeadPerson = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index ca35adfff..9a8c5f39d 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Studio).Name },
+ IncludeItemTypes = new[] { nameof(Studio) },
IsDeadStudio = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index ca60c3366..fcc2d1eeb 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
@@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
Limit = 1,
DtoOptions = new DtoOptions(true)
{
@@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true)
{
@@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
Name = program.Name
}).ToArray();
@@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var result = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds,
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 28aabc159..aacadde50 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -15,6 +15,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
+ private readonly ICryptoProvider _cryptoProvider;
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
- IApplicationHost appHost)
+ IApplicationHost appHost,
+ ICryptoProvider cryptoProvider)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
+ _cryptoProvider = cryptoProvider;
}
private string UserAgent => _appHost.ApplicationUserAgent;
@@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken)
{
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
- options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
+ var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
+ string hashedPassword = Hex.Encode(hashedPasswordBytes);
+ options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 49ad73af3..6af49dd45 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
@@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
{
program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 5bdd1c23c..9c7d624ee 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids,
IsSports = query.IsSports,
IsSeries = query.IsSeries,
- IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvChannel) },
TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked,
@@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate,
@@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
IsAiring = query.IsAiring,
HasAired = query.HasAired,
IsNews = query.IsNews,
@@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase)
{
- CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
- CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
+ CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
+ CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
}
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsMovie.Value)
{
- includeItemTypes.Add(typeof(Movie).Name);
+ includeItemTypes.Add(nameof(Movie));
}
else
{
- excludeItemTypes.Add(typeof(Movie).Name);
+ excludeItemTypes.Add(nameof(Movie));
}
}
@@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsSeries.Value)
{
- includeItemTypes.Add(typeof(Episode).Name);
+ includeItemTypes.Add(nameof(Episode));
}
else
{
- excludeItemTypes.Add(typeof(Episode).Name);
+ excludeItemTypes.Add(nameof(Episode));
}
}
@@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
ChannelIds = channelIds,
MaxStartDate = now,
MinEndDate = now,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 6730751d5..858c10030 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
+ public string GetFilePath()
+ {
+ return TempFilePath;
+ }
+
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 8107bc427..4b170b2e4 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+ return await new M3uParser(Logger, _httpClientFactory, _appHost)
+ .Parse(info, channelIdPrefix, cancellationToken)
+ .ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index f066a749e..c064e2fe6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
@@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_appHost = appHost;
}
- public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
+ public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{
// Read the file and display it line by line.
- using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
+ using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
- return GetChannels(reader, channelIdPrefix, tunerHostId);
+ return GetChannels(reader, channelIdPrefix, info.Id);
}
}
@@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
- public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
+ public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{
- if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- return _httpClientFactory.CreateClient(NamedClient.Default)
- .GetStreamAsync(url);
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+ if (!string.IsNullOrEmpty(info.UserAgent))
+ {
+ requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
+ }
+
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(requestMessage, cancellationToken)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
- return Task.FromResult((Stream)File.OpenRead(url));
+ return File.OpenRead(info.Url);
}
private const string ExtInfPrefix = "#EXTINF:";
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 6c10fca8c..2e1b89509 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ // Response stream is disposed manually.
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
@@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
+ public string GetFilePath()
+ {
+ return TempFilePath;
+ }
+
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index b34fad066..fb31b01ff 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internetové kanály",
"TasksApplicationCategory": "Aplikace",
"TasksLibraryCategory": "Knihovna",
- "TasksMaintenanceCategory": "Údržba"
+ "TasksMaintenanceCategory": "Údržba",
+ "TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
+ "TaskCleanActivityLog": "Smazat záznam aktivity"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 97ad1694a..c81de8218 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internet Kanäle",
"TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek",
- "TasksMaintenanceCategory": "Wartung"
+ "TasksMaintenanceCategory": "Wartung",
+ "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
+ "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 92c54fb0e..6d8b222b4 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -95,6 +95,8 @@
"TasksLibraryCategory": "Library",
"TasksApplicationCategory": "Application",
"TasksChannelsCategory": "Internet Channels",
+ "TaskCleanActivityLog": "Clean Activity Log",
+ "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanCache": "Clean Cache Directory",
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index d4e0b299d..d6af40c40 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
- "TvShows": "Programas de televisión",
+ "TvShows": "Series",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
@@ -113,5 +113,7 @@
"TaskRefreshChannels": "Actualizar canales",
"TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
"TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
- "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos."
+ "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos.",
+ "TaskCleanActivityLogDescription": "Elimina todos los registros de actividad anteriores a la fecha configurada.",
+ "TaskCleanActivityLog": "Limpiar registro de actividad"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index f4ca8575a..cc9243f37 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -113,5 +113,7 @@
"TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Bibliothèque",
- "TasksMaintenanceCategory": "Maintenance"
+ "TasksMaintenanceCategory": "Maintenance",
+ "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
+ "TaskCleanActivityLog": "Nettoyer le journal d'activité"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index a3c936240..9be91b724 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -5,13 +5,13 @@
"Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige",
- "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
+ "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije",
- "DeviceOfflineWithName": "{0} se odspojilo",
- "DeviceOnlineWithName": "{0} je spojeno",
- "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
+ "DeviceOfflineWithName": "{0} je prekinuo vezu",
+ "DeviceOnlineWithName": "{0} je povezan",
+ "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
@@ -23,95 +23,97 @@
"HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
- "HeaderNextUp": "Sljedeće je",
+ "HeaderNextUp": "Slijedi",
"HeaderRecordingGroups": "Grupa snimka",
- "HomeVideos": "Kućni videi",
+ "HomeVideos": "Kućni video",
"Inherit": "Naslijedi",
"ItemAddedWithName": "{0} je dodano u biblioteku",
- "ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
+ "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije",
- "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
- "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
- "MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
+ "MessageApplicationUpdated": "Jellyfin server je ažuriran",
+ "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
+ "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"MixedContent": "Miješani sadržaj",
"Movies": "Filmovi",
"Music": "Glazba",
"MusicVideos": "Glazbeni spotovi",
"NameInstallFailed": "{0} neuspješnih instalacija",
"NameSeasonNumber": "Sezona {0}",
- "NameSeasonUnknown": "Nepoznata sezona",
+ "NameSeasonUnknown": "Sezona nepoznata",
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
- "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
- "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
- "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
- "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
- "NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
- "NotificationOptionInstallationFailed": "Instalacija neuspješna",
- "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
- "NotificationOptionPluginError": "Dodatak otkazao",
+ "NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
+ "NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
+ "NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
+ "NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
+ "NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
+ "NotificationOptionInstallationFailed": "Instalacija nije uspjela",
+ "NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
+ "NotificationOptionPluginError": "Dodatak zakazao",
"NotificationOptionPluginInstalled": "Dodatak instaliran",
- "NotificationOptionPluginUninstalled": "Dodatak uklonjen",
- "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak",
- "NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera",
- "NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen",
+ "NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
+ "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
+ "NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
+ "NotificationOptionTaskFailed": "Greška zakazanog zadatka",
"NotificationOptionUserLockedOut": "Korisnik zaključan",
- "NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
- "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
- "Photos": "Slike",
- "Playlists": "Popis za reprodukciju",
+ "NotificationOptionVideoPlayback": "Reprodukcija videa započela",
+ "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
+ "Photos": "Fotografije",
+ "Playlists": "Popisi za reprodukciju",
"Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano",
"PluginUpdatedWithName": "{0} je ažurirano",
- "ProviderValue": "Pružitelj: {0}",
+ "ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto",
- "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
+ "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Serije",
"Songs": "Pjesme",
- "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
+ "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
- "SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
- "Sync": "Sink.",
- "System": "Sistem",
+ "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
+ "Sync": "Sinkronizacija",
+ "System": "Sustav",
"TvShows": "Serije",
"User": "Korisnik",
- "UserCreatedWithName": "Korisnik {0} je stvoren",
+ "UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan",
- "UserDownloadingItemWithValues": "{0} se preuzima {1}",
+ "UserDownloadingItemWithValues": "{0} preuzima {1}",
"UserLockedOutWithName": "Korisnik {0} je zaključan",
- "UserOfflineFromDevice": "{0} se odspojilo od {1}",
- "UserOnlineFromDevice": "{0} je online od {1}",
+ "UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
+ "UserOnlineFromDevice": "{0} povezan od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
- "UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
- "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
- "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
+ "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
+ "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
+ "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
- "ValueSpecialEpisodeName": "Specijal - {0}",
+ "ValueSpecialEpisodeName": "Posebno - {0}",
"VersionNumber": "Verzija {0}",
- "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
- "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
- "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
- "TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
- "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
- "TaskCleanCache": "Očisti priručnu memoriju",
+ "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
+ "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
+ "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
+ "TaskRefreshChapterImages": "Izdvoji slike poglavlja",
+ "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
+ "TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje",
- "TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
- "TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
- "TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
+ "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
+ "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
+ "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale",
- "TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
- "TaskCleanTranscode": "Očisti direktorij za transkodiranje",
- "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
+ "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
+ "TaskCleanTranscode": "Očisti mapu transkodiranja",
+ "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke",
- "TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
- "TaskRefreshPeople": "Osvježi ljude",
- "TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
- "TaskCleanLogs": "Očisti direktorij sa logovima",
+ "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
+ "TaskRefreshPeople": "Osvježi osobe",
+ "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
+ "TaskCleanLogs": "Očisti mapu dnevnika zapisa",
"TasksChannelsCategory": "Internet kanali",
- "TasksLibraryCategory": "Biblioteka"
+ "TasksLibraryCategory": "Biblioteka",
+ "TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
+ "TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 0a6238578..9e37ddc27 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -113,5 +113,7 @@
"TasksChannelsCategory": "Canali su Internet",
"TasksApplicationCategory": "Applicazione",
"TasksLibraryCategory": "Libreria",
- "TasksMaintenanceCategory": "Manutenzione"
+ "TasksMaintenanceCategory": "Manutenzione",
+ "TaskCleanActivityLog": "Attività di Registro Completate",
+ "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 35004f0eb..02bf8496f 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -96,7 +96,7 @@
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
"TaskRefreshLibrary": "メディアライブラリのスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
- "TaskCleanCache": "キャッシュの掃除",
+ "TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション",
"TasksLibraryCategory": "ライブラリ",
@@ -112,5 +112,7 @@
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
"TaskRefreshChapterImages": "チャプター画像を抽出する",
- "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
+ "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
+ "TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
+ "TaskCleanActivityLog": "アクティビティの履歴を消去"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index fb01e4645..b8b39833c 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -27,7 +27,7 @@
"HeaderRecordingGroups": "녹화 그룹",
"HomeVideos": "홈 비디오",
"Inherit": "상속",
- "ItemAddedWithName": "{0}가 라이브러리에 추가됨",
+ "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
"LabelIpAddressValue": "IP 주소: {0}",
"LabelRunningTimeValue": "상영 시간: {0}",
@@ -113,5 +113,7 @@
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
"TaskCleanCache": "캐시 폴더 청소",
"TasksChannelsCategory": "인터넷 채널",
- "TasksLibraryCategory": "라이브러리"
+ "TasksLibraryCategory": "라이브러리",
+ "TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
+ "TaskCleanActivityLog": "활동내역청소"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 95b93afb8..c0db2cf7f 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -113,5 +113,7 @@
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
- "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
+ "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
+ "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
+ "TaskCleanActivityLog": "Очистить журнал активности"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index ff4b9e84f..66681f025 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -3,20 +3,20 @@
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
"Application": "Aplikacija",
"Artists": "Izvajalci",
- "AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno",
+ "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil",
"Books": "Knjige",
- "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}",
+ "CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan",
- "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
+ "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
"Favorites": "Priljubljeno",
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
- "HeaderContinueWatching": "Nadaljuj gledanje",
+ "HeaderContinueWatching": "Nadaljuj z ogledom",
"HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Priljubljene epizode",
@@ -32,23 +32,23 @@
"LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Najnovejše",
- "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
- "MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
+ "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
+ "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen",
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
- "MixedContent": "Razne vsebine",
+ "MixedContent": "Mešane vsebine",
"Movies": "Filmi",
"Music": "Glasba",
"MusicVideos": "Glasbeni videi",
"NameInstallFailed": "{0} namestitev neuspešna",
"NameSeasonNumber": "Sezona {0}",
- "NameSeasonUnknown": "Season neznana",
+ "NameSeasonUnknown": "Neznana sezona",
"NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.",
"NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
"NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
- "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto",
- "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno",
- "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen",
+ "NotificationOptionAudioPlayback": "Predvajanje zvoka se je začelo",
+ "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka se je ustavilo",
+ "NotificationOptionCameraImageUploaded": "Fotografija naložena",
"NotificationOptionInstallationFailed": "Namestitev neuspešna",
"NotificationOptionNewLibraryContent": "Nove vsebine dodane",
"NotificationOptionPluginError": "Napaka dodatka",
@@ -56,41 +56,41 @@
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
- "NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
+ "NotificationOptionTaskFailed": "Načrtovano opravilo neuspešno",
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
"Photos": "Fotografije",
"Playlists": "Seznami predvajanja",
- "Plugin": "Plugin",
+ "Plugin": "Dodatek",
"PluginInstalledWithName": "{0} je bil nameščen",
"PluginUninstalledWithName": "{0} je bil odstranjen",
"PluginUpdatedWithName": "{0} je bil posodobljen",
- "ProviderValue": "Provider: {0}",
+ "ProviderValue": "Ponudnik: {0}",
"ScheduledTaskFailedWithName": "{0} ni uspelo",
"ScheduledTaskStartedWithName": "{0} začeto",
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
"Shows": "Serije",
"Songs": "Pesmi",
- "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
+ "StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
"Sync": "Sinhroniziraj",
- "System": "System",
+ "System": "Sistem",
"TvShows": "TV serije",
- "User": "User",
+ "User": "Uporabnik",
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
"UserDownloadingItemWithValues": "{0} prenaša {1}",
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
- "UserOnlineFromDevice": "{0} je aktiven iz {1}",
+ "UserOnlineFromDevice": "{0} je aktiven na {1}",
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
- "ValueSpecialEpisodeName": "Poseben - {0}",
+ "ValueSpecialEpisodeName": "Posebna - {0}",
"VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
@@ -102,7 +102,7 @@
"TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
"TaskRefreshPeople": "Osveži osebe",
"TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.",
- "TaskCleanLogs": "Počisti mapo dnevnika",
+ "TaskCleanLogs": "Počisti mapo dnevnikov",
"TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
"TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
"TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 1e5f2cf19..54d3a65f0 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}",
- "Collections": "Koleksiyonlar",
+ "Collections": "Koleksiyon",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
@@ -23,7 +23,7 @@
"HeaderFavoriteShows": "Favori Diziler",
"HeaderFavoriteSongs": "Favori Şarkılar",
"HeaderLiveTV": "Canlı TV",
- "HeaderNextUp": "Sonraki hafta",
+ "HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları",
"HomeVideos": "Ev videoları",
"Inherit": "Devral",
@@ -113,5 +113,7 @@
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
- "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler."
+ "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
+ "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
+ "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi."
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index ac74deff8..ba58e4beb 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -112,5 +112,7 @@
"Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
"Application": "Ứng Dụng",
- "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
+ "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
+ "TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
+ "TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 53a902de2..3ae0fe5e7 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -113,5 +113,7 @@
"TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
"TaskCleanCache": "清理缓存目录",
"TasksApplicationCategory": "应用程序",
- "TasksMaintenanceCategory": "维护"
+ "TasksMaintenanceCategory": "维护",
+ "TaskCleanActivityLog": "清理程序日志",
+ "TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 30f726630..d2e3d77a3 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -112,5 +112,7 @@
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網路頻道",
"TasksApplicationCategory": "應用程式",
- "TasksMaintenanceCategory": "維修"
+ "TasksMaintenanceCategory": "維護",
+ "TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
+ "TaskCleanActivityLog": "清除活動紀錄"
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index bc01f9543..56f4133a0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -703,7 +703,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
MaxRuntimeTicks = info.MaxRuntimeTicks
};
- if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -717,7 +717,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
};
}
- if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -737,7 +737,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
};
}
- if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
{
if (!info.IntervalTicks.HasValue)
{
@@ -751,7 +751,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
};
}
- if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{
return new StartupTrigger();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
new file mode 100644
index 000000000..4abbf784b
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old activity log entries.
+ /// </summary>
+ public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly ILocalizationManager _localization;
+ private readonly IActivityManager _activityManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
+ /// </summary>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public CleanActivityLogTask(
+ ILocalizationManager localization,
+ IActivityManager activityManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _localization = localization;
+ _activityManager = activityManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
+
+ /// <inheritdoc />
+ public string Key => "CleanActivityLog";
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
+ if (!retentionDays.HasValue || retentionDays <= 0)
+ {
+ throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
+ }
+
+ var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1);
+ return _activityManager.CleanAsync(startDate);
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return Enumerable.Empty<TaskTriggerInfo>();
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index dfdd4200e..ac589b03c 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -104,6 +104,6 @@ namespace Emby.Server.Implementations
public string InternalMetadataPath { get; set; }
/// <inheritdoc />
- public string VirtualInternalMetadataPath { get; } = "%MetadataPath%";
+ public string VirtualInternalMetadataPath => "%MetadataPath%";
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index d1818deff..ccd1446dd 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.TV
.GetItemList(
new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
SeriesPresentationUniqueKey = presentationUniqueKey,
Limit = limit,
@@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) },
Limit = 1,
IsPlayed = false,
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 003cf3c74..fd1f43e62 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -10,19 +10,21 @@ using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
-using MediaBrowser.Common.System;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.System;
namespace Emby.Server.Implementations.Updates
{
@@ -36,6 +38,7 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ILogger<InstallationManager> _logger;
private readonly IApplicationPaths _appPaths;
+ private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerConfigurationManager _config;
@@ -45,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
- private readonly IApplicationHost _applicationHost;
+ private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient;
@@ -63,25 +66,22 @@ namespace Emby.Server.Implementations.Updates
public InstallationManager(
ILogger<InstallationManager> logger,
- IApplicationHost appHost,
+ IServerApplicationHost appHost,
IApplicationPaths appPaths,
+ IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IJsonSerializer jsonSerializer,
IServerConfigurationManager config,
IFileSystem fileSystem,
IZipClient zipClient)
{
- if (logger == null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
-
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
_logger = logger;
_applicationHost = appHost;
_appPaths = appPaths;
+ _eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_jsonSerializer = jsonSerializer;
_config = config;
@@ -90,27 +90,6 @@ namespace Emby.Server.Implementations.Updates
}
/// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstalling;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
- /// <inheritdoc />
- public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
- /// <inheritdoc />
- public event EventHandler<IPlugin> PluginUninstalled;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PluginUpdated;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PluginInstalled;
-
- /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
@@ -237,7 +216,8 @@ namespace Emby.Server.Implementations.Updates
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
- foreach (var plugin in _applicationHost.Plugins)
+ var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
+ foreach (var plugin in plugins)
{
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
@@ -268,11 +248,11 @@ namespace Emby.Server.Implementations.Updates
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
- PackageInstalling?.Invoke(this, package);
+ await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
try
{
- await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
+ var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
lock (_currentInstallationsLock)
{
@@ -280,8 +260,11 @@ namespace Emby.Server.Implementations.Updates
}
_completedInstallationsInternal.Add(package);
+ await _eventManager.PublishAsync(isUpdate
+ ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
+ : new PluginInstalledEventArgs(package)).ConfigureAwait(false);
- PackageInstallationCompleted?.Invoke(this, package);
+ _applicationHost.NotifyPendingRestart();
}
catch (OperationCanceledException)
{
@@ -292,7 +275,7 @@ namespace Emby.Server.Implementations.Updates
_logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
- PackageInstallationCancelled?.Invoke(this, package);
+ await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false);
throw;
}
@@ -305,11 +288,11 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple);
}
- PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs
+ await _eventManager.PublishAsync(new InstallationFailedEventArgs
{
InstallationInfo = package,
Exception = ex
- });
+ }).ConfigureAwait(false);
throw;
}
@@ -326,7 +309,7 @@ namespace Emby.Server.Implementations.Updates
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
- private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+ private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
@@ -336,20 +319,9 @@ namespace Emby.Server.Implementations.Updates
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
- if (plugin == null)
- {
- _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version);
+ _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
- PluginInstalled?.Invoke(this, package);
- }
- else
- {
- _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version);
-
- PluginUpdated?.Invoke(this, package);
- }
-
- _applicationHost.NotifyPendingRestart();
+ return plugin != null;
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@@ -467,7 +439,7 @@ namespace Emby.Server.Implementations.Updates
_config.SaveConfiguration();
}
- PluginUninstalled?.Invoke(this, plugin);
+ _eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_applicationHost.NotifyPendingRestart();
}
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index d732b6bc6..7d68aecf9 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -50,6 +50,13 @@ namespace Jellyfin.Api.Auth
bool localAccessOnly = false,
bool requiredDownloadPermission = false)
{
+ // ApiKey is currently global admin, always allow.
+ var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
+ if (isApiKey)
+ {
+ return true;
+ }
+
// Ensure claim has userId.
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
if (!userId.HasValue)
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 733c6959e..27a1f61be 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,10 +1,10 @@
using System.Globalization;
-using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
@@ -43,24 +43,23 @@ namespace Jellyfin.Api.Auth
try
{
var authorizationInfo = _authService.Authenticate(Request);
- if (authorizationInfo == null)
+ var role = UserRoles.User;
+ if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
- return Task.FromResult(AuthenticateResult.NoResult());
- // TODO return when legacy API is removed.
- // Don't spam the log with "Invalid User"
- // return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
+ role = UserRoles.Administrator;
}
var claims = new[]
{
- new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
- new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+ new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
+ new Claim(ClaimTypes.Role, role),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
+ new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
index b5913daab..be77b7a4e 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{
var validated = ValidateClaims(context.User);
- if (!validated)
+ if (validated)
+ {
+ context.Succeed(requirement);
+ }
+ else
{
context.Fail();
- return Task.CompletedTask;
}
- context.Succeed(requirement);
return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
index 5213bc4cb..a7623556a 100644
--- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
@@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
{
var validated = ValidateClaims(context.User, ignoreSchedule: true);
- if (!validated)
+ if (validated)
+ {
+ context.Succeed(requirement);
+ }
+ else
{
context.Fail();
- return Task.CompletedTask;
}
- context.Succeed(requirement);
return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
index af73352bc..d772ec554 100644
--- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
@@ -29,13 +29,13 @@ namespace Jellyfin.Api.Auth.LocalAccessPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
{
var validated = ValidateClaims(context.User, localAccessOnly: true);
- if (!validated)
+ if (validated)
{
- context.Fail();
+ context.Succeed(requirement);
}
else
{
- context.Succeed(requirement);
+ context.Fail();
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
index 4d7c7135d..8323312e5 100644
--- a/Jellyfin.Api/Constants/InternalClaimTypes.cs
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -34,5 +34,10 @@
/// Token.
/// </summary>
public const string Token = "Jellyfin-Token";
+
+ /// <summary>
+ /// Is Api Key.
+ /// </summary>
+ public const string IsApiKey = "Jellyfin-IsApiKey";
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 610c2aa64..a35d7a556 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -100,7 +101,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -146,9 +147,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
@@ -299,7 +300,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -309,7 +310,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -355,9 +356,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index fda00b8d4..20fc96ba8 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortOrder,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? sortBy,
[FromQuery] string? fields)
{
@@ -196,7 +197,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? fields,
[FromQuery] string? channelIds)
{
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 2fc697a6a..eae06b767 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -83,14 +83,14 @@ namespace Jellyfin.Api.Controllers
/// Adds items to a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
- /// <param name="itemIds">Item ids, comma delimited.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
/// <response code="204">Items added to collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
+ public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{
- await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true);
+ await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
return NoContent();
}
@@ -98,14 +98,14 @@ namespace Jellyfin.Api.Controllers
/// Removes items from a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
- /// <param name="itemIds">Item ids, comma delimited.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
/// <response code="204">Items removed from collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
+ public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{
- await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false);
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 74380c2ef..b3e3490c2 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.RequiresElevation)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
@@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Devices retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
@@ -62,7 +61,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpGet("Info")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
@@ -84,7 +82,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpGet("Options")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
@@ -107,7 +104,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpPost("Options")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 874467c75..76f5717e3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -81,6 +81,9 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+ // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
+ _displayPreferencesManager.SaveChanges();
+
return dto;
}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 271ae293b..4e6455eaa 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -77,6 +77,7 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
@@ -94,6 +95,7 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
@@ -111,8 +113,12 @@ namespace Jellyfin.Api.Controllers
/// Process a content directory control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [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);
@@ -122,8 +128,12 @@ namespace Jellyfin.Api.Controllers
/// Process a connection manager control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [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);
@@ -133,8 +143,12 @@ namespace Jellyfin.Api.Controllers
/// Process a media receiver registrar control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [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);
@@ -144,11 +158,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</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)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
@@ -158,11 +176,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</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)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{
return ProcessEventRequest(_contentDirectory);
@@ -172,11 +194,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</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)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{
return ProcessEventRequest(_connectionManager);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 1153a601e..e07690e11 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -295,6 +295,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@@ -351,6 +352,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -403,7 +405,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -623,6 +625,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@@ -677,6 +680,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -729,7 +733,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -959,6 +963,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@@ -1017,6 +1022,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -1069,7 +1075,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 409d8e863..f6e0772ec 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,10 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -48,7 +48,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all genres from a given item, folder, or the entire library.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
@@ -56,22 +55,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@@ -83,7 +69,6 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -91,22 +76,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
- [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -117,42 +89,22 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -170,87 +122,20 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
+ QueryResult<(BaseItem, ItemCounts)> result;
+ if (parentItem is ICollectionFolder parentCollectionFolder
+ && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
+ || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
{
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
+ result = _libraryManager.GetMusicGenres(query);
}
-
- foreach (var filter in filters)
+ else
{
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
+ result = _libraryManager.GetGenres(query);
}
- var result = new QueryResult<(BaseItem, ItemCounts)>();
-
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index e6e6b3e70..2f342d0a8 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -72,7 +73,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -109,7 +110,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var album = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -146,7 +147,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -183,7 +184,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -219,7 +220,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -256,7 +257,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -293,7 +294,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -316,9 +317,9 @@ namespace Jellyfin.Api.Controllers
TotalRecordCount = list.Count
};
- if (limit.HasValue)
+ if (limit.HasValue && limit < list.Count)
{
- list = list.Take(limit.Value).ToList();
+ list = list.GetRange(0, limit.Value);
}
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 33fa14c64..adc95f757 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] LocationType[] excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -182,10 +183,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
- [FromQuery] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery] string? genres,
@@ -194,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -536,7 +537,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index d290e3c5b..94995650c 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -75,7 +76,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name,
[FromQuery] string? collectionType,
- [FromQuery] string[] paths,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
{
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 22f140ea6..31253cbbc 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common;
@@ -146,7 +147,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy,
@@ -263,7 +264,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie,
@@ -350,7 +351,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
@@ -561,7 +562,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId,
@@ -592,7 +593,7 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids,
IsSports = isSports,
SeriesTimerId = seriesTimerId,
- Genres = RequestHelpers.Split(genres, ',', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds)
};
@@ -648,7 +649,7 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids,
IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId,
- Genres = RequestHelpers.Split(body.Genres, ',', true),
+ Genres = RequestHelpers.Split(body.Genres, '|', true),
GenreIds = RequestHelpers.GetGuids(body.GenreIds)
};
@@ -705,7 +706,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? genreIds,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
@@ -1220,11 +1221,8 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- await using var memoryStream = new MemoryStream();
- await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(memoryStream, CancellationToken.None)
- .ConfigureAwait(false);
- return File(memoryStream, MimeTypes.GetMimeType("file." + container));
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
private void AssertUserCanManageLiveTv()
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 4c21999b1..186024585 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
@@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? openToken,
[FromQuery] Guid? userId,
[FromQuery] string? playSessionId,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 7fcfc749d..afb536e74 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
IncludeItemTypes = new[]
{
nameof(Movie),
- // typeof(Trailer).Name,
- // typeof(LiveTvProgram).Name
+ // nameof(Trailer),
+ // nameof(LiveTvProgram)
},
// IsMovie = true
OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 2a1312273..e434f190a 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,10 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -48,7 +48,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all music genres from a given item, folder, or the entire library.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
@@ -56,22 +55,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@@ -81,8 +67,8 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Music genres returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
[HttpGet]
+ [Obsolete("Use GetGenres instead")]
public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -90,22 +76,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
- [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -116,42 +89,22 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -169,87 +122,10 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in filters)
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
var result = _libraryManager.GetMusicGenres(query);
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index bd84c9228..80395950c 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,10 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -27,6 +27,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
/// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class.
@@ -34,84 +35,53 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
public PersonsController(
ILibraryManager libraryManager,
IDtoService dtoService,
- IUserManager userManager)
+ IUserManager userManager,
+ IUserDataManager userDataManager)
{
_libraryManager = libraryManager;
_dtoService = dtoService;
_userManager = userManager;
+ _userDataManager = userDataManager;
}
/// <summary>
- /// Gets all persons from a given item, folder, or the entire library.
+ /// Gets all persons.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
/// <param name="userId">User id.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Persons returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
- [FromQuery] double? minCommunityRating,
- [FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
[FromQuery] string? fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? excludePersonTypes,
[FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
+ [FromQuery] bool? enableImages = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
@@ -119,136 +89,28 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
- BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
- var query = new InternalItemsQuery(user)
+ var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
- };
-
- if (!string.IsNullOrWhiteSpace(parentId))
- {
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { new Guid(parentId) };
- }
- else
- {
- query.ItemIds = new[] { new Guid(parentId) };
- }
- }
-
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in filters)
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = new QueryResult<(BaseItem, ItemCounts)>();
-
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
+ ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
+ NameContains = searchTerm,
+ User = user,
+ IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
+ AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
+ Limit = limit ?? 0
});
return new QueryResult<BaseItemDto>
{
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
+ Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
+ TotalRecordCount = peopleItems.Count
};
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 0419b2436..f2213f20d 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null)
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 565670962..e506ac7bf 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult PostCapabilities(
[FromQuery] string? id,
[FromQuery] string? playableMediaTypes,
- [FromQuery] GeneralCommandType[] supportedCommands,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true)
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 1d2bf2255..c60d14479 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,9 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all studios from a given item, folder, or the entire library.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param>
@@ -54,22 +53,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@@ -81,7 +68,6 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetStudios(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -89,22 +75,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -117,44 +91,23 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
- var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
- MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -172,84 +125,9 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|').Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null).Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in filters)
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = new QueryResult<(BaseItem, ItemCounts)>();
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, itemCounts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = itemCounts.ItemCount;
- dto.ProgramCount = itemCounts.ProgramCount;
- dto.SeriesCount = itemCounts.SeriesCount;
- dto.EpisodeCount = itemCounts.EpisodeCount;
- dto.MovieCount = itemCounts.MovieCount;
- dto.TrailerCount = itemCounts.TrailerCount;
- dto.AlbumCount = itemCounts.AlbumCount;
- dto.SongCount = itemCounts.SongCount;
- dto.ArtistCount = itemCounts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var result = _libraryManager.GetStudios(query);
+ var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index cc682ed54..a01ae31a0 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -11,6 +11,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.SubtitleDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -21,6 +24,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Subtitles;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -34,6 +38,7 @@ namespace Jellyfin.Api.Controllers
[Route("")]
public class SubtitleController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager;
private readonly ISubtitleEncoder _subtitleEncoder;
@@ -46,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
/// </summary>
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
@@ -55,6 +61,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
public SubtitleController(
+ IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
ISubtitleManager subtitleManager,
ISubtitleEncoder subtitleEncoder,
@@ -64,6 +71,7 @@ namespace Jellyfin.Api.Controllers
IAuthorizationContext authContext,
ILogger<SubtitleController> logger)
{
+ _serverConfigurationManager = serverConfigurationManager;
_libraryManager = libraryManager;
_subtitleManager = subtitleManager;
_subtitleEncoder = subtitleEncoder;
@@ -320,6 +328,33 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
+ /// Upload an external subtitle file.
+ /// </summary>
+ /// <param name="itemId">The item the subtitle belongs to.</param>
+ /// <param name="body">The request body.</param>
+ /// <response code="204">Subtitle uploaded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Videos/{itemId}/Subtitles")]
+ public async Task<ActionResult> UploadSubtitle(
+ [FromRoute, Required] Guid itemId,
+ [FromBody, Required] UploadSubtitleDto body)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
+ var data = Convert.FromBase64String(body.Data);
+ await using var memoryStream = new MemoryStream(data);
+ await _subtitleManager.UploadSubtitle(
+ video,
+ new SubtitleResponse
+ {
+ Format = body.Format,
+ Language = body.Language,
+ IsForced = body.IsForced,
+ Stream = memoryStream
+ }).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
/// Encodes a subtitle in the specified format.
/// </summary>
/// <param name="id">The media id.</param>
@@ -351,5 +386,95 @@ namespace Jellyfin.Api.Controllers
copyTimestamps,
CancellationToken.None);
}
+
+ /// <summary>
+ /// Gets a list of available fallback font files.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
+ [HttpGet("FallbackFont/Fonts")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FontFile> GetFallbackFontList()
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+ if (!string.IsNullOrEmpty(fallbackFontPath))
+ {
+ var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
+ var fontFiles = files
+ .Select(i => new FontFile
+ {
+ Name = i.Name,
+ Size = i.Length,
+ DateCreated = _fileSystem.GetCreationTimeUtc(i),
+ DateModified = _fileSystem.GetLastWriteTimeUtc(i)
+ })
+ .OrderBy(i => i.Size)
+ .ThenBy(i => i.Name)
+ .ThenByDescending(i => i.DateModified)
+ .ThenByDescending(i => i.DateCreated);
+ // max total size 20M
+ const int MaxSize = 20971520;
+ var sizeCounter = 0L;
+ foreach (var fontFile in fontFiles)
+ {
+ sizeCounter += fontFile.Size;
+ if (sizeCounter >= MaxSize)
+ {
+ _logger.LogWarning("Some fonts will not be sent due to size limitations");
+ yield break;
+ }
+
+ yield return fontFile;
+ }
+ }
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+ }
+
+ /// <summary>
+ /// Gets a fallback font file.
+ /// </summary>
+ /// <param name="name">The name of the fallback font file to get.</param>
+ /// <response code="200">Fallback font file retrieved.</response>
+ /// <returns>The fallback font file.</returns>
+ [HttpGet("FallbackFont/Fonts/{name}")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetFallbackFont([FromRoute, Required] string name)
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+ if (!string.IsNullOrEmpty(fallbackFontPath))
+ {
+ var fontFile = _fileSystem.GetFiles(fallbackFontPath)
+ .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ var fileSize = fontFile?.Length;
+
+ if (fontFile != null && fileSize != null && fileSize > 0)
+ {
+ _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
+ return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
+ }
+ else
+ {
+ _logger.LogWarning("The selected font is null or empty");
+ }
+ }
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+
+ // returning HTTP 204 will break the SubtitlesOctopus
+ return Ok();
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index d7c81a3ab..ad64adfba 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -18,6 +20,7 @@ namespace Jellyfin.Api.Controllers
/// The suggestions controller.
/// </summary>
[Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class SuggestionsController : BaseJellyfinApiController
{
private readonly IDtoService _dtoService;
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 889c0e128..6b4eee718 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,5 +1,6 @@
using System;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -125,7 +126,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] LocationType[] excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -147,10 +148,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
- [FromQuery] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
- [FromQuery] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery] string? genres,
@@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 49a6c386f..55cdae39d 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -135,7 +136,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -207,7 +208,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy)
{
@@ -326,7 +327,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index df20a92b3..e10f1fe91 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -76,6 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="transcodingContainer">Optional. The container to transcode to.</param>
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
@@ -88,23 +89,22 @@ namespace Jellyfin.Api.Controllers
/// <response code="302">Redirected to remote audio stream.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
- [HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
- [HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
- [FromRoute] string? container,
+ [FromQuery] string? container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] string? transcodingContainer,
[FromQuery] string? transcodingProtocol,
@@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls,
RequireAvc = true,
@@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+ AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels,
CopyTimestamps = true,
@@ -270,20 +270,24 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = new DeviceProfile();
- var directPlayProfiles = new List<DirectPlayProfile>();
-
var containers = RequestHelpers.Split(container, ',', true);
-
- foreach (var cont in containers)
+ int len = containers.Length;
+ var directPlayProfiles = new DirectPlayProfile[len];
+ for (int i = 0; i < len; i++)
{
- var parts = RequestHelpers.Split(cont, ',', true);
+ var parts = RequestHelpers.Split(containers[i], '|', true);
- var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
+ var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
- directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
+ directPlayProfiles[i] = new DirectPlayProfile
+ {
+ Type = DlnaProfileType.Audio,
+ Container = parts[0],
+ AudioCodec = audioCodecs
+ };
}
- deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
+ deviceProfile.DirectPlayProfiles = directPlayProfiles;
deviceProfile.TranscodingProfiles = new[]
{
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 7b0897bfb..0f7c25d0e 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -530,6 +530,33 @@ namespace Jellyfin.Api.Controllers
return result;
}
+ /// <summary>
+ /// Gets the user based on auth token.
+ /// </summary>
+ /// <response code="200">User returned.</response>
+ /// <response code="400">Token is not owned by a user.</response>
+ /// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
+ [HttpGet("Me")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult<UserDto> GetCurrentUser()
+ {
+ var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+ if (userId == null)
+ {
+ return BadRequest();
+ }
+
+ var user = _userManager.GetUserById(userId.Value);
+ if (user == null)
+ {
+ return BadRequest();
+ }
+
+ return _userManager.GetUserDto(user);
+ }
+
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index a52af1781..faa8a0634 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -272,7 +273,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 2c685309a..1712a6a54 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index df235ced2..29e6b4193 100644
--- a/Jellyfin.Api/Helpers/ClaimHelpers.cs
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -63,6 +63,19 @@ namespace Jellyfin.Api.Helpers
public static string? GetToken(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Token);
+ /// <summary>
+ /// Gets a flag specifying whether the request is using an api key.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>The flag specifying whether the request is using an api key.</returns>
+ public static bool GetIsApiKey(in ClaimsPrincipal user)
+ {
+ var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
+ return !string.IsNullOrEmpty(claimValue)
+ && bool.TryParse(claimValue, out var parsedClaimValue)
+ && parsedClaimValue;
+ }
+
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
{
return user?.Identities
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 6b516977e..366301d3e 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -123,9 +123,8 @@ namespace Jellyfin.Api.Helpers
state.Dispose();
}
- await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
- return new FileStreamResult(httpContext.Response.Body, contentType);
+ var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
finally
{
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index e78f63b25..0d8315dee 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -166,7 +166,7 @@ namespace Jellyfin.Api.Helpers
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
- long? maxBitrate,
+ int? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
@@ -551,7 +551,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
new file mode 100644
index 000000000..824870c7e
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -0,0 +1,166 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Model.IO;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// A progressive file stream for transferring transcoded files as they are written to.
+ /// </summary>
+ public class ProgressiveFileStream : Stream
+ {
+ private readonly FileStream _fileStream;
+ private readonly TranscodingJobDto? _job;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly bool _allowAsyncFileRead;
+ private int _bytesWritten;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="filePath">The path to the transcoded file.</param>
+ /// <param name="job">The transcoding job information.</param>
+ /// <param name="transcodingJobHelper">The transcoding job helper.</param>
+ public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper)
+ {
+ _job = job;
+ _transcodingJobHelper = transcodingJobHelper;
+ _bytesWritten = 0;
+
+ var fileOptions = FileOptions.SequentialScan;
+ _allowAsyncFileRead = false;
+
+ // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ fileOptions |= FileOptions.Asynchronous;
+ _allowAsyncFileRead = true;
+ }
+
+ _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+ }
+
+ /// <inheritdoc />
+ public override bool CanRead => _fileStream.CanRead;
+
+ /// <inheritdoc />
+ public override bool CanSeek => false;
+
+ /// <inheritdoc />
+ public override bool CanWrite => false;
+
+ /// <inheritdoc />
+ public override long Length => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ /// <inheritdoc />
+ public override void Flush()
+ {
+ _fileStream.Flush();
+ }
+
+ /// <inheritdoc />
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _fileStream.Read(buffer, offset, count);
+ }
+
+ /// <inheritdoc />
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ int totalBytesRead = 0;
+ int remainingBytesToRead = count;
+
+ int newOffset = offset;
+ while (remainingBytesToRead > 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ int bytesRead;
+ if (_allowAsyncFileRead)
+ {
+ bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
+ }
+
+ remainingBytesToRead -= bytesRead;
+ newOffset += bytesRead;
+
+ if (bytesRead > 0)
+ {
+ _bytesWritten += bytesRead;
+ totalBytesRead += bytesRead;
+
+ if (_job != null)
+ {
+ _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+ }
+ }
+ else
+ {
+ // If the job is null it's a live stream and will require user action to close
+ if (_job?.HasExited ?? false)
+ {
+ break;
+ }
+
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ return totalBytesRead;
+ }
+
+ /// <inheritdoc />
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ if (disposing)
+ {
+ _fileStream.Dispose();
+
+ if (_job != null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ }
+ }
+ }
+ finally
+ {
+ _disposed = true;
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 78d2b831c..13d6da317 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,11 +1,13 @@
using System;
-using System.Collections.Generic;
using System.Linq;
-using System.Net;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
@@ -163,31 +165,39 @@ namespace Jellyfin.Api.Helpers
.ToArray();
}
- /// <summary>
- /// Gets the item fields.
- /// </summary>
- /// <param name="imageTypes">The image types string.</param>
- /// <returns>IEnumerable{ItemFields}.</returns>
- internal static ImageType[] GetImageTypes(string? imageTypes)
+ internal static QueryResult<BaseItemDto> CreateQueryResult(
+ QueryResult<(BaseItem, ItemCounts)> result,
+ DtoOptions dtoOptions,
+ IDtoService dtoService,
+ bool includeItemTypes,
+ User? user)
{
- if (string.IsNullOrEmpty(imageTypes))
+ var dtos = result.Items.Select(i =>
{
- return Array.Empty<ImageType>();
- }
+ var (baseItem, counts) = i;
+ var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- return Split(imageTypes, ',', true)
- .Select(v =>
+ if (includeItemTypes)
{
- if (Enum.TryParse(v, true, out ImageType value))
- {
- return (ImageType?)value;
- }
-
- return null;
- })
- .Where(i => i.HasValue)
- .Select(i => i!.Value)
- .ToArray();
+ dto.ChildCount = counts.ItemCount;
+ dto.ProgramCount = counts.ProgramCount;
+ dto.SeriesCount = counts.SeriesCount;
+ dto.EpisodeCount = counts.EpisodeCount;
+ dto.MovieCount = counts.MovieCount;
+ dto.TrailerCount = counts.TrailerCount;
+ dto.AlbumCount = counts.AlbumCount;
+ dto.SongCount = counts.SongCount;
+ dto.ArtistCount = counts.ArtistCount;
+ }
+
+ return dto;
+ });
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
}
}
}
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
index b922e76cf..6b06f87cd 100644
--- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
+++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
@@ -50,9 +50,9 @@ namespace Jellyfin.Api.Helpers
var returnItems = items;
- if (limit.HasValue)
+ if (limit.HasValue && limit < returnItems.Count)
{
- returnItems = returnItems.Take(limit.Value).ToList();
+ returnItems = returnItems.GetRange(0, limit.Value);
}
var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
index 208566dc8..4f012cab2 100644
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Api.ModelBinders
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
- var elementType = bindingContext.ModelType.GetElementType();
+ var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
if (valueProviderResult.Length > 1)
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs
deleted file mode 100644
index b9785a73b..000000000
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
-
-namespace Jellyfin.Api.ModelBinders
-{
- /// <summary>
- /// Comma delimited array model binder provider.
- /// </summary>
- public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider
- {
- private readonly IModelBinder _binder;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinderProvider"/> class.
- /// </summary>
- public CommaDelimitedArrayModelBinderProvider()
- {
- _binder = new CommaDelimitedArrayModelBinder();
- }
-
- /// <inheritdoc />
- public IModelBinder? GetBinder(ModelBinderProviderContext context)
- {
- return context.Metadata.ModelType.IsArray ? _binder : null;
- }
- }
-}
diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
new file mode 100644
index 000000000..30473255e
--- /dev/null
+++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.SubtitleDtos
+{
+ /// <summary>
+ /// Upload subtitles dto.
+ /// </summary>
+ public class UploadSubtitleDto
+ {
+ /// <summary>
+ /// Gets or sets the subtitle language.
+ /// </summary>
+ [Required]
+ public string Language { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the subtitle format.
+ /// </summary>
+ [Required]
+ public string Format { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the subtitle is forced.
+ /// </summary>
+ [Required]
+ public bool IsForced { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle data.
+ /// </summary>
+ [Required]
+ public string Data { get; set; } = string.Empty;
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 5926abfe0..7bde4f35b 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -72,6 +72,18 @@ namespace Jellyfin.Server.Implementations.Activity
};
}
+ /// <inheritdoc />
+ public async Task CleanAsync(DateTime startDate)
+ {
+ await using var dbContext = _provider.CreateContext();
+ var entries = dbContext.ActivityLogs
+ .AsQueryable()
+ .Where(entry => entry.DateCreated <= startDate);
+
+ dbContext.RemoveRange(entries);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
{
return new ActivityLogEntry
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index d7b9da5c2..cc98955df 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
+using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.DownloadPolicy;
@@ -16,7 +17,6 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
-using Jellyfin.Api.ModelBinders;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
@@ -28,6 +28,8 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
@@ -167,8 +169,6 @@ namespace Jellyfin.Server.Extensions
opts.OutputFormatters.Add(new CssOutputFormatter());
opts.OutputFormatters.Add(new XmlOutputFormatter());
-
- opts.ModelBinderProviders.Insert(0, new CommaDelimitedArrayModelBinderProvider());
})
// Clear app parts to avoid other assemblies being picked up
@@ -212,7 +212,19 @@ namespace Jellyfin.Server.Extensions
{
return serviceCollection.AddSwaggerGen(c =>
{
- c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+ c.SwaggerDoc("api-docs", new OpenApiInfo
+ {
+ Title = "Jellyfin API",
+ Version = "v1",
+ Extensions = new Dictionary<string, IOpenApiExtension>
+ {
+ {
+ "x-jellyfin-version",
+ new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
+ }
+ }
+ });
+
c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 07f08ec72..a64d2e1cd 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -43,7 +43,7 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 844dae61f..aca165408 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -23,7 +23,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultPluginRepository),
typeof(Routines.MigrateUserDb),
typeof(Routines.ReaddDefaultPluginRepository),
- typeof(Routines.MigrateDisplayPreferencesDb)
+ typeof(Routines.MigrateDisplayPreferencesDb),
+ typeof(Routines.RemoveDownloadImagesInAdvance)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
new file mode 100644
index 000000000..42b87ec5f
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
@@ -0,0 +1,46 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
+ /// </summary>
+ internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
+ {
+ private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
+ private readonly ILibraryManager _libraryManager;
+
+ public RemoveDownloadImagesInAdvance(ILogger<RemoveDownloadImagesInAdvance> logger, ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{A81F75E0-8F43-416F-A5E8-516CCAB4D8CC}");
+
+ /// <inheritdoc/>
+ public string Name => "RemoveDownloadImagesInAdvance";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var virtualFolders = _libraryManager.GetVirtualFolders(false);
+ _logger.LogInformation("Removing 'RemoveDownloadImagesInAdvance' settings in all the libraries");
+ foreach (var virtualFolder in virtualFolders)
+ {
+ var libraryOptions = virtualFolder.LibraryOptions;
+ var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(virtualFolder.ItemId);
+ // The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
+ collectionFolder.UpdateLibraryOptions(libraryOptions);
+ _logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 5573c0439..97a51c202 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -290,23 +290,19 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
options.Listen(address, appHost.HttpPort);
+
if (appHost.ListenWithHttps)
{
- options.Listen(address, appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps(appHost.Certificate);
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.Listen(
+ address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
else if (builderContext.HostingEnvironment.IsDevelopment())
{
try
{
- options.Listen(address, appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps();
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.Listen(address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
}
catch (InvalidOperationException ex)
{
@@ -322,21 +318,15 @@ namespace Jellyfin.Server
if (appHost.ListenWithHttps)
{
- options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps(appHost.Certificate);
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.ListenAnyIP(
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
else if (builderContext.HostingEnvironment.IsDevelopment())
{
try
{
- options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps();
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.ListenAnyIP(appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
}
catch (InvalidOperationException ex)
{
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
index b7b1daf76..24ed3ea19 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
@@ -21,8 +21,8 @@ namespace MediaBrowser.Common.Json.Converters
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
- var structType = typeToConvert.GetElementType();
+ var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs
new file mode 100644
index 000000000..7927c663d
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Local plugin struct.
+ /// </summary>
+ public class LocalPlugin : IEquatable<LocalPlugin>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
+ /// </summary>
+ /// <param name="id">The plugin id.</param>
+ /// <param name="name">The plugin name.</param>
+ /// <param name="version">The plugin version.</param>
+ /// <param name="path">The plugin path.</param>
+ public LocalPlugin(Guid id, string name, Version version, string path)
+ {
+ Id = id;
+ Name = name;
+ Version = version;
+ Path = path;
+ DllFiles = new List<string>();
+ }
+
+ /// <summary>
+ /// Gets the plugin id.
+ /// </summary>
+ public Guid Id { get; }
+
+ /// <summary>
+ /// Gets the plugin name.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// Gets the plugin version.
+ /// </summary>
+ public Version Version { get; }
+
+ /// <summary>
+ /// Gets the plugin path.
+ /// </summary>
+ public string Path { get; }
+
+ /// <summary>
+ /// Gets the list of dll files for this plugin.
+ /// </summary>
+ public List<string> DllFiles { get; }
+
+ /// <summary>
+ /// == operator.
+ /// </summary>
+ /// <param name="left">Left item.</param>
+ /// <param name="right">Right item.</param>
+ /// <returns>Comparison result.</returns>
+ public static bool operator ==(LocalPlugin left, LocalPlugin right)
+ {
+ return left.Equals(right);
+ }
+
+ /// <summary>
+ /// != operator.
+ /// </summary>
+ /// <param name="left">Left item.</param>
+ /// <param name="right">Right item.</param>
+ /// <returns>Comparison result.</returns>
+ public static bool operator !=(LocalPlugin left, LocalPlugin right)
+ {
+ return !left.Equals(right);
+ }
+
+ /// <summary>
+ /// Compare two <see cref="LocalPlugin"/>.
+ /// </summary>
+ /// <param name="a">The first item.</param>
+ /// <param name="b">The second item.</param>
+ /// <returns>Comparison result.</returns>
+ public static int Compare(LocalPlugin a, LocalPlugin b)
+ {
+ var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+ // Id is not equal but name is.
+ if (a.Id != b.Id && compare == 0)
+ {
+ compare = a.Id.CompareTo(b.Id);
+ }
+
+ return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
+ }
+
+ /// <inheritdoc />
+ public override bool Equals(object obj)
+ {
+ return obj is LocalPlugin other && this.Equals(other);
+ }
+
+ /// <inheritdoc />
+ public override int GetHashCode()
+ {
+ return Name.GetHashCode(StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc />
+ public bool Equals(LocalPlugin other)
+ {
+ return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
+ && Id.Equals(other.Id);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index 169aca2ca..6aa16fea7 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -11,29 +11,6 @@ namespace MediaBrowser.Common.Updates
{
public interface IInstallationManager : IDisposable
{
- event EventHandler<InstallationInfo> PackageInstalling;
-
- event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
- event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
- event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
- /// <summary>
- /// Occurs when a plugin is uninstalled.
- /// </summary>
- event EventHandler<IPlugin> PluginUninstalled;
-
- /// <summary>
- /// Occurs when a plugin is updated.
- /// </summary>
- event EventHandler<InstallationInfo> PluginUpdated;
-
- /// <summary>
- /// Occurs when a plugin is installed.
- /// </summary>
- event EventHandler<InstallationInfo> PluginInstalled;
-
/// <summary>
/// Gets the completed installations.
/// </summary>
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 397a68ff7..c5e50cf45 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Entities.Audio
{
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) };
query.ArtistIds = new[] { Id };
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index 5a117a6b1..f0c076108 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) };
return LibraryManager.GetItemList(query);
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 35ddaffad..a76c8a376 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -723,7 +723,7 @@ namespace MediaBrowser.Controller.Entities
private bool RequiresPostFiltering2(InternalItemsQuery query)
{
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase))
{
Logger.LogDebug("Query requires post-filtering due to BoxSet query");
return true;
@@ -813,7 +813,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsPlayed.HasValue)
{
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series)))
{
Logger.LogDebug("Query requires post-filtering due to IsPlayed");
return true;
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index db6c85caf..74a170204 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -59,7 +59,13 @@ namespace MediaBrowser.Controller.Entities
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.ExcludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+ query.ExcludeItemTypes = new[]
+ {
+ nameof(MusicVideo),
+ nameof(Entities.Audio.Audio),
+ nameof(MusicAlbum),
+ nameof(MusicArtist)
+ };
return LibraryManager.GetItemList(query);
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index 4e09ee573..5b96a5af6 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Entities
{
@@ -23,6 +24,10 @@ namespace MediaBrowser.Controller.Entities
public string NameContains { get; set; }
+ public User User { get; set; }
+
+ public bool? IsFavorite { get; set; }
+
public InternalPeopleQuery()
{
PersonTypes = Array.Empty<string>();
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 75a746bfb..e8afa9a49 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -151,7 +151,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
}
query.IsVirtualItem = false;
@@ -207,7 +207,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
- query.IncludeItemTypes = new[] { typeof(Season).Name };
+ query.IncludeItemTypes = new[] { nameof(Season) };
query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
if (user != null && !user.DisplayMissingEpisodes)
@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) };
}
query.IsVirtualItem = false;
@@ -253,7 +253,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
+ IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
};
@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
};
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 7bb311900..a262fee15 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -142,7 +142,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
}
return parent.QueryRecursive(query);
@@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -178,7 +178,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -189,7 +189,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
return _libraryManager.GetItemsResult(query);
}
@@ -200,7 +200,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.IncludeItemTypes = new[] { nameof(BoxSet) };
query.SetUser(user);
query.Recursive = true;
@@ -223,7 +223,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -236,7 +236,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -255,7 +255,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Movie).Name },
+ IncludeItemTypes = new[] { nameof(Movie) },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -286,7 +286,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
query.IsVirtualItem = false;
return ConvertToResult(_libraryManager.GetItemList(query));
@@ -362,7 +362,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -373,7 +373,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -382,7 +382,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -413,7 +413,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index b35f83096..6658269bd 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -12,6 +12,9 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the display preferences for the user and client.
/// </summary>
+ /// <remarks>
+ /// This will create the display preferences if it does not exist, but it will not save automatically.
+ /// </remarks>
/// <param name="userId">The user's id.</param>
/// <param name="client">The client string.</param>
/// <returns>The associated display preferences.</returns>
@@ -20,6 +23,9 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the default item display preferences for the user and client.
/// </summary>
+ /// <remarks>
+ /// This will create the item display preferences if it does not exist, but it will not save automatically.
+ /// </remarks>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index cfad17fb7..ffbb147b0 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -6,8 +6,8 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.System;
-using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller
{
@@ -56,10 +56,11 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the system info.
/// </summary>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
/// <returns>SystemInfo.</returns>
- Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
+ Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken = default);
- Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
+ Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken = default);
/// <summary>
/// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
@@ -67,7 +68,7 @@ namespace MediaBrowser.Controller
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
/// <returns>A list containing all the local IP addresses of the server.</returns>
- Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
+ Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken = default);
/// <summary>
/// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
@@ -75,7 +76,7 @@ namespace MediaBrowser.Controller
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
/// <returns>The server URL.</returns>
- Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
+ Task<string> GetLocalApiUrl(CancellationToken cancellationToken = default);
/// <summary>
/// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
@@ -119,5 +120,13 @@ namespace MediaBrowser.Controller
string ExpandVirtualPath(string path);
string ReverseVirtualPath(string path);
+
+ /// <summary>
+ /// Gets the list of local plugins.
+ /// </summary>
+ /// <param name="path">Plugin base directory.</param>
+ /// <param name="cleanup">Cleanup old plugins.</param>
+ /// <returns>Enumerable of local plugins.</returns>
+ IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
}
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 32703c2fd..c7c79df76 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -570,5 +570,7 @@ namespace MediaBrowser.Controller.Library
List<MediaStream> streams,
string videoPath,
string[] files);
+
+ BaseItem GetParentItem(string parentId, Guid? userId);
}
}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 22bf9488f..21c6ef2af 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -115,5 +115,7 @@ namespace MediaBrowser.Controller.Library
public interface IDirectStreamProvider
{
Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
+
+ string GetFilePath();
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 996b1b5c1..5846a603a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2675,9 +2675,10 @@ namespace MediaBrowser.Controller.MediaEncoding
state.MediaSource = mediaSource;
var request = state.BaseRequest;
- if (!string.IsNullOrWhiteSpace(request.AudioCodec))
+ var supportedAudioCodecs = state.SupportedAudioCodecs;
+ if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0)
{
- var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
+ var supportedAudioCodecsList = supportedAudioCodecs.ToList();
ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
@@ -3084,7 +3085,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- var whichCodec = videoStream.Codec.ToLowerInvariant();
+ var whichCodec = videoStream.Codec?.ToLowerInvariant();
switch (whichCodec)
{
case "avc":
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 6cd0c70d2..6e9362cd1 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -287,6 +287,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return BaseRequest.AudioChannels;
}
+ if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
+ {
+ return BaseRequest.TranscodingMaxAudioChannels;
+ }
+
if (!string.IsNullOrEmpty(codec))
{
var value = BaseRequest.GetOption(codec, "audiochannels");
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index 735c46ef8..0194c596f 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,10 +1,11 @@
-#pragma warning disable CS1591
-
using System;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// The request authorization info.
+ /// </summary>
public class AuthorizationInfo
{
/// <summary>
@@ -43,6 +44,19 @@ namespace MediaBrowser.Controller.Net
/// <value>The token.</value>
public string Token { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the authorization is from an api key.
+ /// </summary>
+ public bool IsApiKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user making the request.
+ /// </summary>
public User User { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the token is authenticated.
+ /// </summary>
+ public bool IsAuthenticated { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 216dd2709..e8b7be7e2 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -160,7 +160,7 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = new[] { musicGenre.Id },
OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
@@ -172,7 +172,7 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
ArtistIds = new[] { musicArtist.Id },
OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index f43d523a6..feb26bc10 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -53,6 +54,14 @@ namespace MediaBrowser.Controller.Subtitles
Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken);
/// <summary>
+ /// Upload new subtitle.
+ /// </summary>
+ /// <param name="video">The video the subtitle belongs to.</param>
+ /// <param name="response">The subtitle response.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task UploadSubtitle(Video video, SubtitleResponse response);
+
+ /// <summary>
/// Gets the remote subtitles.
/// </summary>
/// <param name="id">The identifier.</param>
diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs
index 3e4ea208e..28073fb8d 100644
--- a/MediaBrowser.Model/Activity/IActivityManager.cs
+++ b/MediaBrowser.Model/Activity/IActivityManager.cs
@@ -16,5 +16,12 @@ namespace MediaBrowser.Model.Activity
Task CreateAsync(ActivityLog entry);
Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query);
+
+ /// <summary>
+ /// Remove all activity logs before the specified date.
+ /// </summary>
+ /// <param name="startDate">Activity log start date.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task CleanAsync(DateTime startDate);
}
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 2cd637c5b..c34825667 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Configuration
public string TranscodingTempPath { get; set; }
+ public string FallbackFontPath { get; set; }
+
+ public bool EnableFallbackFont { get; set; }
+
public double DownMixAudioBoost { get; set; }
public int MaxMuxingQueueSize { get; set; }
@@ -69,6 +73,7 @@ namespace MediaBrowser.Model.Configuration
public EncodingOptions()
{
+ EnableFallbackFont = false;
DownMixAudioBoost = 2;
MaxMuxingQueueSize = 2048;
EnableThrottling = false;
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 54ef49ea6..77ac11d69 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -17,8 +17,6 @@ namespace MediaBrowser.Model.Configuration
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
- public bool DownloadImagesInAdvance { get; set; }
-
public MediaPathInfo[] PathInfos { get; set; }
public bool SaveLocalMetadata { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 8b78ad842..23a5201f7 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -272,6 +272,11 @@ namespace MediaBrowser.Model.Configuration
public string[] KnownProxies { get; set; }
/// <summary>
+ /// Gets or sets the number of days we should retain activity logs.
+ /// </summary>
+ public int? ActivityLogRetentionDays { get; set; }
+
+ /// <summary>
/// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
public ServerConfiguration()
@@ -381,6 +386,7 @@ namespace MediaBrowser.Model.Configuration
SlowResponseThresholdMs = 500;
CorsHosts = new[] { "*" };
KnownProxies = Array.Empty<string>();
+ ActivityLogRetentionDays = 30;
}
}
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs
index 67e4ffe03..bbb8bf426 100644
--- a/MediaBrowser.Model/Dlna/AudioOptions.cs
+++ b/MediaBrowser.Model/Dlna/AudioOptions.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// The application's configured quality setting.
/// </summary>
- public long? MaxBitrate { get; set; }
+ public int? MaxBitrate { get; set; }
/// <summary>
/// Gets or sets the context.
@@ -67,7 +67,7 @@ namespace MediaBrowser.Model.Dlna
/// Gets the maximum bitrate.
/// </summary>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
- public long? GetMaxBitrate(bool isAudio)
+ public int? GetMaxBitrate(bool isAudio)
{
if (MaxBitrate.HasValue)
{
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 44412f3e4..e842efead 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -62,9 +62,9 @@ namespace MediaBrowser.Model.Dlna
public int? MaxIconHeight { get; set; }
- public long? MaxStreamingBitrate { get; set; }
+ public int? MaxStreamingBitrate { get; set; }
- public long? MaxStaticBitrate { get; set; }
+ public int? MaxStaticBitrate { get; set; }
public int? MusicStreamingTranscodingBitrate { get; set; }
diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
index 83bda5d56..a8ea405e2 100644
--- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
+++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
@@ -37,7 +37,7 @@ namespace MediaBrowser.Model.MediaInfo
public string PlaySessionId { get; set; }
- public long? MaxStreamingBitrate { get; set; }
+ public int? MaxStreamingBitrate { get; set; }
public long? StartTimeTicks { get; set; }
diff --git a/MediaBrowser.Model/Subtitles/FontFile.cs b/MediaBrowser.Model/Subtitles/FontFile.cs
new file mode 100644
index 000000000..115c49295
--- /dev/null
+++ b/MediaBrowser.Model/Subtitles/FontFile.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Model.Subtitles
+{
+ /// <summary>
+ /// Class FontFile.
+ /// </summary>
+ public class FontFile
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ /// <value>The size.</value>
+ public long Size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <value>The date modified.</value>
+ public DateTime DateModified { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index d0bdbd7c9..a57a85376 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -517,13 +517,8 @@ namespace MediaBrowser.Providers.Manager
return true;
}
}
-
- if (libraryOptions.DownloadImagesInAdvance)
- {
- return false;
- }
-
- return true;
+ // We always want to use prefetched images
+ return false;
}
private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls)
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index f110eafa5..dca8acb7d 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -252,7 +252,13 @@ namespace MediaBrowser.Providers.Manager
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
{
- await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false);
+ personEntity.SetImage(
+ new ItemImageInfo
+ {
+ Path = person.ImageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
saveEntity = true;
updateType |= ItemUpdateType.ImageUpdate;
@@ -266,30 +272,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken)
- {
- if (libraryOptions.DownloadImagesInAdvance)
- {
- try
- {
- await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
- return;
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error in AddPersonImage");
- }
- }
-
- personEntity.SetImage(
- new ItemImageInfo
- {
- Path = imageUrl,
- Type = ImageType.Primary
- },
- 0);
- }
-
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
{
item.AfterMetadataRefresh();
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 24400eae5..9465fe42c 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -19,7 +19,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
- <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
+ <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.2" />
<PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
<PackageReference Include="TvDbSharper" Version="3.2.2" />
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
index dc3c60dee..a5cd425f6 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
@@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
PersonIds = new[] { item.Id },
DtoOptions = new DtoOptions(false)
{
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index f25d3d5ee..f3fbe2d12 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -150,37 +150,11 @@ namespace MediaBrowser.Providers.Subtitles
var parts = subtitleId.Split(new[] { '_' }, 2);
var provider = GetProvider(parts[0]);
- var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
-
try
{
var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
- using (var stream = response.Stream)
- using (var memoryStream = new MemoryStream())
- {
- await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
- memoryStream.Position = 0;
-
- var savePaths = new List<string>();
- var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
-
- if (response.IsForced)
- {
- saveFileName += ".forced";
- }
-
- saveFileName += "." + response.Format.ToLowerInvariant();
-
- if (saveInMediaFolder)
- {
- savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
- }
-
- savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
-
- await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
- }
+ await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false);
}
catch (RateLimitExceededException)
{
@@ -199,6 +173,47 @@ namespace MediaBrowser.Providers.Subtitles
}
}
+ /// <inheritdoc />
+ public Task UploadSubtitle(Video video, SubtitleResponse response)
+ {
+ var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
+ return TrySaveSubtitle(video, libraryOptions, response);
+ }
+
+ private async Task TrySaveSubtitle(
+ Video video,
+ LibraryOptions libraryOptions,
+ SubtitleResponse response)
+ {
+ var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
+
+ using (var stream = response.Stream)
+ using (var memoryStream = new MemoryStream())
+ {
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
+
+ var savePaths = new List<string>();
+ var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
+
+ if (response.IsForced)
+ {
+ saveFileName += ".forced";
+ }
+
+ saveFileName += "." + response.Format.ToLowerInvariant();
+
+ if (saveInMediaFolder)
+ {
+ savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
+ }
+
+ savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
+
+ await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
+ }
+ }
+
private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
{
Exception exceptionToThrow = null;
diff --git a/debian/control b/debian/control
index 39c2aa055..9216d24fe 100644
--- a/debian/control
+++ b/debian/control
@@ -20,7 +20,6 @@ Breaks: jellyfin (<<10.6.0)
Architecture: any
Depends: at,
libsqlite3-0,
- jellyfin-ffmpeg (>= 4.2.1-2),
libfontconfig1,
libfreetype6,
libssl1.1
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 4ea5094b6..a46d94457 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Auth;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
@@ -68,14 +69,14 @@ namespace Jellyfin.Api.Tests.Auth
}
[Fact]
- public async Task HandleAuthenticateAsyncShouldFailOnSecurityException()
+ public async Task HandleAuthenticateAsyncShouldFailOnAuthenticationException()
{
var errorMessage = _fixture.Create<string>();
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>()))
- .Throws(new SecurityException(errorMessage));
+ .Throws(new AuthenticationException(errorMessage));
var authenticateResult = await _sut.AuthenticateAsync();
@@ -128,6 +129,7 @@ namespace Jellyfin.Api.Tests.Auth
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
authorizationInfo.User = _fixture.Create<User>();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
+ authorizationInfo.IsApiKey = false;
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index e7993d2e7..0236f2ac1 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.13.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" />
+ <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="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
index a4dd4e409..c4ce39885 100644
--- a/tests/Jellyfin.Api.Tests/TestHelpers.cs
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -45,7 +45,7 @@ namespace Jellyfin.Api.Tests
{
new Claim(ClaimTypes.Role, role),
new Claim(ClaimTypes.Name, "jellyfin"),
- new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.UserId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.Device, "test"),
new Claim(InternalClaimTypes.Client, "test"),
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
index 16f8a690e..0d2bdd1af 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
@@ -11,82 +11,82 @@ namespace Jellyfin.Common.Tests.Json
[Fact]
public static void Deserialize_String_Valid_Success()
{
- var desiredValue = new GenericBodyModel<string>
+ var desiredValue = new GenericBodyArrayModel<string>
{
Value = new[] { "a", "b", "c" }
};
var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_String_Space_Valid_Success()
{
- var desiredValue = new GenericBodyModel<string>
+ var desiredValue = new GenericBodyArrayModel<string>
{
Value = new[] { "a", "b", "c" }
};
var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Valid_Success()
{
- var desiredValue = new GenericBodyModel<GeneralCommandType>
+ var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Space_Valid_Success()
{
- var desiredValue = new GenericBodyModel<GeneralCommandType>
+ var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_String_Array_Valid_Success()
{
- var desiredValue = new GenericBodyModel<string>
+ var desiredValue = new GenericBodyArrayModel<string>
{
Value = new[] { "a", "b", "c" }
};
var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Array_Valid_Success()
{
- var desiredValue = new GenericBodyModel<GeneralCommandType>
+ var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs
new file mode 100644
index 000000000..34ad9bac7
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs
@@ -0,0 +1,92 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Jellyfin.Common.Tests.Models;
+using MediaBrowser.Model.Session;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public static class JsonCommaDelimitedIReadOnlyListTests
+ {
+ [Fact]
+ public static void Deserialize_String_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_String_Space_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Space_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_String_Array_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Array_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs
index 9a6c382fe..276e1bfbe 100644
--- a/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs
+++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.Common.Tests.Models
/// The generic body model.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
- public class GenericBodyModel<T>
+ public class GenericBodyArrayModel<T>
{
/// <summary>
/// Gets or sets the value.
@@ -17,4 +17,4 @@ namespace Jellyfin.Common.Tests.Models
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public T[] Value { get; set; } = default!;
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs
new file mode 100644
index 000000000..627454b25
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
+
+namespace Jellyfin.Common.Tests.Models
+{
+ /// <summary>
+ /// The generic body <c>IReadOnlyList</c> model.
+ /// </summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ public class GenericBodyIReadOnlyListModel<T>
+ {
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<T> Value { get; set; } = default!;
+ }
+}
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 6e5a90802..05323490e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -14,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.13.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
+ <PackageReference Include="AutoFixture" Version="4.14.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.7" />
<PackageReference Include="xunit" Version="2.4.1" />